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 ( +
+
+ + עמדת ועדת הערר + + +
+