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>
96 lines
2.8 KiB
TypeScript
96 lines
2.8 KiB
TypeScript
/**
|
|
* 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),
|
|
};
|
|
},
|
|
);
|
|
},
|
|
});
|
|
}
|