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
97,839 changes: 97,839 additions & 0 deletions data/medications-snapshot.json

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions src/app/api/medications/[slug]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { NextResponse } from "next/server";

import { isDemoMode, isLocalNoAuthMode } from "@/lib/env";
import { jsonError } from "@/lib/http";
import { getMedicationRecord } from "@/lib/medication-snapshot";
import { deriveGovernanceFromSections, normalizeMedicationSlug } from "@/lib/medication-records";

export const runtime = "nodejs";

function medicationResponse(payload: Record<string, unknown>, init?: { status?: number }) {
return NextResponse.json(payload, {
status: init?.status ?? 200,
headers: { "Cache-Control": "private, no-store" },
});
}

function notFoundResponse(slug: string) {
return medicationResponse({ error: `No medication found for "${slug}".` }, { status: 404 });
}

function publicMedicationDetailPayload(slug: string) {
const record = getMedicationRecord(slug);
if (!record) return null;
const governance = deriveGovernanceFromSections(record);
return {
record,
governance: {
sourceStatus: governance.source_status,
validationStatus: governance.validation_status,
},
};
}

export async function GET(_request: Request, context: { params: Promise<{ slug: string }> }) {
try {
const { slug } = await context.params;
const normalizedSlug = normalizeMedicationSlug(slug);
const payload = publicMedicationDetailPayload(normalizedSlug);
if (!payload) return notFoundResponse(normalizedSlug);

return medicationResponse({
...payload,
demoMode: isDemoMode() || isLocalNoAuthMode(),
publicAccess: true,
});
} catch (error) {
return jsonError(error);
}
}
90 changes: 90 additions & 0 deletions src/app/api/medications/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { NextResponse } from "next/server";
import { z } from "zod";

import { isDemoMode, isLocalNoAuthMode } from "@/lib/env";
import { jsonError } from "@/lib/http";
import { defaultMedicationRecords } from "@/lib/medication-seed";
import { medicationSourceStatus, medicationValidationStatus } from "@/lib/medication-records";
import {
medicationToSearchResult,
rankMedicationRecords,
type MedicationRecord,
type MedicationSearchMatch,
} from "@/lib/medications";
import { parseRequestQuery, queryInteger } from "@/lib/validation/query";

export const runtime = "nodejs";

const medicationListQuerySchema = z.object({
q: z
.string()
.trim()
.max(200)
.optional()
.transform((value) => (value ? value : undefined)),
limit: queryInteger({ fallback: 50, min: 1, max: 100 }),
fields: z.enum(["index"]).optional(),
});

function toIndexRecords(records: MedicationRecord[]): MedicationRecord[] {
return records.map((record) => ({
slug: record.slug,
name: record.name,
class: record.class,
subclass: record.subclass,
category: record.category,
accent: record.accent,
tag: record.tag,
schedule: record.schedule,
stats: [],
sections: [],
quick: [],
}));
}

function medicationResponse(payload: Record<string, unknown>) {
return NextResponse.json(payload, { headers: { "Cache-Control": "private, no-store" } });
}

function matchesPayload(matches: MedicationSearchMatch[]) {
return matches.map((match) => ({
medication: match.medication,
result: medicationToSearchResult(match),
score: match.score,
reasons: match.reasons,
}));
}

