במקום "טיוטת-ביניים-v{N}.docx" — שם מדבר יותר: "טיוטה-טענות_הצדדים_{N}.docx".
הפורמט הסופי ("טיוטה-v{N}") לא משתנה.
- docx_exporter._draft_naming: (prefix, sep) פר-מצב; interim=("טיוטה-טענות_הצדדים","_"),
final=("טיוטה","-v"). glob/parse משתמשים ב-rsplit(sep,1) — נכון גם כשה-prefix
עצמו מכיל "_".
- app.py _generation_output_ready + frontend pickLatestVersioned: matcher-prefix
מעודכן; regex-הגרסה עוגן-לסוף /[_v](\d+)$/ (ה-"_" שבתוך הprefix לא מבלבל).
- docstrings + legal-ceo.md + הערות מעודכנים לשם החדש. types.ts (auto-gen)
יתעדכן ב-api:types.
שינוי-נתון (שם-קובץ), לא layout — בגדר חריג-שער-העיצוב. tsc+eslint+py_compile
ירוקים. הערה: ייצור-השם רץ ב-MCP המקומי (host) → דורש reload; ה-matchers
בקונטיינר (Coolify).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
149 lines
5.4 KiB
TypeScript
149 lines
5.4 KiB
TypeScript
/**
|
||
* Document-generation domain hooks (TaskMaster #214).
|
||
*
|
||
* The "הפקת מסמכים" card in the case "טיוטות והערות" tab fires two deterministic,
|
||
* host-side generation actions through the CEO (structured-action wakeup):
|
||
* • party_claims_summary → an executive summary of the parties' claims
|
||
* • interim_draft → a partial draft (blocks ה–ט, up to the discussion)
|
||
*
|
||
* Generation runs on the host (claude_session → claude -p), NOT in the container,
|
||
* so the trigger endpoints return 202-accepted and the UI POLLS for completion:
|
||
* • summary → GET /api/cases/{n}/research/party-claims-summary (200 = ready)
|
||
* • interim draft → the exports list grows a "טיוטה-טענות_הצדדים_{N}.docx" file
|
||
* (polled via useExports — see exports.ts).
|
||
*
|
||
* The full-decision export ("הפק טיוטת החלטה מלאה") is NOT here — it reuses the
|
||
* existing in-container useExportDocx (exports.ts), no LLM, no wakeup.
|
||
*/
|
||
|
||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||
import { ApiError, apiRequest } from "./client";
|
||
|
||
/** Shape returned by the structured-action wakeup endpoints (#214). */
|
||
export type GenerateTriggerResult = {
|
||
status: "ok" | "skipped" | "error";
|
||
action?: string;
|
||
issue_id?: string;
|
||
ceo_id?: string;
|
||
reason?: string;
|
||
error?: string;
|
||
};
|
||
|
||
/** The two host-side background generation actions (poll target of the status chip). */
|
||
export type GenerateAction = "party_claims_summary" | "interim_draft";
|
||
|
||
export const generateKeys = {
|
||
all: ["generate"] as const,
|
||
partyClaimsSummary: (caseNumber: string) =>
|
||
[...generateKeys.all, "party-claims-summary", caseNumber] as const,
|
||
status: (caseNumber: string, action: GenerateAction) =>
|
||
[...generateKeys.all, "status", action, caseNumber] as const,
|
||
};
|
||
|
||
/**
|
||
* Server-derived background-run status (#227 ג). Reconstructed on the backend
|
||
* from the CEO child issue + its heartbeat_run + the output artifact — so it is
|
||
* correct after navigating away and back; it never depends on the browser having
|
||
* stayed on the page. `started_at`/`finished_at` are server ISO-8601 UTC stamps,
|
||
* so the client renders a correct elapsed time on return.
|
||
*/
|
||
export type GenerateState = "idle" | "queued" | "running" | "done" | "failed";
|
||
|
||
export type GenerateStatus = {
|
||
case_number: string;
|
||
action: GenerateAction;
|
||
state: GenerateState;
|
||
started_at: string | null;
|
||
finished_at: string | null;
|
||
output_ready: boolean;
|
||
detail: string;
|
||
issue_id: string | null;
|
||
};
|
||
|
||
export function useGenerateStatus(
|
||
caseNumber: string | undefined,
|
||
action: GenerateAction,
|
||
) {
|
||
return useQuery<GenerateStatus>({
|
||
queryKey: generateKeys.status(caseNumber ?? "", action),
|
||
queryFn: ({ signal }) =>
|
||
apiRequest<GenerateStatus>(
|
||
`/api/cases/${caseNumber}/generate/${action}/status`,
|
||
{ signal },
|
||
),
|
||
enabled: Boolean(caseNumber),
|
||
// Poll only while a run is in flight; a terminal state stops the interval.
|
||
refetchInterval: (query) => {
|
||
const s = query.state.data?.state;
|
||
return s === "running" || s === "queued" ? 5_000 : false;
|
||
},
|
||
staleTime: 2_000,
|
||
});
|
||
}
|
||
|
||
/* ── Read: party-claims executive summary (poll target for button 1) ──
|
||
404 (not generated yet) is a normal "not ready" state, not an error — we
|
||
coerce it to null so the panel renders the empty/idle state cleanly. */
|
||
export type PartyClaimsSummary = { markdown: string } | null;
|
||
|
||
export function usePartyClaimsSummary(caseNumber: string | undefined) {
|
||
return useQuery<PartyClaimsSummary>({
|
||
queryKey: generateKeys.partyClaimsSummary(caseNumber ?? ""),
|
||
queryFn: async ({ signal }) => {
|
||
try {
|
||
return await apiRequest<{ markdown: string }>(
|
||
`/api/cases/${caseNumber}/research/party-claims-summary`,
|
||
{ signal },
|
||
);
|
||
} catch (e) {
|
||
if (e instanceof ApiError && e.status === 404) return null;
|
||
throw e;
|
||
}
|
||
},
|
||
enabled: Boolean(caseNumber),
|
||
staleTime: 5_000,
|
||
retry: false,
|
||
});
|
||
}
|
||
|
||
/* ── Trigger: party-claims executive summary (button 1) ── */
|
||
export function useGeneratePartyClaimsSummary(caseNumber: string) {
|
||
const qc = useQueryClient();
|
||
return useMutation({
|
||
mutationFn: () =>
|
||
apiRequest<GenerateTriggerResult>(
|
||
`/api/cases/${caseNumber}/generate/party-claims-summary`,
|
||
{ method: "POST" },
|
||
),
|
||
onSuccess: () => {
|
||
// Start polling fresh — the file will appear after the host run completes,
|
||
// and the status chip should pick up the new queued/running run.
|
||
qc.invalidateQueries({
|
||
queryKey: generateKeys.partyClaimsSummary(caseNumber),
|
||
});
|
||
qc.invalidateQueries({
|
||
queryKey: generateKeys.status(caseNumber, "party_claims_summary"),
|
||
});
|
||
},
|
||
});
|
||
}
|
||
|
||
/* ── Trigger: interim ("party-claims") partial draft (button 2) ── */
|
||
export function useGenerateInterimDraft(caseNumber: string) {
|
||
const qc = useQueryClient();
|
||
return useMutation({
|
||
mutationFn: () =>
|
||
apiRequest<GenerateTriggerResult>(
|
||
`/api/cases/${caseNumber}/generate/interim-draft`,
|
||
{ method: "POST" },
|
||
),
|
||
// The result lands in the exports list (טיוטה-טענות_הצדדים_*.docx, polled by
|
||
// useExports); re-arm the status chip so it shows the new run immediately.
|
||
onSuccess: () => {
|
||
qc.invalidateQueries({
|
||
queryKey: generateKeys.status(caseNumber, "interim_draft"),
|
||
});
|
||
},
|
||
});
|
||
}
|