Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/therapy-compass/bindings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import type { Pathway, ReferenceData, Therapy } from "./data/types";

const KNOWN_SCREENS = ["search", "detail", "compare", "recommend", "pathways", "brief", "home", "sheets"] as const;
const MAX_COMPARE = 4;
export const MAX_COMPARE = 4;

type SheetSectionKey = "about" | "steps" | "practice" | "coping" | "contacts";

Expand Down
1 change: 1 addition & 0 deletions src/components/therapy-compass/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export const CopyIcon = makeIcon(
<path d="M4 16V6a2 2 0 0 1 2-2h10" />
</>,
);
export const CheckIcon = makeIcon(<path d="m5 12 5 5 9-11" />, 2);
export const PrinterIcon = makeIcon(
<path d="M6 9V3h12v6M6 18H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-2M6 14h12v7H6Z" />,
);
Expand Down
38 changes: 28 additions & 10 deletions src/components/therapy-compass/screens/brief-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { useMemo, useState } from "react";
import { useTcBindings } from "../bindings";
import { commandControl, outlineControl } from "../controls";
import { parseSteps, summarise } from "../data/select";
import { AlertIcon, CopyIcon, ExternalLinkIcon, FileTextIcon, SaveIcon, SearchIcon } from "../icons";
import { AlertIcon, CheckIcon, CopyIcon, ExternalLinkIcon, FileTextIcon, SearchIcon } from "../icons";
import { s } from "../style-utils";
import { LoadingState } from "../ui";
import { useClipboard } from "../use-clipboard";

