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 שאלות מחקר הופקו. נדרשת ביקורתך לפני המשך."
|
"סיכום: 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 עם פירוט מה נכשל, שלח מייל לחיים.
|
**אם בדיקות שלב 6 נכשלו** — סטטוס issue = "blocked", פרסם comment עם פירוט מה נכשל, שלח מייל לחיים.
|
||||||
|
|
||||||
## מבנה הפלט המלא — analysis-and-research.md
|
## מבנה הפלט המלא — analysis-and-research.md
|
||||||
|
|||||||
@@ -284,21 +284,32 @@ tools:
|
|||||||
|
|
||||||
## מפת סטטוסים
|
## מפת סטטוסים
|
||||||
|
|
||||||
| סטטוס | פעולה |
|
**סטטוסים של התיק (`cases.status`) — כל סטטוס מתאים לפעולה אחת בדיוק:**
|
||||||
|--------|-------|
|
|
||||||
| new + יש מסמכים + לא הוגהו | → צור issue למגיה מסמכים (410c0167) |
|
| סטטוס | מי שינה לזה | פעולה הבאה |
|
||||||
| new + מסמכים הוגהו + אין claims | → צור issue למנתח משפטי |
|
|--------|-------------|------------|
|
||||||
| new + יש claims + לא עבר אימות מנתח | → שלב A (אימות איכות פלט מנתח) |
|
| `new` | (יצירת תיק) | → בדוק extraction_status של מסמכים. אם יש `pending` → צור issue למגיה (410c0167). אם כולם `completed`/`proofread` → צור issue למנתח |
|
||||||
| analyst_verified + יש claims + יש מחקר | → שלב B (סיכום + סיווג + שאלת תוצאה) |
|
| `proofread` | מגיה | → צור issue למנתח משפטי (ראה תבנית למטה) |
|
||||||
| outcome_set + אין claim_handling | → שלב B המשך (טבלת טיפול בטענות) |
|
| `documents_ready` | מנתח | → שלב A (בדיקות שלמות + שליליות + מתודולוגיה). אם עובר → עדכן ל-`analyst_verified` |
|
||||||
| outcome_set + יש claim_handling | → שלב C (כיוונים סילוגיסטיים) |
|
| `analyst_verified` | CEO (אחרי שלב A) | → האם יש מחקר תקדימים? אם לא → צור issue לחוקר (35022af0). אם כן → שלב B |
|
||||||
| brainstorming + comment מחיים | → שלב D (אימות שלמות + approve + הפעל כותב) |
|
| `research_complete` | חוקר | → שלב B (סיכום + סיווג + שאלת תוצאה לחיים) |
|
||||||
| direction_approved + chair_directions שלם | → ודא שכותב עובד |
|
| `outcome_set` | CEO (אחרי שחיים בחר) | → האם יש claim_handling? אם לא → שלב B המשך (טבלת bundle/skip). אם כן → שלב C |
|
||||||
| direction_approved + chair_directions חסר | → חזור לשלב D (השלמה מול חיים) |
|
| `direction_approved` | CEO (אחרי שחיים אישר) | → בדוק chair_directions שלם? אם כן → צור issue לכותב (7ed8686f). אם חסר → חזור לחיים |
|
||||||
| drafted | → צור issue לבודק איכות |
|
| `drafted` | כותב | → צור issue לבודק איכות (1a5b229e) |
|
||||||
| qa_review pass | → שלב F (export via מייצא טיוטה d0dc703b) |
|
| `qa_passed` | QA | → צור issue למייצא (d0dc703b) |
|
||||||
| qa_review fail — בעיה טכנית | → צור issue תיקון לכותב |
|
| `qa_failed` | QA | → בעיה טכנית → issue תיקון לכותב. בעיה מתודולוגית → חזור לשלב C/D |
|
||||||
| qa_review fail — בעיה מתודולוגית | → חזור לשלב 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 קודם
|
1. **לעולם אל תייצא בלי בדיקה** — תמיד הרץ validate_decision קודם
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ tools:
|
|||||||
- mcp__legal-ai__case_get
|
- mcp__legal-ai__case_get
|
||||||
- mcp__legal-ai__document_list
|
- mcp__legal-ai__document_list
|
||||||
- mcp__legal-ai__document_get_text
|
- mcp__legal-ai__document_get_text
|
||||||
|
- mcp__legal-ai__case_update
|
||||||
---
|
---
|
||||||
|
|
||||||
# מגיה מסמכים — סוכן הגהת OCR
|
# מגיה מסמכים — סוכן הגהת OCR
|
||||||
@@ -68,8 +69,11 @@ psql -h localhost -p 5432 -U "${DB_USER:-legal_ai}" -d "${DB_NAME:-legal_ai}" \
|
|||||||
```
|
```
|
||||||
אם עדכון DB לא אפשרי, עדכן רק את הקובץ ודווח.
|
אם עדכון DB לא אפשרי, עדכן רק את הקובץ ודווח.
|
||||||
|
|
||||||
### שלב 5: דיווח
|
### שלב 5: עדכון סטטוס ודיווח
|
||||||
פרסם comment ב-Paperclip עם:
|
|
||||||
|
1. **עדכן סטטוס**: `case_update(case_number, status='proofread')`
|
||||||
|
|
||||||
|
2. פרסם comment ב-Paperclip עם:
|
||||||
```
|
```
|
||||||
## דוח הגהת מסמכים — תיק {case_number}
|
## דוח הגהת מסמכים — תיק {case_number}
|
||||||
|
|
||||||
@@ -95,3 +99,23 @@ psql -h localhost -p 5432 -U "${DB_USER:-legal_ai}" -d "${DB_NAME:-legal_ai}" \
|
|||||||
4. **דווח מקומות מסופקים** — סמן `[?]` ותן לאדם להחליט
|
4. **דווח מקומות מסופקים** — סמן `[?]` ותן לאדם להחליט
|
||||||
5. **אל תמציא טקסט** — אם חסר משהו, סמן `[...]` ואל תנחש
|
5. **אל תמציא טקסט** — אם חסר משהו, סמן `[...]` ואל תנחש
|
||||||
6. **קרא את כל המסמך** — לפעמים הקשר ממסמך שלם עוזר להבין מילה שבורה
|
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 | מדווח, לא חוסם |
|
||||||
| מספור | warning | מדווח, לא חוסם |
|
| מספור | warning | מדווח, לא חוסם |
|
||||||
| מתודולוגיה | warning | מדווח, לא חוסם |
|
| מתודולוגיה | critical | חוסם ייצוא |
|
||||||
|
|
||||||
## תהליך עבודה
|
## תהליך עבודה
|
||||||
|
|
||||||
@@ -104,3 +104,23 @@ tools:
|
|||||||
- רשימת שגיאות מפורטת (אם יש)
|
- רשימת שגיאות מפורטת (אם יש)
|
||||||
- האם מותר לייצא (כל הקריטיים pass?)
|
- האם מותר לייצא (כל הקריטיים pass?)
|
||||||
- עדכן סטטוס ל-qa_review (אם נכשל) או drafted (אם עבר)
|
- עדכן סטטוס ל-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. בנה ציר זמן כרונולוגי של ההליך
|
2. בנה ציר זמן כרונולוגי של ההליך
|
||||||
|
|
||||||
### שלב 5: דיווח — חובה!
|
### שלב 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 שורות לכל אחד)
|
- סיכום כל פסק דין (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: הכנה
|
||||||
1. קרא פרטי התיק (`case_get`)
|
1. **קרא את המתודולוגיה**: `Read docs/decision-methodology.md` — חובה לפני כל כתיבה
|
||||||
2. קרא טענות מחולצות (`get_claims`)
|
2. קרא פרטי התיק (`case_get`)
|
||||||
3. **קרא את עמדות יו"ר הוועדה (`get_chair_directions`) — חובה!**
|
3. קרא טענות מחולצות (`get_claims`)
|
||||||
4. קבל תבנית החלטה (`get_decision_template`)
|
4. **קרא את עמדות יו"ר הוועדה (`get_chair_directions`) — חובה!**
|
||||||
5. קרא מדריך סגנון (`get_style_guide`)
|
5. קבל תבנית החלטה (`get_decision_template`)
|
||||||
|
6. קרא מדריך סגנון (`get_style_guide`)
|
||||||
|
|
||||||
### שלב 1ב: בדיקת עמדות יו"ר — חובה לפני כתיבה!
|
### שלב 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 — בודק האיכות לא יוכל לרוץ!**
|
**אם לא תעדכן סטטוס ל-drafted — בודק האיכות לא יוכל לרוץ!**
|
||||||
|
|
||||||
## בלוק י — דיון (הבלוק החשוב ביותר)
|
## בלוק י — דיון (הבלוק החשוב ביותר)
|
||||||
|
|||||||
@@ -4,3 +4,12 @@ mcp-server/.venv/
|
|||||||
**/__pycache__/
|
**/__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
.git/
|
.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",
|
"priority": "high",
|
||||||
"subtasks": [],
|
"subtasks": [],
|
||||||
"updatedAt": "2026-04-11T19:20:56.040Z"
|
"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": {
|
"metadata": {
|
||||||
"version": "1.0.0",
|
"created": "2026-04-13T14:20:54.888Z",
|
||||||
"lastModified": "2026-04-11T19:20:56.040Z",
|
"updated": "2026-04-13T14:20:54.888Z",
|
||||||
"taskCount": 60,
|
"description": "Tasks for master context"
|
||||||
"completedCount": 57,
|
|
||||||
"tags": [
|
|
||||||
"master"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
# The container runs both:
|
||||||
# Coolify's default /Dockerfile lookup builds the new Next.js staging
|
# - FastAPI (uvicorn) on :8000 — the API backend
|
||||||
# UI. The FastAPI Dockerfile lives on `main` and is unaffected.
|
# - Next.js (node) on :3000 — the frontend (proxies /api/* to :8000)
|
||||||
#
|
#
|
||||||
# When the rewrite is merged to main, decide between:
|
# start.sh launches both processes.
|
||||||
# (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.
|
|
||||||
# ══════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# ── Stage 1: Node deps ────────────────────────────────────────
|
||||||
FROM node:20-alpine AS deps
|
FROM node:20-alpine AS deps
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY web-ui/package.json web-ui/package-lock.json ./
|
COPY web-ui/package.json web-ui/package-lock.json ./
|
||||||
RUN npm ci --no-audit --no-fund
|
RUN npm ci --no-audit --no-fund
|
||||||
|
|
||||||
|
# ── Stage 2: Build Next.js ────────────────────────────────────
|
||||||
FROM node:20-alpine AS builder
|
FROM node:20-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
@@ -23,18 +22,49 @@ COPY web-ui/ ./
|
|||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
RUN npm run build
|
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
|
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 NODE_ENV=production
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
ENV PORT=3000
|
ENV PORT=3000
|
||||||
ENV HOSTNAME=0.0.0.0
|
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/public ./public
|
||||||
COPY --from=builder /app/.next/standalone ./
|
COPY --from=builder /app/.next/standalone ./
|
||||||
COPY --from=builder /app/.next/static ./.next/static
|
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
|
EXPOSE 3000
|
||||||
|
|
||||||
CMD ["node", "server.js"]
|
CMD ["./start.sh"]
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ dependencies = [
|
|||||||
"rq>=1.16.0",
|
"rq>=1.16.0",
|
||||||
"pillow>=10.0.0",
|
"pillow>=10.0.0",
|
||||||
"google-cloud-vision>=3.7.0",
|
"google-cloud-vision>=3.7.0",
|
||||||
|
"fastapi>=0.115.0",
|
||||||
|
"uvicorn[standard]>=0.30.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|||||||
@@ -747,6 +747,22 @@ async def update_decision(decision_id: UUID, **fields) -> None:
|
|||||||
await conn.execute(sql, decision_id, *values)
|
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 ───────────────────────────────────────────────
|
# ── Chunks & Vectors ───────────────────────────────────────────────
|
||||||
|
|
||||||
async def delete_document_chunks(document_id: UUID) -> int:
|
async def delete_document_chunks(document_id: UUID) -> int:
|
||||||
@@ -1050,6 +1066,91 @@ async def search_precedents(
|
|||||||
return results[:limit]
|
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 ────────────────────────────────────────────────
|
# ── Chair feedback ────────────────────────────────────────────────
|
||||||
|
|
||||||
async def record_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";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Staging config — proxies /api/* and /openapi.json to the production FastAPI
|
* Proxies /api/* and /openapi.json to the FastAPI backend.
|
||||||
* at legal-ai.nautilus.marcusgroup.org. This lets the new Next.js UI call the
|
* In Docker both processes run in the same container, so the default
|
||||||
* existing backend without CORS and without running a second FastAPI instance.
|
* target is http://127.0.0.1:8000. Override with NEXT_PUBLIC_API_ORIGIN
|
||||||
*
|
* if the backend lives elsewhere (e.g. during local dev).
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const API_ORIGIN =
|
const API_ORIGIN =
|
||||||
process.env.NEXT_PUBLIC_API_ORIGIN ??
|
process.env.NEXT_PUBLIC_API_ORIGIN ?? "http://127.0.0.1:8000";
|
||||||
"https://legal-ai.nautilus.marcusgroup.org";
|
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
|
|||||||
@@ -78,10 +78,25 @@ export default function ComposePage({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<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">
|
<Button asChild variant="outline">
|
||||||
<Link href={`/cases/${caseNumber}`}>חזרה לתיק</Link>
|
<Link href={`/cases/${caseNumber}`}>חזרה לתיק</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
<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 { CaseHeader } from "@/components/cases/case-header";
|
||||||
import { CaseEditDialog } from "@/components/cases/case-edit-dialog";
|
import { CaseEditDialog } from "@/components/cases/case-edit-dialog";
|
||||||
import { WorkflowTimeline } from "@/components/cases/workflow-timeline";
|
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 { DocumentsPanel } from "@/components/cases/documents-panel";
|
||||||
import { UploadSheet } from "@/components/documents/upload-sheet";
|
import { UploadSheet } from "@/components/documents/upload-sheet";
|
||||||
import { expectedOutcomes } from "@/lib/schemas/case";
|
import { expectedOutcomes } from "@/lib/schemas/case";
|
||||||
@@ -76,7 +78,6 @@ export default function CaseDetailPage({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="actions">פעולות</TabsTrigger>
|
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<UploadSheet caseNumber={caseNumber} />
|
<UploadSheet caseNumber={caseNumber} />
|
||||||
</div>
|
</div>
|
||||||
@@ -101,14 +102,7 @@ export default function CaseDetailPage({
|
|||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
<div className="flex items-center gap-3 flex-wrap pt-2 border-t border-rule">
|
||||||
|
|
||||||
<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">
|
|
||||||
<Button asChild className="bg-navy hover:bg-navy-soft text-parchment">
|
<Button asChild className="bg-navy hover:bg-navy-soft text-parchment">
|
||||||
<Link href={`/cases/${caseNumber}/compose`}>
|
<Link href={`/cases/${caseNumber}/compose`}>
|
||||||
פתח בעורך ההחלטה
|
פתח בעורך ההחלטה
|
||||||
@@ -117,6 +111,10 @@ export default function CaseDetailPage({
|
|||||||
{data && <CaseEditDialog data={data} />}
|
{data && <CaseEditDialog data={data} />}
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="documents" className="mt-5">
|
||||||
|
<DocumentsPanel data={data} />
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -125,6 +123,8 @@ export default function CaseDetailPage({
|
|||||||
<CardContent className="px-6 py-5">
|
<CardContent className="px-6 py-5">
|
||||||
<h2 className="text-navy text-base mb-4">שלב בתהליך</h2>
|
<h2 className="text-navy text-base mb-4">שלב בתהליך</h2>
|
||||||
<WorkflowTimeline status={data?.status} />
|
<WorkflowTimeline status={data?.status} />
|
||||||
|
<StatusChanger caseNumber={caseNumber} currentStatus={data?.status} />
|
||||||
|
<StatusGuide />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,28 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import {
|
||||||
import { CheckCircle2, Clock, Loader2, XCircle } from "lucide-react";
|
Dialog,
|
||||||
import type { CaseDetail } from "@/lib/api/cases";
|
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
|
* 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;
|
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 docs = data?.documents ?? [];
|
||||||
|
const caseNumber = data?.case_number ?? "";
|
||||||
|
|
||||||
if (docs.length === 0) {
|
if (docs.length === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -154,40 +377,13 @@ export function DocumentsPanel({ data }: { data?: CaseDetail }) {
|
|||||||
</div>
|
</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">
|
<ul className="divide-y divide-rule" dir="rtl">
|
||||||
{sorted.map((doc) => {
|
{sorted.map((doc) => (
|
||||||
const displayName = doc.title || filenameFromPath(doc.file_path);
|
<DocumentRow key={doc.id} doc={doc} caseNumber={caseNumber} />
|
||||||
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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
</ul>
|
||||||
</ScrollArea>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
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 { CaseStatus } from "@/lib/api/cases";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
const STATUS_LABELS: Record<CaseStatus, string> = {
|
const STATUS_LABELS: Record<CaseStatus, string> = {
|
||||||
new: "חדש",
|
new: "חדש",
|
||||||
@@ -17,6 +23,38 @@ const STATUS_LABELS: Record<CaseStatus, string> = {
|
|||||||
final: "סופי",
|
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:
|
/* Status color groups:
|
||||||
* intake → new, uploading, processing (muted parchment)
|
* intake → new, uploading, processing (muted parchment)
|
||||||
* prep → documents_ready, outcome_set (info blue)
|
* prep → documents_ready, outcome_set (info blue)
|
||||||
@@ -40,14 +78,16 @@ const STATUS_TONE: Record<CaseStatus, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function StatusBadge({ status }: { status: CaseStatus }) {
|
export function StatusBadge({ status }: { status: CaseStatus }) {
|
||||||
|
const Icon = STATUS_ICONS[status];
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
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}
|
{STATUS_LABELS[status] ?? status}
|
||||||
</Badge>
|
</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";
|
"use client";
|
||||||
|
|
||||||
import type { CaseStatus } from "@/lib/api/cases";
|
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.
|
* Vertical RTL workflow timeline showing the 13-status case pipeline.
|
||||||
@@ -13,15 +17,16 @@ import { STATUS_LABELS } from "@/components/cases/status-badge";
|
|||||||
type Phase = {
|
type Phase = {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
icon: LucideIcon;
|
||||||
statuses: CaseStatus[];
|
statuses: CaseStatus[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const PHASES: Phase[] = [
|
const PHASES: Phase[] = [
|
||||||
{ key: "intake", label: "קליטה ועיבוד", statuses: ["new", "uploading", "processing"] },
|
{ key: "intake", label: "קליטה ועיבוד", icon: FolderInput, statuses: ["new", "uploading", "processing"] },
|
||||||
{ key: "prep", label: "הכנת תיק", statuses: ["documents_ready", "outcome_set"] },
|
{ key: "prep", label: "הכנת תיק", icon: ClipboardList, statuses: ["documents_ready", "outcome_set"] },
|
||||||
{ key: "thinking", label: "ניתוח וכיוון", statuses: ["brainstorming", "direction_approved"] },
|
{ key: "thinking", label: "ניתוח וכיוון", icon: Brain, statuses: ["brainstorming", "direction_approved"] },
|
||||||
{ key: "writing", label: "כתיבת טיוטה", statuses: ["drafting", "qa_review", "drafted"] },
|
{ key: "writing", label: "כתיבת טיוטה", icon: PenLine, statuses: ["drafting", "qa_review", "drafted"] },
|
||||||
{ key: "done", label: "סגירה", statuses: ["exported", "reviewed", "final"] },
|
{ key: "done", label: "סגירה", icon: CheckCircle2, statuses: ["exported", "reviewed", "final"] },
|
||||||
];
|
];
|
||||||
|
|
||||||
function phaseIndexOf(status?: CaseStatus): number {
|
function phaseIndexOf(status?: CaseStatus): number {
|
||||||
@@ -55,19 +60,36 @@ export function WorkflowTimeline({ status }: { status?: CaseStatus }) {
|
|||||||
: state === "current" ? "text-navy font-semibold"
|
: state === "current" ? "text-navy font-semibold"
|
||||||
: "text-ink-muted";
|
: "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 (
|
return (
|
||||||
<li key={phase.key} className="relative flex items-start gap-3 ps-7">
|
<li key={phase.key} className="relative flex items-start gap-3 ps-7">
|
||||||
<span
|
<span
|
||||||
className={`absolute right-[5px] top-1 inline-block w-3 h-3 rounded-full border-2 ${dotTone}`}
|
className={`absolute right-[5px] top-1 inline-block w-3 h-3 rounded-full border-2 ${dotTone}`}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col gap-0.5">
|
||||||
<span className={`text-sm ${labelTone}`}>{phase.label}</span>
|
<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 && (
|
{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]}
|
{STATUS_LABELS[status]}
|
||||||
</span>
|
</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>
|
</div>
|
||||||
</li>
|
</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}")
|
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):
|
class ChairPositionRequest(BaseModel):
|
||||||
section_id: str
|
section_id: str
|
||||||
position: str = ""
|
position: str = ""
|
||||||
@@ -2436,6 +2449,31 @@ async def api_reprocess_document(case_number: str, doc_id: str):
|
|||||||
return {"status": "reprocessing"}
|
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 ──────────────────────────────────────
|
# ── Chair feedback endpoints ──────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user