From 03b25bc2732c93d3ffa3d8497243855f7ec95555 Mon Sep 17 00:00:00 2001 From: Chaim Date: Sat, 11 Apr 2026 16:09:09 +0000 Subject: [PATCH] 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) --- .../app/cases/[caseNumber]/compose/page.tsx | 198 ++++++++++++++++++ .../src/components/compose/chair-editor.tsx | 96 +++++++++ .../components/compose/subsection-card.tsx | 88 ++++++++ web-ui/src/lib/api/research.ts | 95 +++++++++ 4 files changed, 477 insertions(+) create mode 100644 web-ui/src/app/cases/[caseNumber]/compose/page.tsx create mode 100644 web-ui/src/components/compose/chair-editor.tsx create mode 100644 web-ui/src/components/compose/subsection-card.tsx create mode 100644 web-ui/src/lib/api/research.ts diff --git a/web-ui/src/app/cases/[caseNumber]/compose/page.tsx b/web-ui/src/app/cases/[caseNumber]/compose/page.tsx new file mode 100644 index 0000000..8d9950a --- /dev/null +++ b/web-ui/src/app/cases/[caseNumber]/compose/page.tsx @@ -0,0 +1,198 @@ +"use client"; + +import { use } from "react"; +import Link from "next/link"; +import { AppShell } from "@/components/app-shell"; +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { SubsectionCard } from "@/components/compose/subsection-card"; +import { useCase } from "@/lib/api/cases"; +import { useResearchAnalysis } from "@/lib/api/research"; + +function ProseSection({ title, content }: { title: string; content?: string }) { + if (!content?.trim()) return null; + return ( +
+

+ {title} +

+

+ {content.trim()} +

+
+ ); +} + +export default function ComposePage({ + params, +}: { + params: Promise<{ caseNumber: string }>; +}) { + const { caseNumber } = use(params); + const caseQuery = useCase(caseNumber); + const analysis = useResearchAnalysis(caseNumber); + + const isNotFound = + analysis.error instanceof Error && + /404|לא נמצא|טרם בוצע/.test(analysis.error.message); + + return ( + +
+ {/* Header strip */} +
+
+ +

ניתוח משפטי וכתיבת עמדה

+ {caseQuery.data?.title && ( +

+ {caseQuery.data.title} +

+ )} +
+ +
+ +
+ + {analysis.isPending ? ( + + + + + + + + + ) : isNotFound ? ( + + +
+

+ טרם בוצע ניתוח משפטי לתיק זה +

+

+ לאחר שקובץ analysis-and-research.md ייווצר, תוכלי + לערוך כאן את עמדת הוועדה לכל טענת סף וסוגיה. +

+
+
+ ) : analysis.error ? ( + + +

{analysis.error.message}

+
+
+ ) : analysis.data ? ( +
+ {/* Main editable column */} +
+ {/* Threshold claims */} + {analysis.data.threshold_claims && + analysis.data.threshold_claims.length > 0 && ( +
+
+

טענות סף

+ + {analysis.data.threshold_claims.length} + +
+
+ {analysis.data.threshold_claims.map((tc, i) => ( + + ))} +
+
+ )} + + {/* Issues */} + {analysis.data.issues && analysis.data.issues.length > 0 && ( +
+
+

סוגיות להכרעה

+ + {analysis.data.issues.length} + +
+
+ {analysis.data.issues.map((iss) => ( + + ))} +
+
+ )} + + {(!analysis.data.threshold_claims?.length && + !analysis.data.issues?.length) && ( + + + לא נמצאו טענות סף או סוגיות בניתוח זה. + + + )} +
+ + {/* Side column: background prose + conclusions */} + +
+ ) : null} +
+
+ ); +} diff --git a/web-ui/src/components/compose/chair-editor.tsx b/web-ui/src/components/compose/chair-editor.tsx new file mode 100644 index 0000000..c122796 --- /dev/null +++ b/web-ui/src/components/compose/chair-editor.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { useSaveChairPosition } from "@/lib/api/research"; + +/* + * Chair-position editor for a single threshold claim or issue. + * + * Autosaves on blur, with an optimistic in-memory "last saved" value so the + * user sees immediate feedback. No debounced per-keystroke save — the user + * writes in long paragraphs and the backend writes to a file, so per-blur + * is the right granularity (matches the vanilla UI's behavior). + */ + +type SaveState = + | { kind: "idle" } + | { kind: "saving" } + | { kind: "saved"; at: Date } + | { kind: "error"; message: string }; + +export function ChairEditor({ + caseNumber, + sectionId, + initialValue, +}: { + caseNumber: string; + sectionId: string; + initialValue: string; +}) { + const [value, setValue] = useState(initialValue); + const [state, setState] = useState({ kind: "idle" }); + const lastSaved = useRef(initialValue); + const mutate = useSaveChairPosition(caseNumber); + + /* Reset when the upstream analysis refetches (e.g. after initial load) */ + useEffect(() => { + setValue(initialValue); + lastSaved.current = initialValue; + }, [initialValue]); + + const save = async () => { + const trimmed = value.trim(); + if (trimmed === lastSaved.current.trim()) return; + setState({ kind: "saving" }); + try { + await mutate.mutateAsync({ sectionId, position: trimmed }); + lastSaved.current = trimmed; + setState({ kind: "saved", at: new Date() }); + } catch (e) { + setState({ + kind: "error", + message: e instanceof Error ? e.message : "שגיאה בשמירה", + }); + } + }; + + return ( +
+
+ + עמדת ועדת הערר + + +
+