Research analysis cards with inline chair-position editor

New feature on case view: the analysis-and-research.md produced by the
legal-analyst agent is now rendered as structured cards in the UI,
with inline editing of "עמדת ועדת הערר" that writes directly back to
the markdown file (atomic rename).

Backend (research_md.py):
- parse(Path) → dict with header, prose sections, threshold_claims[],
  issues[], conclusions, other_sections
- Tolerant field extractor handles both block ("**LABEL:**\ncontent")
  and inline ("**LABEL:** content") variants
- Detects [ימולא ע"י יו"ר הוועדה] placeholder → empty chair_position
- update_chair_position(path, section_id, text) locates the exact
  subsection by ordinal, replaces or appends the chair field, writes
  atomically via temp file + os.replace
- Section IDs: threshold_N / issue_N (1-based)

Endpoints:
- GET /api/cases/{n}/research/analysis — returns parsed JSON or 404
- PATCH /api/cases/{n}/research/analysis/chair-position — {section_id, position}

Frontend (#page-case):
- New card "ניתוח משפטי ומחקר" below local-files card
- Prose sections as justified text panels (background + gold border)
- Threshold claims and issues as collapsible <details> items with
  gold right-border on open, numbered pills
- Each item shows all extracted fields with label above content
- Chair position editor: gold-wash background, 📝 icon label, textarea
  with placeholder prompt
- onblur → PATCH with save indicator:  שומר → ✓ נשמר HH:MM → fade
- Status pill next to each item title: "ממתין לעמדה" / "✓ עמדה נקבעה"
- First threshold claim opens by default, rest closed
- Card hidden entirely when no analysis file exists (404)

Tested against real file: case 1033-25 with 3 threshold claims and
6 issues, all chair positions correctly empty, update writes only the
targeted section, atomic rewrite preserves all other content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-11 12:47:36 +00:00
parent ffa089e1df
commit 753fe0d57d
3 changed files with 798 additions and 1 deletions

View File

@@ -28,7 +28,7 @@ from pydantic import BaseModel
import asyncpg
from legal_mcp import config
from legal_mcp.services import chunker, db, embeddings, extractor, processor, proofreader
from legal_mcp.services import chunker, db, embeddings, extractor, processor, proofreader, research_md
from legal_mcp.tools import cases as cases_tools, search as search_tools, workflow as workflow_tools, drafting as drafting_tools
# Import integration clients (same directory)
@@ -1578,6 +1578,51 @@ async def api_read_local_file(case_number: str, folder: str, filename: str):
return FileResponse(path, media_type="text/plain; charset=utf-8", filename=filename)
# ── Research analysis (analysis-and-research.md) — parse + edit ────
def _research_file_path(case_number: str) -> Path:
"""Resolve analysis-and-research.md path for a case."""
case_dir = config.find_case_dir(case_number)
return case_dir / "documents" / "research" / "analysis-and-research.md"
@app.get("/api/cases/{case_number}/research/analysis")
async def api_research_analysis(case_number: str):
"""Return parsed structure of analysis-and-research.md for UI rendering."""
path = _research_file_path(case_number)
if not path.exists():
raise HTTPException(404, "טרם בוצע ניתוח משפטי לתיק זה")
try:
return research_md.parse(path)
except Exception as e:
logger.exception("Failed to parse %s", path)
raise HTTPException(500, f"שגיאה בעיבוד הקובץ: {e}")
class ChairPositionRequest(BaseModel):
section_id: str
position: str = ""
@app.patch("/api/cases/{case_number}/research/analysis/chair-position")
async def api_research_chair_position(case_number: str, req: ChairPositionRequest):
"""Update the chair_position field of a specific subsection, writing
directly to analysis-and-research.md (atomic rename)."""
path = _research_file_path(case_number)
if not path.exists():
raise HTTPException(404, "הקובץ לא נמצא")
if not re.match(r"^(threshold|issue)_\d+$", req.section_id):
raise HTTPException(400, "section_id לא תקין")
try:
return research_md.update_chair_position(path, req.section_id, req.position)
except ValueError as e:
raise HTTPException(404, str(e))
except Exception as e:
logger.exception("Failed to update chair position")
raise HTTPException(500, f"שגיאה בשמירה: {e}")
# ── Exports API — drafts, versions, download, upload, mark-final ──