16 Commits

Author SHA1 Message Date
437472be85 Add build number and semver tags to CI images
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 26s
Every push to main tags with latest + build-N (run number).
Pushing a git tag like v1.0.0 also tags the image with that version.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 19:15:02 +00:00
fdbf22c699 Add download button for analysis-and-research.md
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m22s
New GET /api/cases/{n}/research/analysis/download endpoint returns the
raw markdown file. UI adds a "הורד ניתוח" button next to "חזרה לתיק"
on the compose page, visible only when analysis data is loaded.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 19:08:07 +00:00
2d0e987803 Add missing case_precedents CRUD functions to db module
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 3m14s
Four functions were called by tools/precedents.py but never implemented
in services/db.py: create_case_precedent, list_case_precedents,
delete_case_precedent, search_precedent_library. This caused 500 errors
on the /api/cases/{n}/precedents endpoint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:44:50 +00:00
35276eab41 Fix CI: use coolify network for job containers
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 7s
Job containers were on isolated network, couldn't reach Coolify API.
Now runner config sets container.network=coolify and curl targets
http://coolify:8080 (internal container name).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:33:34 +00:00
ef448be530 Trigger CI/CD workflow test
Some checks failed
Build & Deploy / build-and-deploy (push) Failing after 5m17s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:27:02 +00:00
1d2d9c71d8 Fix duplicate Docker socket mount in CI workflow
Some checks failed
Build & Deploy / build-and-deploy (push) Failing after 1m7s
Runner already passes Docker socket to job containers —
explicit container.volumes caused duplicate mount error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:59 +00:00
5eab006780 Add Gitea Actions CI/CD: build image + trigger Coolify deploy
Some checks failed
Build & Deploy / build-and-deploy (push) Failing after 46s
On push to main, the workflow builds a Docker image, pushes to
Gitea Container Registry, then triggers Coolify to pull and redeploy.
Replaces the old Dockerfile-build-on-deploy approach.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:23:33 +00:00
bc1456672b Fix document scroll and preview dialog
ScrollArea (Radix) injected display:table on viewport, preventing
scroll — replaced with plain div + overflow-y-auto. Preview dialog
never loaded text because onOpenChange doesn't fire on initial mount —
replaced with useEffect that fetches on open.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:58:22 +00:00
2b431e75ab Add document preview, delete, and fix scroll in documents panel
Documents tab was limited to ~9 visible items due to fixed max-height
without overflow-hidden. Now uses 70vh with proper overflow. Added
click-to-preview (shows extracted text in dialog) and delete button
with confirmation dialog + backend DELETE endpoint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:45:01 +00:00
2b988fd805 Add UI updates PRD for task master
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:54:32 +00:00
62a67e3f31 Add status icons, descriptions, status guide, manual status changer, and merge action buttons into overview tab
- StatusBadge: added icons (lucide-react) and Hebrew descriptions for all 13 statuses
- WorkflowTimeline: added phase icons and current-status description display
- StatusGuide: new collapsible component showing all statuses grouped by phase with explanations
- StatusChanger: new dropdown for manual status override on the case detail sidebar
- Case detail page: merged action buttons into overview tab, removed separate actions tab

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 15:19:28 +00:00
bf595975bf Fix start.sh: redirect uvicorn output to Docker logs
uvicorn was running in background with no output capture,
making it impossible to debug crashes. Now redirects stderr
to stdout and checks if uvicorn started successfully.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:55:04 +00:00
626d39d1bb Fix Dockerfile: use python:3.12-slim for pre-built wheels
pymupdf and other native deps need compilation on Alpine.
Switch to Debian-based python:3.12-slim which has pre-built
wheels available, avoiding the need for gcc/build-essential.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:36:16 +00:00
94bc66d7c1 Bundle FastAPI backend into Next.js Docker container
The Next.js app was proxying /api/* to the old Flask/FastAPI server
at legal-ai.nautilus.marcusgroup.org. When that server went down,
the Next.js app's API calls failed with 503.

Now both services run in the same container:
- FastAPI (uvicorn) on :8000 — the API backend
- Next.js (node) on :3000 — proxies /api/* to localhost:8000

Changes:
- Dockerfile: multi-stage build with Python 3.12 + Node.js
- next.config.ts: default proxy target is now 127.0.0.1:8000
- start.sh: launches uvicorn in background + node in foreground
- pyproject.toml: add fastapi + uvicorn as explicit deps

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:33:52 +00:00
cc50f0ffde Fix CEO status map — align with actual statuses written by agents
The status map was using informal descriptions ("מסמכים הוגהו")
instead of actual DB values. Now each row shows:
- The exact status string in cases.status
- Which agent sets it
- What the CEO should do next

New statuses added: proofread, analyst_verified, research_complete,
qa_passed, qa_failed, exported. Removed ambiguous conditions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:05:54 +00:00
3f6a130cf9 Make all agent instructions self-contained — no reliance on hope
Every agent now has explicit instructions in its own definition file,
not just in HEARTBEAT.md. An agent following only its own step-by-step
instructions will do the right thing on any new case.

All 6 non-CEO agents: explicit wakeup CEO block in completion step
  (curl API + psql fallback, with agent name customized)

legal-ceo.md: issue template for analyst with 5 mandatory items
  (document mapping table, no-extract list, split large docs,
   wakeup CEO, blocked if failed)

legal-writer.md: explicit Read of decision-methodology.md as step 1
  (before case_get, not just "read before starting")

legal-qa.md: methodology_compliance severity → critical
  (was warning — decisions without syllogisms/steel-man now blocked)

legal-proofreader.md: added case_update tool + status='proofread'
  (was missing entirely — CEO couldn't know proofreading was done)

legal-researcher.md: added case_update + mail notification
  (was missing — CEO couldn't know research was done)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:17:44 +00:00
24 changed files with 1024 additions and 109 deletions

View File

@@ -206,6 +206,26 @@ FROM documents d WHERE d.case_id = '{case_id}' AND d.doc_type IN ('appeal', 'res
"סיכום: X סוגיות זוהו, Y שאלות מחקר הופקו. נדרשת ביקורתך לפני המשך."
```
### העֵר את העוזר המשפטי (CEO) — חובה!
```bash
curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
"$PAPERCLIP_API_URL/api/agents/752cebdd-6748-4a04-aacd-c7ab0294ef33/wake" \
-d '{"reason": "מנתח משפטי סיים משימה [issue-id] בסטטוס [done/blocked]"}'
```
אם ה-API לא עובד:
```bash
PGPASSWORD="paperclip" psql -h 127.0.0.1 -p 54329 -U paperclip -d paperclip -c "
INSERT INTO agent_wakeup_requests (company_id, agent_id, source, reason, status, requested_by_actor_type)
VALUES (
(SELECT company_id FROM agents WHERE id = '\$PAPERCLIP_AGENT_ID'),
'752cebdd-6748-4a04-aacd-c7ab0294ef33',
'agent_completion',
'מנתח משפטי סיים משימה — נדרשת בדיקה',
'pending', 'agent'
);"
```
**אם בדיקות שלב 6 נכשלו** — סטטוס issue = "blocked", פרסם comment עם פירוט מה נכשל, שלח מייל לחיים.
## מבנה הפלט המלא — analysis-and-research.md

View File

@@ -284,21 +284,32 @@ tools:
## מפת סטטוסים
| סטטוס | פעולה |
|--------|-------|
| new + יש מסמכים + לא הוגהו | → צור issue למגיה מסמכים (410c0167) |
| new + מסמכים הוגהו + אין claims | → צור issue למנתח משפטי |
| new + יש claims + לא עבר אימות מנתח | → שלב A (אימות איכות פלט מנתח) |
| analyst_verified + יש claims + יש מחקר | → שלב B (סיכום + סיווג + שאלת תוצאה) |
| outcome_set + אין claim_handling | → שלב B המשך (טבלת טיפול בטענות) |
| outcome_set + יש claim_handling | → שלב C (כיוונים סילוגיסטיים) |
| brainstorming + comment מחיים | → שלב D (אימות שלמות + approve + הפעל כותב) |
| direction_approved + chair_directions שלם | → ודא שכותב עובד |
| direction_approved + chair_directions חסר |חזור לשלב D (השלמה מול חיים) |
| drafted | → צור issue לבודק איכות |
| qa_review pass | → שלב F (export via מייצא טיוטה d0dc703b) |
| qa_review fail — בעיה טכנית | צור issue תיקון לכותב |
| qa_review fail — בעיה מתודולוגית | → חזור לשלב C/D |
**סטטוסים של התיק (`cases.status`) — כל סטטוס מתאים לפעולה אחת בדיוק:**
| סטטוס | מי שינה לזה | פעולה הבאה |
|--------|-------------|------------|
| `new` | (יצירת תיק) | → בדוק extraction_status של מסמכים. אם יש `pending` → צור issue למגיה (410c0167). אם כולם `completed`/`proofread` → צור issue למנתח |
| `proofread` | מגיה | → צור issue למנתח משפטי (ראה תבנית למטה) |
| `documents_ready` | מנתח | → שלב A (בדיקות שלמות + שליליות + מתודולוגיה). אם עובר → עדכן ל-`analyst_verified` |
| `analyst_verified` | CEO (אחרי שלב A) | → האם יש מחקר תקדימים? אם לא → צור issue לחוקר (35022af0). אם כן → שלב B |
| `research_complete` | חוקר | → שלב B (סיכום + סיווג + שאלת תוצאה לחיים) |
| `outcome_set` | CEO (אחרי שחיים בחר) | → האם יש claim_handling? אם לא → שלב B המשך (טבלת bundle/skip). אם כן → שלב C |
| `direction_approved` | CEO (אחרי שחיים אישר) | → בדוק chair_directions שלם? אם כןצור issue לכותב (7ed8686f). אם חסר → חזור לחיים |
| `drafted` | כותב | → צור issue לבודק איכות (1a5b229e) |
| `qa_passed` | QA | → צור issue למייצא (d0dc703b) |
| `qa_failed` | QA | → בעיה טכנית → issue תיקון לכותב. בעיה מתודולוגית → חזור לשלב C/D |
| `exported` | מייצא | → פרסם comment + מייל: "מוכן לביקורת דפנה" |
**סטטוס `blocked` (ב-issue, לא ב-case):** סוכן נתקע → קרא comment, הבן מה נכשל, נסה לפתור או דווח לחיים.
---
**תבנית issue למנתח — חובה בכל תיק:**
1. **טבלת מיפוי מסמכים** — לכל מסמך: שם, claim_type, party_role. בנה מ-`document_list`.
2. **רשימת מסמכים שלא לחלץ מהם** (reference, plan, decision, court_decision)
3. **הנחיה לפיצול מסמכים גדולים** — מעל 15,000 תווים → חלץ בחלקים
4. **הנחיה לשלוח wakeup ל-CEO בסיום**
5. **הנחיה לסיים כ-blocked אם מסמך נכשל**
## כללים

View File

@@ -74,6 +74,26 @@ tools:
- ממצאי הבדיקה הסופית (אם היו הערות)
- גודל הקובץ
### העֵר את העוזר המשפטי (CEO) — חובה!
```bash
curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
"$PAPERCLIP_API_URL/api/agents/752cebdd-6748-4a04-aacd-c7ab0294ef33/wake" \
-d '{"reason": "מייצא טיוטה סיים משימה [issue-id] בסטטוס [done/blocked]"}'
```
אם ה-API לא עובד:
```bash
PGPASSWORD="paperclip" psql -h 127.0.0.1 -p 54329 -U paperclip -d paperclip -c "
INSERT INTO agent_wakeup_requests (company_id, agent_id, source, reason, status, requested_by_actor_type)
VALUES (
(SELECT company_id FROM agents WHERE id = '\$PAPERCLIP_AGENT_ID'),
'752cebdd-6748-4a04-aacd-c7ab0294ef33',
'agent_completion',
'מייצא טיוטה סיים משימה — נדרשת בדיקה',
'pending', 'agent'
);"
```
## כללים קריטיים
1. **לעולם אל תייצא בלי בדיקה** — תמיד הרץ validate_decision קודם

View File

@@ -11,6 +11,7 @@ tools:
- mcp__legal-ai__case_get
- mcp__legal-ai__document_list
- mcp__legal-ai__document_get_text
- mcp__legal-ai__case_update
---
# מגיה מסמכים — סוכן הגהת OCR
@@ -68,8 +69,11 @@ psql -h localhost -p 5432 -U "${DB_USER:-legal_ai}" -d "${DB_NAME:-legal_ai}" \
```
אם עדכון DB לא אפשרי, עדכן רק את הקובץ ודווח.
### שלב 5: דיווח
פרסם comment ב-Paperclip עם:
### שלב 5: עדכון סטטוס ודיווח
1. **עדכן סטטוס**: `case_update(case_number, status='proofread')`
2. פרסם comment ב-Paperclip עם:
```
## דוח הגהת מסמכים — תיק {case_number}
@@ -95,3 +99,23 @@ psql -h localhost -p 5432 -U "${DB_USER:-legal_ai}" -d "${DB_NAME:-legal_ai}" \
4. **דווח מקומות מסופקים** — סמן `[?]` ותן לאדם להחליט
5. **אל תמציא טקסט** — אם חסר משהו, סמן `[...]` ואל תנחש
6. **קרא את כל המסמך** — לפעמים הקשר ממסמך שלם עוזר להבין מילה שבורה
### העֵר את העוזר המשפטי (CEO) — חובה!
```bash
curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
"$PAPERCLIP_API_URL/api/agents/752cebdd-6748-4a04-aacd-c7ab0294ef33/wake" \
-d '{"reason": "מגיה מסמכים סיים משימה [issue-id] בסטטוס [done/blocked]"}'
```
אם ה-API לא עובד:
```bash
PGPASSWORD="paperclip" psql -h 127.0.0.1 -p 54329 -U paperclip -d paperclip -c "
INSERT INTO agent_wakeup_requests (company_id, agent_id, source, reason, status, requested_by_actor_type)
VALUES (
(SELECT company_id FROM agents WHERE id = '\$PAPERCLIP_AGENT_ID'),
'752cebdd-6748-4a04-aacd-c7ab0294ef33',
'agent_completion',
'מגיה מסמכים סיים משימה — נדרשת בדיקה',
'pending', 'agent'
);"
```

View File

@@ -76,7 +76,7 @@ tools:
| משקלות | warning | מדווח, לא חוסם |
| כפילות | warning | מדווח, לא חוסם |
| מספור | warning | מדווח, לא חוסם |
| מתודולוגיה | warning | מדווח, לא חוסם |
| מתודולוגיה | critical | חוסם ייצוא |
## תהליך עבודה
@@ -104,3 +104,23 @@ tools:
- רשימת שגיאות מפורטת (אם יש)
- האם מותר לייצא (כל הקריטיים pass?)
- עדכן סטטוס ל-qa_review (אם נכשל) או drafted (אם עבר)
### העֵר את העוזר המשפטי (CEO) — חובה!
```bash
curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
"$PAPERCLIP_API_URL/api/agents/752cebdd-6748-4a04-aacd-c7ab0294ef33/wake" \
-d '{"reason": "בודק איכות סיים משימה [issue-id] בסטטוס [done/blocked]"}'
```
אם ה-API לא עובד:
```bash
PGPASSWORD="paperclip" psql -h 127.0.0.1 -p 54329 -U paperclip -d paperclip -c "
INSERT INTO agent_wakeup_requests (company_id, agent_id, source, reason, status, requested_by_actor_type)
VALUES (
(SELECT company_id FROM agents WHERE id = '\$PAPERCLIP_AGENT_ID'),
'752cebdd-6748-4a04-aacd-c7ab0294ef33',
'agent_completion',
'בודק איכות סיים משימה — נדרשת בדיקה',
'pending', 'agent'
);"
```

View File

@@ -75,7 +75,17 @@ tools:
2. בנה ציר זמן כרונולוגי של ההליך
### שלב 5: דיווח — חובה!
פרסם comment ב-Paperclip עם:
1. **עדכן סטטוס**: `case_update(case_number, status='research_complete')`
2. **שלח מייל**:
```bash
python3 /home/chaim/legal-ai/scripts/notify.py \
"מחקר תקדימים הושלם — ערר {case_number}" \
"סיכום: X פסקי דין נותחו, Y תכניות מופו. נדרשת ביקורתך לפני המשך."
```
3. פרסם comment ב-Paperclip עם:
- סיכום כל פסק דין (2-3 שורות לכל אחד)
- מיפוי הוראות תכנית רלוונטיות
- ציר זמן ההליך
@@ -84,6 +94,26 @@ tools:
- **תקדים**: אילו פסקי דין הכי חזקים (עם ציון היררכיה ומעמד — הלכה/אגב)
- **מדיניות**: אילו שיקולים תכנוניים עולים מהחומר
### העֵר את העוזר המשפטי (CEO) — חובה!
```bash
curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
"$PAPERCLIP_API_URL/api/agents/752cebdd-6748-4a04-aacd-c7ab0294ef33/wake" \
-d '{"reason": "חוקר תקדימים סיים משימה [issue-id] בסטטוס [done/blocked]"}'
```
אם ה-API לא עובד:
```bash
PGPASSWORD="paperclip" psql -h 127.0.0.1 -p 54329 -U paperclip -d paperclip -c "
INSERT INTO agent_wakeup_requests (company_id, agent_id, source, reason, status, requested_by_actor_type)
VALUES (
(SELECT company_id FROM agents WHERE id = '\$PAPERCLIP_AGENT_ID'),
'752cebdd-6748-4a04-aacd-c7ab0294ef33',
'agent_completion',
'חוקר תקדימים סיים משימה — נדרשת בדיקה',
'pending', 'agent'
);"
```
## כללים
- **דיוק** — ציין מספרי סעיפים, תאריכים, שמות שופטים
- **רלוונטיות** — התמקד במה שרלוונטי לתיק הנוכחי, לא בסיכום כללי

View File

@@ -71,11 +71,12 @@ tools:
## תהליך עבודה
### שלב 1: הכנה
1. קרא פרטי התיק (`case_get`)
2. קרא טענות מחולצות (`get_claims`)
3. **קרא את עמדות יו"ר הוועדה (`get_chair_directions`) — חובה!**
4. קבל תבנית החלטה (`get_decision_template`)
5. קרא מדריך סגנון (`get_style_guide`)
1. **קרא את המתודולוגיה**: `Read docs/decision-methodology.md` — חובה לפני כל כתיבה
2. קרא פרטי התיק (`case_get`)
3. קרא טענות מחולצות (`get_claims`)
4. **קרא את עמדות יו"ר הוועדה (`get_chair_directions`) — חובה!**
5. קבל תבנית החלטה (`get_decision_template`)
6. קרא מדריך סגנון (`get_style_guide`)
### שלב 1ב: בדיקת עמדות יו"ר — חובה לפני כתיבה!
@@ -142,6 +143,26 @@ case_update(case_number, status="drafted")
- ספירת מילים לכל בלוק
- יחסי משקל (% מהמסמך)
### העֵר את העוזר המשפטי (CEO) — חובה!
```bash
curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
"$PAPERCLIP_API_URL/api/agents/752cebdd-6748-4a04-aacd-c7ab0294ef33/wake" \
-d '{"reason": "כותב החלטה סיים משימה [issue-id] בסטטוס [done/blocked]"}'
```
אם ה-API לא עובד:
```bash
PGPASSWORD="paperclip" psql -h 127.0.0.1 -p 54329 -U paperclip -d paperclip -c "
INSERT INTO agent_wakeup_requests (company_id, agent_id, source, reason, status, requested_by_actor_type)
VALUES (
(SELECT company_id FROM agents WHERE id = '\$PAPERCLIP_AGENT_ID'),
'752cebdd-6748-4a04-aacd-c7ab0294ef33',
'agent_completion',
'כותב החלטה סיים משימה — נדרשת בדיקה',
'pending', 'agent'
);"
```
**אם לא תעדכן סטטוס ל-drafted — בודק האיכות לא יוכל לרוץ!**
## בלוק י — דיון (הבלוק החשוב ביותר)

View File

@@ -4,3 +4,12 @@ mcp-server/.venv/
**/__pycache__/
*.pyc
.git/
.taskmaster/
web/static/
web/__pycache__/
scripts/
skills/
docs/
legacy/
node_modules/
.next/

