Phase 3c: Compose view with chair-position editor
New /cases/[caseNumber]/compose route ports the research analysis +
chair-position editing flow from the vanilla UI onto the Next.js
stack. Reads /api/cases/{n}/research/analysis, renders background
prose in the side column and threshold claims + issues as collapsible
cards in the main column, each with a blur-autosaved chair editor
wired through a TanStack Query mutation with optimistic cache patching
(so concurrent reads don't steal editor focus).
Handles the common "analysis not yet generated" 404 with a dedicated
empty state rather than an error card.
Phase 3 task 85 is now ready for review end-to-end.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
95
web-ui/src/lib/api/research.ts
Normal file
95
web-ui/src/lib/api/research.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Research analysis hooks — reads and mutates the
|
||||
* `analysis-and-research.md` file that backs each case's compose screen.
|
||||
*
|
||||
* Schema mirrors research_md.parse() in the FastAPI backend. Kept as
|
||||
* hand-typed interfaces because the endpoint does not declare a
|
||||
* response_model in the OpenAPI schema.
|
||||
*/
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { apiRequest } from "./client";
|
||||
|
||||
export type ResearchField = {
|
||||
label: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type ResearchSubsection = {
|
||||
id: string; // e.g. "threshold_1" or "issue_3"
|
||||
number: string;
|
||||
title: string;
|
||||
fields: ResearchField[];
|
||||
chair_position?: string;
|
||||
};
|
||||
|
||||
export type ResearchAnalysis = {
|
||||
header?: {
|
||||
date?: string;
|
||||
modified_at?: string;
|
||||
[k: string]: unknown;
|
||||
};
|
||||
represented_party?: string;
|
||||
procedural_background?: string;
|
||||
agreed_facts?: string;
|
||||
disputed_facts?: string;
|
||||
threshold_claims?: ResearchSubsection[];
|
||||
issues?: ResearchSubsection[];
|
||||
conclusions?: string;
|
||||
other_sections?: Array<{ title: string; body: string }>;
|
||||
};
|
||||
|
||||
export const researchKeys = {
|
||||
all: ["research"] as const,
|
||||
analysis: (caseNumber: string) =>
|
||||
[...researchKeys.all, "analysis", caseNumber] as const,
|
||||
};
|
||||
|
||||
export function useResearchAnalysis(caseNumber: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: researchKeys.analysis(caseNumber ?? ""),
|
||||
queryFn: ({ signal }) =>
|
||||
apiRequest<ResearchAnalysis>(
|
||||
`/api/cases/${caseNumber}/research/analysis`,
|
||||
{ signal },
|
||||
),
|
||||
enabled: Boolean(caseNumber),
|
||||
/* No polling — the user is editing; refetching would clobber focus */
|
||||
staleTime: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useSaveChairPosition(caseNumber: string | undefined) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (vars: { sectionId: string; position: string }) =>
|
||||
apiRequest<unknown>(
|
||||
`/api/cases/${caseNumber}/research/analysis/chair-position`,
|
||||
{
|
||||
method: "PATCH",
|
||||
body: { section_id: vars.sectionId, position: vars.position },
|
||||
},
|
||||
),
|
||||
onSuccess: (_res, vars) => {
|
||||
/* Locally patch the cached analysis so other consumers stay in sync
|
||||
without an immediate refetch that would steal focus from the editor. */
|
||||
qc.setQueryData<ResearchAnalysis | undefined>(
|
||||
researchKeys.analysis(caseNumber ?? ""),
|
||||
(prev) => {
|
||||
if (!prev) return prev;
|
||||
const patch = (arr?: ResearchSubsection[]) =>
|
||||
arr?.map((s) =>
|
||||
s.id === vars.sectionId
|
||||
? { ...s, chair_position: vars.position }
|
||||
: s,
|
||||
);
|
||||
return {
|
||||
...prev,
|
||||
threshold_claims: patch(prev.threshold_claims),
|
||||
issues: patch(prev.issues),
|
||||
};
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user