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
10 changes: 10 additions & 0 deletions docs/codebase-index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -250,6 +250,16 @@ Golden retrieval fixture: `scripts/fixtures/rag-retrieval-golden.json`
- Registry modes: services, forms, medications, differentials
- Demo mode: synthetic data when Supabase unavailable (`demo-data.ts`, `isDemoMode()` in `env.ts`)

### Global search composer placement rules

One shared composer (`master-search-header.tsx`) serves every mode. Placement:

- **Mode homes** (`/services`, `/forms`, `/favourites`, `/differentials`, `/applications`, and dashboard homes): inline in the hero via the `mode-home-composer-slot` portal, on phone and tablet+ alike.
- **Result and detail views**: fixed bottom dock on phone (compact variant on submitted searches), sticky top from `sm` up.
- **Results routing**: standalone routes own their submitted searches via `?q=…&run=1` (`/services` → `ServicesNavigatorPage`, `/forms` → `FormsSearchResultsPage`, `/differentials` → `DifferentialsHome` results view, `/favourites` filters the command library in place). Answer, Documents, and Prescribing submitted searches render inside `ClinicalDashboard` — intentional, since they need retrieval/answer state. `/?mode=favourites` redirects to `/favourites`; `/?mode=differentials` redirects to `/differentials`.
- **Intentionally composer-free routes**: `/differentials/presentations/*` (comparison workflow owns its chrome), `/documents/[id]` viewer (has its own in-document ask composer), `/documents/source/*` (document flow owns mobile chrome). Do not re-flag these in search-consistency audits.
- **Local filter fields** (sidebar "Search chats", document drawer "Find a document"/"Find a source PDF") are scoped filters, not global search; they share the `fieldControlWithIcon`/`fieldIcon` primitives.

---

## Key config files
Expand Down
2 changes: 1 addition & 1 deletion public/llms.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ Purpose: Clinical Guide is a local clinical knowledge-base interface for searchi
Agent / codebase orientation: docs/codebase-index.md (module map, APIs, Supabase, worker). Route index: docs/site-map.md.

Key routes:
- / opens the main dashboard. Use ?mode=answer, ?mode=documents, ?mode=tools, ?mode=favourites, ?mode=differentials, or ?mode=prescribing to choose the workspace.
- / opens the main dashboard. Use ?mode=answer, ?mode=documents, ?mode=tools, ?mode=differentials, or ?mode=prescribing to choose the workspace. ?mode=favourites redirects to /favourites.
- /documents/search opens the documents search command centre after submitting a documents-mode query.
- /documents/:id opens an indexed source document.
- /services opens source-backed service records.
Expand Down
6 changes: 3 additions & 3 deletions scripts/capture-chrome-parity.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -90,16 +90,16 @@ const selectorGroups: Array<{ key: string; selector: string; pseudo?: string }>
type Snapshot = Record<string, Record<string, string>>;

