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 = ""