function publicMedicationPayload(q: string | undefined, limit: number, fields?: "index") {
const records = fields === "index" ? toIndexRecords(defaultMedicationRecords()) : defaultMedicationRecords();
const governance = Object.fromEntries(
records.map((record) => [
record.slug,
{
sourceStatus: medicationSourceStatus("current"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive list governance instead of marking every medication current

The list API hard-codes every record as current, but the same snapshot contains medications with no source section (for example edoxaban, alimemazine, and levomepromazine) that deriveGovernanceFromSections reports as unknown in the detail API. Any client using /api/medications governance will display these as reviewed/current even though the underlying record has no source review metadata; derive the status before stripping index fields instead of assigning current universally.

Useful? React with 👍 / 👎.

validationStatus: medicationValidationStatus("locally_reviewed"),
},
]),
);
const matches = q ? rankMedicationRecords(records, q, limit) : undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Strip stopwords before ranking medication API queries

For natural-language requests such as /api/medications?q=what is the maximum dose of haloperidol in agitation, passing the raw query into the medication ranker leaves short stopwords like in in the term list; the shared ranker then prefix-matches that token against medication names, so insulin records receive high name scores and can outrank the named medication. Cross-mode matching already uses keyword extraction first, but this API path does not, so the new endpoint returns misleading top matches for question-shaped queries.

Useful? React with 👍 / 👎.

return {
records,
matches: matches ? matchesPayload(matches) : undefined,
total: records.length,
governance,
};
}

export async function GET(request: Request) {
try {
const { q, limit, fields } = parseRequestQuery(request, medicationListQuerySchema, "Invalid medication query.");

return medicationResponse({
...publicMedicationPayload(q, limit, fields),
demoMode: isDemoMode() || isLocalNoAuthMode(),
publicAccess: true,
});
} catch (error) {
return jsonError(error);
}
}
13 changes: 12 additions & 1 deletion src/components/ClinicalDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ import { SafeBoldText } from "@/components/SafeBoldText";
import { Sheet } from "@/components/ui/sheet";
import { AccountSetupDialog } from "@/components/clinical-dashboard/account-setup-dialog";
import { StagedAnswerResultSurface } from "@/components/clinical-dashboard/answer-result-surface";
import { CrossModeLinksSection } from "@/components/clinical-dashboard/cross-mode-links";
import { RelatedDocumentsPanel } from "@/components/clinical-dashboard/document-results";
import { AnswerFollowUpSuggestions } from "@/components/clinical-dashboard/answer-follow-up-suggestions";
import { AuthPanel } from "@/components/clinical-dashboard/auth-panel";
Expand Down Expand Up @@ -172,6 +173,7 @@ import {
DrawerGroupLabel,
type DocumentDrawerMode,
type DocumentDrawerStatusFilter,
type LabelReviewMutationBody,
} from "@/components/clinical-dashboard/document-admin";


Expand All @@ -195,7 +197,7 @@ export const ApplicationsLauncherWorkspace = dynamic(
{ ssr: false },
);
const DocumentDrawer = dynamic(
() => import("@/components/clinical-dashboard/document-admin/document-drawer").then((m) => m.DocumentDrawer),
() => import("@/components/clinical-dashboard/document-admin").then((m) => m.DocumentDrawer),
{ ssr: false },
);

Expand Down Expand Up @@ -4079,6 +4081,10 @@ export function ClinicalDashboard({
const priorQueries = [...priorAnswerTurns.map((turn) => turn.query), latestAnswerQuery];
return buildAnswerFollowUpSuggestions(latestAnswerQuery, answer, priorQueries);
}, [answer, latestAnswerQuery, priorAnswerTurns]);
const crossModeQueries = useMemo(
() => [...priorAnswerTurns.map((turn) => turn.query), latestAnswerQuery],
[priorAnswerTurns, latestAnswerQuery],
);
const hiddenPriorTurnCount = Math.max(0, priorAnswerTurns.length - maxVisiblePriorTurns);
const visiblePriorTurns = useMemo(() => {
if (showEarlierTurns || hiddenPriorTurnCount === 0) return priorAnswerTurns;
Expand Down Expand Up @@ -4628,6 +4634,9 @@ export function ClinicalDashboard({
) : (
<>
<ScopeAndGovernanceNotice scope={searchScope} warnings={sourceGovernanceWarnings} />
{searchMode === "documents" && modeSearchSubmitted && (
<CrossModeLinksSection queries={[query]} onModeSearch={crossModeSearch} />
)}
<DocumentSearchResultsPanel
matches={documentMatches}
recordMatches={recordSearchMatches}
Expand Down Expand Up @@ -4706,6 +4715,8 @@ export function ClinicalDashboard({
followUpSuggestions={answerFollowUpSuggestions}
onPickFollowUpSuggestion={handlePickFollowUpSuggestion}
followUpSuggestionsDisabled={loading}
crossModeQueries={crossModeQueries}
onCrossModeSearch={crossModeSearch}
/>
</>
) : null
Expand Down
10 changes: 10 additions & 0 deletions src/components/clinical-dashboard/answer-result-surface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ClipboardCheck, ExternalLink, Layers, ShieldAlert } from "lucide-react"

import { type AnswerFeedbackType } from "@/lib/answer-feedback";
import { AnswerFollowUpSuggestions } from "@/components/clinical-dashboard/answer-follow-up-suggestions";
import { CrossModeLinksSection } from "@/components/clinical-dashboard/cross-mode-links";
import { NaturalLanguageAnswer, UserQuestionBubble } from "@/components/clinical-dashboard/answer-content";
import {
AnswerSupportSummaryCard,
Expand All @@ -24,6 +25,7 @@ import { InlineTableCard, MobileEvidenceSheetContent } from "@/components/clinic
import { Sheet } from "@/components/ui/sheet";
import { answerSurface, cn, iconTilePremium, subtleStatusPill } from "@/components/ui-primitives";
import { type AnswerRenderModel } from "@/lib/answer-render-policy";
import { type AppModeId } from "@/lib/app-modes";
import { extractSafetyFindings } from "@/lib/clinical-safety";
import { type SourceGovernanceWarning } from "@/lib/source-governance";
import type {
Expand Down Expand Up @@ -61,6 +63,8 @@ export function StagedAnswerResultSurface({
followUpSuggestions,
onPickFollowUpSuggestion,
followUpSuggestionsDisabled = false,
crossModeQueries,
onCrossModeSearch,
}: {
answer: RagAnswer;
query: string;
Expand All @@ -86,6 +90,8 @@ export function StagedAnswerResultSurface({
followUpSuggestions?: string[];
onPickFollowUpSuggestion?: (suggestion: string) => void;
followUpSuggestionsDisabled?: boolean;
crossModeQueries?: Array<string | null | undefined>;
onCrossModeSearch?: (mode: AppModeId, query: string) => void;
}) {
const noteCount = clinicalNotesCount(answer);
const showClinicalNotes =
Expand Down Expand Up @@ -226,6 +232,10 @@ export function StagedAnswerResultSurface({
/>
) : null}

{crossModeQueries?.length && onCrossModeSearch ? (
<CrossModeLinksSection queries={crossModeQueries} onModeSearch={onCrossModeSearch} />
) : null}

{followUpSuggestions?.length && onPickFollowUpSuggestion ? (
<AnswerFollowUpSuggestions
suggestions={followUpSuggestions}
Expand Down
Loading