diff --git a/web-ui/src/app/cases/[caseNumber]/compose/page.tsx b/web-ui/src/app/cases/[caseNumber]/compose/page.tsx
index aed02e5..6d7dbbd 100644
--- a/web-ui/src/app/cases/[caseNumber]/compose/page.tsx
+++ b/web-ui/src/app/cases/[caseNumber]/compose/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { use } from "react";
+import { use, useRef, useState } from "react";
import Link from "next/link";
import { AppShell } from "@/components/app-shell";
import { Card, CardContent } from "@/components/ui/card";
@@ -25,6 +25,91 @@ function ProseSection({ title, content }: { title: string; content?: string }) {
);
}
+function AnalysisActions({
+ caseNumber,
+ hasAnalysis,
+ onUploaded,
+}: {
+ caseNumber: string;
+ hasAnalysis: boolean;
+ onUploaded: () => void;
+}) {
+ const fileRef = useRef(null);
+ const [uploading, setUploading] = useState(false);
+ const [uploadMsg, setUploadMsg] = useState<{ ok: boolean; text: string } | null>(null);
+
+ async function handleUpload(file: File) {
+ setUploading(true);
+ setUploadMsg(null);
+ try {
+ const form = new FormData();
+ form.append("file", file);
+ const res = await fetch(`/api/cases/${caseNumber}/research/analysis/upload`, {
+ method: "PUT",
+ body: form,
+ });
+ const data = await res.json();
+ if (!res.ok) {
+ setUploadMsg({ ok: false, text: data.detail || "שגיאה בהעלאה" });
+ return;
+ }
+ setUploadMsg({
+ ok: true,
+ text: `הקובץ הועלה בהצלחה — ${data.sections.threshold_claims} טענות סף, ${data.sections.issues} סוגיות`,
+ });
+ onUploaded();
+ } catch {
+ setUploadMsg({ ok: false, text: "שגיאת רשת" });
+ } finally {
+ setUploading(false);
+ if (fileRef.current) fileRef.current.value = "";
+ }
+ }
+
+ return (
+
+ {uploadMsg && (
+
+ {uploadMsg.text}
+
+ )}
+ {
+ const f = e.target.files?.[0];
+ if (f) handleUpload(f);
+ }}
+ />
+
+ {hasAnalysis && (
+
+ )}
+
+
+ );
+}
+
export default function ComposePage({
params,
}: {
@@ -78,24 +163,7 @@ export default function ComposePage({
)}
-
- {analysis.data && (
-
- )}
-
-
+ analysis.refetch()} />
diff --git a/web/app.py b/web/app.py
index fd2bfd1..9cafc60 100644
--- a/web/app.py
+++ b/web/app.py
@@ -1633,6 +1633,91 @@ async def api_research_analysis_download(case_number: str):
)
+@app.put("/api/cases/{case_number}/research/analysis/upload")
+async def api_research_analysis_upload(
+ case_number: str,
+ file: UploadFile = File(...),
+):
+ """Upload an updated analysis-and-research.md file.
+
+ Validates that:
+ 1. The file is markdown (text)
+ 2. It can be parsed by the research_md parser
+ 3. It contains at least one structural section (issues or threshold_claims)
+ 4. The case number in the file matches the URL
+
+ On success, backs up the existing file and replaces it.
+ """
+ if not file.filename or not file.filename.endswith(".md"):
+ raise HTTPException(400, "הקובץ חייב להיות בפורמט Markdown (.md)")
+
+ content = await file.read()
+ if len(content) > 5 * 1024 * 1024:
+ raise HTTPException(400, "הקובץ גדול מדי — מקסימום 5MB")
+
+ try:
+ text = content.decode("utf-8")
+ except UnicodeDecodeError:
+ raise HTTPException(400, "הקובץ חייב להיות בקידוד UTF-8")
+
+ if len(text.strip()) < 100:
+ raise HTTPException(400, "הקובץ ריק מדי — נראה שחסר תוכן")
+
+ # Write to a temp file so parse() can work on it
+ dest = _research_file_path(case_number)
+ tmp = dest.with_suffix(".md.upload-tmp")
+ try:
+ dest.parent.mkdir(parents=True, exist_ok=True)
+ tmp.write_text(text, encoding="utf-8")
+ parsed = research_md.parse(tmp)
+ except Exception as e:
+ tmp.unlink(missing_ok=True)
+ raise HTTPException(
+ 400,
+ f"שגיאה בפרסור הקובץ — המבנה לא תקין: {e}",
+ )
+
+ # Validate structure
+ issues = parsed.get("issues", [])
+ thresholds = parsed.get("threshold_claims", [])
+ if not issues and not thresholds:
+ tmp.unlink(missing_ok=True)
+ raise HTTPException(
+ 400,
+ "הקובץ חייב להכיל לפחות סעיף אחד של טענות סף או סוגיות להכרעה",
+ )
+
+ # Validate case number matches
+ file_case = parsed.get("header", {}).get("case_number", "")
+ if file_case and file_case != case_number:
+ tmp.unlink(missing_ok=True)
+ raise HTTPException(
+ 400,
+ f"מספר התיק בקובץ ({file_case}) לא תואם לתיק הנוכחי ({case_number})",
+ )
+
+ # Backup existing file
+ if dest.exists():
+ backup_dir = dest.parent / "backup"
+ backup_dir.mkdir(exist_ok=True)
+ ts = time.strftime("%Y%m%d-%H%M%S")
+ backup_path = backup_dir / f"analysis-and-research-{ts}.md"
+ shutil.copy2(dest, backup_path)
+
+ # Replace with uploaded file
+ tmp.replace(dest)
+
+ return {
+ "status": "ok",
+ "sections": {
+ "threshold_claims": len(thresholds),
+ "issues": len(issues),
+ "has_conclusions": bool(parsed.get("conclusions", "").strip()),
+ },
+ "file_size": len(content),
+ }
+
+
class ChairPositionRequest(BaseModel):
section_id: str
position: str = ""