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
4 changes: 2 additions & 2 deletions docs/design-system/COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -976,7 +976,7 @@ This generated snapshot is a local source-derived inventory. It does not assert
| `AnswerFooter` | answer | yes | yes | inherited-global-root | yes | no | 1 |
| `AsyncButton` | controls | yes | yes | inherited-global-root | yes | no | 4 |
| `Breadcrumb` | layout | yes | yes | inherited-global-root | yes | no | 1 |
| `Button` | controls | yes | yes | inherited-global-root | yes | no | 16 |
| `Button` | controls | yes | yes | inherited-global-root | yes | no | 17 |
| `Checkbox` | controls | yes | yes | no | yes | no | 0 |
| `Chip` | controls | yes | yes | inherited-global-root | yes | no | 5 |
| `ChoiceChip` | controls | yes | yes | inherited-global-root | yes | no | 4 |
Expand Down Expand Up @@ -1012,7 +1012,7 @@ This generated snapshot is a local source-derived inventory. It does not assert
| `SearchField` | controls | yes | yes | no | yes | no | 0 |
| `SegmentedControl` | controls | yes | yes | inherited-global-root | yes | no | 8 |
| `Select` | controls | yes | yes | inherited-global-root | yes | no | 2 |
| `Sheet` | layout | yes | yes | inherited-global-root | yes | no | 30 |
| `Sheet` | layout | yes | yes | inherited-global-root | yes | no | 31 |
| `Skeleton` | feedback | yes | yes | inherited-global-root | yes | no | 6 |
| `SourceDesignationBadge` | source | yes | yes | inherited-global-root | yes | no | 4 |
| `SourceProvenance` | source | yes | yes | inherited-global-root | yes | no | 1 |
Expand Down
8 changes: 6 additions & 2 deletions docs/design-system/adoption-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@
"src/components/AccessibleTable.tsx",
"src/components/clinical-dashboard/signed-image.tsx",
"src/components/privacy-quiet-signal-page.tsx",
"src/components/therapy-compass/recommend-scenario-control.tsx",
"src/components/therapy-compass/record/compare-action.tsx",
"src/components/therapy-compass/screens/brief-screen.tsx",
"src/components/therapy-compass/screens/compare-screen.tsx",
Expand All @@ -299,6 +300,7 @@
"src/components/AccessibleTable.tsx",
"src/components/clinical-dashboard/signed-image.tsx",
"src/components/privacy-quiet-signal-page.tsx",
"src/components/therapy-compass/recommend-scenario-control.tsx",
"src/components/therapy-compass/record/compare-action.tsx",
"src/components/therapy-compass/screens/brief-screen.tsx",
"src/components/therapy-compass/screens/compare-screen.tsx",
Expand Down Expand Up @@ -423,13 +425,13 @@
"directImportFiles": [
"src/components/clinical-dashboard/patient-profile-panel.tsx",
"src/components/clinical-dashboard/result-filter-control.tsx",
"src/components/therapy-compass/screens/recommend-screen.tsx",
"src/components/therapy-compass/recommend-scenario-fields.tsx",
"src/components/therapy-compass/screens/sheets-screen.tsx"
],
"productImportFiles": [
"src/components/clinical-dashboard/patient-profile-panel.tsx",
"src/components/clinical-dashboard/result-filter-control.tsx",
"src/components/therapy-compass/screens/recommend-screen.tsx",
"src/components/therapy-compass/recommend-scenario-fields.tsx",
"src/components/therapy-compass/screens/sheets-screen.tsx"
],
"designSync": {
Expand Down Expand Up @@ -1661,6 +1663,7 @@
"src/components/forms/form-priority-facts-section.tsx",
"src/components/in-page-nav/in-page-nav-header.tsx",
"src/components/mode-nav/mode-nav.tsx",
"src/components/therapy-compass/recommend-scenario-control.tsx",
"src/components/therapy-compass/record/key-facts.tsx",
"src/components/therapy-compass/therapy-compare-tray.tsx",
"src/components/tools/tools-search-results-page.tsx",
Expand Down Expand Up @@ -1694,6 +1697,7 @@
"src/components/forms/form-priority-facts-section.tsx",
"src/components/in-page-nav/in-page-nav-header.tsx",
"src/components/mode-nav/mode-nav.tsx",
"src/components/therapy-compass/recommend-scenario-control.tsx",
"src/components/therapy-compass/record/key-facts.tsx",
"src/components/therapy-compass/therapy-compare-tray.tsx",
"src/components/tools/tools-search-results-page.tsx",
Expand Down
143 changes: 143 additions & 0 deletions src/components/therapy-compass/recommend-scenario-control.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
"use client";

import { useId, useRef, useState } from "react";
import { Pencil, Sparkles } from "lucide-react";

import { Button } from "@/components/ui/button";
import { InteractiveRow } from "@/components/ui/interactive-row";
import { Sheet } from "@/components/ui/sheet";
import { cn } from "@/components/ui-primitives";

import { RECOMMEND_CONSTRAINTS } from "./data/select";
import { RecommendScenarioFields, type RecommendScenarioFieldsProps } from "./recommend-scenario-fields";

type RecommendScenarioControlProps = RecommendScenarioFieldsProps & {
loading: boolean;
matchCount: number;
};

const PLACEHOLDER = "Describe the clinical situation…";

function truncatePreview(text: string, maxLength = 72) {
const trimmed = text.trim();
if (!trimmed) return "";
if (trimmed.length <= maxLength) return trimmed;
return `${trimmed.slice(0, maxLength - 1).trimEnd()}…`;
}

export function RecommendScenarioControl({
recQuery,
setRecQuery,
isConstraintActive,
isConstraintInferred,
toggleConstraint,
idPrefix,
loading,
matchCount,
}: RecommendScenarioControlProps) {
const [sheetOpen, setSheetOpen] = useState(false);
const panelId = useId();
const triggerRef = useRef<HTMLButtonElement>(null);

const activeConstraintCount = RECOMMEND_CONSTRAINTS.filter((constraint) => isConstraintActive(constraint.key)).length;
const preview = truncatePreview(recQuery);
const matchLabel = loading
? "Ranking clinical matches…"
: `${matchCount} ranked ${matchCount === 1 ? "match" : "matches"}`;

return (
<div className="mb-6 sm:hidden">
<InteractiveRow
ref={triggerRef}
type="button"
testId="therapy-recommend-scenario-trigger"
aria-expanded={sheetOpen}
aria-haspopup="dialog"
aria-controls={sheetOpen ? panelId : undefined}
onClick={() => setSheetOpen(true)}
className="w-full gap-3 px-3 py-2.5"
>
<span
aria-hidden="true"
className="grid size-9 shrink-0 place-items-center rounded-lg bg-[color:var(--surface-subtle)] text-[color:var(--clinical-accent)]"
>
{preview ? (
<Pencil aria-hidden="true" className="size-icon-md" />
) : (
<Sparkles aria-hidden="true" className="size-icon-md" />
)}
</span>
<span className="min-w-0 flex-1">
<span className="block text-xs font-semibold text-[color:var(--text-heading)]">Clinical scenario</span>
<span
className={cn(
"mt-0.5 block truncate text-sm leading-snug",
preview ? "text-[color:var(--text)]" : "text-[color:var(--text-muted)]",
)}
>
{preview || PLACEHOLDER}
</span>
</span>
{activeConstraintCount > 0 ? (
<span className="search-band-badge nums grid h-search-band-badge min-w-search-band-badge shrink-0 place-items-center rounded-full bg-[color:var(--search-band-badge-bg)] px-1 text-2xs font-bold text-[color:var(--clinical-accent)]">
{activeConstraintCount}
</span>
) : null}
<span className="sr-only">
{activeConstraintCount > 0
? `${activeConstraintCount} constraint${activeConstraintCount === 1 ? "" : "s"} active`
: "No constraints active"}
. Opens clinical scenario editor.
</span>
</InteractiveRow>

<Sheet
open={sheetOpen}
onClose={() => setSheetOpen(false)}
returnFocusRef={triggerRef}
title="Clinical scenario"
descriptionContent={
<p className="text-xs font-medium leading-5 text-[color:var(--text-muted)]">
Describe the presentation and constraints. Rankings update as you type.
</p>
}
headerClassName="items-start px-4 py-3 sm:px-5 sm:py-3.5"
titleClassName="text-base sm:text-lg"
closeButtonClassName={cn(
"grid h-tap w-tap shrink-0 place-items-center rounded-lg text-[color:var(--text-muted)] transition hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)] motion-reduce:transition-none",
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]",
)}
portal
placement="responsive-right"
id={panelId}
testId="therapy-recommend-scenario-panel"
footer={
<div className="flex flex-wrap items-center justify-between gap-2">
<span aria-live="polite" className="min-w-0 text-xs font-semibold text-[color:var(--text-muted)]">
{matchLabel}
</span>
<Button
variant="primary"
size="sm"
testId="therapy-recommend-scenario-done"
onClick={() => setSheetOpen(false)}
>
Done
</Button>
</div>
}
>
<form data-therapy-recommend-composer className="min-w-0" onSubmit={(event) => event.preventDefault()}>
<RecommendScenarioFields
recQuery={recQuery}
setRecQuery={setRecQuery}
isConstraintActive={isConstraintActive}
isConstraintInferred={isConstraintInferred}
toggleConstraint={toggleConstraint}
idPrefix={idPrefix}
/>
</form>
</Sheet>
</div>
);
}
80 changes: 80 additions & 0 deletions src/components/therapy-compass/recommend-scenario-fields.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"use client";