async function mockApis(page: Page) {
await page.route("**/api/setup-status**", async (route) => {
await page.route("**/api/setup-status**", async (route: Route) => {
await route.fulfill({ json: { demoMode: true, checks: readySetupChecks } });
});
await page.route(/\/api\/documents\/[0-9a-f-]+(?:\?.*)?$/, async (route) => {
await page.route(/\/api\/documents\/[0-9a-f-]+(?:\?.*)?$/, async (route: Route) => {
const id = new URL(route.request().url()).pathname.split("/").pop() ?? "";
const payload = getDemoDocumentPayload(id);
if (payload) await route.fulfill({ json: payload });
else await route.fulfill({ status: 404, json: { error: "not found" } });
});
await page.route(/\/api\/documents(?:\?.*)?$/, async (route) => {
await page.route(/\/api\/documents(?:\?.*)?$/, async (route: Route) => {
await route.fulfill({
json: {
documents: demoDocuments,
Expand Down
1 change: 0 additions & 1 deletion src/app/api/medications/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,6 @@ import {
medicationValidationStatus,
rowGovernance,
rowToMedicationRecord,
type MedicationRecordRow,
} from "@/lib/medication-records";
import {
medicationToSearchResult,
Expand Down
2 changes: 1 addition & 1 deletion src/app/applications/layout.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search

export default function ApplicationsLayout({ children }: { children: ReactNode }) {
return (
<GlobalSearchShell initialMode="tools" searchComposerVisible={false}>
<GlobalSearchShell initialMode="tools" desktopSearchPlacement="hero">
{children}
</GlobalSearchShell>
);
Expand Down
17 changes: 15 additions & 2 deletions src/app/applications/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,19 @@ export const metadata: Metadata = {
description: "Launch Clinical KB applications, workflows, and connected clinical tools.",
};

export default function ApplicationsRoute() {
return <ApplicationsLauncherPage />;
type ApplicationsPageProps = {
searchParams?: Promise<{
q?: string | string[];
}>;
};

function firstSearchParam(value: string | string[] | undefined) {
return Array.isArray(value) ? value[0] : value;
}

export default async function ApplicationsRoute({ searchParams }: ApplicationsPageProps) {
const params = searchParams ? await searchParams : {};
const query = firstSearchParam(params.q)?.trim();

return query ? <ApplicationsLauncherPage key={query} query={query} /> : <ApplicationsLauncherPage />;
}
6 changes: 5 additions & 1 deletion src/app/differentials/layout.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,5 +3,9 @@ import type { ReactNode } from "react";
import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search-shell";

export default function DifferentialsLayout({ children }: { children: ReactNode }) {
return <GlobalSearchShell initialMode="differentials">{children}</GlobalSearchShell>;
return (
<GlobalSearchShell initialMode="differentials" desktopSearchPlacement="hero">
{children}
</GlobalSearchShell>
);
}
9 changes: 5 additions & 4 deletions src/app/differentials/page.tsx
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
import { DifferentialsHomePage } from "@/components/differentials/differentials-home-page";

type DifferentialsRouteProps = {
searchParams?: Promise<{ query?: string | string[]; q?: string | string[] }>;
searchParams?: Promise<{ query?: string | string[]; q?: string | string[]; run?: string | string[] }>;
};

function firstSearchParam(value?: string | string[]) {
Expand All@@ -10,11 +10,12 @@ function firstSearchParam(value?: string | string[]) {

export default async function DifferentialsHomeRoute({ searchParams }: DifferentialsRouteProps) {
const params = searchParams ? await searchParams : {};
const query = firstSearchParam(params.query ?? params.q)?.trim();
const query = (firstSearchParam(params.q) ?? firstSearchParam(params.query) ?? "").trim();
const hasSubmittedSearch = firstSearchParam(params.run) === "1" && query.length > 0;

if (!query) {
if (!hasSubmittedSearch) {
return <DifferentialsHomePage />;
}

return <DifferentialsHomePage query={query} />;
return <DifferentialsHomePage query={query} autoRunSearch />;
}
2 changes: 1 addition & 1 deletion src/app/favourites/layout.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search

export default function FavouritesLayout({ children }: { children: ReactNode }) {
return (
<GlobalSearchShell initialMode="favourites" availableModeIds={["favourites"]}>
<GlobalSearchShell initialMode="favourites" availableModeIds={["favourites"]} desktopSearchPlacement="hero">
{children}
</GlobalSearchShell>
);
Expand Down
23 changes: 21 additions & 2 deletions src/app/forms/page.tsx
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
import { FormsHomePage } from "@/components/forms/forms-home-page";
import { FormsSearchResultsPage } from "@/components/forms/forms-search-results-page";

export default function FormsPage() {
return <FormsHomePage />;
type FormsSearchParams = Promise<{ [key: string]: string | string[] | undefined }>;

function readFirstSearchParam(value: string | string[] | undefined) {
return Array.isArray(value) ? value[0] : value;
}

export default async function FormsPage({ searchParams }: { searchParams: FormsSearchParams }) {
const resolvedSearchParams = await searchParams;
const query = (
readFirstSearchParam(resolvedSearchParams.q) ??
readFirstSearchParam(resolvedSearchParams.query) ??
""
).trim();
const hasSubmittedSearch = readFirstSearchParam(resolvedSearchParams.run) === "1" && query.length > 0;

if (!hasSubmittedSearch) {
return <FormsHomePage />;
}

return <FormsSearchResultsPage query={query} />;
}
33 changes: 28 additions & 5 deletions src/app/globals.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1533,7 +1533,10 @@ summary::-webkit-details-marker {
padding-bottom: max(0.45rem, var(--safe-area-bottom));
}

.answer-footer-search-dock[data-scroll-hidden="true"] {
/* Must beat the edge-to-edge dock rule above (transform: none) so scroll-hide
actually slides the bar off-screen once data-scroll-hidden is set. */
.answer-footer-search-dock.document-mobile-search-edge.answer-footer-search-edge[data-scroll-hidden="true"],
.answer-footer-search-dock.dashboard-composer-edge.answer-footer-search-edge[data-scroll-hidden="true"] {
transform: translateY(calc(100% + env(safe-area-inset-bottom)));
pointer-events: none;
}
Expand DownExpand Up@@ -1852,14 +1855,29 @@ summary::-webkit-details-marker {
box-shadow: 0 0 0 4px color-mix(in srgb, var(--focus) 25%, transparent) !important;
}

/* Premium Hover Transitions for Source Capsules and Action row chips */
/* Premium hover transitions for source capsules */
.source-capsule-hover {
transition: all 180ms cubic-bezier(0.34, 1.56, 0.64, 1) !important;
box-shadow: var(--glow-soft), var(--shadow-inset);
transition:
transform 180ms cubic-bezier(0.34, 1.56, 0.64, 1),
box-shadow 180ms cubic-bezier(0.22, 1, 0.36, 1),
border-color 150ms ease,
background-color 150ms ease !important;
}

.source-capsule-hover:hover {
transform: translateY(-1px) scale(1.015) !important;
box-shadow: 0 4px 12px color-mix(in srgb, var(--primary) 8%, transparent) !important;
transform: translateY(-1px) scale(1.01);
box-shadow: var(--shadow-tight), var(--shadow-inset);
}

.source-capsule-hover[aria-expanded="true"] {
border-color: var(--clinical-accent);
box-shadow: var(--glow-soft), var(--shadow-inset);
}

.source-capsule-hover[aria-expanded="true"]:hover {
transform: translateY(-1px) scale(1.01);
box-shadow: var(--shadow-tight), var(--shadow-inset);
}

.polished-scroll {
Expand DownExpand Up@@ -1902,6 +1920,11 @@ summary::-webkit-details-marker {
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
}

.source-capsule-hover:hover,
.source-capsule-hover[aria-expanded="true"]:hover {
transform: none !important;
}
}

@media (forced-colors: active) {
Expand Down
6 changes: 5 additions & 1 deletion src/app/medications/layout.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,5 +3,9 @@ import type { ReactNode } from "react";
import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search-shell";

export default function MedicationsLayout({ children }: { children: ReactNode }) {
return <GlobalSearchShell initialMode="prescribing">{children}</GlobalSearchShell>;
return (
<GlobalSearchShell initialMode="prescribing" desktopSearchPlacement="hero">
{children}
</GlobalSearchShell>
);
}
24 changes: 24 additions & 0 deletions src/app/page.tsx
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
import { redirect } from "next/navigation";

import { HomePageClient } from "@/app/home-page-client";
import { isAppModeId, isAppModeVisible, type AppModeId } from "@/lib/app-modes";

Expand All@@ -20,5 +22,27 @@ export default async function Home({ searchParams }: HomeProps) {
const initialSearchMode: AppModeId =
isAppModeId(requestedMode) && isAppModeVisible(requestedMode) ? requestedMode : "answer";

// /favourites is the canonical favourites surface; deep links via the
// dashboard mode param would otherwise open a divergent hub view.
if (initialSearchMode === "favourites") {
const favouriteParams = new URLSearchParams();
const query = firstSearchParam(params.q)?.trim();
if (query) favouriteParams.set("q", query);
if (firstSearchParam(params.focus) === "1") favouriteParams.set("focus", "1");
if (firstSearchParam(params.run) === "1") favouriteParams.set("run", "1");
const suffix = favouriteParams.toString();
redirect(suffix ? `/favourites?${suffix}` : "/favourites");
}

if (initialSearchMode === "differentials") {
const differentialParams = new URLSearchParams();
const query = firstSearchParam(params.q)?.trim();
if (query) differentialParams.set("q", query);
if (firstSearchParam(params.focus) === "1") differentialParams.set("focus", "1");
if (firstSearchParam(params.run) === "1") differentialParams.set("run", "1");
const suffix = differentialParams.toString();
redirect(suffix ? `/differentials?${suffix}` : "/differentials");
}

return <HomePageClient initialMode={initialSearchMode} />;
}
2 changes: 2 additions & 0 deletions src/components/ClinicalDashboard.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -1542,7 +1542,7 @@
const [loadingMoreDocuments, setLoadingMoreDocuments] = useState(false);
const [jobs, setJobs] = useState<IngestionJob[]>([]);
const [batches, setBatches] = useState<ImportBatch[]>([]);
const [qualityItems, setQualityItems] = useState<IngestionQualityReviewItem[]>([]);

Check warning on line 1545 in src/components/ClinicalDashboard.tsx

View workflow job for this annotation

GitHub Actions/ verify

React Hook useEffect has missing dependencies: 'executeSearch' and 'scopeFilters'. Either include them or remove the dependency array
const jobsRef = useRef(jobs);
const batchesRef = useRef(batches);
const answerThreadBootstrappedRef = useRef(false);
Expand DownExpand Up@@ -4124,6 +4124,8 @@
followUpSuggestions={answerFollowUpSuggestions}
onPickFollowUpSuggestion={handlePickFollowUpSuggestion}
followUpSuggestionsDisabled={loading}
crossModeQueries={[...priorAnswerTurns.map((turn) => turn.query), latestAnswerQuery]}
onCrossModeSearch={crossModeSearch}
/>
</>
) : null
Expand Down
11 changes: 7 additions & 4 deletions src/components/applications-launcher-page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,8 +24,10 @@ import {
import { type FormEvent, useMemo, useState } from "react";

import { ModeHomeHero, ModeHomeVerificationFooter } from "@/components/mode-home-template";
import { useSearchCommand } from "@/components/clinical-dashboard/search-command-context";
import { cn } from "@/components/ui-primitives";
import { Sheet } from "@/components/ui/sheet";
import { modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer";
import {
toolCatalogRecords,
type ToolCatalogArea,
Expand DownExpand Up@@ -666,11 +668,12 @@ export function ApplicationsLauncherWorkspace({
desktopComposerSlotId,
className,
}: ApplicationsLauncherWorkspaceProps) {
const searchCommand = useSearchCommand();
const [localQuery, setLocalQuery] = useState("");
const [activeFilter, setActiveFilter] = useState<LauncherFilter>("all");
const [detailOpen, setDetailOpen] = useState(false);
const copy = toolsLauncherCopy;
const query = controlledQuery ?? localQuery;
const query = controlledQuery ?? searchCommand?.query ?? localQuery;
const normalizedQuery = query.trim().toLowerCase();
const queryDerivedId = useMemo(() => initialToolId(query), [query]);
const [selection, setSelection] = useState(() => ({
Expand DownExpand Up@@ -711,7 +714,7 @@ export function ApplicationsLauncherWorkspace({
: copy.allSectionLabel;

function updateQuery(nextQuery: string) {
if (controlledQuery === undefined) setLocalQuery(nextQuery);
if (controlledQuery === undefined && !searchCommand) setLocalQuery(nextQuery);
}

function openTool(id: string) {
Expand DownExpand Up@@ -827,6 +830,6 @@ export function ApplicationsLauncherWorkspace({
);
}

export function ApplicationsLauncherPage() {
return <ApplicationsLauncherWorkspace />;
export function ApplicationsLauncherPage({ query }: { query?: string }) {
return <ApplicationsLauncherWorkspace query={query} desktopComposerSlotId={modeHomeDesktopComposerSlotId} />;
}
13 changes: 10 additions & 3 deletions src/components/clinical-dashboard/ClinicalSidebar.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,14 @@ import {
} from "lucide-react";
import { appModeIcons } from "@/lib/app-mode-icons";
import { BrandMark } from "@/components/clinical-dashboard/brand";
import { cn, sidebarItem, statusDotReady, textMuted } from "@/components/ui-primitives";
import {
cn,
fieldControlWithIcon,
fieldIcon,
sidebarItem,
statusDotReady,
textMuted,
} from "@/components/ui-primitives";

function useClientMounted() {
return useSyncExternalStore(
Expand DownExpand Up@@ -161,14 +168,14 @@ export function ClinicalSidebarContent({
pinned. */}
<div className="flex min-h-0 min-w-0 flex-1 flex-col gap-4 overflow-y-auto">
<label className="relative block shrink-0">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[color:var(--text-soft)]" />
<Search className={fieldIcon} />
<input
type="search"
placeholder="Search chats"
value={chatFilter}
onChange={(event) => setChatFilter(event.target.value)}
aria-label="Search recent chats"
className="clinical-sidebar-search-input h-11 w-full rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] pl-9 pr-3 text-sm font-medium text-[color:var(--text)] shadow-[var(--shadow-inset)] outline-none placeholder:text-[color:var(--text-soft)] focus:border-[color:var(--focus)] focus:ring-4 focus:ring-[color:var(--focus)]/20"
className={cn(fieldControlWithIcon, "font-medium")}
/>
</label>

Expand Down
Loading