View File

@@ -0,0 +1,58 @@
name: Build & Deploy
on:
push:
branches: [main]
tags: ["v*"]
env:
REGISTRY: gitea.nautilus.marcusgroup.org
IMAGE: ezer-mishpati/legal-ai
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to Gitea Registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | \
docker login ${{ env.REGISTRY }} \
-u "${{ secrets.REGISTRY_USER }}" --password-stdin
- name: Build and tag image
run: |
BASE="${{ env.REGISTRY }}/${{ env.IMAGE }}"
TAGS="-t ${BASE}:latest -t ${BASE}:build-${{ github.run_number }}"
# If this is a version tag (v*), add the semver tag
REF="${{ github.ref }}"
if [[ "$REF" == refs/tags/v* ]]; then
VERSION="${REF#refs/tags/}"
TAGS="$TAGS -t ${BASE}:${VERSION}"
echo "📦 Release: ${VERSION}"
fi
echo "🏗️ Building with tags: build-${{ github.run_number }}, latest"
docker build $TAGS .
- name: Push image
run: |
BASE="${{ env.REGISTRY }}/${{ env.IMAGE }}"
docker push "${BASE}:latest"
docker push "${BASE}:build-${{ github.run_number }}"
REF="${{ github.ref }}"
if [[ "$REF" == refs/tags/v* ]]; then
VERSION="${REF#refs/tags/}"
docker push "${BASE}:${VERSION}"
echo "✅ Pushed ${VERSION}"
fi
- name: Trigger Coolify redeploy
run: |
curl -sf \
"http://coolify:8080/api/v1/deploy?uuid=my85gabx37ele9aouub8t8ju&force=true" \
-H "Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}"