import { cn, eyebrowText, searchShell, searchShellInput } from "@/components/ui-primitives";
import { ChoiceChip } from "@/components/ui/chip";

import { RECOMMEND_CONSTRAINT_GROUPS, RECOMMEND_CONSTRAINTS } from "./data/select";

export type RecommendScenarioFieldsProps = {
recQuery: string;
setRecQuery: (q: string) => void;
isConstraintActive: (key: string) => boolean;
isConstraintInferred: (key: string) => boolean;
toggleConstraint: (key: string) => void;
/** Prefix for label/input ids — must be unique per mount (desktop vs sheet). */
idPrefix: string;
};

export function RecommendScenarioFields({
recQuery,
setRecQuery,
isConstraintActive,
isConstraintInferred,
toggleConstraint,
idPrefix,
}: RecommendScenarioFieldsProps) {
const situationId = `${idPrefix}-rec-q`;

return (
<>
<label htmlFor={situationId} className="block text-xs font-semibold text-[color:var(--text-heading)]">
Clinical situation
</label>
<div className={cn(searchShell, "mt-2 min-w-0 bg-[color:var(--surface)] px-2.5 py-2")}>
<textarea
id={situationId}
value={recQuery}
onChange={(event) => setRecQuery(event.target.value)}
placeholder="e.g. 28-year-old with panic attacks in outpatient clinic, 15 minutes available, no trauma work yet"
className={cn(
searchShellInput,
"min-h-20 resize-y py-1.5 text-base leading-normal text-[color:var(--text)] sm:text-sm",
)}
/>
</div>

<div className="mt-5 grid grid-cols-1 gap-4 sm:grid-cols-2">
{RECOMMEND_CONSTRAINT_GROUPS.map((group) => {
const chips = RECOMMEND_CONSTRAINTS.filter((constraint) => constraint.group === group.id);
return (
<fieldset key={group.id} className="min-w-0">
<legend className={cn(eyebrowText, "mb-2 px-0")}>{group.label}</legend>
<div className="flex flex-wrap gap-2">
{chips.map((constraint) => {
const active = isConstraintActive(constraint.key);
const inferred = isConstraintInferred(constraint.key);
return (
<ChoiceChip
key={constraint.key}
pressed={active}
onPressedChange={() => toggleConstraint(constraint.key)}
title={inferred ? `${constraint.label} — inferred from the situation` : undefined}
>
{constraint.label}
</ChoiceChip>
);
})}
</div>
</fieldset>
);
})}
</div>

{RECOMMEND_CONSTRAINTS.some((constraint) => isConstraintInferred(constraint.key)) ? (
<p className="mt-3 text-xs text-[color:var(--text-muted)]">
From the situation: accent chips were inferred from the typed presentation. Toggle any chip to override.
</p>
) : null}
</>
);
}
Loading
Loading