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
28 changes: 15 additions & 13 deletions docs/branch-review-ledger.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const chromiumExecutablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH;

export default defineConfig({
testDir: "./tests",
testMatch: /.*ui-(smoke|stress|accessibility|tools|tools-task-directory|overlap|universal-search)\.spec\.ts/,
testMatch:
/.*ui-(smoke|stress|accessibility|tools|tools-collapse|tools-task-directory|overlap|universal-search)\.spec\.ts/,
timeout: 60_000,
retries: process.env.CI ? 1 : 0,
expect: {
Expand Down
42 changes: 23 additions & 19 deletions src/app/api/documents/[id]/table-facts/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,21 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id
return NextResponse.json({ error: "Table fact not found." }, { status: 404 });
}

let sourceImage: { id: string; metadata: unknown } | null = null;
if (fact.source_image_id) {
const { data: image, error: imageError } = await supabase
.from("document_images")
.select("id,metadata")
.eq("id", fact.source_image_id)
.eq("document_id", id)
.maybeSingle();
if (imageError) throw new Error(imageError.message);
if (image && !isCommittedGenerationMetadata({ rowMetadata: image.metadata, committedGeneration })) {
return NextResponse.json({ error: "Table fact not found." }, { status: 404 });
}
sourceImage = image;
}

const reviewMetadata = tableReviewMetadata({
reviewClass: parsed.reviewClass,
notes: parsed.notes,
Expand All @@ -109,26 +124,15 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id
.single();
if (updateError) throw new Error(updateError.message);

if (fact.source_image_id) {
const { data: image } = await supabase
if (sourceImage && fact.source_image_id) {
const { error: imageUpdateError } = await supabase
.from("document_images")
.select("id,metadata")
.eq("id", fact.source_image_id)
.eq("document_id", id)
.maybeSingle();
if (image) {
if (!isCommittedGenerationMetadata({ rowMetadata: image.metadata, committedGeneration })) {
return NextResponse.json({ error: "Table fact not found." }, { status: 404 });
}
const { error: imageUpdateError } = await supabase
.from("document_images")
.update({
metadata: { ...metadataRecord(image.metadata), ...reviewMetadata },
searchable: parsed.reviewClass === "clinical_useful" || parsed.reviewClass === "reference",
})
.eq("id", fact.source_image_id);
if (imageUpdateError) throw new Error(imageUpdateError.message);
}
.update({
metadata: { ...metadataRecord(sourceImage.metadata), ...reviewMetadata },
searchable: parsed.reviewClass === "clinical_useful" || parsed.reviewClass === "reference",
})
.eq("id", fact.source_image_id);
if (imageUpdateError) throw new Error(imageUpdateError.message);
}

invalidateRagCachesForOwner(user.id);
Expand Down
6 changes: 4 additions & 2 deletions src/app/api/documents/bulk/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ const bulkMetadataSchema = z.object({
.default({}),
titleEdit: z
.object({
prefix: z.string().trim().max(40).optional(),
suffix: z.string().trim().max(40).optional(),
// Preserve intentional spacing at the title boundary. `editTitle` trims the
// completed title, so outer whitespace is still discarded safely.
prefix: z.string().max(40).optional(),
suffix: z.string().max(40).optional(),
find: z.string().trim().max(80).optional(),
replace: z.string().trim().max(80).optional(),
})
Expand Down
30 changes: 17 additions & 13 deletions src/app/api/ingestion/quality/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,19 @@ function tableRowsFromMetadata(metadata: Record<string, unknown>) {
return Array.isArray(rows) ? rows.length : 0;
}

function groupRowsByDocument<Row extends { document_id: string }>(rows: Row[]) {
const groupedRows = new Map<string, Row[]>();
for (const row of rows) {
const documentRows = groupedRows.get(row.document_id);
if (documentRows) {
documentRows.push(row);
} else {
groupedRows.set(row.document_id, [row]);
}
}
return groupedRows;
}

function itemPriority(item: ReviewItem) {
const severityRank = item.severity === "danger" ? 0 : item.severity === "warning" ? 1 : 2;
const typeRank: Record<ReviewType, number> = {
Expand All @@ -137,19 +150,10 @@ function buildReviewItems(args: {
images: ImageRow[];
}) {
const qualityByDocument = new Map(args.qualityRows.map((row) => [row.document_id, row]));
const jobsByDocument = new Map<string, JobRow[]>();
const stagesByDocument = new Map<string, StageRow[]>();
const pagesByDocument = new Map<string, PageRow[]>();
const imagesByDocument = new Map<string, ImageRow[]>();

for (const row of args.jobs)
jobsByDocument.set(row.document_id, [...(jobsByDocument.get(row.document_id) ?? []), row]);
for (const row of args.stages)
stagesByDocument.set(row.document_id, [...(stagesByDocument.get(row.document_id) ?? []), row]);
for (const row of args.pages)
pagesByDocument.set(row.document_id, [...(pagesByDocument.get(row.document_id) ?? []), row]);
for (const row of args.images)
imagesByDocument.set(row.document_id, [...(imagesByDocument.get(row.document_id) ?? []), row]);
const jobsByDocument = groupRowsByDocument(args.jobs);
const stagesByDocument = groupRowsByDocument(args.stages);
const pagesByDocument = groupRowsByDocument(args.pages);
const imagesByDocument = groupRowsByDocument(args.images);

const items: ReviewItem[] = [];
const pushItem = (document: DocumentRow, item: Omit<ReviewItem, "documentId" | "documentTitle" | "fileName">) => {
Expand Down
21 changes: 12 additions & 9 deletions src/components/ClinicalDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ import { isWeakRelevance } from "@/components/clinical-dashboard/relevance";
import {
answerPayloadIsUsable,
classifyAnswerError,
isAnswerPayload,
isRetryableError,
isRetryableMessage,
isRetryableStatus,
Expand Down Expand Up @@ -346,7 +347,6 @@ async function readAnswerStream(
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let finalPayload: AnswerPayload | null = null;

function processEvent(block: string) {
const lines = block.split(/\r?\n/);
Expand Down Expand Up @@ -398,11 +398,13 @@ async function readAnswerStream(
);
}
if (event === "final") {
finalPayload = data as AnswerPayload;
return true;
if (!isAnswerPayload(data)) {
throw makeSearchError("Answer stream returned an invalid final payload.", 502, true);
}
return data;
}

return false;
return null;
}

while (true) {
Expand All @@ -413,19 +415,20 @@ async function readAnswerStream(
while (separator) {
const block = buffer.slice(0, separator.index).trim();
buffer = buffer.slice(separator.index + separator.length);
if (block && processEvent(block) && finalPayload) {
const finalPayload = block ? processEvent(block) : null;
if (finalPayload) {
await reader.cancel().catch(() => undefined);
return finalPayload as AnswerPayload;
return finalPayload;
}
separator = findSseSeparator(buffer);
}

if (done) break;
}

if (buffer.trim() && processEvent(buffer.trim()) && finalPayload) return finalPayload as AnswerPayload;
if (!finalPayload) throw makeSearchError("Answer stream ended before a final answer was received.", undefined, true);
return finalPayload as AnswerPayload;
const finalPayload = buffer.trim() ? processEvent(buffer.trim()) : null;
if (finalPayload) return finalPayload;
throw makeSearchError("Answer stream ended before a final answer was received.", undefined, true);
}

// Provisional view shown while an answer streams in. The prose is content-preserving (the same
Expand Down
15 changes: 15 additions & 0 deletions src/components/clinical-dashboard/search-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ export { keywordQueryFromNaturalLanguage } from "@/lib/keyword-query";

export type AnswerPayload = RagAnswer & { demoMode?: boolean };

const answerConfidenceValues = new Set<AnswerPayload["confidence"]>(["high", "medium", "low", "unsupported"]);

export function isAnswerPayload(value: unknown): value is AnswerPayload {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
const payload = value as Record<string, unknown>;
return (
typeof payload.answer === "string" &&
typeof payload.grounded === "boolean" &&
answerConfidenceValues.has(payload.confidence as AnswerPayload["confidence"]) &&
Array.isArray(payload.citations) &&
Array.isArray(payload.sources) &&
(payload.demoMode === undefined || typeof payload.demoMode === "boolean")
);
}

export type SearchError = Error & {
status?: number;
retryable?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,8 @@ function CommandDropdown({
onMouseEnter={() => onHoverItem(item.id)}
onMouseDown={(event) => {
event.preventDefault();
item.onSelect();
}}
onClick={item.onSelect}
className="cursor-pointer"
>
{item.render(activeItemId === item.id)}
Expand All @@ -227,8 +227,8 @@ function CommandDropdown({
onMouseEnter={() => onHoverItem(item.id)}
onMouseDown={(event) => {
event.preventDefault();
item.onSelect();
}}
onClick={item.onSelect}
className="cursor-pointer"
>
{item.render(activeItemId === item.id)}
Expand Down Expand Up @@ -502,7 +502,7 @@ export function UniversalSearchCommandSurface({
label: action.label,
onSelect: () => {
onDropdownOpenChange(false);
router.push(action.href);
onCrossMode("answer", trimmedQuery);
},
render: (active) => (
<OptionShell active={active} hint="Answer">
Expand Down
Loading
Loading