View File

@@ -0,0 +1,30 @@
# UI Updates — Legal AI Next.js
## Context
The legal-ai system uses a Next.js 15 UI at web-ui/. The workflow pipeline was significantly updated with new statuses, methodology, and agent improvements. The UI needs to reflect these changes.
## Task 1: Remove old Flask UI from Coolify
The old Flask app runs at legal-ai.nautilus.marcusgroup.org via Docker/Coolify. It should be archived and removed to save resources. The Next.js UI (legal-ai-next.nautilus.marcusgroup.org) becomes the sole UI. After removal, DNS should point legal-ai.nautilus.marcusgroup.org to the Next.js app.
Files: Coolify dashboard, DNS config.
## Task 2: Update WorkflowTimeline component with new statuses
The WorkflowTimeline component in web-ui/src/app/cases/[caseNumber]/page.tsx (line 127) only knows old statuses. It needs to support the full pipeline:
- new → proofread → documents_ready → analyst_verified → research_complete → outcome_set → direction_approved → drafted → qa_passed → exported
- Plus: qa_failed, blocked
Each status needs: Hebrew label, color, icon, description tooltip.
Files: web-ui/src/app/cases/[caseNumber]/page.tsx, possibly a new WorkflowTimeline component file.
## Task 3: Status overview page or component
Create a page or modal that shows all possible statuses with explanations — what each status means, which agent sets it, what happens next. Could be a /statuses page or a help tooltip in the WorkflowTimeline.
## Task 4: Manual status editing in case page
Add a dropdown or modal in the case page that allows manually changing the case status. This is needed for cases where the automated pipeline gets stuck or needs to be reset. Should call case_update API endpoint.
Files: web-ui/src/app/cases/[caseNumber]/page.tsx, web-ui/src/lib/api/.
## Task 5: Merge action buttons into overview card
Currently there's a separate "פעולות" (actions) card with 2 buttons: "פתח בעורך החלטה" and "עריכת פרטי תיק". These should move into the main overview/summary card at the top of the case page. The separate actions card should be removed — it wastes space for just 2 buttons.
Files: web-ui/src/app/cases/[caseNumber]/page.tsx.

View File

