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
26 changes: 16 additions & 10 deletions src/app/(search-app)/documents/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "@/lib/document-detail";
import type { DocumentDetailPayload } from "@/lib/document-detail-contract";
import { PublicApiError } from "@/lib/http";
import { RecentPageTitle } from "@/components/recent-pages/recent-page-title";

export default async function DocumentPage({
params,
Expand Down Expand Up @@ -41,15 +42,20 @@ export default async function DocumentPage({
}

return (
<DocumentViewer
// Key on document identity only — page/chunk updates are handled inside
// DocumentViewer via URL sync. Remounting on every page flip reloaded the PDF.
key={id}
documentId={id}
initialPage={initialPage}
chunkId={query.chunk}
initialDetail={initialDetail}
initialError={initialError}
/>
<>
{/* The viewer's tab title does not name the document, so Recent pages is
handed the title already loaded for the viewer (no extra read). */}
<RecentPageTitle path={`/documents/${id}`} title={initialDetail?.document.title} />
Comment thread
BigSimmo marked this conversation as resolved.
Resolved
<DocumentViewer
// Key on document identity only — page/chunk updates are handled inside
// DocumentViewer via URL sync. Remounting on every page flip reloaded the PDF.
key={id}
documentId={id}
initialPage={initialPage}
chunkId={query.chunk}
initialDetail={initialDetail}
initialError={initialError}
/>
</>
);
}
2 changes: 2 additions & 0 deletions src/app/(search-app)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ReactNode } from "react";

import { SharedSearchAppShell } from "@/components/clinical-dashboard/shared-search-app-shell";
import { PsychiatryVisitRecorder } from "@/components/psychiatry/psychiatry-visit-recorder";
import { RecentPagesRecorderDeferred } from "@/components/recent-pages/recent-pages-recorder-deferred";
import { WorkModeLaunchProvider } from "@/components/work-mode-launch/work-mode-launch-provider";
import { clinicalAskModeEnabled } from "@/lib/clinical-ask/authority-registry";
import { projectClinicalAskAvailableModeIds } from "@/lib/clinical-ask/capabilities";
Expand All @@ -27,6 +28,7 @@ export default async function SearchAppLayout({ children }: { children: ReactNod
<SharedSearchAppShell clinicalAskAvailableModeIds={clinicalAskAvailableModeIds}>
{children}
<PsychiatryVisitRecorder />
<RecentPagesRecorderDeferred />
</SharedSearchAppShell>
</WorkModeLaunchProvider>
);
Expand Down
3 changes: 3 additions & 0 deletions src/components/clinical-dashboard/ClinicalSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import type { ThemePreference } from "@/lib/theme";
import {
CLINICAL_MENU_PANEL_ID,
ClinicalMenuBell,
ClinicalMenuRecentPages,
ClinicalMenuTabs,
ClinicalMenuWorkAreas,
clinicalMenuTabId,
Expand Down Expand Up @@ -987,6 +988,8 @@ export function ClinicalSidebarContent({
</div>
</section>

{drawer ? <ClinicalMenuRecentPages variant="clinical" onNavigate={() => onNavigate?.()} /> : null}

<section className="min-w-0 shrink-0">
<div className={sectionHeading}>
<p className="text-2xs font-bold uppercase tracking-eyebrow text-[color:var(--text-muted)]">
Expand Down
2 changes: 2 additions & 0 deletions src/components/clinical-dashboard/answer-status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from "@/components/clinical-dashboard/answer-evidence-preview";
import type { VerifiedEvidencePreviewUnit } from "@/lib/answer-stream-contract";
import { useAppPreferences } from "@/components/clinical-dashboard/use-app-preferences";
import { ModeHomeStartList, modeHomeStartListConfig } from "@/components/mode-home-start-list";
import { useModeResultsWarmup } from "@/components/clinical-dashboard/use-mode-results-warmup";
import { ModeHomeTemplate } from "@/components/mode-home-template";
import { LazyMyDayHomeCard } from "@/components/my-day/my-day-home-card-lazy";
Expand Down Expand Up @@ -94,6 +95,7 @@ export function SharedHomeEmptyState({
}
actionsLabel={sharedHomeEmptyState.starterActionsLabel}
actions={[]}
startList={modeHomeStartListConfig(modeId) ? <ModeHomeStartList modeId={modeId} /> : undefined}
footer={
<div className="grid w-full gap-3">
{/* My Day sits under the composer, never above it: the composer
Expand Down
98 changes: 97 additions & 1 deletion src/components/clinical-dashboard/clinical-menu-drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ import { Bell, BriefcaseMedical, Stethoscope, type LucideIcon } from "lucide-rea
import Link from "next/link";
import { useRef, type KeyboardEvent } from "react";

import { cn } from "@/components/ui-primitives";
import { useRecentPages } from "@/components/recent-pages/use-recent-pages";
import { cn, sidebarItem } from "@/components/ui-primitives";
import { rememberedWorkAreaPage, type WorkSideCountsState } from "@/components/work-frame/work-frame-store";
import { appModeIcons } from "@/lib/app-mode-icons";
import { notificationBadgeText } from "@/lib/needs-you/feed";
import type { ModeMenuSideId } from "@/lib/phone-mode-groups";
import { RECENT_PAGE_SECTION_LABEL, RECENT_PAGES_SHOWN } from "@/lib/recent-pages";
import { WORK_AREAS, type WorkAreaId } from "@/lib/work-frame/areas";
import {
WORK_SIDE_AREAS,
Expand Down Expand Up @@ -195,6 +197,23 @@ export function ClinicalMenuWorkAreas({
counts: WorkSideCountsState | null;
currentArea: WorkAreaId | null;
onNavigate: () => void;
}) {
return (
<>
<WorkAreasNav counts={counts} currentArea={currentArea} onNavigate={onNavigate} />
<ClinicalMenuRecentPages variant="work" onNavigate={onNavigate} />
</>
);
}

function WorkAreasNav({
counts,
currentArea,
onNavigate,
}: {
counts: WorkSideCountsState | null;
currentArea: WorkAreaId | null;
onNavigate: () => void;
}) {
return (
<nav aria-labelledby="clinical-menu-work-areas" className="min-w-0 shrink-0" data-testid="clinical-menu-work">
Expand Down Expand Up @@ -232,6 +251,83 @@ export function ClinicalMenuWorkAreas({
);
}

/**
* "Recent pages" in the phone menu: the last five pages opened on this device,
* across Work and Clinical, under the work areas on the Work tab and under the
* recent chats on the Clinical tab. Paths and page titles only; hidden when
* there are none.
*/
export function ClinicalMenuRecentPages({
variant,
onNavigate,
}: {
variant: "clinical" | "work";
onNavigate: () => void;
}) {
const pages = useRecentPages({ limit: RECENT_PAGES_SHOWN });
if (pages.length === 0) return null;
const headingId = `clinical-menu-recent-pages-${variant}`;
return (
<section
aria-labelledby={headingId}
className="min-w-0 shrink-0"
data-testid={`clinical-menu-recent-pages-${variant}`}
>
<h3
id={headingId}
className="flex h-9 items-center px-1 text-2xs font-bold uppercase tracking-eyebrow text-[color:var(--text-muted)]"
>
Recent pages
</h3>
<ul className="grid grid-cols-[minmax(0,1fr)] gap-0.5">
{pages.map((page) => {
const Icon = appModeIcons[page.section];
const sectionLabel = RECENT_PAGE_SECTION_LABEL[page.section];
const work = page.mode === "work";
return (
<li key={page.path}>
<Link
href={page.path}
onClick={onNavigate}
title={page.title}
data-mode-identity={work ? page.section : undefined}
className={cn(
sidebarItem,
"border border-transparent",
variant === "work" && "gap-3 px-2 text-[color:var(--text-heading)]",
)}
>
{variant === "work" ? (
<span
className={cn(
"grid size-7.5 shrink-0 place-items-center rounded-lg",
work
? "bg-[color:var(--mode-identity-soft)] text-[color:var(--mode-identity)]"
: "bg-[color:var(--surface-subtle)] text-[color:var(--text-muted)]",
)}
aria-hidden="true"
>
<Icon aria-hidden="true" className="size-icon-sm" strokeWidth={2} />
</span>
) : (
<Icon aria-hidden="true" className="h-4 w-4 shrink-0" />
)}
<span className="min-w-0 flex-1 truncate text-left">{page.title}</span>
{sectionLabel !== page.title ? (
<span className="max-w-[40%] shrink-0 truncate text-xs font-medium text-[color:var(--text-muted)]">
<span className="sr-only">, </span>
{sectionLabel}
</span>
) : null}
</Link>
</li>
);
})}
</ul>
</section>
);
}

function RowCount({ count }: { count: WorkSideCount | undefined }) {
if (!count || count.total === 0) return null;
return (
Expand Down
7 changes: 5 additions & 2 deletions src/components/clinical-dashboard/settings-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
} from "@/components/clinical-dashboard/use-app-preferences";
import { useScrollHideReporter } from "@/components/clinical-dashboard/use-hide-on-scroll";
import { clearMedicineVisits, countMedicineVisits } from "@/lib/medicines-recent";
import { clearRecentPages, countRecentPages } from "@/lib/recent-pages";
import { clearPsychiatryVisits, countPsychiatryVisits } from "@/lib/psychiatry-hub/visits";
import { clearRecentQueries, countRecentQueries } from "@/lib/recent-query-storage";
import { cn, floatingControl, InlineNotice, primaryControl, toggleThumbSurface } from "@/components/ui-primitives";
Expand Down Expand Up @@ -192,8 +193,8 @@ function forgetWorkSearchRecents() {

function readRecentQueryCount(): number {
if (typeof window === "undefined") return 0;
// The Psychiatry and Medicines hubs' recently opened records ride on the same switch and the same Clear.
return countRecentQueries() + countPsychiatryVisits() + countMedicineVisits();
// The Psychiatry and Medicines hubs' recently opened records, and Recent pages, ride on the same switch and the same Clear.
return countRecentQueries() + countPsychiatryVisits() + countMedicineVisits() + countRecentPages();
}

/**
Expand Down Expand Up @@ -635,6 +636,7 @@ export function SettingsDialog({
clearRecentQueries();
clearPsychiatryVisits();
clearMedicineVisits();
clearRecentPages();
forgetWorkSearchRecents();
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Resolved
refreshRecentQueryCount();
setPrivacyNotice("Recent searches cleared.");
Expand All @@ -655,6 +657,7 @@ export function SettingsDialog({
clearRecentQueries();
clearPsychiatryVisits();
clearMedicineVisits();
clearRecentPages();
forgetWorkSearchRecents();
refreshRecentQueryCount();
setPrivacyNotice("Recent searches turned off and existing ones cleared.");
Expand Down
92 changes: 92 additions & 0 deletions src/components/mode-home-favourites-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"use client";

import { useEffect, useState } from "react";

import { useAccountData } from "@/components/account-data-provider";
import { ModeHomeStartGroup, ModeHomeStartRow } from "@/components/mode-home-start-parts";
import { appModeIcons } from "@/lib/app-mode-icons";
import type { AppModeId } from "@/lib/app-modes";

/** How many favourites a mode home lists; the Favourites page holds the rest. */
const FAVOURITES_SHOWN = 3;

type FavouriteRow = { readonly slug: string; readonly title: string; readonly href: string };

/**
* This mode's Favourites on its home. Reads only this mode's saved slugs from
* the account data already loaded for the app (nothing here writes, and no
* service or form registry is fetched). Hidden when there are none, while they
* load, and for a signed-out reader.
*/
export function ModeHomeFavouritesGroup({
modeId,
favouriteType,
}: {
modeId: AppModeId;
favouriteType: "differentials" | "therapies";
}) {
// While an account's favourites are loading (including straight after a switch
// of account), the previous account's list is still in memory: show nothing.
const { ready } = useAccountData();
if (!ready) return null;
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Resolved
return favouriteType === "therapies" ? (
<TherapyFavourites modeId={modeId} />
) : (
<DifferentialFavourites modeId={modeId} />
);
}

/** Differential favourites are named from their slug, exactly as the Favourites page names them. */
function DifferentialFavourites({ modeId }: { modeId: AppModeId }) {
const slugs = useAccountData().favourites.differential.slice(0, FAVOURITES_SHOWN);
const rows = slugs.map((slug) => ({
slug,
title: slug
.split("-")
.filter(Boolean)
.map((word) => word[0]?.toUpperCase() + word.slice(1))
.join(" "),
href: `/differentials/diagnoses/${encodeURIComponent(slug)}`,
}));
return <FavouritesRows modeId={modeId} rows={rows} />;
}

/** Therapy favourites take their names from the therapy catalogue, loaded only on the Therapy home and only when some are saved. */
function TherapyFavourites({ modeId }: { modeId: AppModeId }) {
const saved = useAccountData().favourites.therapy;
const key = saved.slice(0, FAVOURITES_SHOWN).join("\u0000");
const [loaded, setLoaded] = useState<{ key: string; rows: FavouriteRow[] }>({ key: "", rows: [] });

useEffect(() => {
if (!key) return undefined;
let active = true;
void import("@/lib/therapies")
.then(({ findTherapyRecord }) => {
if (!active) return;
const rows = key.split("\u0000").flatMap((slug) => {
const therapy = findTherapyRecord(slug);
return therapy ? [{ slug, title: therapy.name, href: `/therapy-compass/${therapy.slug}` }] : [];
});
setLoaded({ key, rows });
})
.catch(() => {
// The group stays hidden; the Favourites page still lists them.
});
return () => {
active = false;
};
}, [key]);

return <FavouritesRows modeId={modeId} rows={loaded.key === key ? loaded.rows : []} />;
}

function FavouritesRows({ modeId, rows }: { modeId: AppModeId; rows: readonly FavouriteRow[] }) {
if (rows.length === 0) return null;
return (
<ModeHomeStartGroup id={`${modeId}-home-favourites`} title="Favourites" testId="mode-home-favourites">
{rows.map((row) => (
<ModeHomeStartRow key={row.slug} href={row.href} icon={appModeIcons[modeId]} title={row.title} />
))}
</ModeHomeStartGroup>
);
}
Loading
Loading