const CHECKLIST = [
"Confirm the primary problem",
Expand All @@ -20,6 +21,7 @@ export function BriefScreen() {
const b = useTcBindings();
const t = b.selectedTherapy;
const [filter, setFilter] = useState("");
const { copied, copy } = useClipboard();

const briefTherapies = useMemo(
() =>
Expand All @@ -41,6 +43,20 @@ export function BriefScreen() {
t.briefVersion
: t.briefVersion;
const steps = parseSteps(durationText, 6);
const interventionText = [
`${t.name} — ${durationLabel} intervention`,
"",
...steps.map((st, i) => `${i + 1}. ${st}`),
...(t.clinicianScripts.length
? [
"",
"Clinician script:",
...t.clinicianScripts
.slice(0, 2)
.map((c) => (c.scriptType ? `${c.scriptType}: ${c.body ?? ""}` : (c.body ?? ""))),
]
: []),
].join("\n");

return (
<section data-screen-label="Brief" style={s(`max-width:1240px;margin:0 auto;`)}>
Expand Down Expand Up @@ -231,12 +247,13 @@ export function BriefScreen() {
<button
type="button"
className="tc-btn"
onClick={() => copy(step, `step-${i}`)}
title="Copy step"
style={s(
`display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:var(--text-soft);flex:none;cursor:pointer;`,
`display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border:1px solid ${copied === `step-${i}` ? "var(--clinical-accent-border)" : "var(--border)"};border-radius:8px;background:var(--surface);color:${copied === `step-${i}` ? "var(--clinical-accent)" : "var(--text-soft)"};flex:none;cursor:pointer;`,
)}
>
<CopyIcon size={14} />
{copied === `step-${i}` ? <CheckIcon size={14} /> : <CopyIcon size={14} />}
</button>
</div>
</div>
Expand Down Expand Up @@ -315,13 +332,14 @@ export function BriefScreen() {
</div>

<div style={s(`display:flex;gap:10px;flex-wrap:wrap;`)}>
<button type="button" className="tc-btn" style={s(outlineControl + "height:46px;")}>
<CopyIcon size={16} />
Copy intervention
</button>
<button type="button" className="tc-btn" style={s(outlineControl + "height:46px;")}>
<SaveIcon size={16} />
Save brief
<button
type="button"
className="tc-btn"
onClick={() => copy(interventionText, "intervention")}
style={s(outlineControl + "height:46px;")}
>
{copied === "intervention" ? <CheckIcon size={16} /> : <CopyIcon size={16} />}
{copied === "intervention" ? "Copied" : "Copy intervention"}
</button>
<button
type="button"
Expand Down
41 changes: 32 additions & 9 deletions src/components/therapy-compass/screens/compare-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,27 @@

import { useMemo, useState, type ReactNode } from "react";

import { useTcBindings } from "../bindings";
import { MAX_COMPARE, useTcBindings } from "../bindings";
import { commandControl, outlineControl } from "../controls";
import { needsReviewCount, parseSteps, searchTherapies, shortestDelivery, summarise } from "../data/select";
import type { Therapy } from "../data/types";
import {
AlertIcon,
CheckIcon,
ClockIcon,
CopyIcon,
CrosshairIcon,
InfoIcon,
PlayIcon,
PlusIcon,
SaveIcon,
ScaleIcon,
SearchIcon,
ShieldIcon,
XIcon,
} from "../icons";
import { s } from "../style-utils";
import { EmptyState } from "../ui";
import { useClipboard } from "../use-clipboard";

type Row = {
key: string;
Expand Down Expand Up @@ -69,6 +71,7 @@ const ROWS: Row[] = [
export function CompareScreen() {
const b = useTcBindings();
const items = b.compareTherapies;
const { copied, copy } = useClipboard();

const rows = useMemo(() => {
if (b.cmpTab === "priorities") return ROWS.filter((r) => r.priority);
Expand All @@ -78,6 +81,16 @@ export function CompareScreen() {
return ROWS;
}, [b.cmpTab, items]);

const copySet = () =>
copy(
[
`Therapy comparison — ${items.map((t) => t.name).join(" vs ")}`,
"",
...ROWS.map((r) => `${r.label}: ${items.map((t) => r.get(t)).join(" | ")}`),
].join("\n"),
"set",
);

const cols = `minmax(180px,1.1fr) ${items.map(() => "minmax(160px,1fr)").join(" ")}`;
const dense = b.density === "dense";
const cellPad = dense ? "11px 16px" : "15px 20px";
Expand Down Expand Up @@ -115,9 +128,15 @@ export function CompareScreen() {
Dense
</button>
</div>
<button type="button" className="tc-btn" style={s(outlineControl + "height:42px;")}>
<SaveIcon size={16} />
Save set
<button
type="button"
className="tc-btn"
onClick={copySet}
disabled={items.length < 2}
style={s(outlineControl + `height:42px;${items.length < 2 ? "opacity:0.5;cursor:not-allowed;" : ""}`)}
>
{copied === "set" ? <CheckIcon size={16} /> : <CopyIcon size={16} />}
{copied === "set" ? "Copied" : "Copy set"}
</button>
<button type="button" className="tc-btn" onClick={b.clearCompare} style={s(outlineControl + "height:42px;")}>
Clear
Expand Down Expand Up @@ -310,12 +329,13 @@ function SummaryCell({
function AddPicker() {
const b = useTcBindings();
const [q, setQ] = useState("");
const atLimit = b.compareSlugs.length >= MAX_COMPARE;
const matches = useMemo(() => {
if (!q.trim()) return [];
if (atLimit || !q.trim()) return [];
return searchTherapies(b.therapies, { query: q, tags: [], briefOnly: false, sheetOnly: false, reviewedOnly: false })
.filter((t) => !b.isInCompare(t.slug))
.slice(0, 6);
}, [q, b]);
}, [q, b, atLimit]);

return (
<div style={s(`position:relative;flex:1;min-width:260px;`)}>
Expand All @@ -324,10 +344,13 @@ function AddPicker() {
<input
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder="Add a therapy to the comparison…"
disabled={atLimit}
placeholder={
atLimit ? "Maximum of 4 selected — remove one to add another" : "Add a therapy to the comparison…"
}
aria-label="Add a therapy to compare"
style={s(
`width:100%;height:46px;padding:0 14px 0 40px;border:1px dashed var(--border-strong);border-radius:12px;background:var(--surface);color:var(--text);font-size:14px;font-family:inherit;outline:none;`,
`width:100%;height:46px;padding:0 14px 0 40px;border:1px dashed var(--border-strong);border-radius:12px;background:var(--surface);color:var(--text);font-size:14px;font-family:inherit;outline:none;${atLimit ? "opacity:0.6;cursor:not-allowed;" : ""}`,
)}
/>
</label>
Expand Down
15 changes: 8 additions & 7 deletions src/components/therapy-compass/screens/detail-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ReactNode } from "react";

import { useTcBindings } from "../bindings";
import { card, heroCard, outlineControl } from "../controls";
import { complexityLabel, parseSteps } from "../data/select";
import { complexityLabel, parseSteps, summarise } from "../data/select";
import type { Therapy } from "../data/types";
import {
AlertIcon,
Expand All @@ -16,7 +16,6 @@ import {
DatabaseIcon,
FileTextIcon,
InfoIcon,
MessageIcon,
PersonIcon,
ScaleIcon,
ShieldIcon,
Expand Down Expand Up @@ -99,13 +98,13 @@ export function DetailScreen() {
icon={ShieldIcon}
eyebrow="USE WHEN"
tone="accent"
text={t.bestUsedFor || t.indications || "See clinical record."}
text={summarise(t.bestUsedFor || t.indications, 1) || "See clinical record."}
/>
<Tile
icon={AlertIcon}
eyebrow="AVOID / MODIFY"
tone="warning"
text={t.contraindicationsOrCautions || "Confirm suitability against source before use."}
text={summarise(t.contraindicationsOrCautions, 1) || "Confirm suitability against source before use."}
/>
<Tile
icon={ClockIcon}
Expand All @@ -126,7 +125,6 @@ export function DetailScreen() {

{/* BODY */}
<div style={s(card + "padding:6px 24px;")}>
<BodyRow icon={MessageIcon} title="Clinical snapshot" body={t.clinicalSummary} />
{t.mechanism ? <BodyRow icon={CrosshairIcon} title="How it works" body={t.mechanism} /> : null}
<BodyRow icon={PersonIcon} title="When to use" body={t.indications || t.bestUsedFor} />
{steps.length ? (
Expand Down Expand Up @@ -204,7 +202,6 @@ export function DetailScreen() {
At a glance
</div>
<div style={s(`display:flex;flex-direction:column;gap:15px;`)}>
<GlanceRow icon={ShieldIcon} title="Best used for" body={t.bestUsedFor || t.indications} />
<GlanceRow icon={TargetIcon} title="Target symptoms" body={t.targetSymptoms || t.patientPopulation} />
<GlanceRow
icon={ClockIcon}
Expand Down Expand Up @@ -375,7 +372,11 @@ function BodyRow({
}

function SafetyRow({ therapy }: { therapy: Therapy }) {
const text = [therapy.contraindicationsOrCautions, therapy.limitations].filter(Boolean).join(" ");
const contra = therapy.contraindicationsOrCautions?.trim() ?? "";
const lim = therapy.limitations?.trim() ?? "";
// `limitations` frequently repeats the tail of `contraindicationsOrCautions`;
// only append it when it adds something new so the box doesn't echo itself.
const text = lim && !contra.includes(lim) ? `${contra} ${lim}`.trim() : contra;
if (!text) return null;
return (
<div
Expand Down
4 changes: 2 additions & 2 deletions src/components/therapy-compass/screens/home-screen.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState } from "react";
import { useState, type ReactNode } from "react";

import { useTcBindings } from "../bindings";
import { commandControl, linkButton } from "../controls";
Expand Down Expand Up @@ -215,7 +215,7 @@ function QuickTool({
body,
onClick,
}: {
icon: (p: { size?: number; strokeWidth?: number }) => React.ReactNode;
icon: (p: { size?: number; strokeWidth?: number }) => ReactNode;
title: string;
body: string;
onClick: () => void;
Expand Down
38 changes: 33 additions & 5 deletions src/components/therapy-compass/screens/recommend-screen.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,40 @@
"use client";

import type { ReactNode } from "react";

import { useTcBindings } from "../bindings";
import { commandControl, outlineControl } from "../controls";
import { RECOMMEND_CONSTRAINTS, summarise } from "../data/select";
import { ArrowRightIcon, SaveIcon, SearchIcon, ShieldIcon, SparkleIcon } from "../icons";
import { ArrowRightIcon, CheckIcon, CopyIcon, SearchIcon, ShieldIcon, SparkleIcon } from "../icons";
import { s } from "../style-utils";
import { LoadingState } from "../ui";
import { useClipboard } from "../use-clipboard";

export function RecommendScreen() {
const b = useTcBindings();
const { copied, copy } = useClipboard();
const ranked = b.recommendations;
const top = ranked[0]?.therapy;
const rest = ranked.slice(1, 6);

const copyShortlist = () =>
copy(
[
"Recommendation shortlist",
b.recQuery.trim() ? `Question: ${b.recQuery.trim()}` : "",
b.recConstraints.length
? `Constraints: ${RECOMMEND_CONSTRAINTS.filter((c) => b.recConstraints.includes(c.key))
.map((c) => c.label)
.join(", ")}`
: "",
"",
...ranked.map((r, i) => `${i + 1}. ${r.therapy.name}`),
]
.filter(Boolean)
.join("\n"),
"shortlist",
);

return (
<section data-screen-label="Recommend" style={s(`max-width:1180px;margin:0 auto;`)}>
<h1 style={s(`margin:0 0 6px;font-size:27px;font-weight:680;color:var(--text-heading);letter-spacing:-0.02em;`)}>
Expand Down Expand Up @@ -85,9 +107,15 @@ export function RecommendScreen() {
`display:flex;align-items:center;justify-content:space-between;margin-top:18px;gap:12px;flex-wrap:wrap;`,
)}
>
<button type="button" className="tc-btn" style={s(outlineControl)}>
<SaveIcon size={16} />
Save workflow
<button
type="button"
className="tc-btn"
onClick={copyShortlist}
disabled={!ranked.length}
style={s(outlineControl + (ranked.length ? "" : "opacity:0.5;cursor:not-allowed;"))}
>
{copied === "shortlist" ? <CheckIcon size={16} /> : <CopyIcon size={16} />}
{copied === "shortlist" ? "Copied" : "Copy shortlist"}
</button>
<button type="button" className="tc-btn" onClick={b.goSearch} style={s(commandControl)}>
<SearchIcon size={16} strokeWidth={1.9} />
Expand Down Expand Up @@ -266,7 +294,7 @@ function MatchCell({
eyebrow: string;
text: string;
tone?: "accent";
children?: React.ReactNode;
children?: ReactNode;
}) {
const bg = tone === "accent" ? "var(--clinical-accent-soft)" : "var(--surface)";
const head = tone === "accent" ? "var(--clinical-accent-hover)" : "var(--clinical-accent)";
Expand Down
6 changes: 1 addition & 5 deletions src/components/therapy-compass/screens/search-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useTcBindings } from "../bindings";
import { outlineControl, softControl } from "../controls";
import { SearchIcon, SearchXIcon, SlidersIcon, XIcon } from "../icons";
import { SearchIcon, SearchXIcon, XIcon } from "../icons";
import { s } from "../style-utils";
import { EmptyState, LoadingState } from "../ui";
import { ResultCard } from "../therapy-card";
Expand Down Expand Up @@ -39,10 +39,6 @@ export function SearchScreen() {
)}
/>
</label>
<button type="button" className="tc-btn" style={s(outlineControl + "height:52px;padding:0 18px;")}>
<SlidersIcon size={17} />
Filters
</button>
</div>

<div style={s(`display:flex;flex-wrap:wrap;gap:10px;margin-bottom:24px;`)}>
Expand Down
Loading
Loading