Trigger appraiser-facts extraction from the UI
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 36s

Extraction is expensive (multi-minute LLM calls) and runs across every
appraisal in the case at once, so we don't kick it off silently on every
tag save. The chair tags the appraisals, then runs extraction once when
they're ready.

- New POST /api/cases/{n}/extract-appraiser-facts endpoint returns the
  extractor's summary as-is: status=completed with fact counts and
  conflicts, or status=sides_missing with the list of still-untagged
  appraisal docs.
- DocumentTypeEditor now has a two-phase popover. After a successful
  save on an appraisal doc, the body switches to a confirmation view
  with a "חלץ עובדות שמאיות עכשיו" button. The result (completed /
  sides_missing / no_appraisals / error) renders in the same popover
  so the chair sees exactly which appraisals still need tagging
  without closing and reopening anything.
- useExtractAppraiserFacts React-Query mutation invalidates the case
  detail on success so downstream views (conflict rendering in
  block-tet context) pick up the new facts.
This commit is contained in:
2026-04-19 09:42:49 +00:00
parent c536ed0e63
commit eac7784b87
3 changed files with 294 additions and 69 deletions

View File

@@ -136,6 +136,61 @@ export function usePatchDocument(caseNumber: string) {
}
// ── Extract appraiser facts (on-demand, per case) ──────────────────
export type ExtractAppraiserFactsResponse =
| {
status: "completed";
appraisal_count: number;
total_facts: number;
conflicts: unknown[];
by_document?: unknown[];
}
| {
status: "no_appraisals";
appraisal_count: 0;
total_facts: 0;
conflicts: unknown[];
}
| {
status: "sides_missing";
appraisal_count: number;
missing: { document_id: string; title: string; current_side: string }[];
message: string;
};
async function extractAppraiserFacts(
caseNumber: string,
): Promise<ExtractAppraiserFactsResponse> {
const res = await fetch(
`/api/cases/${encodeURIComponent(caseNumber)}/extract-appraiser-facts`,
{ method: "POST" },
);
const contentType = res.headers.get("content-type") ?? "";
const parsed = contentType.includes("application/json")
? await res.json().catch(() => null)
: await res.text().catch(() => null);
if (!res.ok) {
throw new ApiError(
`Extraction failed with ${res.status}`,
res.status,
parsed,
);
}
return parsed as ExtractAppraiserFactsResponse;
}
export function useExtractAppraiserFacts(caseNumber: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: () => extractAppraiserFacts(caseNumber),
onSuccess: () => {
qc.invalidateQueries({ queryKey: casesKeys.detail(caseNumber) });
},
});
}
export function useProgress(taskId: string | null) {
const [event, setEvent] = useState<ProgressEvent | null>(null);