@@ -908,16 +908,71 @@
"priority": "high",
"subtasks": [],
"updatedAt": "2026-04-11T19:20:56.040Z"
},
{
"id": 92,
"title": "הסרת אפליקציית Flask הישנה מ-Coolify",
"description": "ארכיון והסרה של אפליקציית Flask הישנה מ-Coolify, וכיוון DNS כך ש-legal-ai.nautilus.marcusgroup.org יצביע על אפליקציית Next.js",
"details": "## פסאודו-קוד:\n```\n1. גיבוי הגדרות Flask מ-Coolify לפני מחיקה\n2. ב-Coolify dashboard:\n - מצא את הקונטיינר legal-ai-flask (או שם דומה)\n - עצור את הקונטיינר\n - צור snapshot או ארכיון של ההגדרות\n - מחק את הקונטיינר והסרוויס\n3. ב-DNS (Cloudflare/Coolify proxy):\n - שנה את legal-ai.nautilus.marcusgroup.org\n - הפנה ל-IP/service של legal-ai-next (Next.js app)\n4. ב-Next.js app (Coolify):\n - הוסף domain alias: legal-ai.nautilus.marcusgroup.org\n - עדכן SSL certificate\n```\n\n## קבצים מושפעים:\n- Coolify dashboard settings\n- DNS records (Cloudflare או ספק אחר)\n- Coolify proxy/Traefik configuration\n\n## הערות:\n- **אין שינויים בקוד** - רק הגדרות תשתית\n- ודא שה-Next.js app עובד עם שני הדומיינים במקביל לפני הסרת Flask\n- שמור לוגים מ-Flask לפני מחיקה למקרה של rollback",
"testStrategy": "## בדיקות:\n1. **לפני הסרה**: ודא ש-legal-ai-next.nautilus.marcusgroup.org עובד תקין\n2. **אחרי שינוי DNS**: \n - `curl -I https://legal-ai.nautilus.marcusgroup.org` - צריך להחזיר 200\n - בדוק SSL certificate תקין\n3. **בדיקת UI**: \n - פתח את legal-ai.nautilus.marcusgroup.org בדפדפן\n - ודא שזה אותו UI כמו legal-ai-next\n4. **בדיקת API**: \n - `curl https://legal-ai.nautilus.marcusgroup.org/api/cases`\n - ודא שמחזיר נתונים",
"priority": "high",
"dependencies": [],
"status": "pending",
"subtasks": []
},
{
"id": 93,
"title": "עדכון סטטוסים ב-WorkflowTimeline וב-status-badge",
"description": "עדכון רשימת הסטטוסים בממשק לפי ה-pipeline החדש: new → proofread → documents_ready → analyst_verified → research_complete → outcome_set → direction_approved → drafted → qa_passed → exported, כולל qa_failed ו-blocked",
"details": "## קבצים לעדכון:\n1. `web-ui/src/lib/api/cases.ts` - עדכון type CaseStatus\n2. `web-ui/src/components/cases/status-badge.tsx` - תוויות ועיצוב\n3. `web-ui/src/components/cases/workflow-timeline.tsx` - שלבי pipeline\n\n## פסאודו-קוד:\n\n### 1. cases.ts - עדכון הטיפוס:\n```typescript\nexport type CaseStatus =\n | \"new\"\n | \"proofread\"\n | \"documents_ready\"\n | \"analyst_verified\"\n | \"research_complete\"\n | \"outcome_set\"\n | \"direction_approved\"\n | \"drafted\"\n | \"qa_passed\"\n | \"exported\"\n | \"qa_failed\"\n | \"blocked\";\n```\n\n### 2. status-badge.tsx - תוויות עבריות וצבעים:\n```typescript\nconst STATUS_LABELS: Record<CaseStatus, string> = {\n new: \"חדש\",\n proofread: \"הוגה\",\n documents_ready: \"מסמכים מוכנים\",\n analyst_verified: \"אומת ע״י אנליסט\",\n research_complete: \"מחקר הושלם\",\n outcome_set: \"תוצאה נקבעה\",\n direction_approved: \"כיוון אושר\",\n drafted: \"טיוטה\",\n qa_passed: \"עבר QA\",\n exported: \"יוצא\",\n qa_failed: \"נכשל QA\",\n blocked: \"חסום\",\n};\n\nconst STATUS_TONE: Record<CaseStatus, string> = {\n new: \"bg-rule-soft text-ink-muted border-rule\",\n proofread: \"bg-info-bg text-info border-info/30\",\n documents_ready: \"bg-info-bg text-info border-info/40\",\n analyst_verified: \"bg-info-bg text-info border-info/50\",\n research_complete: \"bg-gold-wash text-gold-deep border-gold/40\",\n outcome_set: \"bg-gold-wash text-gold-deep border-gold/50\",\n direction_approved: \"bg-gold-wash text-gold-deep border-gold/60\",\n drafted: \"bg-warn-bg text-warn border-warn/40\",\n qa_passed: \"bg-success-bg text-success border-success/40\",\n exported: \"bg-success-bg text-success border-success/60\",\n qa_failed: \"bg-danger-bg text-danger border-danger/40\",\n blocked: \"bg-danger-bg text-danger border-danger/50\",\n};\n```\n\n### 3. workflow-timeline.tsx - קבוצות שלבים חדשות:\n```typescript\nconst PHASES: Phase[] = [\n { key: \"intake\", label: \"קליטה ועיבוד\", statuses: [\"new\", \"proofread\", \"documents_ready\"] },\n { key: \"analysis\", label: \"ניתוח\", statuses: [\"analyst_verified\", \"research_complete\"] },\n { key: \"direction\", label: \"קביעת כיוון\", statuses: [\"outcome_set\", \"direction_approved\"] },\n { key: \"writing\", label: \"כתיבה וביקורת\", statuses: [\"drafted\", \"qa_passed\"] },\n { key: \"done\", label: \"סגירה\", statuses: [\"exported\"] },\n];\n\n// טיפול בסטטוסי שגיאה (qa_failed, blocked) - הצגה מיוחדת\nif (status === \"qa_failed\" || status === \"blocked\") {\n // הצג באדום עם אייקון אזהרה\n}\n```",
"testStrategy": "## בדיקות:\n1. **Unit Tests** (אם קיימים):\n - ודא שכל הסטטוסים מופו נכון\n - בדוק שאין סטטוס חסר ב-STATUS_LABELS ו-STATUS_TONE\n\n2. **Visual Testing**:\n - צור/ערוך תיק ידנית ב-DB לכל סטטוס\n - ודא שהתווית מוצגת בעברית נכונה\n - ודא שהצבע מתאים (כחול לעיבוד, זהב לניתוח, ירוק להצלחה, אדום לשגיאה)\n\n3. **WorkflowTimeline**:\n - ודא שהשלב הנוכחי מודגש בצהוב\n - ודא ששלבים שהושלמו מסומנים בירוק\n - ודא שסטטוסי שגיאה (qa_failed, blocked) מוצגים עם אינדיקציה ויזואלית מיוחדת",
"priority": "high",
"dependencies": [],
"status": "pending",
"subtasks": []
},
{
"id": 94,
"title": "דף/קומפוננטה להצגת כל הסטטוסים עם הסברים",
"description": "יצירת דף /statuses או מודל עזרה שמסביר את כל הסטטוסים האפשריים - מה כל סטטוס אומר, איזה agent קובע אותו, ומה קורה אחר כך",
"details": "## אפשרויות מימוש:\n\n### אפשרות A: Popover tooltip בתוך WorkflowTimeline (מומלץ)\n```typescript\n// web-ui/src/components/cases/workflow-timeline.tsx\n// הוסף אייקון (?) ליד הכותרת שפותח popover\n\nconst STATUS_INFO: Record<CaseStatus, StatusInfo> = {\n new: {\n description: \"תיק נוצר, ממתין להעלאת מסמכים\",\n agent: \"משתמש\",\n nextStep: \"העלאת מסמכים → proofread\"\n },\n proofread: {\n description: \"מסמכים הועלו, עוברים הגהה אוטומטית\",\n agent: \"Proofread Agent\",\n nextStep: \"הגהה הושלמה → documents_ready\"\n },\n documents_ready: {\n description: \"מסמכים מוכנים לניתוח\",\n agent: \"Document Processor\",\n nextStep: \"בדיקת אנליסט → analyst_verified\"\n },\n analyst_verified: {\n description: \"אנליסט אימת את חילוץ הטענות\",\n agent: \"Analyst Agent\",\n nextStep: \"מחקר → research_complete\"\n },\n research_complete: {\n description: \"מחקר משפטי הושלם, פסיקה זוהתה\",\n agent: \"Research Agent\",\n nextStep: \"קביעת תוצאה → outcome_set\"\n },\n outcome_set: {\n description: \"דפנה קבעה את התוצאה (דחייה/קבלה)\",\n agent: \"משתמש (דפנה)\",\n nextStep: \"אישור כיוון → direction_approved\"\n },\n direction_approved: {\n description: \"כיוון ההחלטה אושר, מוכן לכתיבה\",\n agent: \"משתמש\",\n nextStep: \"כתיבה → drafted\"\n },\n drafted: {\n description: \"טיוטת החלטה נכתבה\",\n agent: \"Writing Agent\",\n nextStep: \"בדיקת QA → qa_passed\"\n },\n qa_passed: {\n description: \"טיוטה עברה בדיקת איכות\",\n agent: \"QA Agent\",\n nextStep: \"ייצוא → exported\"\n },\n exported: {\n description: \"ההחלטה יוצאה כ-DOCX\",\n agent: \"Export Service\",\n nextStep: \"הושלם\"\n },\n qa_failed: {\n description: \"טיוטה נכשלה בבדיקת QA\",\n agent: \"QA Agent\",\n nextStep: \"חזרה לכתיבה → drafted\"\n },\n blocked: {\n description: \"תיק חסום - דורש התערבות ידנית\",\n agent: \"מערכת\",\n nextStep: \"טיפול ידני\"\n },\n};\n```\n\n### אפשרות B: דף /statuses נפרד\n```typescript\n// web-ui/src/app/statuses/page.tsx\n// דף עצמאי עם טבלה של כל הסטטוסים\n```\n\n## המלצה: אפשרות A - פשוטה יותר ומשתלבת ב-UX הקיים",
"testStrategy": "## בדיקות:\n1. **UI Testing**:\n - לחיצה על אייקון העזרה פותחת popover/tooltip\n - כל סטטוס מציג: תיאור, agent, שלב הבא\n - סגירת ה-popover עובדת (לחיצה מחוץ/Escape)\n\n2. **Accessibility**:\n - ה-popover נגיש למקלדת (Tab, Enter, Escape)\n - aria-label מתאים\n - RTL מוצג נכון\n\n3. **Content Review**:\n - כל ההסברים בעברית תקנית\n - הזרימה בין סטטוסים מובנת",
"priority": "medium",
"dependencies": [
93
],
"status": "pending",
"subtasks": []
},
{
"id": 95,
"title": "עריכה ידנית של סטטוס בדף התיק",
"description": "הוספת dropdown או מודל בדף התיק לשינוי סטטוס ידני, לטיפול במקרים שבהם ה-pipeline נתקע או צריך reset",
"details": "## מיקום: בתוך הכרטיס של WorkflowTimeline (בצד ימין של דף התיק)\n\n## פסאודו-קוד:\n\n### 1. קומפוננטת StatusEditor:\n```typescript\n// web-ui/src/components/cases/status-editor.tsx\n\nimport { Select } from \"@/components/ui/select\";\nimport { Button } from \"@/components/ui/button\";\nimport { useUpdateCase } from \"@/lib/api/cases\";\nimport { toast } from \"sonner\";\n\nexport function StatusEditor({ caseNumber, currentStatus }: Props) {\n const [selectedStatus, setSelectedStatus] = useState(currentStatus);\n const updateCase = useUpdateCase(caseNumber);\n\n const handleSave = async () => {\n if (selectedStatus === currentStatus) return;\n \n try {\n await updateCase.mutateAsync({ status: selectedStatus });\n toast.success(\"סטטוס עודכן בהצלחה\");\n } catch (error) {\n toast.error(\"שגיאה בעדכון הסטטוס\");\n }\n };\n\n return (\n <div className=\"flex items-center gap-2 mt-4\">\n <Select value={selectedStatus} onValueChange={setSelectedStatus}>\n {ALL_STATUSES.map(status => (\n <SelectItem key={status} value={status}>\n {STATUS_LABELS[status]}\n </SelectItem>\n ))}\n </Select>\n <Button \n onClick={handleSave} \n disabled={selectedStatus === currentStatus || updateCase.isPending}\n size=\"sm\"\n >\n עדכן\n </Button>\n </div>\n );\n}\n```\n\n### 2. שילוב בדף התיק:\n```typescript\n// web-ui/src/app/cases/[caseNumber]/page.tsx\n// בתוך הכרטיס של WorkflowTimeline\n\n<Card className=\"bg-surface border-rule shadow-sm h-fit\">\n <CardContent className=\"px-6 py-5\">\n <h2 className=\"text-navy text-base mb-4\">שלב בתהליך</h2>\n <WorkflowTimeline status={data?.status} />\n {data && <StatusEditor caseNumber={caseNumber} currentStatus={data.status} />}\n </CardContent>\n</Card>\n```\n\n### 3. עדכון ה-API (אם נדרש):\nה-`useUpdateCase` כבר תומך ב-status field לפי `caseUpdateSchema`.",
"testStrategy": "## בדיקות:\n1. **Functionality**:\n - בחירת סטטוס חדש מה-dropdown\n - לחיצה על \"עדכן\" שולחת PUT request ל-API\n - הסטטוס מתעדכן ב-UI אחרי הצלחה\n - toast הודעה מוצגת\n\n2. **Edge Cases**:\n - לחיצה על \"עדכן\" כשהסטטוס לא השתנה - כפתור disabled\n - טיפול בשגיאת API - הודעת שגיאה\n - כפתור disabled בזמן loading\n\n3. **Integration**:\n - ה-WorkflowTimeline מתעדכן מיד אחרי שינוי סטטוס\n - ה-StatusBadge בכותרת מתעדכן\n - הנתונים מסונכרנים עם ה-DB",
"priority": "medium",
"dependencies": [
93
],
"status": "pending",
"subtasks": []
},
{
"id": 96,
"title": "מיזוג כפתורי פעולות לכרטיס הסקירה הראשי",
"description": "העברת הכפתורים 'פתח בעורך ההחלטה' ו'עריכת פרטי תיק' מהלשונית 'פעולות' לכרטיס הכותרת העליון, והסרת הלשונית המיותרת",
"details": "## קבצים לעדכון:\n- `web-ui/src/app/cases/[caseNumber]/page.tsx`\n- `web-ui/src/components/cases/case-header.tsx`\n\n## פסאודו-קוד:\n\n### 1. עדכון CaseHeader להוספת כפתורי פעולה:\n```typescript\n// web-ui/src/components/cases/case-header.tsx\n\nimport Link from \"next/link\";\nimport { Button } from \"@/components/ui/button\";\nimport { CaseEditDialog } from \"@/components/cases/case-edit-dialog\";\n\nexport function CaseHeader({ data }: { data?: CaseDetail }) {\n return (\n <Card className=\"bg-surface border-rule shadow-sm\">\n <CardContent className=\"px-6 py-5\">\n {/* ... breadcrumb קיים ... */}\n \n <div className=\"flex items-start justify-between gap-6 flex-wrap\">\n <div className=\"space-y-2\">\n {/* ... כותרת וסטטוס קיימים ... */}\n </div>\n\n {/* כפתורי פעולה - חדש */}\n <div className=\"flex items-center gap-3 flex-wrap\">\n <Button asChild className=\"bg-navy hover:bg-navy-soft text-parchment\">\n <Link href={`/cases/${data?.case_number}/compose`}>\n פתח בעורך ההחלטה\n </Link>\n </Button>\n {data && <CaseEditDialog data={data} />}\n </div>\n </div>\n\n {/* ... תאריכים קיימים ... */}\n </CardContent>\n </Card>\n );\n}\n```\n\n### 2. הסרת לשונית \"פעולות\" מדף התיק:\n```typescript\n// web-ui/src/app/cases/[caseNumber]/page.tsx\n\n// הסר את TabsTrigger value=\"actions\"\n<TabsList className=\"bg-rule-soft/60\">\n <TabsTrigger value=\"overview\">סקירה</TabsTrigger>\n <TabsTrigger value=\"documents\">מסמכים (...)</TabsTrigger>\n {/* הוסר: <TabsTrigger value=\"actions\">פעולות</TabsTrigger> */}\n</TabsList>\n\n// הסר את TabsContent value=\"actions\"\n// הקוד הבא נמחק:\n// <TabsContent value=\"actions\" className=\"mt-5\">\n// <div className=\"flex items-center gap-3 flex-wrap\">\n// <Button asChild>...</Button>\n// {data && <CaseEditDialog data={data} />}\n// </div>\n// </TabsContent>\n```\n\n### 3. עדכון CaseHeader props:\n```typescript\n// צריך להעביר caseNumber ל-CaseHeader אם עדיין לא קיים\n<CaseHeader data={data} caseNumber={caseNumber} />\n```",
"testStrategy": "## בדיקות:\n1. **Visual**:\n - כפתורי הפעולה מופיעים בכרטיס העליון\n - הכפתורים מיושרים ימינה (RTL)\n - responsive - נגלשים נכון במסכים קטנים\n\n2. **Functionality**:\n - \"פתח בעורך ההחלטה\" מנווט ל-/cases/{caseNumber}/compose\n - \"עריכת פרטי תיק\" פותח את ה-CaseEditDialog\n - ה-dialog עובד כרגיל\n\n3. **Removal**:\n - לשונית \"פעולות\" לא מופיעה יותר ב-Tabs\n - אין שגיאות קונסול\n - ניווט ל-#actions לא עובד (ולא אמור)\n\n4. **Regression**:\n - לשוניות \"סקירה\" ו\"מסמכים\" עובדות כרגיל\n - שאר הדף לא נפגע",
"priority": "low",
"dependencies": [],
"status": "pending",
"subtasks": []
}
],
"metadata": {
"version": "1.0.0",
"lastModified": "2026-04-11T19:20:56.040Z",
"taskCount": 60,
"completedCount": 57,
"tags": [
"master"
]
"created": "2026-04-13T14:20:54.888Z",
"updated": "2026-04-13T14:20:54.888Z",
"description": "Tasks for master context"
}
}
}

