From abc35c5b44458e873bd4cf20f3c4b3937b7a6559 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 06:59:26 +0000 Subject: [PATCH 1/4] Wire Therapy Compass into the app as a routed mode + universal search Promote Therapy Compass from a single client-side screen-switching route to a first-class, URL-driven mode, and expose its 211-therapy library to cross-entity universal search. Routing - Convert the eight in-tool screens to real routes under /therapy-compass: /search, /recommend, /compare, /pathways, /review, /[slug] (detail), /[slug]/brief, /[slug]/sheet. Detail/brief/sheet are server components with generateStaticParams + generateMetadata + notFound over all 211 therapies. - Rewrite the TcProvider to derive the active screen from the pathname and the selected therapy from the [slug] param; nav + selection now update the URL (back/forward, deep links, new tabs). Screen UI is unchanged. - Mount the provider + nav once in a layout-level workspace so the dataset and interaction state are shared across every /therapy-compass/* route. Universal search - Add a "therapies" domain backed by a new server catalogue (src/lib/therapies.ts + a trimmed src/data/therapies-index.json projection generated by scripts/build-therapies-index.mjs). Wire the adapter, dispatch table, view-all href, mode<->domain mapping, and command-surface heading. Hits deep-link to /therapy-compass/[slug]; review status is surfaced inline. Visibility - Remove the devOnly gate so the mode appears in the production sidebar, MODE dropdown, and universal search. Unreviewed records keep their "Needs source review" badge at the point of use. Also retires the now-superseded /mockups/therapy-compass prototype, regenerates docs/site-map.md, and updates the app-modes / universal-search / provider tests. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01DG9gLBEwDZiqsZcVFCNmCh --- docs/site-map.md | 226 +- scripts/build-therapies-index.mjs | 39 + scripts/generate-site-map.ts | 12 + src/app/mockups/mockups-layout-client.tsx | 5 - src/app/mockups/therapy-compass/page.tsx | 13 - src/app/therapy-compass/[slug]/brief/page.tsx | 23 + src/app/therapy-compass/[slug]/page.tsx | 26 + src/app/therapy-compass/[slug]/sheet/page.tsx | 23 + src/app/therapy-compass/compare/page.tsx | 12 + src/app/therapy-compass/layout.tsx | 7 +- src/app/therapy-compass/page.tsx | 14 +- src/app/therapy-compass/pathways/page.tsx | 12 + src/app/therapy-compass/recommend/page.tsx | 12 + src/app/therapy-compass/review/page.tsx | 12 + src/app/therapy-compass/search/page.tsx | 12 + .../universal-search-command-surface.tsx | 2 + src/components/therapy-compass/bindings.tsx | 106 +- src/components/therapy-compass/index.ts | 2 +- src/components/therapy-compass/styles.tsx | 6 +- .../therapy-compass/therapy-compass-page.tsx | 88 - src/components/therapy-compass/workspace.tsx | 55 + src/data/therapies-index.json | 4535 +++++++++++++++++ src/lib/app-modes.ts | 9 +- src/lib/therapies.ts | 118 + src/lib/universal-search-domains.ts | 6 + src/lib/universal-search-mode-context.ts | 7 +- src/lib/universal-search.ts | 21 + tests/app-modes.test.ts | 16 +- tests/therapy-compass-mode-wiring.test.ts | 11 +- ...therapy-compass-provider-seed.dom.test.tsx | 48 +- tests/universal-search.test.ts | 14 +- 31 files changed, 5306 insertions(+), 186 deletions(-) create mode 100644 scripts/build-therapies-index.mjs delete mode 100644 src/app/mockups/therapy-compass/page.tsx create mode 100644 src/app/therapy-compass/[slug]/brief/page.tsx create mode 100644 src/app/therapy-compass/[slug]/page.tsx create mode 100644 src/app/therapy-compass/[slug]/sheet/page.tsx create mode 100644 src/app/therapy-compass/compare/page.tsx create mode 100644 src/app/therapy-compass/pathways/page.tsx create mode 100644 src/app/therapy-compass/recommend/page.tsx create mode 100644 src/app/therapy-compass/review/page.tsx create mode 100644 src/app/therapy-compass/search/page.tsx delete mode 100644 src/components/therapy-compass/therapy-compass-page.tsx create mode 100644 src/components/therapy-compass/workspace.tsx create mode 100644 src/data/therapies-index.json create mode 100644 src/lib/therapies.ts diff --git a/docs/site-map.md b/docs/site-map.md index 8497dc327..5fd153cbb 100644 --- a/docs/site-map.md +++ b/docs/site-map.md @@ -30,7 +30,15 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check` - `/specifiers/builder` - Structured diagnostic wording builder. Source: `src/app/specifiers/builder/page.tsx`. - `/specifiers/compare` - Side-by-side psychiatric specifier comparison. Source: `src/app/specifiers/compare/page.tsx`. - `/specifiers/map` - Psychiatric specifier family map. Source: `src/app/specifiers/map/page.tsx`. -- `/therapy-compass` - Route discovered from app directory Source: `src/app/therapy-compass/page.tsx`. +- `/therapy-compass` - Therapy Compass home (source-grounded therapy decision support). Source: `src/app/therapy-compass/page.tsx`. +- `/therapy-compass/[slug]` - Therapy record detail. Source: `src/app/therapy-compass/[slug]/page.tsx`. +- `/therapy-compass/[slug]/brief` - Therapy brief-intervention view. Source: `src/app/therapy-compass/[slug]/brief/page.tsx`. +- `/therapy-compass/[slug]/sheet` - Therapy patient-sheet builder. Source: `src/app/therapy-compass/[slug]/sheet/page.tsx`. +- `/therapy-compass/compare` - Side-by-side therapy comparison. Source: `src/app/therapy-compass/compare/page.tsx`. +- `/therapy-compass/pathways` - Problem-based clinical therapy pathways. Source: `src/app/therapy-compass/pathways/page.tsx`. +- `/therapy-compass/recommend` - Recommend a therapy from a clinical question and constraints. Source: `src/app/therapy-compass/recommend/page.tsx`. +- `/therapy-compass/review` - Therapy records awaiting qualified-clinician source review. Source: `src/app/therapy-compass/review/page.tsx`. +- `/therapy-compass/search` - Therapy library search surface. Source: `src/app/therapy-compass/search/page.tsx`. - `/tools` - Route discovered from app directory Source: `src/app/tools/page.tsx`. ## Mode/query routes @@ -750,6 +758,221 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check` - `splitting` - `worry` +### Therapy slugs + +- `/therapy-compass/[slug]` - Dynamic route family. +- `acceptance-and-commitment-therapy-act` +- `adherence-focused-brief-work-adherence-therapy` +- `adolescent-cbt` +- `adolescent-focused-psychotherapy-for-anorexia-nervosa` +- `advocacy-informed-therapeutic-work` +- `app-supported-therapy` +- `applied-relaxation-relaxation-based-therapy` +- `arts-therapies-for-psychosis` +- `behaviour-therapy` +- `behavioural-activation` +- `behavioural-activation-ba` +- `behavioural-couples-therapy-bct` +- `behavioural-couples-therapy-for-substance-use-disorders-bct-sud` +- `behavioural-parent-training` +- `brief-low-intensity-cbt` +- `brief-supportive-psychotherapy` +- `bright-light-therapy` +- `carer-focused-education-and-support-carer-psychoeducation-in-psychosis` +- `carer-groups` +- `carer-interventions` +- `carer-supported-meal-based-interventions` +- `cbt-informed-psychological-intervention-for-self-harm` +- `child-cbt` +- `circadian-rhythm-based-interventions` +- `client-directed-feedback-informed-brief-work` +- `cognitive-analytic-therapy-cat` +- `cognitive-behavioural-therapy-cbt` +- `cognitive-behavioural-therapy-for-bipolar-depression-bipolar-relapse-prevention` +- `cognitive-behavioural-therapy-for-insomnia` +- `cognitive-behavioural-therapy-for-insomnia-cbt-i` +- `cognitive-behavioural-therapy-for-psychosis-cbtp` +- `cognitive-processing-therapy-cpt` +- `cognitive-remediation-therapy-crt` +- `cognitive-stimulation-therapy` +- `cognitive-therapy` +- `cognitive-therapy-for-ptsd-ct-ptsd` +- `community-based-psychosocial-intervention` +- `community-reinforcement-and-family-training-craft` +- `community-reinforcement-approach` +- `compassion-focused-therapy` +- `constructivist-therapies` +- `contingency-management-cm` +- `coping-skills-interventions` +- `couples-therapy` +- `craving-management-interventions` +- `crisis-intervention-crisis-oriented-brief-therapy` +- `cue-exposure-therapy-cet-for-substance-use-disorders` +- `dbt-informed-adjunctive-emotion-regulation-work-for-eating-disorders` +- `dbt-skills-groups` +- `developmental-social-skills-interventions` +- `developmentally-adapted-dbt` +- `developmentally-adapted-erp` +- `dialectical-behaviour-therapy-dbt` +- `dignity-therapy` +- `dyadic-therapy` +- `dynamic-interpersonal-therapy` +- `eating-disorder-focused-cognitive-behavioural-therapy-cbt-ed-cbt-e` +- `eating-disorder-focused-focal-psychodynamic-therapy-fpt-for-adult-anorexia-nervosa` +- `ect` +- `emdr` +- `emotion-focused-therapy` +- `empowerment-focused-practice` +- `existential-psychotherapy` +- `experiential-therapies` +- `exposure-and-response-prevention-erp` +- `exposure-and-response-prevention-for-tics-erp-for-tics` +- `exposure-based-cbt-exposure-therapy` +- `exposure-therapy` +- `eye-movement-desensitisation-and-reprocessing-emdr` +- `family-based-treatment-for-adolescent-anorexia-nervosa-ft-an` +- `family-based-treatment-for-bulimia-nervosa-ft-bn` +- `family-focused-psychoeducation-for-bipolar-disorder` +- `family-focused-therapy-for-bipolar-disorder-fft-bd` +- `family-intervention-for-psychosis` +- `family-problem-solving-interventions` +- `family-psychoeducation-for-psychosis` +- `family-therapy` +- `functional-family-therapy-fft` +- `gestalt-therapy` +- `good-psychiatric-management` +- `graded-exposure` +- `group-exposure-programmes` +- `group-psychoeducation` +- `group-psychoeducation-for-bipolar-disorder` +- `group-relapse-prevention-programmes` +- `group-social-skills-training` +- `guided-self-help` +- `guided-self-help-for-binge-eating-disorder` +- `guided-self-help-for-bulimia-nervosa` +- `habit-reversal-training-cbit` +- `habit-reversal-training-for-trichotillomania-and-excoriation-disorder` +- `harm-reduction-counselling` +- `health-anxiety-focused-cbt` +- `humanistic-psychotherapy` +- `illness-management-and-recovery-style-interventions-imr-style-interventions` +- `imagery-rehearsal-therapy-irt-for-nightmare-disorder` +- `imagery-rescripting` +- `inference-based-cbt-for-ocd` +- `integrated-cbt-for-ptsd-and-substance-use-disorders` +- `integrated-dual-diagnosis-psychotherapy` +- `intensive-psychodynamic-psychotherapy` +- `internet-delivered-cbt` +- `interoceptive-exposure` +- `interpersonal-and-social-rhythm-therapy-ipsrt` +- `interpersonal-psychotherapy-ipt` +- `interpersonal-therapy` +- `life-review-therapy-reminiscence-therapy` +- `low-intensity-relapse-prevention-programmes` +- `matrix-model` +- `maudsley-anorexia-nervosa-treatment-for-adults-mantra` +- `meaning-centred-psychotherapy` +- `mentalisation-based-therapy-mbt` +- `metacognitive-therapy-mct` +- `mindfulness-based-cognitive-therapy` +- `mindfulness-based-cognitive-therapy-mbct` +- `mindfulness-based-relapse-prevention-mbrp` +- `mindfulness-based-stress-reduction` +- `mindfulness-based-therapies-not-primarily-cbt-anchored` +- `mindfulness-based-therapy-for-insomnia-mbti` +- `mother-infant-therapy-mother-infant-psychotherapy` +- `motivational-enhancement-therapy-met` +- `motivational-interviewing` +- `motivational-interviewing-mi-for-substance-use-disorders` +- `multi-family-interventions` +- `multidimensional-family-therapy` +- `multisystemic-therapy-mst` +- `narrative-exposure-therapy-net` +- `narrative-informed-community-work` +- `narrative-therapy` +- `neurodevelopmentally-adapted-psychosocial-interventions` +- `panic-focused-cbt` +- `parent-and-child-training` +- `parent-child-interaction-therapy-pcit` +- `parent-child-relational-therapy` +- `parent-management-training-pmt` +- `parent-training` +- `peer-led-groups` +- `peer-recovery-interventions` +- `peer-support` +- `person-centred-rogerian-therapy` +- `phase-oriented-trauma-therapy` +- `play-therapy-play-based-therapies` +- `problem-management-plus-pm` +- `problem-solving-therapy` +- `problem-solving-therapy-pst` +- `process-groups` +- `prolonged-exposure-pe` +- `prolonged-exposure-therapy` +- `psychoanalysis` +- `psychodynamic-psychotherapy` +- `psychoeducation` +- `psychoeducation-for-psychosis` +- `psychoeducational-recovery-groups` +- `psychosis-specific-self-management-programmes` +- `psychosocial-case-planning` +- `recovery-groups` +- `recovery-oriented-case-management` +- `recovery-oriented-psychosocial-interventions` +- `recovery-planning-for-addiction` +- `relapse-prevention-psychotherapy` +- `relapse-prevention-therapy-for-substance-use-disorders` +- `relational-cultural-therapy` +- `relational-psychodynamic-therapies` +- `relationship-focused-therapy` +- `resource-linkage-coordination-as-therapeutic-intervention` +- `rtms` +- `schema-therapy` +- `school-refusal-cbt` +- `seeking-safety` +- `self-management-interventions` +- `short-term-psychodynamic-psychotherapy-for-depression-stpp` +- `short-term-psychodynamic-psychotherapy-stpp` +- `skills-training-groups` +- `sleep-compression-therapy-for-insomnia` +- `social-anxiety-focused-cbt` +- `social-cognition-training` +- `social-communication-parent-mediated-autism-interventions` +- `social-role-restoration-work` +- `social-skills-training-sst` +- `solution-focused-brief-therapy` +- `specialist-supportive-clinical-management-sscm` +- `stabilisation-focused-trauma-preparation` +- `stair-skills-training-in-affective-and-interpersonal-regulation` +- `strengths-based-case-management` +- `strengths-based-practice` +- `structured-clinical-management` +- `support-groups` +- `supported-digital-cbt` +- `supported-digital-trauma-focused-cbt` +- `supported-digital-trauma-focused-cbt-self-help-and-digital-therapies` +- `supported-education` +- `supported-employment-individual-placement-and-support-ips` +- `supportive-expressive-psychodynamic-counselling-approaches-for-depression` +- `supportive-psychotherapy` +- `systemic-family-therapy` +- `task-centred-practice` +- `telehealth-delivered-psychotherapy` +- `telephone-delivered-cbt` +- `transference-focused-psychotherapy-tfp` +- `trauma-focused-cognitive-behavioural-therapy-tf-cbt` +- `trauma-informed-skills-work` +- `twelve-step-facilitation-tsf` +- `values-based-therapies` +- `vocational-rehabilitation` +- `waitlist-bridging-brief-interventions` +- `wake-therapy-sleep-deprivation-chronotherapy` +- `workbook-based-therapy` +- `worry-focused-cbt` +- `written-exposure-therapy` +- `youth-ipt-ipt-a` +- `youth-trauma-focused-cbt` + ### Medication slugs - `/medications/[slug]` - Dynamic route family. @@ -789,7 +1012,6 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check` - `/mockups/settings-search-clinical` - Route discovered from app directory Source: `src/app/mockups/settings-search-clinical/page.tsx`. - `/mockups/settings-search-general` - Route discovered from app directory Source: `src/app/mockups/settings-search-general/page.tsx`. - `/mockups/settings-search-privacy` - Route discovered from app directory Source: `src/app/mockups/settings-search-privacy/page.tsx`. -- `/mockups/therapy-compass` - Route discovered from app directory Source: `src/app/mockups/therapy-compass/page.tsx`. - `/mockups/tools-action-workbench` - Route discovered from app directory Source: `src/app/mockups/tools-action-workbench/page.tsx`. - `/mockups/tools-clinical-lanes` - Route discovered from app directory Source: `src/app/mockups/tools-clinical-lanes/page.tsx`. - `/mockups/tools-command-center` - Route discovered from app directory Source: `src/app/mockups/tools-command-center/page.tsx`. diff --git a/scripts/build-therapies-index.mjs b/scripts/build-therapies-index.mjs new file mode 100644 index 000000000..acfd9913e --- /dev/null +++ b/scripts/build-therapies-index.mjs @@ -0,0 +1,39 @@ +// Generates src/data/therapies-index.json — a trimmed, server-importable projection +// of public/therapy-compass-data/therapies.json (~2.5 MB). The full dataset is fetched +// client-side by the interactive Therapy Compass screens; the server only needs a small +// rankable/metadata projection for routing (generateStaticParams / notFound / metadata) +// and the universal-search "therapies" domain. Re-run after editing the source dataset, +// then run Prettier (which owns the committed file's exact formatting): +// +// node scripts/build-therapies-index.mjs && npm run format +// +import { readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const source = join(root, "public", "therapy-compass-data", "therapies.json"); +const target = join(root, "src", "data", "therapies-index.json"); + +const therapies = JSON.parse(readFileSync(source, "utf8")); + +const projected = therapies + .map((t) => ({ + slug: t.slug, + name: t.name, + category: t.category ?? null, + modality: t.modality ?? null, + clinicalSummary: t.clinicalSummary ?? null, + bestUsedFor: t.bestUsedFor ?? null, + targetSymptoms: t.targetSymptoms ?? null, + indications: t.indications ?? null, + reviewStatus: t.reviewStatus ?? "needs_review", + patientSheetAvailable: Boolean(t.patientSheetAvailable), + briefInterventionAvailable: Boolean(t.briefInterventionAvailable), + tags: Array.isArray(t.tags) ? t.tags : [], + aliases: Array.isArray(t.aliases) ? t.aliases : [], + })) + .sort((a, b) => a.name.localeCompare(b.name)); + +writeFileSync(target, `${JSON.stringify(projected, null, 2)}\n`); +console.log(`Wrote ${projected.length} therapy records to ${target}`); diff --git a/scripts/generate-site-map.ts b/scripts/generate-site-map.ts index 921cf2b90..64a471a60 100644 --- a/scripts/generate-site-map.ts +++ b/scripts/generate-site-map.ts @@ -11,6 +11,7 @@ import { formulationMechanisms } from "@/lib/formulation"; import { formRecords } from "@/lib/forms"; import { serviceRecords } from "@/lib/services"; import { specifierRecords } from "@/lib/specifiers"; +import { therapySlugs } from "@/lib/therapies"; const appDir = path.join(process.cwd(), "src", "app"); const siteMapPath = path.join(process.cwd(), "docs", "site-map.md"); @@ -69,6 +70,15 @@ const routeDescriptions: Record = { "/specifiers/builder": "Structured diagnostic wording builder.", "/specifiers/compare": "Side-by-side psychiatric specifier comparison.", "/specifiers/map": "Psychiatric specifier family map.", + "/therapy-compass": "Therapy Compass home (source-grounded therapy decision support).", + "/therapy-compass/search": "Therapy library search surface.", + "/therapy-compass/recommend": "Recommend a therapy from a clinical question and constraints.", + "/therapy-compass/compare": "Side-by-side therapy comparison.", + "/therapy-compass/pathways": "Problem-based clinical therapy pathways.", + "/therapy-compass/review": "Therapy records awaiting qualified-clinician source review.", + "/therapy-compass/[slug]": "Therapy record detail.", + "/therapy-compass/[slug]/brief": "Therapy brief-intervention view.", + "/therapy-compass/[slug]/sheet": "Therapy patient-sheet builder.", }; const apiDescriptions: Record = { @@ -426,6 +436,8 @@ function renderSiteMapRaw(data = collectSiteMapData()) { formulationMechanisms.map((mechanism) => mechanism.id), ), "", + ...renderSlugInventory("Therapy slugs", "/therapy-compass/[slug]", therapySlugs()), + "", ...renderSlugInventory("Medication slugs", "/medications/[slug]", medicationSlugs), ]), ...section("Document viewer route", [ diff --git a/src/app/mockups/mockups-layout-client.tsx b/src/app/mockups/mockups-layout-client.tsx index 583d30e68..ed508daad 100644 --- a/src/app/mockups/mockups-layout-client.tsx +++ b/src/app/mockups/mockups-layout-client.tsx @@ -13,10 +13,6 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) { const isSourceOverlayRedesignMockup = pathname === "/mockups/document-search/source-overlays"; const isStandaloneDocumentFlow = pathname === "/mockups/document-search"; const isUniversalSearchRedesignMockup = pathname === "/mockups/universal-search-redesign"; - // Therapy Compass keeps the universal header + rail but provides its own - // primary search surface, so the shared bottom composer is hidden (as with - // the tools/favourites mockups). - const isTherapyCompassMockup = pathname.startsWith("/mockups/therapy-compass"); // The calculators search page owns its own search input (top on desktop, docked // at the bottom on phones), so the shared universal composer is suppressed here // to avoid a second, floating search bar. @@ -38,7 +34,6 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) { !isFavouritesPageMockup && !isStandaloneDocumentFlow && !isUniversalSearchRedesignMockup && - !isTherapyCompassMockup && !isCalculatorsSearchPageMockup } chromeVisible={!isSourceOverlayRedesignMockup} diff --git a/src/app/mockups/therapy-compass/page.tsx b/src/app/mockups/therapy-compass/page.tsx deleted file mode 100644 index 95bf7d331..000000000 --- a/src/app/mockups/therapy-compass/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { Metadata } from "next"; - -import { TherapyCompassPage } from "@/components/therapy-compass"; - -export const metadata: Metadata = { - title: "Therapy Compass Mockup - Clinical KB", - description: - "Source-grounded therapy decision-support mockup: search, compare, recommend, pathways, brief interventions and patient sheets.", -}; - -export default function TherapyCompassMockupRoute() { - return ; -} diff --git a/src/app/therapy-compass/[slug]/brief/page.tsx b/src/app/therapy-compass/[slug]/brief/page.tsx new file mode 100644 index 000000000..b950bc262 --- /dev/null +++ b/src/app/therapy-compass/[slug]/brief/page.tsx @@ -0,0 +1,23 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; + +import { BriefScreen } from "@/components/therapy-compass/screens/brief-screen"; +import { findTherapyRecord, therapySlugs } from "@/lib/therapies"; + +export function generateStaticParams() { + return therapySlugs().map((slug) => ({ slug })); +} + +export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise { + const { slug } = await params; + const record = findTherapyRecord(slug); + return { + title: record ? `${record.name} · Brief intervention - Therapy Compass` : "Therapy not found - Therapy Compass", + }; +} + +export default async function TherapyCompassBriefRoute({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + if (!findTherapyRecord(slug)) notFound(); + return ; +} diff --git a/src/app/therapy-compass/[slug]/page.tsx b/src/app/therapy-compass/[slug]/page.tsx new file mode 100644 index 000000000..65a8e12e6 --- /dev/null +++ b/src/app/therapy-compass/[slug]/page.tsx @@ -0,0 +1,26 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; + +import { DetailScreen } from "@/components/therapy-compass/screens/detail-screen"; +import { findTherapyRecord, therapyNeedsReview, therapySlugs } from "@/lib/therapies"; + +export function generateStaticParams() { + return therapySlugs().map((slug) => ({ slug })); +} + +export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise { + const { slug } = await params; + const record = findTherapyRecord(slug); + if (!record) return { title: "Therapy not found - Therapy Compass" }; + const summary = record.clinicalSummary ?? record.bestUsedFor ?? "Source-grounded therapy record."; + return { + title: `${record.name} - Therapy Compass`, + description: therapyNeedsReview(record) ? `${summary} (Awaiting source review.)` : summary, + }; +} + +export default async function TherapyCompassDetailRoute({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + if (!findTherapyRecord(slug)) notFound(); + return ; +} diff --git a/src/app/therapy-compass/[slug]/sheet/page.tsx b/src/app/therapy-compass/[slug]/sheet/page.tsx new file mode 100644 index 000000000..53b956aa7 --- /dev/null +++ b/src/app/therapy-compass/[slug]/sheet/page.tsx @@ -0,0 +1,23 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; + +import { SheetsScreen } from "@/components/therapy-compass/screens/sheets-screen"; +import { findTherapyRecord, therapySlugs } from "@/lib/therapies"; + +export function generateStaticParams() { + return therapySlugs().map((slug) => ({ slug })); +} + +export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise { + const { slug } = await params; + const record = findTherapyRecord(slug); + return { + title: record ? `${record.name} · Patient sheet - Therapy Compass` : "Therapy not found - Therapy Compass", + }; +} + +export default async function TherapyCompassSheetRoute({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + if (!findTherapyRecord(slug)) notFound(); + return ; +} diff --git a/src/app/therapy-compass/compare/page.tsx b/src/app/therapy-compass/compare/page.tsx new file mode 100644 index 000000000..b92c9debf --- /dev/null +++ b/src/app/therapy-compass/compare/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from "next"; + +import { CompareScreen } from "@/components/therapy-compass/screens/compare-screen"; + +export const metadata: Metadata = { + title: "Compare therapies - Therapy Compass", + description: "Compare therapies side by side on indications, delivery, evidence, and review status.", +}; + +export default function TherapyCompassCompareRoute() { + return ; +} diff --git a/src/app/therapy-compass/layout.tsx b/src/app/therapy-compass/layout.tsx index 8e0c5ca66..1b32af7bf 100644 --- a/src/app/therapy-compass/layout.tsx +++ b/src/app/therapy-compass/layout.tsx @@ -1,14 +1,17 @@ import type { ReactNode } from "react"; import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search-shell"; +import { TherapyCompassWorkspace } from "@/components/therapy-compass"; // Therapy Compass keeps the universal header + rail but provides its own primary // search surface (the in-tool therapy search), so the shared search composer is -// suppressed here — mirroring how the mockup mounted it. +// suppressed here. The workspace is mounted at the layout level so the therapy +// dataset and interaction state are shared across every /therapy-compass/* route, +// while each route renders its own screen into the workspace's main content. export default function TherapyCompassLayout({ children }: { children: ReactNode }) { return ( - {children} + {children} ); } diff --git a/src/app/therapy-compass/page.tsx b/src/app/therapy-compass/page.tsx index 45e31b924..7069f9664 100644 --- a/src/app/therapy-compass/page.tsx +++ b/src/app/therapy-compass/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; +import { redirect } from "next/navigation"; -import { TherapyCompassPage } from "@/components/therapy-compass"; +import { HomeScreen } from "@/components/therapy-compass/screens/home-screen"; export const metadata: Metadata = { title: "Therapy Compass - Clinical KB", @@ -8,7 +9,7 @@ export const metadata: Metadata = { "Source-grounded therapy decision support: search, compare, recommend, pathways, brief interventions and patient sheets.", }; -type TherapyCompassRouteProps = { +type TherapyCompassHomeProps = { searchParams?: Promise<{ q?: string | string[]; run?: string | string[] }>; }; @@ -16,9 +17,12 @@ function firstParam(value: string | string[] | undefined) { return Array.isArray(value) ? value[0] : value; } -export default async function TherapyCompassRoute({ searchParams }: TherapyCompassRouteProps) { +export default async function TherapyCompassHome({ searchParams }: TherapyCompassHomeProps) { const params = searchParams ? await searchParams : {}; const query = firstParam(params.q)?.trim() ?? ""; - const autoRunSearch = firstParam(params.run) === "1" && query.length > 0; - return ; + const autoRun = firstParam(params.run) === "1" && query.length > 0; + // A run-enabled mode search resolves to the home href (appModeHomeHref); send it + // on to the dedicated, deep-linkable search route so the workspace opens on results. + if (autoRun) redirect(`/therapy-compass/search?q=${encodeURIComponent(query)}&run=1`); + return ; } diff --git a/src/app/therapy-compass/pathways/page.tsx b/src/app/therapy-compass/pathways/page.tsx new file mode 100644 index 000000000..099e39409 --- /dev/null +++ b/src/app/therapy-compass/pathways/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from "next"; + +import { PathwaysScreen } from "@/components/therapy-compass/screens/pathways-screen"; + +export const metadata: Metadata = { + title: "Clinical pathways - Therapy Compass", + description: "Problem-based, step-by-step therapy pathways linking source-grounded therapy records.", +}; + +export default function TherapyCompassPathwaysRoute() { + return ; +} diff --git a/src/app/therapy-compass/recommend/page.tsx b/src/app/therapy-compass/recommend/page.tsx new file mode 100644 index 000000000..3c09b0860 --- /dev/null +++ b/src/app/therapy-compass/recommend/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from "next"; + +import { RecommendScreen } from "@/components/therapy-compass/screens/recommend-screen"; + +export const metadata: Metadata = { + title: "Recommend a therapy - Therapy Compass", + description: "Match a clinical question and constraints to ranked, source-grounded therapy options.", +}; + +export default function TherapyCompassRecommendRoute() { + return ; +} diff --git a/src/app/therapy-compass/review/page.tsx b/src/app/therapy-compass/review/page.tsx new file mode 100644 index 000000000..ce768b6b4 --- /dev/null +++ b/src/app/therapy-compass/review/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from "next"; + +import { OtherScreen } from "@/components/therapy-compass/screens/other-screen"; + +export const metadata: Metadata = { + title: "Review queue - Therapy Compass", + description: "Therapy records still awaiting qualified-clinician source review.", +}; + +export default function TherapyCompassReviewRoute() { + return ; +} diff --git a/src/app/therapy-compass/search/page.tsx b/src/app/therapy-compass/search/page.tsx new file mode 100644 index 000000000..b92b6efea --- /dev/null +++ b/src/app/therapy-compass/search/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from "next"; + +import { SearchScreen } from "@/components/therapy-compass/screens/search-screen"; + +export const metadata: Metadata = { + title: "Search therapies - Therapy Compass", + description: "Search the source-grounded therapy library by problem, symptom, skill, or population.", +}; + +export default function TherapyCompassSearchRoute() { + return ; +} diff --git a/src/components/clinical-dashboard/universal-search-command-surface.tsx b/src/components/clinical-dashboard/universal-search-command-surface.tsx index 3389983d1..19b48042c 100644 --- a/src/components/clinical-dashboard/universal-search-command-surface.tsx +++ b/src/components/clinical-dashboard/universal-search-command-surface.tsx @@ -49,6 +49,7 @@ const domainsByTargetMode: Partial> = dsm: ["dsm"], specifiers: ["specifiers"], formulation: ["formulation"], + "therapy-compass": ["therapies"], tools: ["tools"], }; @@ -62,6 +63,7 @@ const domainHeadings: Record = { dsm: "DSM-5 Diagnoses", specifiers: "Specifiers", formulation: "Formulation", + therapies: "Therapies", tools: "Tools", }; diff --git a/src/components/therapy-compass/bindings.tsx b/src/components/therapy-compass/bindings.tsx index 6e5945321..dabd10d01 100644 --- a/src/components/therapy-compass/bindings.tsx +++ b/src/components/therapy-compass/bindings.tsx @@ -1,6 +1,7 @@ "use client"; import { createContext, useContext, useMemo, useState, type CSSProperties, type ReactNode } from "react"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { s } from "./style-utils"; import { useTherapyData } from "./data/use-therapy-data"; @@ -18,6 +19,31 @@ import type { Pathway, ReferenceData, Therapy } from "./data/types"; const KNOWN_SCREENS = ["search", "detail", "compare", "recommend", "pathways", "brief", "home", "sheets"] as const; export const MAX_COMPARE = 4; +// Therapy Compass now owns a route family under this base. Screen state is derived +// from the pathname (not React state) so every destination is a real URL: Home is the +// base, the fixed workspaces are static children, and a therapy detail / brief / sheet +// is `${BASE}/[/brief|/sheet]`. Reserved segments never collide with therapy +// slugs (verified in scripts) so a first segment that is not reserved is a slug. +const BASE = "/therapy-compass"; +const RESERVED_SEGMENTS = new Set(["search", "recommend", "compare", "pathways", "review"]); + +function screenHref(screen: string): string { + return screen === "home" ? BASE : `${BASE}/${screen}`; +} + +/** Resolve the active screen + therapy slug from the current pathname. */ +function resolveRoute(pathname: string): { screen: string; slug: string | null } { + const rest = pathname.startsWith(BASE) ? pathname.slice(BASE.length).replace(/^\/+/, "") : ""; + const segments = rest ? rest.split("/") : []; + if (segments.length === 0) return { screen: "home", slug: null }; + const [first, second] = segments; + if (RESERVED_SEGMENTS.has(first)) return { screen: first, slug: null }; + // A non-reserved first segment is a therapy slug; the optional second segment + // selects the brief-intervention or patient-sheet sub-view. + const screen = second === "brief" ? "brief" : second === "sheet" ? "sheets" : "detail"; + return { screen, slug: first }; +} + type SheetSectionKey = "about" | "steps" | "practice" | "coping" | "contacts"; export type TcBindings = { @@ -198,35 +224,42 @@ function chipStyle(on: boolean): CSSProperties { ); } -export function TcProvider({ - children, - initialQuery = "", - autoRunSearch = false, -}: { - children: ReactNode; - initialQuery?: string; - autoRunSearch?: boolean; -}) { +export function TcProvider({ children }: { children: ReactNode }) { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const { data, loading, error } = useTherapyData(); const therapies = useMemo(() => data?.therapies ?? [], [data]); const pathways = useMemo(() => data?.pathways ?? [], [data]); - // Honor a run-enabled deep link (/therapy-compass?q=…&run=1): open on Search - // with the query seeded, so a query submitted from the universal composer or a - // recent-search pick runs in-tool instead of landing on Home. A fresh deep link - // while already mounted re-seeds via the provider key in TherapyCompassPage, - // which remounts this provider when the run-query changes. - const seededQuery = autoRunSearch ? initialQuery.trim() : ""; - const [screen, setScreen] = useState(seededQuery ? "search" : "home"); + // The active screen and therapy are derived from the URL: each workspace route + // renders the matching screen, and this keeps nav highlighting + the selected + // therapy in sync with the address bar (back/forward, deep links, new tabs). + const { screen, slug: routeSlug } = resolveRoute(pathname); + + // Non-navigational interaction state lives in the provider, which the layout + // keeps mounted across the tool's routes so selections persist between screens. + const qParam = (searchParams.get("q") ?? "").trim(); const [selectedSlug, setSelectedSlug] = useState(null); const [compareSlugs, setCompareSlugs] = useState([]); - const [search, setSearch] = useState( - seededQuery ? { ...EMPTY_SEARCH, query: seededQuery } : EMPTY_SEARCH, + const [search, setSearch] = useState(() => + qParam ? { ...EMPTY_SEARCH, query: qParam } : EMPTY_SEARCH, ); const [recQuery, setRecQuery] = useState("What therapy for anxiety in outpatient care?"); const [recConstraints, setRecConstraints] = useState(["outpatient"]); const [selectedPathwaySlug, setSelectedPathwaySlug] = useState(null); + // Seed the search query from a `?q=` deep link (universal-search "view all" or a + // recent-search pick) and re-seed when the deep link changes, using the render-phase + // "adjust state when a value changes" pattern so live typing between deep links is + // preserved without a setState-in-effect cascade. + const [seededQuery, setSeededQuery] = useState(qParam); + if (qParam !== seededQuery) { + setSeededQuery(qParam); + if (qParam) setSearch((prev) => ({ ...prev, query: qParam })); + } + const [cmpTab, setCmpTab] = useState("differences"); const [density, setDensity] = useState("comfortable"); const [briefTab, setBriefTab] = useState("5min"); @@ -244,7 +277,9 @@ export function TcProvider({ const unreviewedTherapies = useMemo(() => therapies.filter((t) => t.reviewStatus !== "reviewed"), [therapies]); // Default selections once data arrives so detail/brief/sheet/pathways are never empty. - const effectiveSelectedSlug = selectedSlug ?? therapies[0]?.slug ?? null; + // A slug in the URL always wins; otherwise fall back to any imperatively-set slug, + // then the first therapy so the no-arg brief/sheet nav buttons have a target. + const effectiveSelectedSlug = routeSlug ?? selectedSlug ?? therapies[0]?.slug ?? null; const selectedTherapy = effectiveSelectedSlug ? (bySlug.get(effectiveSelectedSlug) ?? null) : null; const effectivePathwaySlug = selectedPathwaySlug ?? pathways[0]?.slug ?? null; const selectedPathway = effectivePathwaySlug ? (pathways.find((p) => p.slug === effectivePathwaySlug) ?? null) : null; @@ -264,9 +299,11 @@ export function TcProvider({ ); const value = useMemo(() => { - const go = (next: string) => setScreen(next); + const go = (next: string) => router.push(screenHref(next)); const toggleSection = (key: SheetSectionKey) => setSheetSections((prev) => ({ ...prev, [key]: !prev[key] })); const patchSearch = (patch: Partial) => setSearch((prev) => ({ ...prev, ...patch })); + const openSlug = (slug: string, sub?: "brief" | "sheet") => + router.push(sub ? `${BASE}/${slug}/${sub}` : `${BASE}/${slug}`); return { loading, @@ -284,9 +321,11 @@ export function TcProvider({ goRecommend: () => go("recommend"), goCompare: () => go("compare"), goPathways: () => go("pathways"), - goBrief: () => go("brief"), - goSheets: () => go("sheets"), - goDetail: () => go("detail"), + // Brief / sheet / detail are therapy sub-routes, so the no-arg nav buttons + // open them for the currently-selected therapy (defaulting to the first). + goBrief: () => (effectiveSelectedSlug ? openSlug(effectiveSelectedSlug, "brief") : go("home")), + goSheets: () => (effectiveSelectedSlug ? openSlug(effectiveSelectedSlug, "sheet") : go("home")), + goDetail: () => (effectiveSelectedSlug ? openSlug(effectiveSelectedSlug) : go("home")), goReview: () => go("review"), isSearch: screen === "search", isDetail: screen === "detail", @@ -310,18 +349,9 @@ export function TcProvider({ selectedSlug: effectiveSelectedSlug, selectedTherapy, relatedForSelected, - open: (slug) => { - setSelectedSlug(slug); - go("detail"); - }, - openBrief: (slug) => { - setSelectedSlug(slug); - go("brief"); - }, - openSheet: (slug) => { - setSelectedSlug(slug); - go("sheets"); - }, + open: (slug) => openSlug(slug), + openBrief: (slug) => openSlug(slug, "brief"), + openSheet: (slug) => openSlug(slug, "sheet"), select: (slug) => setSelectedSlug(slug), search, @@ -329,7 +359,10 @@ export function TcProvider({ setQuery: (q) => patchSearch({ query: q }), submitQuery: (q) => { patchSearch({ query: q }); - go("search"); + const trimmed = q.trim(); + // Keep the query in the URL so the search screen is deep-linkable / shareable + // and the run-enabled link keeps rendering the tool (not the dashboard). + router.push(trimmed ? `${BASE}/search?q=${encodeURIComponent(trimmed)}&run=1` : `${BASE}/search`); }, toggleTag: (tag) => setSearch((prev) => ({ @@ -427,6 +460,7 @@ export function TcProvider({ }, }; }, [ + router, loading, error, data, diff --git a/src/components/therapy-compass/index.ts b/src/components/therapy-compass/index.ts index 8159da27c..cf4099061 100644 --- a/src/components/therapy-compass/index.ts +++ b/src/components/therapy-compass/index.ts @@ -1 +1 @@ -export { TherapyCompassPage } from "./therapy-compass-page"; +export { TherapyCompassWorkspace } from "./workspace"; diff --git a/src/components/therapy-compass/styles.tsx b/src/components/therapy-compass/styles.tsx index 42859b8d5..347f28fc3 100644 --- a/src/components/therapy-compass/styles.tsx +++ b/src/components/therapy-compass/styles.tsx @@ -1,14 +1,14 @@ "use client"; /** - * Scoped helper + print styles for the Therapy Compass mockup. + * Scoped helper + print styles for Therapy Compass. * * The design export shipped a global `