Compare commits
16 Commits
df4d28eb5c
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 437472be85 | |||
| fdbf22c699 | |||
| 2d0e987803 | |||
| 35276eab41 | |||
| ef448be530 | |||
| 1d2d9c71d8 | |||
| 5eab006780 | |||
| bc1456672b | |||
| 2b431e75ab | |||
| 2b988fd805 | |||
| 62a67e3f31 | |||
| bf595975bf | |||
| 626d39d1bb | |||
| 94bc66d7c1 | |||
| cc50f0ffde | |||
| 3f6a130cf9 |
@@ -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
|
||||
|
||||
@@ -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 אם מסמך נכשל**
|
||||
|
||||
## כללים
|
||||
|
||||
|
||||
@@ -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 קודם
|
||||
|
||||
@@ -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'
|
||||
);"
|
||||
```
|
||||
|
||||
@@ -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'
|
||||
);"
|
||||
```
|
||||
|
||||
@@ -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'
|
||||
);"
|
||||
```
|
||||
|
||||
## כללים
|
||||
- **דיוק** — ציין מספרי סעיפים, תאריכים, שמות שופטים
|
||||
- **רלוונטיות** — התמקד במה שרלוונטי לתיק הנוכחי, לא בסיכום כללי
|
||||
|
||||
@@ -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 — בודק האיכות לא יוכל לרוץ!**
|
||||
|
||||
## בלוק י — דיון (הבלוק החשוב ביותר)
|
||||
|
||||
@@ -4,3 +4,12 @@ mcp-server/.venv/
|
||||
**/__pycache__/
|
||||
*.pyc
|
||||
.git/
|
||||
.taskmaster/
|
||||
web/static/
|
||||
web/__pycache__/
|
||||
scripts/
|
||||
skills/
|
||||
docs/
|
||||
legacy/
|
||||
node_modules/
|
||||
.next/
|
||||
|
||||
58
.gitea/workflows/deploy.yaml
Normal file
58
.gitea/workflows/deploy.yaml
Normal 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 }}"
|
||||
30
.taskmaster/docs/ui-updates-prd.txt
Normal file
30
.taskmaster/docs/ui-updates-prd.txt
Normal 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.
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Dockerfile
52
Dockerfile
@@ -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"]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
20
start.sh
Executable 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
|
||||
@@ -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",
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>“{displayName}”</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
84
web-ui/src/components/cases/status-changer.tsx
Normal file
84
web-ui/src/components/cases/status-changer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
72
web-ui/src/components/cases/status-guide.tsx
Normal file
72
web-ui/src/components/cases/status-guide.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
38
web/app.py
38
web/app.py
@@ -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 ──────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user