Files
legal-ai/web/static/index.html
Chaim d8e888ad6a Add sync-to-DB and delete-from-DB actions for skills
- POST /api/admin/skills/{slug}/sync — read SKILL.md from disk, insert/update DB
- DELETE /api/admin/skills/{slug} — remove skill from DB (keeps disk files)
- UI: Sync/Re-sync and Delete buttons per skill in the skills list

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 17:52:00 +00:00

1460 lines
60 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>עוזר משפטי — ניהול תיקים</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f5f5;
color: #1a1a2e;
direction: rtl;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Header */
header {
background: #1a1a2e;
color: #ffffff;
padding: 14px 32px;
display: flex;
align-items: center;
gap: 12px;
}
header h1 { font-size: 1.25em; font-weight: 600; cursor: pointer; }
header .sep { width: 2px; height: 20px; background: rgba(255,255,255,0.2); border-radius: 1px; }
header .subtitle { font-size: 0.85em; opacity: 0.6; }
header nav { margin-right: auto; display: flex; gap: 8px; }
header nav a {
color: rgba(255,255,255,0.7); text-decoration: none; font-size: 0.82em;
padding: 4px 12px; border-radius: 4px; transition: all 0.15s;
}
header nav a:hover, header nav a.active { color: #fff; background: rgba(255,255,255,0.1); }
/* Main */
.main { flex: 1; max-width: 1200px; width: 100%; margin: 0 auto; padding: 28px 24px; }
.page { display: none; }
.page.active { display: block; }
/* Cards */
.card {
background: #ffffff; border-radius: 10px;
box-shadow: 0 1px 4px rgba(0,0,0,0.06), 0 0 1px rgba(0,0,0,0.08);
overflow: hidden; margin-bottom: 16px;
}
.card-header {
padding: 14px 20px; font-size: 0.9em; font-weight: 600; color: #1a1a2e;
border-bottom: 1px solid #eee; display: flex; align-items: center; gap: 8px;
}
.card-body { padding: 20px; }
/* Buttons */
.btn {
padding: 8px 20px; border: none; border-radius: 6px; font-size: 0.88em;
font-family: inherit; cursor: pointer; transition: all 0.15s; font-weight: 500;
}
.btn-primary { background: #e94560; color: #fff; }
.btn-primary:hover { background: #d6304a; }
.btn-secondary { background: #eee; color: #1a1a2e; }
.btn-secondary:hover { background: #ddd; }
.btn-outline { background: transparent; color: #e94560; border: 1px solid #e94560; }
.btn-outline:hover { background: #fff0f3; }
.btn-sm { padding: 4px 12px; font-size: 0.78em; }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-success { background: #27ae60; color: #fff; }
/* Form Elements */
.form-row { display: flex; gap: 12px; flex-wrap: wrap; align-items: end; margin-bottom: 14px; }
.form-group { flex: 1; min-width: 160px; }
.form-group label { display: block; font-size: 0.78em; color: #888; margin-bottom: 4px; font-weight: 500; }
.form-group select, .form-group input[type="text"], .form-group input[type="date"], .form-group textarea {
width: 100%; padding: 8px 10px; border: 1px solid #ddd; border-radius: 6px;
font-size: 0.88em; font-family: inherit; direction: rtl; background: #fff; transition: border-color 0.15s;
}
.form-group textarea { resize: vertical; min-height: 60px; }
.form-group select:focus, .form-group input:focus, .form-group textarea:focus { outline: none; border-color: #e94560; }
/* ── Case List ─────────────────────────────────────────── */
.case-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; }
.case-card {
background: #fff; border-radius: 10px; padding: 20px; cursor: pointer;
box-shadow: 0 1px 4px rgba(0,0,0,0.06); transition: box-shadow 0.2s, transform 0.15s;
border-right: 4px solid #e94560;
}
.case-card:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.1); transform: translateY(-2px); }
.case-card .case-number { font-size: 1.1em; font-weight: 700; color: #e94560; }
.case-card .case-title { font-size: 0.9em; color: #555; margin: 6px 0; line-height: 1.4; }
.case-card .case-meta { font-size: 0.78em; color: #999; display: flex; gap: 16px; flex-wrap: wrap; }
.case-card .badge {
display: inline-block; padding: 2px 8px; border-radius: 4px;
font-size: 0.72em; font-weight: 600; background: #f0f0f0; color: #666;
}
.case-card .badge.new { background: #e3f2fd; color: #1976d2; }
.case-card .badge.in_progress { background: #fff3e0; color: #f57c00; }
.case-card .badge.drafted { background: #e8f5e9; color: #388e3c; }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.page-header h2 { font-size: 1.2em; font-weight: 600; }
/* ── Wizard ────────────────────────────────────────────── */
.wizard-steps {
display: flex; gap: 0; margin-bottom: 24px;
background: #fff; border-radius: 10px; overflow: hidden;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
.wizard-step {
flex: 1; padding: 12px 16px; text-align: center; font-size: 0.82em; color: #999;
border-bottom: 3px solid transparent; transition: all 0.2s;
}
.wizard-step.active { color: #e94560; border-bottom-color: #e94560; font-weight: 600; }
.wizard-step.done { color: #27ae60; border-bottom-color: #27ae60; }
.wizard-panel { display: none; }
.wizard-panel.active { display: block; }
.wizard-nav { display: flex; justify-content: space-between; margin-top: 20px; }
/* Dynamic list (appellants/respondents) */
.dynamic-list { margin-bottom: 12px; }
.dynamic-list .item { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; }
.dynamic-list .item input { flex: 1; }
.dynamic-list .remove-btn {
background: none; border: none; color: #e94560; cursor: pointer; font-size: 1.1em; padding: 4px;
}
/* ── Case View ─────────────────────────────────────────── */
.case-header-bar {
background: #fff; border-radius: 10px; padding: 20px; margin-bottom: 16px;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
display: flex; justify-content: space-between; align-items: start; gap: 20px;
}
.case-header-bar .info h2 { font-size: 1.15em; margin-bottom: 6px; }
.case-header-bar .info .meta { font-size: 0.82em; color: #888; display: flex; gap: 16px; flex-wrap: wrap; }
.case-header-bar .links { display: flex; gap: 8px; flex-shrink: 0; }
.case-header-bar .links a {
font-size: 0.78em; padding: 4px 10px; border-radius: 4px;
text-decoration: none; border: 1px solid #ddd; color: #555; transition: all 0.15s;
}
.case-header-bar .links a:hover { border-color: #e94560; color: #e94560; }
.doc-group { margin-bottom: 16px; }
.doc-group-header {
font-size: 0.85em; font-weight: 600; color: #555; padding: 8px 0;
border-bottom: 1px solid #eee; margin-bottom: 8px;
display: flex; justify-content: space-between;
}
.doc-group-header .count { font-weight: 400; color: #999; }
.doc-item {
display: flex; align-items: center; gap: 10px; padding: 8px 12px;
border-radius: 6px; transition: background 0.15s; font-size: 0.85em;
}
.doc-item:hover { background: #f8f8f8; }
.doc-item .doc-icon { color: #999; }
.doc-item .doc-name { flex: 1; }
.doc-item .doc-status { font-size: 0.75em; color: #999; }
/* Upload zone (reusable) */
.upload-zone {
border: 2px dashed #ccc; border-radius: 10px; padding: 40px 24px;
text-align: center; cursor: pointer; transition: border-color 0.2s, background 0.2s; background: #fafafa;
}
.upload-zone:hover, .upload-zone.dragover { border-color: #e94560; background: #fff5f7; }
.upload-zone h3 { font-size: 0.95em; font-weight: 500; color: #555; margin-bottom: 4px; }
.upload-zone p { font-size: 0.78em; color: #aaa; }
.upload-zone input[type="file"] { display: none; }
/* Pending upload items in case view */
.pending-upload {
border: 1px solid #eee; border-radius: 8px; padding: 14px; margin-bottom: 10px; background: #fafafa;
}
.pending-upload .file-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.pending-upload .file-name { font-weight: 600; font-size: 0.88em; word-break: break-all; }
.pending-upload .tag-row { display: flex; gap: 10px; align-items: end; flex-wrap: wrap; }
.pending-upload .tag-row .form-group { min-width: 120px; }
/* Processing tasks */
.task-item {
display: flex; align-items: center; gap: 12px; padding: 12px;
border: 1px solid #eee; border-radius: 8px; margin-bottom: 8px; background: #fafafa;
}
.task-item .spinner {
width: 20px; height: 20px; border: 2.5px solid #eee; border-top-color: #e94560;
border-radius: 50%; animation: spin 0.8s linear infinite; flex-shrink: 0;
}
.task-item.done .spinner { border-color: #27ae60; border-top-color: #27ae60; animation: none; }
.task-item.failed .spinner { border-color: #e94560; border-top-color: #e94560; animation: none; }
@keyframes spin { to { transform: rotate(360deg); } }
.task-info { flex: 1; min-width: 0; }
.task-info .task-name { font-size: 0.85em; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.task-info .task-status { font-size: 0.75em; color: #999; margin-top: 2px; }
/* Creation progress in wizard */
.creation-steps { list-style: none; }
.creation-steps li { padding: 8px 0; font-size: 0.88em; display: flex; align-items: center; gap: 8px; }
.creation-steps li .step-icon { width: 20px; text-align: center; }
.creation-steps li.pending .step-icon { color: #ccc; }
.creation-steps li.running .step-icon { color: #e94560; }
.creation-steps li.done .step-icon { color: #27ae60; }
.creation-steps li.error .step-icon { color: #e94560; }
/* Status Bar */
.status-bar {
background: #1a1a2e; color: rgba(255,255,255,0.5); font-size: 0.75em;
padding: 8px 32px; display: flex; gap: 24px; align-items: center;
}
.status-bar .stat { display: flex; align-items: center; gap: 6px; }
.status-bar .stat-value { color: rgba(255,255,255,0.85); font-weight: 600; }
/* Toast */
.toast {
position: fixed; bottom: 60px; left: 50%; transform: translateX(-50%) translateY(10px);
background: #1a1a2e; color: white; padding: 10px 24px; border-radius: 8px;
font-size: 0.85em; z-index: 1000; opacity: 0; transition: opacity 0.3s, transform 0.3s; pointer-events: none;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.toast.error { background: #e94560; }
.toast.success { background: #27ae60; }
/* ── Exports / Drafts ─────────────────────────────────── */
.export-item {
display: flex; align-items: center; gap: 12px; padding: 10px 14px;
border: 1px solid #eee; border-radius: 8px; margin-bottom: 8px; background: #fafafa;
transition: background 0.15s;
}
.export-item:hover { background: #f0f0f0; }
.export-item .export-icon { font-size: 1.3em; flex-shrink: 0; }
.export-item .export-info { flex: 1; min-width: 0; }
.export-item .export-name { font-size: 0.88em; font-weight: 600; }
.export-item .export-meta { font-size: 0.75em; color: #999; margin-top: 2px; }
.export-item .export-actions { display: flex; gap: 6px; flex-shrink: 0; }
.export-item.final { border-color: #27ae60; background: #f0faf3; }
.export-item.final .export-icon { color: #27ae60; }
.export-upload-zone {
border: 2px dashed #ccc; border-radius: 8px; padding: 16px;
text-align: center; cursor: pointer; transition: border-color 0.2s, background 0.2s;
background: #fafafa; font-size: 0.85em; color: #888; margin-top: 8px;
}
.export-upload-zone:hover { border-color: #e94560; background: #fff5f7; }
.export-upload-zone input[type="file"] { display: none; }
.empty { text-align: center; color: #bbb; padding: 40px 20px; font-size: 0.88em; line-height: 1.6; }
/* ── Skills Management ───────────────────────────────── */
.skill-list { display: flex; flex-direction: column; gap: 10px; }
.skill-item {
display: flex; align-items: center; gap: 14px; padding: 14px 18px;
border: 1px solid #eee; border-radius: 8px; background: #fafafa;
transition: background 0.15s;
}
.skill-item:hover { background: #f0f0f0; }
.skill-item .skill-icon { font-size: 1.5em; flex-shrink: 0; }
.skill-item .skill-info { flex: 1; min-width: 0; }
.skill-item .skill-name { font-size: 0.95em; font-weight: 600; }
.skill-item .skill-meta { font-size: 0.75em; color: #999; margin-top: 3px; display: flex; gap: 14px; flex-wrap: wrap; }
.skill-item .skill-badges { display: flex; gap: 6px; flex-shrink: 0; }
.skill-item .badge-ok { background: #e8f5e9; color: #388e3c; padding: 2px 8px; border-radius: 4px; font-size: 0.72em; font-weight: 600; }
.skill-item .badge-warn { background: #fff3e0; color: #f57c00; padding: 2px 8px; border-radius: 4px; font-size: 0.72em; font-weight: 600; }
.skill-install-result {
margin-top: 16px; padding: 16px; border-radius: 8px;
background: #e8f5e9; border: 1px solid #c8e6c9; font-size: 0.88em;
}
.skill-install-result.error { background: #ffebee; border-color: #ffcdd2; }
@media (max-width: 800px) {
.main { padding: 16px; }
header { padding: 14px 16px; }
.status-bar { padding: 8px 16px; flex-wrap: wrap; gap: 12px; }
.case-grid { grid-template-columns: 1fr; }
.wizard-steps { flex-wrap: wrap; }
}
</style>
</head>
<body>
<header>
<h1 onclick="navigate('/')">עוזר משפטי</h1>
<div class="sep"></div>
<span class="subtitle" id="pageSubtitle">ניהול תיקים</span>
<nav>
<a href="#/" id="navHome">תיקים</a>
<a href="#/new" id="navNew">+ תיק חדש</a>
<a href="#/upload" id="navUpload">העלאה</a>
<a href="#/skills" id="navSkills">Skills</a>
</nav>
</header>
<div class="main">
<!-- ══ Page: Case List ══ -->
<div class="page" id="page-home">
<div class="page-header">
<h2>תיקי ערר</h2>
<button class="btn btn-primary" onclick="navigate('/new')">+ תיק חדש</button>
</div>
<div class="case-grid" id="caseGrid">
<div class="empty">טוען תיקים...</div>
</div>
</div>
<!-- ══ Page: New Case Wizard ══ -->
<div class="page" id="page-new">
<div class="wizard-steps">
<div class="wizard-step active" data-step="1">1. פרטי תיק</div>
<div class="wizard-step" data-step="2">2. צדדים</div>
<div class="wizard-step" data-step="3">3. לוח זמנים</div>
<div class="wizard-step" data-step="4">4. סיכום ויצירה</div>
</div>
<!-- Step 1: Case Details -->
<div class="wizard-panel active" id="wiz-step-1">
<div class="card"><div class="card-body">
<div class="form-row">
<div class="form-group" style="max-width:180px">
<label>מספר תיק *</label>
<input type="text" id="wiz-case-number" placeholder="1130-25" dir="ltr">
</div>
<div class="form-group">
<label>כותרת *</label>
<input type="text" id="wiz-title" placeholder="ערר על אישור תכנית להוספת קומה">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>סוג ערר</label>
<select id="wiz-committee-type">
<option value="רישוי">רישוי ובניה</option>
<option value="היטל השבחה">היטל השבחה</option>
<option value="פיצויים">פיצויים (ס' 197)</option>
</select>
</div>
<div class="form-group">
<label>כתובת נכס</label>
<input type="text" id="wiz-address" placeholder="רח' אבינדב 23, קריית יערים">
</div>
<div class="form-group" style="max-width:180px">
<label>מספר היתר/תכנית</label>
<input type="text" id="wiz-permit" placeholder="152-1257682" dir="ltr">
</div>
</div>
</div></div>
<div class="wizard-nav">
<span></span>
<button class="btn btn-primary" onclick="wizNext()">הבא &larr;</button>
</div>
</div>
<!-- Step 2: Parties -->
<div class="wizard-panel" id="wiz-step-2">
<div class="card"><div class="card-header">עוררים</div><div class="card-body">
<div class="dynamic-list" id="appellantsList">
<div class="item"><input type="text" placeholder="שם עורר"><button class="remove-btn" onclick="removeItem(this)">&times;</button></div>
</div>
<button class="btn btn-sm btn-secondary" onclick="addItem('appellantsList')">+ הוסף עורר</button>
</div></div>
<div class="card"><div class="card-header">משיבים</div><div class="card-body">
<div class="dynamic-list" id="respondentsList">
<div class="item"><input type="text" placeholder="שם משיב"><button class="remove-btn" onclick="removeItem(this)">&times;</button></div>
</div>
<button class="btn btn-sm btn-secondary" onclick="addItem('respondentsList')">+ הוסף משיב</button>
</div></div>
<div class="wizard-nav">
<button class="btn btn-secondary" onclick="wizPrev()">&rarr; הקודם</button>
<button class="btn btn-primary" onclick="wizNext()">הבא &larr;</button>
</div>
</div>
<!-- Step 3: Schedule -->
<div class="wizard-panel" id="wiz-step-3">
<div class="card"><div class="card-body">
<div class="form-row">
<div class="form-group" style="max-width:200px">
<label>תאריך דיון</label>
<input type="date" id="wiz-hearing-date">
</div>
<div class="form-group">
<label>תוצאה צפויה</label>
<select id="wiz-outcome">
<option value="">לא הוגדר</option>
<option value="rejection">דחייה</option>
<option value="partial_acceptance">קבלה חלקית</option>
<option value="full_acceptance">קבלה מלאה</option>
<option value="betterment_levy">היטל השבחה</option>
</select>
</div>
</div>
<div class="form-group">
<label>הערות</label>
<textarea id="wiz-notes" rows="3" placeholder="הערות חופשיות..."></textarea>
</div>
</div></div>
<div class="wizard-nav">
<button class="btn btn-secondary" onclick="wizPrev()">&rarr; הקודם</button>
<button class="btn btn-primary" onclick="wizNext()">הבא &larr;</button>
</div>
</div>
<!-- Step 4: Review & Create -->
<div class="wizard-panel" id="wiz-step-4">
<div class="card"><div class="card-header">סיכום</div><div class="card-body">
<div id="wizSummary"></div>
</div></div>
<div class="card"><div class="card-header">יצירה אוטומטית</div><div class="card-body">
<ul class="creation-steps" id="creationSteps">
<li class="pending" id="cs-db"><span class="step-icon">&#9711;</span> יצירת תיק במסד הנתונים</li>
<li class="pending" id="cs-gitea"><span class="step-icon">&#9711;</span> יצירת Repository ב-Gitea</li>
<li class="pending" id="cs-paperclip"><span class="step-icon">&#9711;</span> יצירת פרויקט ב-Paperclip</li>
</ul>
<div style="margin-top:16px">
<button class="btn btn-success" id="createCaseBtn" onclick="createCase()">צור תיק</button>
</div>
</div></div>
<div class="wizard-nav">
<button class="btn btn-secondary" onclick="wizPrev()">&rarr; הקודם</button>
<span></span>
</div>
</div>
</div>
<!-- ══ Page: Case View ══ -->
<div class="page" id="page-case">
<div class="case-header-bar" id="caseHeaderBar">
<div class="info">
<h2 id="caseViewTitle">טוען...</h2>
<div class="meta" id="caseViewMeta"></div>
</div>
<div class="links" id="caseViewLinks"></div>
</div>
<!-- Upload to case -->
<div class="card">
<div class="card-header">העלאת מסמכים</div>
<div class="card-body">
<div class="upload-zone" id="caseDropZone">
<h3>גרור קבצים לכאן או לחץ לבחירה</h3>
<p>PDF, DOCX, RTF, TXT — עד 50MB</p>
<input type="file" id="caseFileInput" multiple accept=".pdf,.docx,.rtf,.txt,.md">
</div>
<div id="casePendingUploads" style="margin-top: 12px"></div>
<div id="caseTasks" style="margin-top: 12px"></div>
</div>
</div>
<!-- Documents list -->
<div class="card">
<div class="card-header">מסמכים בתיק</div>
<div class="card-body" id="caseDocsList">
<div class="empty">אין מסמכים</div>
</div>
</div>
<!-- Exports / Drafts -->
<div class="card">
<div class="card-header">
<span>טיוטות וגרסאות</span>
<button class="btn btn-sm btn-primary" id="exportDocxBtn" style="margin-right:auto" onclick="triggerExport()">ייצא טיוטה חדשה</button>
</div>
<div class="card-body">
<div id="exportsList"><div class="empty">אין טיוטות עדיין</div></div>
<div class="export-upload-zone" id="exportUploadZone">
העלאת גרסה מעודכנת (DOCX)
<input type="file" id="exportFileInput" accept=".docx">
</div>
</div>
</div>
</div>
<!-- ══ Page: Skills Management ══ -->
<div class="page" id="page-skills">
<div class="page-header">
<h2>Paperclip Skills</h2>
</div>
<!-- Install Skill -->
<div class="card">
<div class="card-header">התקנה / שדרוג Skill</div>
<div class="card-body">
<div class="upload-zone" id="skillDropZone">
<div style="font-size:3em;color:#ccc;margin-bottom:16px">&#128268;</div>
<h3>גרור קובץ ZIP של Skill או לחץ לבחירה</h3>
<p>ZIP עם SKILL.md, scripts/, references/ — לפי מבנה Anthropic</p>
<input type="file" id="skillFileInput" accept=".zip">
</div>
<div id="skillInstallResult"></div>
</div>
</div>
<!-- Installed Skills -->
<div class="card">
<div class="card-header">Skills מותקנים</div>
<div class="card-body">
<div class="skill-list" id="skillList">
<div class="empty">טוען...</div>
</div>
</div>
</div>
<!-- Restart Paperclip -->
<div class="card">
<div class="card-header">Paperclip Server</div>
<div class="card-body" style="display:flex;align-items:center;gap:16px">
<button class="btn btn-secondary" id="restartPaperclipBtn" onclick="restartPaperclip()">Restart Paperclip</button>
<span id="restartStatus" style="font-size:0.85em;color:#888"></span>
</div>
</div>
</div>
<!-- ══ Page: Legacy Upload ══ -->
<div class="page" id="page-upload">
<div class="card"><div class="card-body">
<div class="upload-zone" id="legacyDropZone">
<div style="font-size:3em;color:#ccc;margin-bottom:16px">&#128196;</div>
<h3>גרור קבצים לכאן או לחץ לבחירה</h3>
<p>PDF, DOCX, RTF, TXT — עד 50MB</p>
<input type="file" id="legacyFileInput" multiple accept=".pdf,.docx,.rtf,.txt,.md">
</div>
</div></div>
<div class="card" id="legacyPendingCard" style="display:none">
<div class="card-header">קבצים ממתינים לסיווג</div>
<div class="card-body" id="legacyPendingList"></div>
</div>
<div class="card" id="legacyTasksCard" style="display:none">
<div class="card-header">עיבוד</div>
<div class="card-body" id="legacyTasksList"></div>
</div>
</div>
</div>
<!-- Status Bar -->
<div class="status-bar">
<div class="stat">תיקים: <span class="stat-value" id="statCases"></span></div>
<div class="stat">מסמכים: <span class="stat-value" id="statDocs"></span></div>
<div class="stat">קטעים: <span class="stat-value" id="statChunks"></span></div>
</div>
<div class="toast" id="toast"></div>
<script>
const API = '/api';
let currentCaseNumber = '';
// ── Router ───────────────────────────────────────────────
function navigate(path) {
window.location.hash = path;
}
function handleRoute() {
const hash = window.location.hash || '#/';
const pages = document.querySelectorAll('.page');
pages.forEach(p => p.classList.remove('active'));
// Nav links
document.querySelectorAll('header nav a').forEach(a => a.classList.remove('active'));
let subtitle = 'ניהול תיקים';
if (hash === '#/' || hash === '#') {
document.getElementById('page-home').classList.add('active');
document.getElementById('navHome').classList.add('active');
loadCaseList();
} else if (hash === '#/new') {
document.getElementById('page-new').classList.add('active');
document.getElementById('navNew').classList.add('active');
subtitle = 'תיק חדש';
resetWizard();
} else if (hash.startsWith('#/case/')) {
const caseNum = hash.replace('#/case/', '');
document.getElementById('page-case').classList.add('active');
subtitle = 'ערר ' + caseNum;
currentCaseNumber = caseNum;
loadCaseView(caseNum);
} else if (hash === '#/skills') {
document.getElementById('page-skills').classList.add('active');
document.getElementById('navSkills').classList.add('active');
subtitle = 'Paperclip Skills';
loadSkillList();
setupSkillUpload();
} else if (hash === '#/upload') {
document.getElementById('page-upload').classList.add('active');
document.getElementById('navUpload').classList.add('active');
subtitle = 'העלאת מסמכים';
loadLegacyPending();
}
document.getElementById('pageSubtitle').textContent = subtitle;
}
window.addEventListener('hashchange', handleRoute);
window.addEventListener('load', () => { handleRoute(); loadStatus(); });
// ── Case List ────────────────────────────────────────────
async function loadCaseList() {
const grid = document.getElementById('caseGrid');
try {
const res = await fetch(API + '/cases?detail=true');
const cases = await res.json();
if (!cases.length) {
grid.innerHTML = '<div class="empty">אין תיקים עדיין.<br>לחץ "+ תיק חדש" כדי להתחיל.</div>';
return;
}
const STATUS_LABELS = {
new: 'חדש', in_progress: 'בתהליך', documents_ready: 'מסמכים מוכנים',
outcome_set: 'תוצאה נקבעה', direction_approved: 'כיוון אושר',
drafting: 'בכתיבה', drafted: 'טיוטה', qa_review: 'QA', reviewed: 'נבדק', final: 'סופי',
};
grid.innerHTML = cases.map(c => `
<div class="case-card" onclick="navigate('/case/${esc(c.case_number)}')">
<div style="display:flex;justify-content:space-between;align-items:center">
<span class="case-number">ערר ${esc(c.case_number)}</span>
<span class="badge ${c.status}">${STATUS_LABELS[c.status] || c.status}</span>
</div>
<div class="case-title">${esc(c.title)}</div>
<div class="case-meta">
${c.document_count !== undefined ? `<span>${c.document_count} מסמכים</span>` : ''}
${c.committee_type ? `<span>${esc(c.committee_type)}</span>` : ''}
${c.hearing_date ? `<span>דיון: ${esc(c.hearing_date)}</span>` : ''}
</div>
</div>
`).join('');
} catch (e) {
grid.innerHTML = '<div class="empty">שגיאה בטעינת תיקים</div>';
}
}
// ── Wizard ────────────────────────────────────────────────
let wizStep = 1;
function resetWizard() {
wizStep = 1;
updateWizardUI();
// Reset creation steps
document.querySelectorAll('.creation-steps li').forEach(li => {
li.className = 'pending';
li.querySelector('.step-icon').innerHTML = '&#9711;';
});
document.getElementById('createCaseBtn').disabled = false;
}
function updateWizardUI() {
document.querySelectorAll('.wizard-step').forEach(s => {
const step = parseInt(s.dataset.step);
s.classList.toggle('active', step === wizStep);
s.classList.toggle('done', step < wizStep);
});
document.querySelectorAll('.wizard-panel').forEach((p, i) => {
p.classList.toggle('active', i + 1 === wizStep);
});
if (wizStep === 4) buildSummary();
}
function wizNext() {
if (wizStep === 1) {
if (!document.getElementById('wiz-case-number').value.trim() || !document.getElementById('wiz-title').value.trim()) {
return toast('יש למלא מספר תיק וכותרת', 'error');
}
}
if (wizStep < 4) { wizStep++; updateWizardUI(); }
}
function wizPrev() {
if (wizStep > 1) { wizStep--; updateWizardUI(); }
}
function addItem(listId) {
const list = document.getElementById(listId);
const div = document.createElement('div');
div.className = 'item';
div.innerHTML = `<input type="text" placeholder="שם"><button class="remove-btn" onclick="removeItem(this)">&times;</button>`;
list.appendChild(div);
div.querySelector('input').focus();
}
function removeItem(btn) {
const list = btn.closest('.dynamic-list');
btn.closest('.item').remove();
if (!list.children.length) addItem(list.id);
}
function getListValues(listId) {
return Array.from(document.querySelectorAll(`#${listId} .item input`))
.map(i => i.value.trim()).filter(Boolean);
}
function buildSummary() {
const data = getWizardData();
const OUTCOME_LABELS = { rejection: 'דחייה', partial_acceptance: 'קבלה חלקית', full_acceptance: 'קבלה מלאה', betterment_levy: 'היטל השבחה' };
document.getElementById('wizSummary').innerHTML = `
<table style="width:100%;font-size:0.88em;border-collapse:collapse">
<tr><td style="padding:6px;color:#888;width:120px">מספר תיק</td><td style="padding:6px;font-weight:600">${esc(data.case_number)}</td></tr>
<tr><td style="padding:6px;color:#888">כותרת</td><td style="padding:6px">${esc(data.title)}</td></tr>
<tr><td style="padding:6px;color:#888">סוג</td><td style="padding:6px">${esc(data.committee_type)}</td></tr>
<tr><td style="padding:6px;color:#888">כתובת</td><td style="padding:6px">${esc(data.property_address || '—')}</td></tr>
<tr><td style="padding:6px;color:#888">עוררים</td><td style="padding:6px">${data.appellants.length ? data.appellants.map(esc).join(', ') : '—'}</td></tr>
<tr><td style="padding:6px;color:#888">משיבים</td><td style="padding:6px">${data.respondents.length ? data.respondents.map(esc).join(', ') : '—'}</td></tr>
<tr><td style="padding:6px;color:#888">תוצאה צפויה</td><td style="padding:6px">${OUTCOME_LABELS[data.expected_outcome] || '—'}</td></tr>
</table>
`;
}
function getWizardData() {
return {
case_number: document.getElementById('wiz-case-number').value.trim(),
title: document.getElementById('wiz-title').value.trim(),
committee_type: document.getElementById('wiz-committee-type').value,
property_address: document.getElementById('wiz-address').value.trim(),
permit_number: document.getElementById('wiz-permit').value.trim(),
appellants: getListValues('appellantsList'),
respondents: getListValues('respondentsList'),
hearing_date: document.getElementById('wiz-hearing-date').value,
expected_outcome: document.getElementById('wiz-outcome').value,
notes: document.getElementById('wiz-notes').value.trim(),
};
}
async function createCase() {
const data = getWizardData();
const btn = document.getElementById('createCaseBtn');
btn.disabled = true;
btn.textContent = 'יוצר...';
const setStep = (id, state, icon) => {
const li = document.getElementById(id);
li.className = state;
li.querySelector('.step-icon').innerHTML = icon;
};
try {
// Step 1: Create in DB
setStep('cs-db', 'running', '&#9881;');
const dbRes = await fetch(API + '/cases/create', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data),
});
if (!dbRes.ok) { const e = await dbRes.json(); throw new Error(e.detail || 'DB error'); }
setStep('cs-db', 'done', '&#10003;');
// Step 2: Gitea repo
setStep('cs-gitea', 'running', '&#9881;');
try {
const giteaRes = await fetch(API + '/integrations/gitea/create-repo', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ case_number: data.case_number, title: data.title }),
});
if (giteaRes.ok) {
setStep('cs-gitea', 'done', '&#10003;');
} else {
setStep('cs-gitea', 'error', '&#10007;');
}
} catch (e) {
setStep('cs-gitea', 'error', '&#10007;');
}
// Step 3: Paperclip project
setStep('cs-paperclip', 'running', '&#9881;');
try {
const pcRes = await fetch(API + '/integrations/paperclip/create-project', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
case_number: data.case_number, title: data.title,
appeal_type: data.committee_type,
description: `${data.appellants.join(', ')} נ' ${data.respondents.join(', ')}`,
}),
});
if (pcRes.ok) {
setStep('cs-paperclip', 'done', '&#10003;');
} else {
setStep('cs-paperclip', 'error', '&#10007;');
}
} catch (e) {
setStep('cs-paperclip', 'error', '&#10007;');
}
toast('התיק נוצר בהצלחה!', 'success');
setTimeout(() => navigate('/case/' + data.case_number), 1000);
} catch (err) {
setStep('cs-db', 'error', '&#10007;');
toast(err.message, 'error');
btn.disabled = false;
btn.textContent = 'צור תיק';
}
}
// ── Case View ────────────────────────────────────────────
const DOC_TYPE_LABELS = {
appeal: 'כתבי ערר', response: 'כתבי תשובה', protocol: 'פרוטוקולים',
plan: 'תכניות', decision: 'החלטות', court_decision: 'פסקי דין',
permit: 'היתרים', appraisal: 'שומות', exhibit: 'נספחים',
objection: 'התנגדויות', reference: 'מסמכי עזר', auto: 'לא מסווג',
};
async function loadCaseView(caseNumber) {
try {
const res = await fetch(API + '/cases/' + encodeURIComponent(caseNumber) + '/details');
if (!res.ok) throw new Error('Case not found');
const data = await res.json();
document.getElementById('caseViewTitle').textContent = `ערר ${caseNumber}${data.title || ''}`;
const STATUS_LABELS = {
new: 'חדש', in_progress: 'בתהליך', documents_ready: 'מסמכים מוכנים',
drafted: 'טיוטה', final: 'סופי',
};
const meta = [];
meta.push(`<span class="badge ${data.status}">${STATUS_LABELS[data.status] || data.status}</span>`);
if (data.committee_type) meta.push(data.committee_type);
if (data.property_address) meta.push(data.property_address);
if (data.appellants?.length) meta.push('עוררים: ' + data.appellants.join(', '));
document.getElementById('caseViewMeta').innerHTML = meta.map(m => `<span>${m}</span>`).join('');
// Links
const links = [];
links.push(`<a href="https://gitea.nautilus.marcusgroup.org/cases/${caseNumber}" target="_blank">Gitea</a>`);
links.push(`<a href="#" onclick="openPaperclip('${esc(caseNumber)}');return false">Paperclip</a>`);
document.getElementById('caseViewLinks').innerHTML = links.join('');
// Documents grouped by type
const docs = data.documents || [];
if (!docs.length) {
document.getElementById('caseDocsList').innerHTML = '<div class="empty">אין מסמכים עדיין — העלה קבצים למעלה</div>';
} else {
const groups = {};
docs.forEach(d => {
const t = d.doc_type || 'reference';
if (!groups[t]) groups[t] = [];
groups[t].push(d);
});
const order = ['appeal','response','protocol','plan','decision','court_decision','permit','appraisal','exhibit','objection','reference'];
let html = '';
for (const type of order) {
if (!groups[type]) continue;
html += `<div class="doc-group">
<div class="doc-group-header">
<span>${DOC_TYPE_LABELS[type] || type}</span>
<span class="count">${groups[type].length}</span>
</div>`;
for (const doc of groups[type]) {
const statusIcon = doc.extraction_status === 'completed' ? '&#10003;' : '&#9711;';
html += `<div class="doc-item">
<span class="doc-icon">&#128196;</span>
<span class="doc-name">${esc(doc.title)}</span>
<span class="doc-status">${statusIcon}</span>
</div>`;
}
html += '</div>';
}
document.getElementById('caseDocsList').innerHTML = html;
}
} catch (e) {
document.getElementById('caseViewTitle').textContent = `תיק ${caseNumber} לא נמצא`;
document.getElementById('caseViewMeta').innerHTML = '';
document.getElementById('caseViewLinks').innerHTML = '';
document.getElementById('caseDocsList').innerHTML = `
<div class="empty">
התיק לא קיים עדיין במערכת.<br>
<button class="btn btn-primary" style="margin-top:12px" onclick="navigate('/new')">צור תיק חדש</button>
</div>`;
return;
}
setupCaseUpload(caseNumber);
loadExports(caseNumber);
setupExportUpload(caseNumber);
}
async function openPaperclip(caseNumber) {
// Try to find the Paperclip project URL
try {
const res = await fetch(API + '/cases/' + encodeURIComponent(caseNumber) + '/details');
const data = await res.json();
// Determine company prefix from committee_type
const prefix = (data.committee_type === 'היטל השבחה' || data.committee_type === 'פיצויים') ? 'CMPA' : 'CMP';
window.open(`https://pc.nautilus.marcusgroup.org/${prefix}/projects`, '_blank');
} catch (e) {
window.open('https://pc.nautilus.marcusgroup.org', '_blank');
}
}
// ── Case Document Upload ─────────────────────────────────
let casePendingFiles = [];
function setupCaseUpload(caseNumber) {
const dropZone = document.getElementById('caseDropZone');
const fileInput = document.getElementById('caseFileInput');
// Remove old listeners by cloning
const newDrop = dropZone.cloneNode(true);
dropZone.parentNode.replaceChild(newDrop, dropZone);
const newInput = newDrop.querySelector('input[type="file"]');
newDrop.addEventListener('click', () => newInput.click());
newDrop.addEventListener('dragover', e => { e.preventDefault(); newDrop.classList.add('dragover'); });
newDrop.addEventListener('dragleave', () => newDrop.classList.remove('dragover'));
newDrop.addEventListener('drop', e => {
e.preventDefault();
newDrop.classList.remove('dragover');
addFilesToCase(e.dataTransfer.files, caseNumber);
});
newInput.addEventListener('change', () => {
if (newInput.files.length) addFilesToCase(newInput.files, caseNumber);
newInput.value = '';
});
casePendingFiles = [];
renderCasePending(caseNumber);
}
function addFilesToCase(files, caseNumber) {
for (const file of files) {
casePendingFiles.push({ file, doc_type: 'auto', party_name: '' });
}
renderCasePending(caseNumber);
}
function renderCasePending(caseNumber) {
const container = document.getElementById('casePendingUploads');
if (!casePendingFiles.length) { container.innerHTML = ''; return; }
container.innerHTML = casePendingFiles.map((pf, i) => `
<div class="pending-upload" data-idx="${i}">
<div class="file-row">
<span class="file-name">${esc(pf.file.name)}</span>
<span style="font-size:0.78em;color:#999">${formatSize(pf.file.size)}</span>
</div>
<div class="tag-row">
<div class="form-group" style="min-width:140px">
<label>סוג מסמך</label>
<select onchange="casePendingFiles[${i}].doc_type=this.value">
<option value="auto">אוטומטי</option>
<option value="appeal">כתב ערר</option>
<option value="response">תשובה</option>
<option value="protocol">פרוטוקול</option>
<option value="plan">תכנית</option>
<option value="decision">החלטה</option>
<option value="court_decision">פסק דין</option>
<option value="permit">היתר</option>
<option value="appraisal">שומה</option>
<option value="exhibit">נספח</option>
<option value="objection">התנגדות</option>
<option value="reference">מסמך עזר</option>
</select>
</div>
<div class="form-group" style="min-width:140px">
<label>שם צד (אופציונלי)</label>
<input type="text" placeholder="לדוגמה: ועדת הראל" onchange="casePendingFiles[${i}].party_name=this.value">
</div>
<button class="btn btn-sm btn-outline" onclick="removeCasePending(${i},'${esc(caseNumber)}')">&times;</button>
</div>
</div>
`).join('') + `
<div style="margin-top:12px">
<button class="btn btn-primary" onclick="uploadAllCaseDocs('${esc(caseNumber)}')">שייך לתיק (${casePendingFiles.length} קבצים)</button>
</div>
`;
}
function removeCasePending(idx, caseNumber) {
casePendingFiles.splice(idx, 1);
renderCasePending(caseNumber);
}
async function uploadAllCaseDocs(caseNumber) {
const tasksContainer = document.getElementById('caseTasks');
for (const pf of casePendingFiles) {
const formData = new FormData();
formData.append('file', pf.file);
formData.append('doc_type', pf.doc_type);
formData.append('party_name', pf.party_name);
try {
const res = await fetch(API + '/cases/' + encodeURIComponent(caseNumber) + '/documents/upload-tagged', {
method: 'POST', body: formData,
});
if (!res.ok) {
const err = await res.json();
toast(err.detail || 'שגיאה', 'error');
continue;
}
const data = await res.json();
trackCaseTask(data.task_id, data.filename, tasksContainer, caseNumber);
} catch (e) {
toast('שגיאת רשת', 'error');
}
}
casePendingFiles = [];
document.getElementById('casePendingUploads').innerHTML = '';
}
function trackCaseTask(taskId, displayName, container, caseNumber) {
const STEP_LABELS = { extracting: 'מחלץ טקסט', chunking: 'מפצל לקטעים', embedding: 'יוצר embeddings' };
const div = document.createElement('div');
div.className = 'task-item';
div.innerHTML = `
<div class="spinner"></div>
<div class="task-info">
<div class="task-name">${esc(displayName)}</div>
<div class="task-status">בתור...</div>
</div>`;
container.prepend(div);
const es = new EventSource(API + '/progress/' + taskId);
es.onmessage = e => {
const data = JSON.parse(e.data);
const statusEl = div.querySelector('.task-status');
let label = { queued: 'בתור...', processing: 'מעבד...', completed: 'הושלם', failed: 'נכשל' }[data.status] || data.status;
if (data.step) label += ' — ' + (STEP_LABELS[data.step] || data.step);
statusEl.textContent = label;
if (data.status === 'completed') {
div.classList.add('done');
es.close();
toast('הועלה: ' + displayName, 'success');
loadCaseView(caseNumber);
loadStatus();
} else if (data.status === 'failed') {
div.classList.add('failed');
es.close();
statusEl.textContent = 'נכשל: ' + (data.error || 'שגיאה');
}
};
es.onerror = () => es.close();
}
// ── Exports / Drafts ────────────────────────────────────
async function loadExports(caseNumber) {
const container = document.getElementById('exportsList');
try {
const res = await fetch(API + '/cases/' + encodeURIComponent(caseNumber) + '/exports');
const files = await res.json();
if (!files.length) {
container.innerHTML = '<div class="empty">אין טיוטות עדיין — לחץ "ייצא טיוטה חדשה" כדי ליצור</div>';
return;
}
container.innerHTML = files.map(f => {
const date = new Date(f.created_at * 1000);
const dateStr = date.toLocaleDateString('he-IL') + ' ' + date.toLocaleTimeString('he-IL', {hour:'2-digit',minute:'2-digit'});
const isFinal = f.is_final;
return `
<div class="export-item ${isFinal ? 'final' : ''}">
<span class="export-icon">${isFinal ? '&#9989;' : '&#128196;'}</span>
<div class="export-info">
<div class="export-name">${esc(f.filename)}</div>
<div class="export-meta">${dateStr} &middot; ${formatSize(f.size)}${isFinal ? ' &middot; <b>גרסה סופית</b>' : ''}</div>
</div>
<div class="export-actions">
<a class="btn btn-sm btn-secondary" href="${API}/cases/${encodeURIComponent(caseNumber)}/exports/${encodeURIComponent(f.filename)}/download" download="${esc(f.filename)}">הורד</a>
${!isFinal ? `<button class="btn btn-sm btn-success" onclick="markFinal('${esc(caseNumber)}','${esc(f.filename)}')">סמן סופי</button>` : ''}
</div>
</div>`;
}).join('');
} catch (e) {
container.innerHTML = '<div class="empty">שגיאה בטעינת טיוטות</div>';
}
}
async function triggerExport() {
if (!currentCaseNumber) return;
const btn = document.getElementById('exportDocxBtn');
btn.disabled = true;
btn.textContent = 'מייצא...';
try {
const res = await fetch(API + '/cases/' + encodeURIComponent(currentCaseNumber) + '/export-docx', { method: 'POST' });
if (!res.ok) {
const err = await res.json();
toast(err.detail || err.message || 'שגיאה בייצוא', 'error');
return;
}
const data = await res.json();
toast('טיוטה יוצאה: ' + (data.path || '').split('/').pop(), 'success');
loadExports(currentCaseNumber);
} catch (e) {
toast('שגיאת רשת', 'error');
} finally {
btn.disabled = false;
btn.textContent = 'ייצא טיוטה חדשה';
}
}
async function markFinal(caseNumber, filename) {
if (!confirm('לסמן את הגרסה כסופית? הקובץ יועתק גם לקורפוס האימון.')) return;
try {
const res = await fetch(API + '/cases/' + encodeURIComponent(caseNumber) + '/exports/' + encodeURIComponent(filename) + '/mark-final', { method: 'POST' });
if (!res.ok) {
const err = await res.json();
toast(err.detail || 'שגיאה', 'error');
return;
}
toast('הגרסה סומנה כסופית', 'success');
loadExports(caseNumber);
loadCaseView(caseNumber);
} catch (e) {
toast('שגיאת רשת', 'error');
}
}
function setupExportUpload(caseNumber) {
const zone = document.getElementById('exportUploadZone');
const fileInput = document.getElementById('exportFileInput');
const newZone = zone.cloneNode(true);
zone.parentNode.replaceChild(newZone, zone);
const newInput = newZone.querySelector('input[type="file"]');
newZone.addEventListener('click', () => newInput.click());
newZone.addEventListener('dragover', e => { e.preventDefault(); newZone.style.borderColor = '#e94560'; });
newZone.addEventListener('dragleave', () => { newZone.style.borderColor = '#ccc'; });
newZone.addEventListener('drop', e => {
e.preventDefault();
newZone.style.borderColor = '#ccc';
if (e.dataTransfer.files.length) uploadExportFile(e.dataTransfer.files[0], caseNumber);
});
newInput.addEventListener('change', () => {
if (newInput.files.length) uploadExportFile(newInput.files[0], caseNumber);
newInput.value = '';
});
}
async function uploadExportFile(file, caseNumber) {
const formData = new FormData();
formData.append('file', file);
try {
const res = await fetch(API + '/cases/' + encodeURIComponent(caseNumber) + '/exports/upload', {
method: 'POST', body: formData,
});
if (!res.ok) {
const err = await res.json();
toast(err.detail || 'שגיאה', 'error');
return;
}
const data = await res.json();
toast('גרסה הועלתה: ' + data.filename, 'success');
loadExports(caseNumber);
} catch (e) {
toast('שגיאת רשת', 'error');
}
}
// ── Legacy Upload Page ───────────────────────────────────
// (Simplified version of original upload functionality)
let legacyCases = [];
function setupLegacyUpload() {
const dropZone = document.getElementById('legacyDropZone');
const fileInput = document.getElementById('legacyFileInput');
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('dragover'); });
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
dropZone.addEventListener('drop', e => {
e.preventDefault(); dropZone.classList.remove('dragover');
handleLegacyFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', () => {
if (fileInput.files.length) handleLegacyFiles(fileInput.files);
fileInput.value = '';
});
}
async function handleLegacyFiles(files) {
for (const file of files) {
const formData = new FormData();
formData.append('file', file);
try {
const res = await fetch(API + '/upload', { method: 'POST', body: formData });
if (res.ok) toast('הקובץ הועלה', 'success');
} catch (e) { toast('שגיאה', 'error'); }
}
loadLegacyPending();
}
async function loadLegacyPending() {
const res = await fetch(API + '/uploads');
const files = await res.json();
const card = document.getElementById('legacyPendingCard');
const list = document.getElementById('legacyPendingList');
if (!files.length) { card.style.display = 'none'; return; }
card.style.display = 'block';
try {
const casesRes = await fetch(API + '/cases');
legacyCases = await casesRes.json();
} catch (e) { legacyCases = []; }
list.innerHTML = files.map(f => `
<div class="pending-upload" data-filename="${esc(f.filename)}">
<div class="file-row">
<span class="file-name">${esc(f.filename.replace(/^\d+_/, ''))}</span>
<span style="font-size:0.78em;color:#999">${formatSize(f.size)}</span>
</div>
<div class="tag-row">
<div class="form-group">
<label>סיווג</label>
<select class="legacy-category" onchange="toggleLegacyFields(this)">
<option value="">בחר...</option>
<option value="case">מסמך תיק</option>
<option value="training">החלטה לאימון</option>
</select>
</div>
<div class="form-group legacy-case-field" style="display:none">
<label>תיק</label>
<select class="legacy-case-select">
<option value="">בחר תיק...</option>
${legacyCases.map(c => `<option value="${esc(c.case_number)}">${esc(c.case_number)}${esc(c.title)}</option>`).join('')}
</select>
</div>
<div class="form-group legacy-case-field" style="display:none">
<label>סוג מסמך</label>
<select class="legacy-doctype-select">
<option value="appeal">כתב ערר</option>
<option value="response">תשובה</option>
<option value="decision">החלטה</option>
<option value="exhibit">נספח</option>
<option value="reference">מסמך עזר</option>
</select>
</div>
<button class="btn btn-sm btn-primary" onclick="processLegacyFile('${esc(f.filename)}')" disabled>עבד</button>
</div>
</div>
`).join('');
}
function toggleLegacyFields(select) {
const container = select.closest('.pending-upload');
const show = select.value === 'case';
container.querySelectorAll('.legacy-case-field').forEach(el => el.style.display = show ? 'block' : 'none');
container.querySelector('.btn-primary').disabled = !select.value;
}
async function processLegacyFile(filename) {
const container = document.querySelector(`.pending-upload[data-filename="${filename}"]`);
const category = container.querySelector('.legacy-category').value;
const body = { filename, category, title: '' };
if (category === 'case') {
body.case_number = container.querySelector('.legacy-case-select').value;
body.doc_type = container.querySelector('.legacy-doctype-select').value;
if (!body.case_number) return toast('יש לבחור תיק', 'error');
}
const res = await fetch(API + '/classify', {
method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body),
});
if (!res.ok) { toast('שגיאה', 'error'); return; }
const data = await res.json();
const tasksCard = document.getElementById('legacyTasksCard');
const tasksList = document.getElementById('legacyTasksList');
tasksCard.style.display = 'block';
trackCaseTask(data.task_id, filename.replace(/^\d+_/, ''), tasksList, body.case_number || '');
setTimeout(loadLegacyPending, 500);
}
// ── Status Bar ───────────────────────────────────────────
async function loadStatus() {
try {
const res = await fetch(API + '/processing-status');
const data = await res.json();
document.getElementById('statCases').textContent = data.cases ?? '—';
document.getElementById('statDocs').textContent = data.documents ?? '—';
document.getElementById('statChunks').textContent = data.chunks ?? '—';
} catch (e) {}
}
// ── Helpers ──────────────────────────────────────────────
function esc(s) {
if (!s) return '';
const d = document.createElement('div');
d.textContent = String(s);
return d.innerHTML;
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
function toast(msg, type = '') {
const el = document.getElementById('toast');
el.textContent = msg;
el.className = 'toast show ' + type;
setTimeout(() => el.className = 'toast', 3000);
}
// ── Skills Management ───────────────────────────────────
async function loadSkillList() {
const list = document.getElementById('skillList');
try {
const res = await fetch(API + '/admin/skills');
const skills = await res.json();
if (!skills.length) {
list.innerHTML = '<div class="empty">אין skills מותקנים</div>';
return;
}
list.innerHTML = skills.map(s => {
const fileCount = s.file_inventory ? s.file_inventory.length : 0;
const updatedStr = s.updated_at ? new Date(s.updated_at).toLocaleDateString('he-IL') : '—';
const inDb = s.db_markdown_chars > 0;
const onDisk = s.disk_exists;
// Action buttons
const actions = [];
if (onDisk && !inDb) {
actions.push(`<button class="btn btn-sm btn-primary" onclick="syncSkill('${esc(s.slug)}')">Sync to DB</button>`);
} else if (onDisk && inDb) {
actions.push(`<button class="btn btn-sm btn-secondary" onclick="syncSkill('${esc(s.slug)}')">Re-sync</button>`);
}
if (inDb) {
actions.push(`<button class="btn btn-sm btn-outline" style="color:#e94560;border-color:#e94560" onclick="deleteSkill('${esc(s.slug)}')">Delete from DB</button>`);
}
return `
<div class="skill-item">
<span class="skill-icon">&#128268;</span>
<div class="skill-info">
<div class="skill-name">${esc(s.name)}</div>
<div class="skill-meta">
<span>${fileCount} files</span>
<span>${s.db_markdown_chars.toLocaleString()} chars</span>
<span>Updated: ${updatedStr}</span>
${s.disk_skill_md_bytes ? `<span>Disk: ${formatSize(s.disk_skill_md_bytes)}</span>` : ''}
</div>
</div>
<div class="skill-badges">
${inDb ? '<span class="badge-ok">DB</span>' : '<span class="badge-warn">No DB</span>'}
${onDisk ? '<span class="badge-ok">Disk</span>' : '<span class="badge-warn">No Disk</span>'}
</div>
<div style="display:flex;gap:6px;flex-shrink:0">${actions.join('')}</div>
</div>`;
}).join('');
} catch (e) {
list.innerHTML = '<div class="empty">שגיאה בטעינת skills</div>';
}
}
function setupSkillUpload() {
const dropZone = document.getElementById('skillDropZone');
const fileInput = document.getElementById('skillFileInput');
const newDrop = dropZone.cloneNode(true);
dropZone.parentNode.replaceChild(newDrop, dropZone);
const newInput = newDrop.querySelector('input[type="file"]');
newDrop.addEventListener('click', () => newInput.click());
newDrop.addEventListener('dragover', e => { e.preventDefault(); newDrop.classList.add('dragover'); });
newDrop.addEventListener('dragleave', () => newDrop.classList.remove('dragover'));
newDrop.addEventListener('drop', e => {
e.preventDefault();
newDrop.classList.remove('dragover');
if (e.dataTransfer.files.length) installSkill(e.dataTransfer.files[0]);
});
newInput.addEventListener('change', () => {
if (newInput.files.length) installSkill(newInput.files[0]);
newInput.value = '';
});
}
async function installSkill(file) {
if (!file.name.toLowerCase().endsWith('.zip')) {
return toast('Only ZIP files are supported', 'error');
}
const resultDiv = document.getElementById('skillInstallResult');
resultDiv.innerHTML = '<div class="skill-install-result">Installing...</div>';
const formData = new FormData();
formData.append('file', file);
try {
const res = await fetch(API + '/admin/skills/install', {
method: 'POST', body: formData,
});
const data = await res.json();
if (!res.ok) {
resultDiv.innerHTML = `<div class="skill-install-result error">${esc(data.detail || 'Installation failed')}</div>`;
toast('Installation failed', 'error');
return;
}
const actionLabel = data.action === 'updated' ? 'Updated' : 'Installed';
resultDiv.innerHTML = `
<div class="skill-install-result">
<b>${actionLabel}: ${esc(data.slug)}</b><br>
${data.files_extracted} files extracted &middot; ${data.markdown_chars.toLocaleString()} chars<br><br>
<button class="btn btn-primary" onclick="confirmRestart()">Restart Paperclip</button>
<span style="font-size:0.82em;color:#888;margin-right:12px">Restart required to apply changes</span>
</div>`;
toast(actionLabel + ': ' + data.slug, 'success');
loadSkillList();
} catch (e) {
resultDiv.innerHTML = `<div class="skill-install-result error">Network error</div>`;
toast('Network error', 'error');
}
}
async function syncSkill(slug) {
try {
const res = await fetch(API + '/admin/skills/' + encodeURIComponent(slug) + '/sync', { method: 'POST' });
const data = await res.json();
if (!res.ok) {
toast(data.detail || 'Sync failed', 'error');
return;
}
toast(`${data.action}: ${slug} (${data.markdown_chars.toLocaleString()} chars, ${data.file_inventory.length} files)`, 'success');
loadSkillList();
} catch (e) {
toast('Network error', 'error');
}
}
async function deleteSkill(slug) {
if (!confirm(`Delete "${slug}" from DB? Files on disk will NOT be removed.`)) return;
try {
const res = await fetch(API + '/admin/skills/' + encodeURIComponent(slug), { method: 'DELETE' });
const data = await res.json();
if (!res.ok) {
toast(data.detail || 'Delete failed', 'error');
return;
}
toast(`Deleted: ${slug}`, 'success');
loadSkillList();
} catch (e) {
toast('Network error', 'error');
}
}
function confirmRestart() {
if (!confirm('Restart Paperclip?')) return;
restartPaperclip();
}
async function restartPaperclip() {
const btn = document.getElementById('restartPaperclipBtn');
const status = document.getElementById('restartStatus');
btn.disabled = true;
btn.textContent = 'Restarting...';
status.textContent = '';
try {
const res = await fetch(API + '/admin/paperclip/restart', { method: 'POST' });
const data = await res.json();
if (!res.ok) {
status.textContent = 'Error: ' + (data.detail || 'failed');
status.style.color = '#e94560';
toast('Restart failed', 'error');
} else {
status.textContent = 'Restarted successfully';
status.style.color = '#27ae60';
toast('Paperclip restarted', 'success');
}
} catch (e) {
status.textContent = 'Network error';
status.style.color = '#e94560';
toast('Network error', 'error');
} finally {
btn.disabled = false;
btn.textContent = 'Restart Paperclip';
}
}
// Init legacy upload listeners
setupLegacyUpload();
</script>
</body>
</html>