View File

@@ -1,21 +1,20 @@
# ══════════════════════════════════════════════════════════════
# Dockerfile — Next.js 16 web-ui (ui-rewrite branch only)
# Dockerfile — Next.js frontend + FastAPI backend (single container)
#
# This file REPLACES the FastAPI Dockerfile on this branch so that
# Coolify's default /Dockerfile lookup builds the new Next.js staging
# UI. The FastAPI Dockerfile lives on `main` and is unaffected.
# The container runs both:
# - FastAPI (uvicorn) on :8000 — the API backend
# - Next.js (node) on :3000 — the frontend (proxies /api/* to :8000)
#
# When the rewrite is merged to main, decide between:
# (a) keeping both via separate Dockerfiles + dockerfile_location config, or
# (b) a multi-stage Dockerfile that serves both, or
# (c) fully replacing FastAPI's StaticFiles with this Next.js front end.
# start.sh launches both processes.
# ══════════════════════════════════════════════════════════════
# ── Stage 1: Node deps ────────────────────────────────────────
FROM node:20-alpine AS deps
WORKDIR /app
COPY web-ui/package.json web-ui/package-lock.json ./
RUN npm ci --no-audit --no-fund
# ── Stage 2: Build Next.js ────────────────────────────────────
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
@@ -23,18 +22,49 @@ COPY web-ui/ ./
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
FROM node:20-alpine AS runner
# ── Stage 3: Install Python deps (use slim for pre-built wheels) ──
FROM python:3.12-slim AS pydeps
WORKDIR /opt/api
COPY mcp-server/ ./mcp-server/
RUN pip install --no-cache-dir ./mcp-server
# ── Stage 4: Runner ───────────────────────────────────────────
FROM python:3.12-slim AS runner
WORKDIR /app
# Install Node.js 20.x
RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& apt-get purge -y curl \
&& rm -rf /var/lib/apt/lists/*
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
# next.config.ts uses output: 'standalone', so we copy only the minimal runtime
# Copy Python packages from pydeps stage
COPY --from=pydeps /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=pydeps /usr/local/bin/uvicorn /usr/local/bin/uvicorn
# Copy Next.js standalone build
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
# Copy FastAPI backend code
COPY web/ ./web/
COPY mcp-server/src/ ./mcp-server/src/
# Make mcp-server source available to web/app.py (it does sys.path.insert for legal_mcp)
ENV PYTHONPATH=/app/mcp-server/src
# Copy startup script
COPY start.sh ./start.sh
RUN chmod +x ./start.sh
EXPOSE 3000
CMD ["node", "server.js"]
CMD ["./start.sh"]

View File

@@ -17,6 +17,8 @@ dependencies = [
"rq>=1.16.0",
"pillow>=10.0.0",
"google-cloud-vision>=3.7.0",
"fastapi>=0.115.0",
"uvicorn[standard]>=0.30.0",
]
[build-system]

View File

@@ -747,6 +747,22 @@ async def update_decision(decision_id: UUID, **fields) -> None:
await conn.execute(sql, decision_id, *values)
# ── Document deletion ──────────────────────────────────────────────
async def delete_document(doc_id: UUID) -> bool:
"""Delete a document and all its chunks. Returns True if deleted."""
pool = await get_pool()
async with pool.acquire() as conn:
async with conn.transaction():
await conn.execute(
"DELETE FROM document_chunks WHERE document_id = $1", doc_id
)
result = await conn.execute(
"DELETE FROM documents WHERE id = $1", doc_id
)
return int(result.split()[-1]) > 0
# ── Chunks & Vectors ───────────────────────────────────────────────
async def delete_document_chunks(document_id: UUID) -> int:
@@ -1050,6 +1066,91 @@ async def search_precedents(
return results[:limit]
# ── Case precedents (CRUD) ────────────────────────────────────────
async def create_case_precedent(
case_id: UUID,
quote: str,
citation: str,
section_id: str | None = None,
chair_note: str = "",
pdf_document_id: UUID | None = None,
practice_area: str | None = None,
) -> dict:
"""Insert a new precedent attached to a case."""
pool = await get_pool()
row = await pool.fetchrow(
"""
INSERT INTO case_precedents
(case_id, section_id, quote, citation, chair_note, pdf_document_id, practice_area)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *
""",
case_id, section_id, quote, citation, chair_note, pdf_document_id, practice_area,
)
return dict(row)
async def list_case_precedents(case_id: UUID) -> list[dict]:
"""List all precedents attached to a case, ordered by section then creation time."""
pool = await get_pool()
rows = await pool.fetch(
"""
SELECT id, case_id, section_id, quote, citation, chair_note,
pdf_document_id, practice_area, created_at, updated_at
FROM case_precedents
WHERE case_id = $1
ORDER BY section_id NULLS LAST, created_at
""",
case_id,
)
return [dict(r) for r in rows]
async def delete_case_precedent(precedent_id: UUID) -> bool:
"""Delete a precedent attachment by ID. Returns True if deleted."""
pool = await get_pool()
result = await pool.execute(
"DELETE FROM case_precedents WHERE id = $1", precedent_id
)
return result == "DELETE 1"
async def search_precedent_library(
query: str, practice_area: str = "", limit: int = 10,
) -> list[dict]:
"""Search all precedents across cases by citation or quote text."""
pool = await get_pool()
pattern = f"%{query}%"
if practice_area:
rows = await pool.fetch(
"""
SELECT id, case_id, section_id, quote, citation, chair_note,
practice_area, created_at
FROM case_precedents
WHERE (citation ILIKE $1 OR quote ILIKE $1)
AND practice_area = $2
ORDER BY created_at DESC
LIMIT $3
""",
pattern, practice_area, limit,
)
else:
rows = await pool.fetch(
"""
SELECT id, case_id, section_id, quote, citation, chair_note,
practice_area, created_at
FROM case_precedents
WHERE citation ILIKE $1 OR quote ILIKE $1
ORDER BY created_at DESC
LIMIT $2
""",
pattern, limit,
)
return [dict(r) for r in rows]
# ── Chair feedback ────────────────────────────────────────────────
async def record_chair_feedback(

20
start.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/sh
# Start FastAPI backend + Next.js frontend in the same container.
# Both processes log to stdout/stderr so Docker captures everything.
set -e
echo "[start.sh] Starting FastAPI backend on :8000 ..."
uvicorn web.app:app --host 127.0.0.1 --port 8000 --workers 1 2>&1 &
UVICORN_PID=$!
# Give uvicorn a moment to start (or crash)
sleep 2
if ! kill -0 $UVICORN_PID 2>/dev/null; then
echo "[start.sh] ERROR: uvicorn failed to start!"
# Don't exit — let Node.js run so the UI is accessible for debugging
fi
echo "[start.sh] Starting Next.js frontend on :3000 ..."
node server.js

View File

@@ -1,17 +1,14 @@
import type { NextConfig } from "next";
/**
* Staging config — proxies /api/* and /openapi.json to the production FastAPI
* at legal-ai.nautilus.marcusgroup.org. This lets the new Next.js UI call the
* existing backend without CORS and without running a second FastAPI instance.
*
* When the rewrite branch is cut over to production, set NEXT_PUBLIC_API_BASE_URL
* and/or move the FastAPI in front of this app via traefik routing.
* Proxies /api/* and /openapi.json to the FastAPI backend.
* In Docker both processes run in the same container, so the default
* target is http://127.0.0.1:8000. Override with NEXT_PUBLIC_API_ORIGIN
* if the backend lives elsewhere (e.g. during local dev).
*/
const API_ORIGIN =
process.env.NEXT_PUBLIC_API_ORIGIN ??
"https://legal-ai.nautilus.marcusgroup.org";
process.env.NEXT_PUBLIC_API_ORIGIN ?? "http://127.0.0.1:8000";
const nextConfig: NextConfig = {
output: "standalone",

View File

@@ -78,9 +78,24 @@ export default function ComposePage({
</p>
)}
</div>
<Button asChild variant="outline">
<Link href={`/cases/${caseNumber}`}>חזרה לתיק</Link>
</Button>
<div className="flex items-center gap-2">
{analysis.data && (
<Button
variant="outline"
onClick={() => {
const a = document.createElement("a");
a.href = `/api/cases/${caseNumber}/research/analysis/download`;
a.download = `analysis-${caseNumber}.md`;
a.click();
}}
>
הורד ניתוח
</Button>
)}
<Button asChild variant="outline">
<Link href={`/cases/${caseNumber}`}>חזרה לתיק</Link>
</Button>
</div>
</div>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />

View File

@@ -10,6 +10,8 @@ import { Skeleton } from "@/components/ui/skeleton";
import { CaseHeader } from "@/components/cases/case-header";
import { CaseEditDialog } from "@/components/cases/case-edit-dialog";
import { WorkflowTimeline } from "@/components/cases/workflow-timeline";
import { StatusGuide } from "@/components/cases/status-guide";
import { StatusChanger } from "@/components/cases/status-changer";
import { DocumentsPanel } from "@/components/cases/documents-panel";
import { UploadSheet } from "@/components/documents/upload-sheet";
import { expectedOutcomes } from "@/lib/schemas/case";
@@ -76,7 +78,6 @@ export default function CaseDetailPage({
</span>
)}
</TabsTrigger>
<TabsTrigger value="actions">פעולות</TabsTrigger>
</TabsList>
<UploadSheet caseNumber={caseNumber} />
</div>
@@ -101,14 +102,7 @@ export default function CaseDetailPage({
</dd>
</dl>
</div>
</TabsContent>
<TabsContent value="documents" className="mt-5">
<DocumentsPanel data={data} />
</TabsContent>
<TabsContent value="actions" className="mt-5">
<div className="flex items-center gap-3 flex-wrap">
<div className="flex items-center gap-3 flex-wrap pt-2 border-t border-rule">
<Button asChild className="bg-navy hover:bg-navy-soft text-parchment">
<Link href={`/cases/${caseNumber}/compose`}>
פתח בעורך ההחלטה
@@ -117,6 +111,10 @@ export default function CaseDetailPage({
{data && <CaseEditDialog data={data} />}
</div>
</TabsContent>
<TabsContent value="documents" className="mt-5">
<DocumentsPanel data={data} />
</TabsContent>
</Tabs>
</CardContent>
</Card>
@@ -125,6 +123,8 @@ export default function CaseDetailPage({
<CardContent className="px-6 py-5">
<h2 className="text-navy text-base mb-4">שלב בתהליך</h2>
<WorkflowTimeline status={data?.status} />
<StatusChanger caseNumber={caseNumber} currentStatus={data?.status} />
<StatusGuide />
</CardContent>
</Card>
</div>

View File

@@ -1,8 +1,28 @@
"use client";
import { useEffect, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import { ScrollArea } from "@/components/ui/scroll-area";
import { CheckCircle2, Clock, Loader2, XCircle } from "lucide-react";
import type { CaseDetail } from "@/lib/api/cases";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import {
CheckCircle2,
Clock,
Eye,
Loader2,
Trash2,
XCircle,
} from "lucide-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@/lib/api/client";
import { casesKeys } from "@/lib/api/cases";
import type { CaseDetail, CaseDocument } from "@/lib/api/cases";
/*
* Document list for the case detail "מסמכים" tab. Uses the real document
@@ -90,8 +110,211 @@ function filenameFromPath(path: string): string {
return parts[parts.length - 1] || path;
}
export function DocumentsPanel({ data }: { data?: CaseDetail }) {
/* ── Document text preview dialog ──────────────────────────────── */
function DocumentPreviewDialog({
doc,
open,
onOpenChange,
}: {
doc: CaseDocument;
open: boolean;
onOpenChange: (v: boolean) => void;
}) {
const [text, setText] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!open) {
setText(null);
setError(null);
return;
}
let cancelled = false;
setLoading(true);
setError(null);
apiRequest<{ text: string }>(`/api/documents/${doc.id}/text`)
.then((res) => { if (!cancelled) setText(res.text || "(ריק)"); })
.catch(() => { if (!cancelled) setError("המסמך עדיין לא עובד או שאין בו טקסט"); })
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, [open, doc.id]);
const displayName = doc.title || filenameFromPath(doc.file_path);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-2xl max-h-[80vh] flex flex-col" dir="rtl">
<DialogHeader>
<DialogTitle className="text-right">{displayName}</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-hidden">
{loading && (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-6 h-6 animate-spin text-gold" />
<span className="ms-2 text-ink-muted text-sm">טוען מסמך...</span>
</div>
)}
{error && (
<div className="text-center py-12 text-danger text-sm">{error}</div>
)}
{text !== null && !loading && (
<div className="h-[60vh] overflow-y-auto" dir="rtl">
<pre className="whitespace-pre-wrap text-sm text-ink leading-relaxed font-sans p-2">
{text}
</pre>
</div>
)}
</div>
<DialogFooter showCloseButton />
</DialogContent>
</Dialog>
);
}
/* ── Delete confirmation dialog ────────────────────────────────── */
function DeleteConfirmDialog({
doc,
caseNumber,
open,
onOpenChange,
}: {
doc: CaseDocument;
caseNumber: string;
open: boolean;
onOpenChange: (v: boolean) => void;
}) {
const qc = useQueryClient();
const deleteMutation = useMutation({
mutationFn: () =>
apiRequest(`/api/cases/${caseNumber}/documents/${doc.id}`, {
method: "DELETE",
}),
onSuccess: () => {
qc.invalidateQueries({ queryKey: casesKeys.all });
onOpenChange(false);
},
});
const displayName = doc.title || filenameFromPath(doc.file_path);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent dir="rtl">
<DialogHeader>
<DialogTitle className="text-right">מחיקת מסמך</DialogTitle>
</DialogHeader>
<p className="text-sm text-ink-muted text-right">
האם למחוק את המסמך <strong>&ldquo;{displayName}&rdquo;</strong>?
<br />
פעולה זו אינה ניתנת לביטול.
</p>
<DialogFooter>
<Button
variant="destructive"
onClick={() => deleteMutation.mutate()}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? (
<Loader2 className="w-4 h-4 animate-spin me-1" />
) : (
<Trash2 className="w-4 h-4 me-1" />
)}
מחק
</Button>
<Button variant="outline" onClick={() => onOpenChange(false)}>
ביטול
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
/* ── Single document row ───────────────────────────────────────── */
function DocumentRow({
doc,
caseNumber,
}: {
doc: CaseDocument;
caseNumber: string;
}) {
const [previewOpen, setPreviewOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const displayName = doc.title || filenameFromPath(doc.file_path);
const canPreview =
doc.extraction_status === "completed" || doc.extraction_status === "proofread";
return (
<>
<li className="py-3 flex items-start gap-3 hover:bg-gold-wash/30 transition-colors px-2 -mx-2 rounded group">
<StatusIcon status={doc.extraction_status} />
<button
type="button"
className="flex-1 min-w-0 space-y-0.5 text-right cursor-pointer hover:underline decoration-gold/40 underline-offset-2 disabled:cursor-default disabled:no-underline"
disabled={!canPreview}
onClick={() => canPreview && setPreviewOpen(true)}
title={canPreview ? "לחץ לצפייה במסמך" : "המסמך עדיין לא עובד"}
>
<div className="text-ink font-medium truncate flex items-center gap-1.5">
{canPreview && <Eye className="w-3.5 h-3.5 text-ink-muted shrink-0" />}
<span>{displayName}</span>
</div>
<div className="text-[0.72rem] text-ink-muted flex items-center gap-3 flex-wrap">
{doc.page_count != null && (
<span className="tabular-nums">{doc.page_count} עמ׳</span>
)}
{doc.created_at && <span>{formatDate(doc.created_at)}</span>}
</div>
</button>
{doc.doc_type && (
<Badge
variant="outline"
className={`rounded-full px-2 py-0.5 text-[0.7rem] shrink-0 ${doctypeTone(doc.doc_type)}`}
>
{doctypeLabel(doc.doc_type)}
</Badge>
)}
<button
type="button"
className="shrink-0 p-1 rounded text-ink-muted/40 hover:text-danger hover:bg-danger-bg transition-colors opacity-0 group-hover:opacity-100"
onClick={() => setDeleteOpen(true)}
title="מחק מסמך"
>
<Trash2 className="w-4 h-4" />
</button>
</li>
{previewOpen && (
<DocumentPreviewDialog
doc={doc}
open={previewOpen}
onOpenChange={setPreviewOpen}
/>
)}
{deleteOpen && (
<DeleteConfirmDialog
doc={doc}
caseNumber={caseNumber}
open={deleteOpen}
onOpenChange={setDeleteOpen}
/>
)}
</>
);
}
/* ── Main panel ────────────────────────────────────────────────── */
export function DocumentsPanel({
data,
}: {
data?: CaseDetail;
}) {
const docs = data?.documents ?? [];
const caseNumber = data?.case_number ?? "";
if (docs.length === 0) {
return (
@@ -154,40 +377,13 @@ export function DocumentsPanel({ data }: { data?: CaseDetail }) {
</div>
)}
<ScrollArea className="max-h-[520px]" dir="rtl">
<div className="max-h-[70vh] overflow-y-auto" dir="rtl">
<ul className="divide-y divide-rule" dir="rtl">
{sorted.map((doc) => {
const displayName = doc.title || filenameFromPath(doc.file_path);
return (
<li
key={doc.id}
className="py-3 flex items-start gap-3 hover:bg-gold-wash/30 transition-colors px-2 -mx-2 rounded"
>
<StatusIcon status={doc.extraction_status} />
<div className="flex-1 min-w-0 space-y-0.5 text-right">
<div className="text-ink font-medium truncate" title={displayName}>
{displayName}
</div>
<div className="text-[0.72rem] text-ink-muted flex items-center gap-3 flex-wrap">
{doc.page_count != null && (
<span className="tabular-nums">{doc.page_count} עמ׳</span>
)}
{doc.created_at && <span>{formatDate(doc.created_at)}</span>}
</div>
</div>
{doc.doc_type && (
<Badge
variant="outline"
className={`rounded-full px-2 py-0.5 text-[0.7rem] shrink-0 ms-auto ${doctypeTone(doc.doc_type)}`}
>
{doctypeLabel(doc.doc_type)}
</Badge>
)}
</li>
);
})}
{sorted.map((doc) => (
<DocumentRow key={doc.id} doc={doc} caseNumber={caseNumber} />
))}
</ul>
</ScrollArea>
</div>
</div>
);
}

View File

@@ -1,5 +1,11 @@
import { Badge } from "@/components/ui/badge";
import {
FilePlus2, Upload, Loader2, FileCheck, Target,
Lightbulb, Compass, PenLine, SearchCheck, FileText,
FileOutput, CheckCircle2, Award,
} from "lucide-react";
import type { CaseStatus } from "@/lib/api/cases";
import type { LucideIcon } from "lucide-react";
const STATUS_LABELS: Record<CaseStatus, string> = {
new: "חדש",
@@ -17,6 +23,38 @@ const STATUS_LABELS: Record<CaseStatus, string> = {
final: "סופי",
};
const STATUS_ICONS: Record<CaseStatus, LucideIcon> = {
new: FilePlus2,
uploading: Upload,
processing: Loader2,
documents_ready: FileCheck,
outcome_set: Target,
brainstorming: Lightbulb,
direction_approved: Compass,
drafting: PenLine,
qa_review: SearchCheck,
drafted: FileText,
exported: FileOutput,
reviewed: CheckCircle2,
final: Award,
};
const STATUS_DESCRIPTIONS: Record<CaseStatus, string> = {
new: "התיק נוצר וממתין להעלאת מסמכים",
uploading: "מסמכים בתהליך העלאה לשרת",
processing: "המערכת מעבדת ומנתחת את המסמכים",
documents_ready: "כל המסמכים עובדו ומוכנים לעבודה",
outcome_set: "נקבעה תוצאה צפויה לערר",
brainstorming: "ניתוח כיוונים אפשריים להחלטה",
direction_approved: "כיוון ההחלטה אושר — ניתן להתחיל כתיבה",
drafting: "טיוטת ההחלטה בתהליך כתיבה",
qa_review: "הטיוטה בבדיקת איכות אוטומטית",
drafted: "טיוטה ראשונה מוכנה לעיון",
exported: "ההחלטה יוצאה לקובץ DOCX",
reviewed: "ההחלטה נבדקה ע\"י היו\"ר",
final: "החלטה סופית — מוכנה להגשה",
};
/* Status color groups:
* intake → new, uploading, processing (muted parchment)
* prep → documents_ready, outcome_set (info blue)
@@ -40,14 +78,16 @@ const STATUS_TONE: Record<CaseStatus, string> = {
};
export function StatusBadge({ status }: { status: CaseStatus }) {
const Icon = STATUS_ICONS[status];
return (
<Badge
variant="outline"
className={`rounded-full px-2.5 py-0.5 text-[0.72rem] font-medium ${STATUS_TONE[status] ?? ""}`}
className={`rounded-full px-2.5 py-0.5 text-[0.72rem] font-medium inline-flex items-center gap-1 ${STATUS_TONE[status] ?? ""}`}
>
{Icon && <Icon className="w-3 h-3 shrink-0" />}
{STATUS_LABELS[status] ?? status}
</Badge>
);
}
export { STATUS_LABELS };
export { STATUS_LABELS, STATUS_ICONS, STATUS_DESCRIPTIONS, STATUS_TONE };

View File

@@ -0,0 +1,84 @@
"use client";
import { useState } from "react";
import { toast } from "sonner";
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { STATUS_LABELS, STATUS_ICONS } from "@/components/cases/status-badge";
import { useUpdateCase, type CaseStatus } from "@/lib/api/cases";
/*
* Dropdown for manually overriding the case status — skip a step
* or revert to an earlier stage. Calls PUT /api/cases/:caseNumber
* with { status: newValue }.
*/
const ALL_STATUSES: CaseStatus[] = [
"new", "uploading", "processing",
"documents_ready", "outcome_set",
"brainstorming", "direction_approved",
"drafting", "qa_review", "drafted",
"exported", "reviewed", "final",
];
export function StatusChanger({
caseNumber,
currentStatus,
}: {
caseNumber: string;
currentStatus?: CaseStatus;
}) {
const [selected, setSelected] = useState<CaseStatus | "">(currentStatus ?? "");
const mutate = useUpdateCase(caseNumber);
const handleSave = async () => {
if (!selected || selected === currentStatus) return;
try {
await mutate.mutateAsync({ status: selected });
toast.success(`הסטטוס עודכן ל${STATUS_LABELS[selected]}`);
} catch (e) {
toast.error(e instanceof Error ? e.message : "שגיאה בעדכון הסטטוס");
}
};
return (
<div className="mt-4 border-t border-rule pt-3 space-y-2">
<label className="text-[0.72rem] text-ink-muted block">שינוי סטטוס ידני</label>
<div className="flex items-center gap-2">
<Select
value={selected || "__current__"}
onValueChange={(v) => setSelected(v === "__current__" ? "" : v as CaseStatus)}
dir="rtl"
>
<SelectTrigger className="text-[0.75rem] h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
{ALL_STATUSES.map((s) => {
const Icon = STATUS_ICONS[s];
return (
<SelectItem key={s} value={s} className="text-[0.75rem]">
<span className="inline-flex items-center gap-1.5">
<Icon className="w-3 h-3 shrink-0" />
{STATUS_LABELS[s]}
</span>
</SelectItem>
);
})}
</SelectContent>
</Select>
<Button
size="sm"
variant="outline"
className="h-8 text-[0.72rem] px-3 shrink-0"
disabled={!selected || selected === currentStatus || mutate.isPending}
onClick={handleSave}
>
{mutate.isPending ? "שומר…" : "עדכן"}
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,72 @@
"use client";
import { useState } from "react";
import { ChevronDown, ChevronUp } from "lucide-react";
import type { CaseStatus } from "@/lib/api/cases";
import { STATUS_LABELS, STATUS_ICONS, STATUS_DESCRIPTIONS, STATUS_TONE } from "@/components/cases/status-badge";
/*
* Collapsible guide showing all 13 statuses grouped by phase,
* each with its icon, Hebrew label, color badge, and description.
* Intended to sit below the WorkflowTimeline in the case sidebar.
*/
type PhaseGroup = {
label: string;
statuses: CaseStatus[];
};
const PHASE_GROUPS: PhaseGroup[] = [
{ label: "קליטה ועיבוד", statuses: ["new", "uploading", "processing"] },
{ label: "הכנת תיק", statuses: ["documents_ready", "outcome_set"] },
{ label: "ניתוח וכיוון", statuses: ["brainstorming", "direction_approved"] },
{ label: "כתיבת טיוטה", statuses: ["drafting", "qa_review", "drafted"] },
{ label: "סגירה", statuses: ["exported", "reviewed", "final"] },
];
export function StatusGuide() {
const [open, setOpen] = useState(false);
return (
<div className="mt-4 border-t border-rule pt-3">
<button
type="button"
onClick={() => setOpen(!open)}
className="flex items-center gap-1.5 text-[0.72rem] text-ink-muted hover:text-navy transition-colors w-full"
>
{open ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
מפת סטטוסים
</button>
{open && (
<div className="mt-3 space-y-3">
{PHASE_GROUPS.map((group) => (
<div key={group.label}>
<h4 className="text-[0.7rem] font-semibold text-navy mb-1.5">
{group.label}
</h4>
<ul className="space-y-1.5">
{group.statuses.map((s) => {
const Icon = STATUS_ICONS[s];
return (
<li key={s} className="flex items-start gap-2">
<span
className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[0.65rem] font-medium shrink-0 ${STATUS_TONE[s]}`}
>
<Icon className="w-2.5 h-2.5" />
{STATUS_LABELS[s]}
</span>
<span className="text-[0.65rem] text-ink-muted leading-snug">
{STATUS_DESCRIPTIONS[s]}
</span>
</li>
);
})}
</ul>
</div>
))}
</div>
)}
</div>
);
}

View File

@@ -1,7 +1,11 @@
"use client";
import type { CaseStatus } from "@/lib/api/cases";
import { STATUS_LABELS } from "@/components/cases/status-badge";
import { STATUS_LABELS, STATUS_ICONS, STATUS_DESCRIPTIONS } from "@/components/cases/status-badge";
import {
FolderInput, ClipboardList, Brain, PenLine, CheckCircle2,
} from "lucide-react";
import type { LucideIcon } from "lucide-react";
/*
* Vertical RTL workflow timeline showing the 13-status case pipeline.
@@ -13,15 +17,16 @@ import { STATUS_LABELS } from "@/components/cases/status-badge";
type Phase = {
key: string;
label: string;
icon: LucideIcon;
statuses: CaseStatus[];
};
const PHASES: Phase[] = [
{ key: "intake", label: "קליטה ועיבוד", statuses: ["new", "uploading", "processing"] },
{ key: "prep", label: "הכנת תיק", statuses: ["documents_ready", "outcome_set"] },
{ key: "thinking", label: "ניתוח וכיוון", statuses: ["brainstorming", "direction_approved"] },
{ key: "writing", label: "כתיבת טיוטה", statuses: ["drafting", "qa_review", "drafted"] },
{ key: "done", label: "סגירה", statuses: ["exported", "reviewed", "final"] },
{ key: "intake", label: "קליטה ועיבוד", icon: FolderInput, statuses: ["new", "uploading", "processing"] },
{ key: "prep", label: "הכנת תיק", icon: ClipboardList, statuses: ["documents_ready", "outcome_set"] },
{ key: "thinking", label: "ניתוח וכיוון", icon: Brain, statuses: ["brainstorming", "direction_approved"] },
{ key: "writing", label: "כתיבת טיוטה", icon: PenLine, statuses: ["drafting", "qa_review", "drafted"] },
{ key: "done", label: "סגירה", icon: CheckCircle2, statuses: ["exported", "reviewed", "final"] },
];
function phaseIndexOf(status?: CaseStatus): number {
@@ -55,19 +60,36 @@ export function WorkflowTimeline({ status }: { status?: CaseStatus }) {
: state === "current" ? "text-navy font-semibold"
: "text-ink-muted";
const iconTone =
state === "done" ? "text-success"
: state === "current" ? "text-gold-deep"
: "text-ink-muted/50";
const PhaseIcon = phase.icon;
const StatusIcon = status ? STATUS_ICONS[status] : null;
return (
<li key={phase.key} className="relative flex items-start gap-3 ps-7">
<span
className={`absolute right-[5px] top-1 inline-block w-3 h-3 rounded-full border-2 ${dotTone}`}
aria-hidden
/>
<div className="flex flex-col">
<span className={`text-sm ${labelTone}`}>{phase.label}</span>
<div className="flex flex-col gap-0.5">
<span className={`text-sm flex items-center gap-1.5 ${labelTone}`}>
<PhaseIcon className={`w-3.5 h-3.5 shrink-0 ${iconTone}`} />
{phase.label}
</span>
{state === "current" && status && (
<span className="text-[0.72rem] text-gold-deep mt-0.5">
<span className="text-[0.72rem] text-gold-deep flex items-center gap-1">
{StatusIcon && <StatusIcon className="w-3 h-3 shrink-0" />}
{STATUS_LABELS[status]}
</span>
)}
{state === "current" && status && STATUS_DESCRIPTIONS[status] && (
<span className="text-[0.65rem] text-ink-muted leading-snug mt-0.5">
{STATUS_DESCRIPTIONS[status]}
</span>
)}
</div>
</li>
);

View File

@@ -1620,6 +1620,19 @@ async def api_research_analysis(case_number: str):
raise HTTPException(500, f"שגיאה בעיבוד הקובץ: {e}")
@app.get("/api/cases/{case_number}/research/analysis/download")
async def api_research_analysis_download(case_number: str):
"""Download the raw analysis-and-research.md file."""
path = _research_file_path(case_number)
if not path.exists():
raise HTTPException(404, "טרם בוצע ניתוח משפטי לתיק זה")
return FileResponse(
path,
media_type="text/markdown; charset=utf-8",
filename=f"analysis-{case_number}.md",
)
class ChairPositionRequest(BaseModel):
section_id: str
position: str = ""
@@ -2436,6 +2449,31 @@ async def api_reprocess_document(case_number: str, doc_id: str):
return {"status": "reprocessing"}
@app.delete("/api/cases/{case_number}/documents/{doc_id}")
async def api_delete_document(case_number: str, doc_id: str):
"""Delete a single document from a case (including its chunks and file)."""
case = await db.get_case_by_number(case_number)
if not case:
raise HTTPException(404, f"תיק {case_number} לא נמצא")
case_id = UUID(case["id"])
document_id = UUID(doc_id)
doc = await db.get_document(document_id)
if not doc or UUID(doc["case_id"]) != case_id:
raise HTTPException(404, "מסמך לא נמצא בתיק")
# Try to remove the physical file
file_path = doc.get("file_path")
if file_path:
import pathlib
p = pathlib.Path(file_path)
if p.exists():
p.unlink(missing_ok=True)
await db.delete_document(document_id)
return {"deleted": True, "doc_id": doc_id}
# ── Chair feedback endpoints ──────────────────────────────────────