feat(search): widen the clinical read for the in-app search and cover the palette end to end

This commit is contained in:
Marc-André Bombeck committed 2026-10-11 12:59:51 +02:00
1 parent 1b5225ba36
commit a36e414895
5 files changed
+140 -16

No files matched your search

@@ -569,7 +569,7 @@ function RecordSection({
}}
onClick={() => onOpen(hit.path)}
className={cn(
"flex min-h-11 cursor-pointer items-start gap-3 rounded-md px-3 py-2 text-sm",
"group flex min-h-11 cursor-pointer items-start gap-3 rounded-md px-3 py-2 text-sm",
selected && "bg-accent text-accent-foreground",
)}
>
@@ -82,7 +82,9 @@ export function RecordHitContent({ hit }: { hit: SearchHit }) {
{hit.title ? <Marked value={hit.title} /> : label}
</span>
<span className="block truncate text-xs">
<span className="text-muted-foreground">
{/* Muted reads too faint on the highlighted row's accent fill, so
the highlighted option sets it in the accent's own ink. */}
<span className="text-muted-foreground group-aria-selected:text-accent-foreground">
{hit.title ? label : null}
{hit.title && hit.day ? " · " : null}
{hit.day ? fmt.dateShortSmartCalendar(hit.day) : null}
+5 -1
View File
@@ -788,6 +788,8 @@ export interface RecordSearchOptions {
* reading. Only the person's own in-app search sets it; MCP never does.
*/
includeAiDeferredText?: boolean;
/** Newest rows read per kind; defaults to `MAX_ROWS_PER_KIND`. */
rowsPerKind?: number;
}
/**
@@ -802,7 +804,9 @@ export async function loadRecordCandidates(
): Promise<SearchCandidate[]> {
const words = queryWords(query);
const empty = normalizeSearchText(query).length === 0;
const take = empty ? EMPTY_QUERY_PER_KIND : MAX_ROWS_PER_KIND;
const take = empty
? EMPTY_QUERY_PER_KIND
: (options.rowsPerKind ?? MAX_ROWS_PER_KIND);
const [kinds, owner] = await Promise.all([
options.kinds ? Promise.resolve(options.kinds) : readableKinds(userId),
loadOwner(userId, options.displayLocale),
+23 -12
View File
@@ -43,11 +43,7 @@ import {
import { normalizeSearchText } from "@/lib/encounters/procedures";
import type { Locale } from "@/lib/i18n/config";
import { getServerTranslator } from "@/lib/i18n/server-translator";
import {
loadRecordCandidates,
MAX_ROWS_PER_KIND,
type RecordKind,
} from "@/lib/mcp/record-search";
import { loadRecordCandidates, type RecordKind } from "@/lib/mcp/record-search";
import { moduleForMeasurementType } from "@/lib/modules/measurement-scope";
import { isSurfaceVisible, type SurfaceModuleMap } from "@/lib/modules/surface";
import type { ShareScope } from "@/lib/sharing/scope";
@@ -83,7 +79,13 @@ import {
/** Rows one batched source reads per round trip. */
export const SCAN_BATCH = 250;
/** Rows one batched source reads at most, per search. */
export const MAX_SCAN_ROWS_PER_SOURCE = 2000;
export const MAX_SCAN_ROWS_PER_SOURCE = 5000;
/**
* Newest rows of each clinical kind (visits, conditions, documents,
* vaccinations) one search reads. Wider than the MCP bound: a person's own
* search is asked about the operation of ten years ago.
*/
export const RECORD_ROWS_PER_KIND = 2000;
/** Wall-clock budget one search shares across its sources. */
export const SEARCH_TIME_BUDGET_MS = 1500;
/** Sources that read at the same time. */
@@ -452,11 +454,19 @@ async function symptomNoteCandidates(
});
}
/** "stairClimber" → "stair climber". */
function sportWords(sportType: string): string {
return sportType
.replace(/([a-z])([A-Z])/g, "$1 $2")
.replace(/_/g, " ")
.toLowerCase();
}
/** The sport's name in the display language, else its words. */
function sportLabel(sportType: string, locale: Locale): string {
const key = `insights.workouts.sport.${sportType}`;
const label = getServerTranslator(locale).t(key);
return label === key ? sportType.toLowerCase().replace(/_/g, " ") : label;
return label === key ? sportWords(sportType) : label;
}
async function workoutCandidates(
@@ -483,7 +493,7 @@ async function workoutCandidates(
text: [
sportLabel(sportType, ctx.display),
sportLabel(sportType, "en"),
sportType.toLowerCase().replace(/_/g, " "),
sportWords(sportType),
].join(" "),
weight: WEIGHT.primary,
},
@@ -512,7 +522,7 @@ async function workoutCandidates(
text: [
label,
sportLabel(row.sportType, "en"),
row.sportType.toLowerCase().replace(/_/g, " "),
sportWords(row.sportType),
].join(" "),
weight: WEIGHT.primary,
},
@@ -866,18 +876,19 @@ export async function searchRecords(
kinds: recordKinds,
displayLocale: args.displayLocale,
includeAiDeferredText: true,
rowsPerKind: RECORD_ROWS_PER_KIND,
},
);
budget.rows += candidates.length;
// Each kind reads its newest `MAX_ROWS_PER_KIND`; a kind that filled
// the read may have older rows the search did not see.
// Each kind reads its newest `RECORD_ROWS_PER_KIND`; a kind that
// filled the read may have older rows the search did not see.
const perKind = new Map<string, number>();
for (const candidate of candidates) {
const kind = candidate.hit?.kind ?? "";
perKind.set(kind, (perKind.get(kind) ?? 0) + 1);
if (keepIfHit(ctx, candidate)) collected.push(candidate);
}
if ([...perKind.values()].some((n) => n >= MAX_ROWS_PER_KIND)) {
if ([...perKind.values()].some((n) => n >= RECORD_ROWS_PER_KIND)) {
budget.truncated = true;
}
});