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:
47
web/app.py
47
web/app.py
@@ -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 ──
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user