Files
legal-ai/web/static/index.html
Chaim 858333b386 Add style report dashboard — Dafna's style portrait
Visual dashboard at #/style-report with 4 sections:
- Hero: 24 decisions, char counts, subject donut, timeline
- Anatomy: average section-length breakdown (intro → ruling → conclusion)
- Signature Phrases Wall: pattern cards with real corpus frequencies, filter
  chips by type, click → modal with examples
- Contribution: per-decision "new vs confirmed" patterns, growth curve SVG

Backend:
- /api/training/style-report endpoint computes all 4 sections in one call
- Headlines in Hebrew are computed server-side from real data
- Backfill script for style_patterns.frequency using _strip_nikud +
  pattern-variant extraction (templates with [placeholders], / alternatives,
  ellipsis all handled)

Real findings from the 24-decision corpus:
- דיון משפטי = 49% of avg decision (the focus)
- 23/24 use "לפנינו ערר" opening formula
- 21/24 use "ניתנה פה אחד" closing
- After 7 decisions we already learned 85% of her style patterns

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

2687 lines
108 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; }
.doc-status.completed { color: #27ae60; font-weight: 700; font-size: 1em; }
.doc-status.processing { display: inline-block; width: 14px; height: 14px; border: 2px solid #e94560; border-top-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; }
.doc-status.pending { color: #ccc; }
.doc-status.failed { color: #e94560; font-weight: 700; }
.btn-retry { background: none; border: 1px solid #e94560; color: #e94560; border-radius: 4px; padding: 2px 8px; font-size: 0.75em; cursor: pointer; margin-right: 6px; }
.btn-retry:hover { background: #e94560; color: #fff; }
.processing-badge { display: inline-flex; align-items: center; gap: 4px; color: #e94560; font-size: 0.78em; font-weight: 500; }
.mini-spinner { display: inline-block; width: 10px; height: 10px; border: 1.5px solid #e94560; border-top-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; }
/* 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; }
/* ── Local files (research, drafts, proofread) ───────── */
.local-file-group { margin-bottom: 12px; }
.local-file-group-header { font-size: 0.82em; font-weight: 600; color: #666; margin-bottom: 6px; padding-bottom: 4px; border-bottom: 1px solid #eee; }
.local-file-item {
display: flex; align-items: center; gap: 10px; padding: 8px 12px;
border: 1px solid #eee; border-radius: 6px; margin-bottom: 4px; background: #fafafa;
cursor: pointer; transition: background 0.15s;
}
.local-file-item:hover { background: #f0f0f0; }
.local-file-item .lf-icon { flex-shrink: 0; font-size: 1.1em; }
.local-file-item .lf-name { flex: 1; font-size: 0.85em; font-weight: 500; word-break: break-all; }
.local-file-item .lf-meta { font-size: 0.72em; color: #999; flex-shrink: 0; }
/* ── 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; }
/* ── Training Corpus Upload ───────────────────────────── */
.training-review {
border: 1px solid #e5e5e5; border-radius: 8px; padding: 14px 16px;
margin-bottom: 12px; background: #fafafa;
}
.training-review .review-header {
display: flex; align-items: center; gap: 10px;
padding-bottom: 10px; margin-bottom: 12px;
border-bottom: 1px solid #eee;
}
.training-review .review-header strong { font-size: 0.95em; color: #1a1a2e; flex: 1; }
.training-review .review-meta { font-size: 0.78em; color: #888; }
.training-review .btn-icon {
background: transparent; border: none; color: #aaa; cursor: pointer;
font-size: 1.1em; padding: 4px 8px; border-radius: 4px;
}
.training-review .btn-icon:hover { background: #ffebee; color: #c62828; }
.training-review .review-fields {
display: grid; grid-template-columns: 1fr 160px; gap: 14px; margin-bottom: 12px;
}
.training-review .review-fields label {
display: flex; flex-direction: column; gap: 4px;
font-size: 0.8em; color: #666; font-weight: 500;
}
.training-review .review-fields input {
padding: 7px 10px; border: 1px solid #ddd; border-radius: 6px;
font-size: 0.88em; font-family: inherit;
}
.training-review .review-fields input:focus {
outline: none; border-color: #e94560;
}
.training-review .review-cats { margin-bottom: 10px; }
.training-review .review-cats-label {
font-size: 0.8em; color: #666; font-weight: 500; margin-bottom: 6px;
}
.training-review .review-cats-list { display: flex; flex-wrap: wrap; gap: 6px; }
.cat-chip {
display: inline-flex; align-items: center; gap: 5px;
padding: 4px 10px; border: 1px solid #ddd; border-radius: 14px;
font-size: 0.78em; cursor: pointer; background: #fff;
transition: background 0.12s;
}
.cat-chip:hover { background: #f0f0f0; }
.cat-chip input[type="checkbox"] { margin: 0; cursor: pointer; }
.cat-chip:has(input:checked) { background: #ffe4ea; border-color: #e94560; color: #c62828; }
.review-preview {
margin-top: 6px; border: 1px solid #eee; border-radius: 6px;
background: #fff; padding: 8px 12px;
}
.review-preview summary {
cursor: pointer; font-size: 0.78em; color: #888; font-weight: 500;
}
.review-preview pre {
margin-top: 10px; font-size: 0.78em; color: #333; direction: rtl;
white-space: pre-wrap; font-family: inherit; line-height: 1.5;
max-height: 250px; overflow-y: auto;
}
.training-task {
padding: 10px 14px; margin-bottom: 6px; border-radius: 6px;
background: #f7f7f7; font-size: 0.85em;
display: flex; align-items: center; gap: 10px;
}
.training-task:last-child { margin-bottom: 0; }
.corpus-table { width: 100%; border-collapse: collapse; font-size: 0.82em; }
.corpus-table th, .corpus-table td {
text-align: right; padding: 8px 10px; border-bottom: 1px solid #eee;
}
.corpus-table th {
background: #f7f7f7; font-weight: 600; color: #555;
font-size: 0.78em; text-transform: uppercase;
}
.corpus-table tr:hover td { background: #fafafa; }
.cat-tag {
display: inline-block; padding: 2px 8px; margin: 0 2px;
background: #e3f2fd; color: #1565c0; border-radius: 10px;
font-size: 0.72em; font-weight: 500;
}
/* Pattern groups */
.pattern-group {
border: 1px solid #eee; border-radius: 8px; margin-bottom: 10px;
background: #fff;
}
.pattern-group[open] { background: #fafafa; }
.pattern-group summary {
padding: 12px 16px; cursor: pointer; font-size: 0.9em;
display: flex; align-items: center; gap: 10px; list-style: none;
}
.pattern-group summary::-webkit-details-marker { display: none; }
.pattern-group summary::before {
content: '▸'; transition: transform 0.15s; font-size: 0.9em; color: #888;
}
.pattern-group[open] summary::before { transform: rotate(90deg); }
.pattern-count {
margin-right: auto; background: #e3f2fd; color: #1565c0;
padding: 2px 10px; border-radius: 10px; font-size: 0.76em; font-weight: 500;
}
.pattern-list {
padding: 4px 16px 14px 16px; display: flex; flex-direction: column; gap: 8px;
}
.pattern-item {
padding: 10px 14px; background: #fff; border: 1px solid #eee;
border-radius: 6px; font-size: 0.84em;
}
.pattern-text { color: #1a1a2e; font-weight: 500; }
.pattern-context { color: #666; font-size: 0.88em; margin-top: 4px; }
.pattern-meta {
color: #999; font-size: 0.78em; margin-top: 6px;
display: flex; gap: 10px;
}
/* ── Style Report (Dafna's Portrait) ────────────────── */
.style-report-header { text-align: center; margin-bottom: 32px; padding-top: 16px; }
.style-report-header h1 { font-size: 2em; font-weight: 600; color: #1a1a2e; margin-bottom: 6px; }
.style-report-header .subtitle-muted { color: #888; font-size: 0.95em; }
.portrait-card {
background: #fff; border-radius: 12px; padding: 28px 32px;
margin-bottom: 20px; box-shadow: 0 1px 4px rgba(0,0,0,0.06), 0 0 1px rgba(0,0,0,0.08);
}
.portrait-section-title {
font-size: 1.3em; font-weight: 600; color: #1a1a2e;
margin-bottom: 8px; padding-bottom: 10px;
border-bottom: 2px solid #f0f0f0;
}
.portrait-headline {
font-size: 1.05em; color: #555; line-height: 1.6;
margin-bottom: 20px; padding: 12px 16px;
background: #fff9ed; border-right: 3px solid #e9a13f;
border-radius: 4px;
}
/* Hero section */
.portrait-hero .hero-body {
display: grid; grid-template-columns: 1fr auto; gap: 32px; align-items: center;
margin-bottom: 24px;
}
.hero-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
.hero-stat { text-align: center; padding: 14px; background: #fafafa; border-radius: 8px; }
.hero-stat-value { font-size: 1.9em; font-weight: 700; color: #1a1a2e; line-height: 1; }
.hero-stat-label { font-size: 0.8em; color: #888; margin-top: 6px; }
.hero-donut-wrap { display: flex; align-items: center; gap: 20px; }
.donut {
width: 160px; height: 160px; border-radius: 50%;
position: relative; flex-shrink: 0;
}
.donut::after {
content: ''; position: absolute; inset: 24%;
background: #fff; border-radius: 50%;
}
.donut-center {
position: absolute; inset: 0; display: flex;
align-items: center; justify-content: center;
font-size: 0.85em; color: #666; z-index: 1; font-weight: 600;
}
.donut-legend {
display: flex; flex-direction: column; gap: 6px; font-size: 0.82em;
}
.donut-legend-item {
display: flex; align-items: center; gap: 8px;
}
.donut-legend-dot {
width: 11px; height: 11px; border-radius: 50%; flex-shrink: 0;
}
.hero-timeline {
position: relative; height: 44px; margin-top: 8px;
background: linear-gradient(to left, #fafafa, #fff, #fafafa);
border-radius: 6px; padding: 0 16px;
}
.hero-timeline-line {
position: absolute; top: 50%; right: 16px; left: 16px;
height: 2px; background: #e5e5e5; transform: translateY(-50%);
}
.hero-timeline-dot {
position: absolute; top: 50%; width: 10px; height: 10px;
border-radius: 50%; background: #e94560; transform: translate(50%, -50%);
cursor: pointer; transition: transform 0.15s;
box-shadow: 0 0 0 2px #fff;
}
.hero-timeline-dot:hover { transform: translate(50%, -50%) scale(1.4); z-index: 1; }
/* Anatomy section */
.anatomy-bar {
display: flex; width: 100%; height: 56px;
border-radius: 8px; overflow: hidden;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.06);
}
.anatomy-segment {
display: flex; align-items: center; justify-content: center;
font-size: 0.82em; font-weight: 600; color: #fff;
transition: filter 0.15s; position: relative; cursor: help;
text-align: center; padding: 0 4px; overflow: hidden;
}
.anatomy-segment:hover { filter: brightness(1.08); }
.anatomy-segment small {
display: block; font-size: 0.72em; font-weight: 400; opacity: 0.85;
}
.anatomy-legend {
display: flex; flex-wrap: wrap; gap: 14px;
margin-top: 14px; font-size: 0.8em; color: #666;
}
.anatomy-legend-item { display: flex; align-items: center; gap: 6px; }
.anatomy-legend-dot {
width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0;
}
/* Phrase wall */
.phrase-filters {
display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px;
}
.phrase-filter {
padding: 6px 14px; border-radius: 18px;
border: 1px solid #ddd; background: #fff;
font-size: 0.82em; cursor: pointer; transition: all 0.12s;
}
.phrase-filter:hover { background: #f5f5f5; }
.phrase-filter.active {
background: #1a1a2e; color: #fff; border-color: #1a1a2e;
}
.phrase-wall {
display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 12px;
}
.phrase-card {
padding: 14px 16px; border-radius: 8px; background: #fafafa;
border-right: 3px solid; cursor: pointer; transition: all 0.15s;
display: flex; flex-direction: column; gap: 6px;
}
.phrase-card:hover { background: #fff; transform: translateY(-1px); box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.phrase-card-text { font-weight: 500; color: #1a1a2e; line-height: 1.4; }
.phrase-card-meta {
display: flex; justify-content: space-between; font-size: 0.75em; color: #999;
margin-top: auto;
}
.phrase-card-freq {
background: #fff; padding: 2px 8px; border-radius: 10px; font-weight: 600;
}
/* Growth curve + Contribution */
.growth-curve-wrap { margin-bottom: 24px; }
.growth-curve-label { font-size: 0.82em; color: #888; margin-bottom: 8px; }
.growth-curve {
width: 100%; height: 160px;
background: linear-gradient(to bottom, #fafafa, #fff);
border-radius: 6px;
}
.growth-curve-path { fill: none; stroke: #e94560; stroke-width: 2.5; }
.growth-curve-area { fill: #fce4e9; opacity: 0.6; }
.growth-curve-dot { fill: #e94560; cursor: pointer; transition: r 0.15s; }
.growth-curve-dot:hover { r: 6; }
.contribution-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 12px;
}
.contribution-card {
padding: 14px 16px; background: #fafafa; border-radius: 8px;
border: 1px solid #eee;
transition: all 0.15s;
}
.contribution-card:hover { background: #fff; box-shadow: 0 2px 6px rgba(0,0,0,0.05); }
.contribution-card-header {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 8px; font-size: 0.82em; color: #555;
}
.contribution-card-number { font-weight: 600; color: #1a1a2e; font-size: 1em; }
.contribution-badges { display: flex; gap: 8px; margin: 10px 0; }
.contribution-badge {
padding: 4px 10px; border-radius: 10px; font-size: 0.78em; font-weight: 600;
}
.contribution-badge.new { background: #e8f5e9; color: #2e7d32; }
.contribution-badge.confirmed { background: #e3f2fd; color: #1565c0; }
.contribution-highlight {
font-size: 0.82em; color: #666; margin-top: 8px; padding-top: 8px;
border-top: 1px dashed #e5e5e5; line-height: 1.5;
}
.contribution-highlight strong { color: #1a1a2e; }
/* Modal */
.phrase-modal {
border: none; border-radius: 12px; padding: 24px 28px;
max-width: 640px; width: 90%; direction: rtl;
box-shadow: 0 20px 60px rgba(0,0,0,0.2);
}
.phrase-modal::backdrop { background: rgba(0,0,0,0.4); }
.phrase-modal-header {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 14px; padding-bottom: 12px; border-bottom: 1px solid #eee;
}
.phrase-modal-type {
font-size: 0.78em; color: #888; text-transform: uppercase;
letter-spacing: 0.05em;
}
.phrase-modal-text {
font-size: 1.1em; font-weight: 600; color: #1a1a2e;
margin-bottom: 12px; line-height: 1.5;
}
.phrase-modal-context {
font-size: 0.88em; color: #666; margin-bottom: 16px; line-height: 1.6;
}
.phrase-modal-examples {
display: flex; flex-direction: column; gap: 10px;
max-height: 300px; overflow-y: auto;
}
.phrase-modal-example {
padding: 10px 14px; background: #fafafa; border-right: 3px solid #e94560;
font-size: 0.86em; line-height: 1.5; color: #333; border-radius: 4px;
}
@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="#/training" id="navTraining">אימון סגנון</a>
<a href="#/style-report" id="navStyleReport">הסגנון שלי</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>
<!-- Research & Work Files -->
<div class="card">
<div class="card-header">מחקר וניתוח</div>
<div class="card-body" id="caseLocalFiles">
<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>
<!-- ══ Page: Training Corpus Upload ══ -->
<div class="page" id="page-training">
<div class="page-header">
<h2>אימון סגנון — העלאת החלטות לקורפוס</h2>
</div>
<div class="card">
<div class="card-body">
<p style="margin-bottom:12px;color:#555;line-height:1.6">
העלה החלטות קודמות של דפנה כדי ללמד את המערכת את סגנון הכתיבה שלה.
הקבצים יעברו <strong>הגהה אוטומטית</strong> (הסרת תוספות נבו, כותרות, סימני מים)
ו<strong>חילוץ מטא-דאטה</strong> (מספר החלטה, תאריך, קטגוריות) לסקירה לפני ההעלאה.
</p>
<div class="upload-zone" id="trainingDropZone">
<div style="font-size:3em;color:#ccc;margin-bottom:16px">&#128218;</div>
<h3>גרור קבצי החלטה לכאן או לחץ לבחירה</h3>
<p>PDF, DOCX, MD — עד 50MB. ניתן להעלות מספר קבצים בבת אחת.</p>
<input type="file" id="trainingFileInput" multiple accept=".pdf,.docx,.md,.txt">
</div>
</div>
</div>
<div class="card" id="trainingAnalysisCard" style="display:none">
<div class="card-header">
<span>סקירת מטא-דאטה לפני ההעלאה</span>
<span id="trainingAnalysisStatus" style="float:left;font-weight:400;color:#888;font-size:0.9em"></span>
</div>
<div class="card-body">
<div id="trainingReviewList"></div>
<div style="display:flex;gap:10px;margin-top:16px;justify-content:flex-end">
<button class="btn btn-ghost" onclick="cancelTrainingReview()">בטל</button>
<button class="btn btn-primary" id="trainingUploadBtn" onclick="uploadAllTraining()">
העלה הכל לקורפוס
</button>
</div>
</div>
</div>
<div class="card" id="trainingTasksCard" style="display:none">
<div class="card-header">עיבוד והעלאה</div>
<div class="card-body" id="trainingTasksList"></div>
</div>
<div class="card">
<div class="card-header">
<span>קורפוס הסגנון</span>
<span id="corpusCount" style="float:left;font-weight:400;color:#888;font-size:0.9em"></span>
</div>
<div class="card-body" id="corpusList">
<div class="empty">טוען...</div>
</div>
</div>
<div class="card">
<div class="card-header">
<span>דוח סגנון — דפוסים שחולצו</span>
<span style="float:left;display:flex;gap:10px;align-items:center">
<span id="patternsCount" style="font-weight:400;color:#888;font-size:0.9em"></span>
<button class="btn btn-primary" id="analyzeStyleBtn" onclick="runStyleAnalysis()">
נתח קורפוס
</button>
</span>
</div>
<div class="card-body" id="patternsList">
<div class="empty">טוען...</div>
</div>
</div>
<div style="margin-top:24px;text-align:center">
<a href="#/style-report" class="btn btn-primary" style="font-size:1em;padding:12px 28px">
← צפי בפורטרט הסגנון המלא
</a>
</div>
</div>
<!-- ══ Page: Style Report (Dafna's Portrait) ══ -->
<div class="page" id="page-style-report">
<div class="style-report-header">
<h1>פורטרט הסגנון שלך</h1>
<p class="subtitle-muted">דוח ויזואלי על סמך הקורפוס שלמדתי ממך</p>
</div>
<div id="styleReportLoading" class="empty" style="padding:60px 20px">
<div class="mini-spinner" style="width:24px;height:24px"></div>
<div style="margin-top:12px">טוען את הפורטרט...</div>
</div>
<div id="styleReportContent" style="display:none">
<!-- Section 1: Hero -->
<section class="portrait-card portrait-hero">
<div class="portrait-headline" id="heroHeadline"></div>
<div class="hero-body">
<div class="hero-stats" id="heroStats"></div>
<div class="hero-donut-wrap">
<div class="donut" id="heroDonut"></div>
<div class="donut-legend" id="heroDonutLegend"></div>
</div>
</div>
<div class="hero-timeline" id="heroTimeline"></div>
</section>
<!-- Section 2: Anatomy -->
<section class="portrait-card portrait-anatomy">
<h2 class="portrait-section-title">איך את בונה החלטה</h2>
<div class="portrait-headline" id="anatomyHeadline"></div>
<div class="anatomy-bar" id="anatomyBar"></div>
<div class="anatomy-legend" id="anatomyLegend"></div>
</section>
<!-- Section 3: Signature Phrases Wall -->
<section class="portrait-card portrait-phrases">
<h2 class="portrait-section-title">הביטויים שחוזרים אצלך</h2>
<div class="portrait-headline" id="phrasesHeadline"></div>
<div class="phrase-filters" id="phraseFilters"></div>
<div class="phrase-wall" id="phraseWall"></div>
</section>
<!-- Section 4: Contribution -->
<section class="portrait-card portrait-contribution">
<h2 class="portrait-section-title">מה תרמה כל החלטה</h2>
<div class="portrait-headline" id="contributionHeadline"></div>
<div class="growth-curve-wrap">
<div class="growth-curve-label">עקומת הלמידה — כמה ידע חדש כל החלטה הביאה</div>
<svg class="growth-curve" id="growthCurve" viewBox="0 0 800 160" preserveAspectRatio="none"></svg>
</div>
<div class="contribution-grid" id="contributionGrid"></div>
</section>
</div>
</div>
</div>
<!-- Modal for pattern examples -->
<dialog id="phraseModal" class="phrase-modal">
<div class="phrase-modal-header">
<span class="phrase-modal-type" id="phraseModalType"></span>
<button class="btn-icon" onclick="document.getElementById('phraseModal').close()"></button>
</div>
<div class="phrase-modal-text" id="phraseModalText"></div>
<div class="phrase-modal-context" id="phraseModalContext"></div>
<div class="phrase-modal-examples" id="phraseModalExamples"></div>
</dialog>
<!-- 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() {
// Clean up polling timer when navigating away
if (window._docPollTimer) {
clearInterval(window._docPollTimer);
window._docPollTimer = null;
}
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();
} else if (hash === '#/training') {
document.getElementById('page-training').classList.add('active');
document.getElementById('navTraining').classList.add('active');
subtitle = 'אימון סגנון';
initTrainingPage();
} else if (hash === '#/style-report') {
document.getElementById('page-style-report').classList.add('active');
document.getElementById('navStyleReport').classList.add('active');
subtitle = 'פורטרט הסגנון שלי';
loadStyleReport();
}
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.processing_count > 0 ? `<span class="processing-badge"><span class="mini-spinner"></span> ${c.processing_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]) {
let statusHtml;
if (doc.extraction_status === 'completed' || doc.extraction_status === 'proofread') {
statusHtml = '<span class="doc-status completed">&#10003;</span>';
} else if (doc.extraction_status === 'processing') {
statusHtml = '<span class="doc-status processing"></span>';
} else if (doc.extraction_status === 'failed') {
statusHtml = `<span class="doc-status failed">&#10007;</span><button class="btn-retry" onclick="retryDoc('${esc(caseNumber)}','${esc(doc.id)}')">נסה שוב</button>`;
} else {
statusHtml = '<span class="doc-status pending">&#9711;</span>';
}
html += `<div class="doc-item">
<span class="doc-icon">&#128196;</span>
<span class="doc-name">${esc(doc.title)}</span>
${statusHtml}
</div>`;
}
html += '</div>';
}
document.getElementById('caseDocsList').innerHTML = html;
// Auto-refresh while documents are still processing
if (data.documents?.some(d => d.extraction_status !== 'completed' && d.extraction_status !== 'proofread')) {
if (!window._docPollTimer) {
window._docPollTimer = setInterval(() => loadCaseView(caseNumber), 5000);
}
} else {
if (window._docPollTimer) {
clearInterval(window._docPollTimer);
window._docPollTimer = null;
}
}
}
} 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);
loadLocalFiles(caseNumber);
loadExports(caseNumber);
setupExportUpload(caseNumber);
}
const FOLDER_LABELS = { research: 'מחקר וניתוח', proofread: 'טקסט מוגה', drafts: 'טיוטות' };
const FOLDER_ICONS = { research: '&#128218;', proofread: '&#9989;', drafts: '&#128221;' };
async function loadLocalFiles(caseNumber) {
const container = document.getElementById('caseLocalFiles');
try {
const res = await fetch(API + '/cases/' + encodeURIComponent(caseNumber) + '/local-files');
const data = await res.json();
const folders = Object.keys(data);
if (!folders.length) {
container.innerHTML = '<div class="empty">אין קבצי מחקר או ניתוח</div>';
return;
}
let html = '';
for (const folder of ['research', 'drafts', 'proofread']) {
if (!data[folder]) continue;
html += `<div class="local-file-group">
<div class="local-file-group-header">${FOLDER_LABELS[folder] || folder} (${data[folder].length})</div>`;
for (const f of data[folder]) {
const date = new Date(f.modified_at * 1000);
const dateStr = date.toLocaleDateString('he-IL') + ' ' + date.toLocaleTimeString('he-IL', {hour: '2-digit', minute: '2-digit'});
const url = API + '/cases/' + encodeURIComponent(caseNumber) + '/local-files/' + encodeURIComponent(folder) + '/' + encodeURIComponent(f.filename);
html += `<a href="${url}" target="_blank" style="text-decoration:none;color:inherit">
<div class="local-file-item">
<span class="lf-icon">${FOLDER_ICONS[folder] || '&#128196;'}</span>
<span class="lf-name">${esc(f.filename)}</span>
<span class="lf-meta">${dateStr} &middot; ${formatSize(f.size)}</span>
</div></a>`;
}
html += '</div>';
}
container.innerHTML = html;
} catch (e) {
container.innerHTML = '<div class="empty">שגיאה בטעינת קבצים מקומיים</div>';
}
}
async function retryDoc(caseNumber, docId) {
const btn = event.target;
btn.disabled = true;
btn.textContent = '...';
try {
await fetch(`/api/cases/${caseNumber}/documents/${docId}/reprocess`, {method: 'POST'});
} catch (e) { /* will show on next refresh */ }
loadCaseView(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();
// ── Training Corpus Upload ─────────────────────────────────────────
const ALL_CATEGORIES = [
'בנייה', 'שימוש חורג', 'תכנית', 'היתר', 'הקלה',
'חלוקה', 'תמ"א 38', 'היטל השבחה', 'פיצויים 197',
];
let _trainingReviews = []; // in-progress metadata awaiting user approval
function initTrainingPage() {
setupTrainingDropZone();
loadCorpusList();
loadStylePatterns();
pollStyleAnalysisStatus();
// Reset review state on re-entry
_trainingReviews = [];
document.getElementById('trainingAnalysisCard').style.display = 'none';
document.getElementById('trainingTasksCard').style.display = 'none';
document.getElementById('trainingReviewList').innerHTML = '';
document.getElementById('trainingTasksList').innerHTML = '';
}
function setupTrainingDropZone() {
const zone = document.getElementById('trainingDropZone');
const input = document.getElementById('trainingFileInput');
if (zone._wired) return;
zone._wired = true;
zone.addEventListener('click', () => input.click());
zone.addEventListener('dragover', (e) => { e.preventDefault(); zone.classList.add('dragging'); });
zone.addEventListener('dragleave', () => zone.classList.remove('dragging'));
zone.addEventListener('drop', (e) => {
e.preventDefault();
zone.classList.remove('dragging');
handleTrainingFiles(e.dataTransfer.files);
});
input.addEventListener('change', () => handleTrainingFiles(input.files));
}
async function handleTrainingFiles(fileList) {
const files = Array.from(fileList || []);
if (!files.length) return;
const card = document.getElementById('trainingAnalysisCard');
const status = document.getElementById('trainingAnalysisStatus');
card.style.display = '';
status.textContent = `מעלה ומנתח ${files.length} קבצים...`;
for (const file of files) {
try {
// 1. Upload to pending dir
status.textContent = `מעלה: ${file.name}...`;
const fd = new FormData();
fd.append('file', file);
const upRes = await fetch(API + '/upload', { method: 'POST', body: fd });
if (!upRes.ok) throw new Error(`Upload failed: ${await upRes.text()}`);
const uploadInfo = await upRes.json();
// 2. Analyze (proofread + extract metadata)
status.textContent = `מנתח: ${file.name}...`;
const analyzeFd = new FormData();
analyzeFd.append('filename', uploadInfo.filename);
const anRes = await fetch(API + '/training/analyze', { method: 'POST', body: analyzeFd });
if (!anRes.ok) throw new Error(`Analyze failed: ${await anRes.text()}`);
const analysis = await anRes.json();
_trainingReviews.push({
...analysis,
_pendingName: uploadInfo.filename,
_originalName: file.name,
_status: 'ready',
});
} catch (e) {
toast(`שגיאה בעיבוד ${file.name}: ${e.message}`, 'error');
}
}
status.textContent = '';
renderTrainingReview();
}
function renderTrainingReview() {
const list = document.getElementById('trainingReviewList');
if (!_trainingReviews.length) {
list.innerHTML = '<div class="empty">אין קבצים לסקירה</div>';
document.getElementById('trainingAnalysisCard').style.display = 'none';
return;
}
list.innerHTML = _trainingReviews.map((r, i) => renderReviewRow(r, i)).join('');
}
function renderReviewRow(r, idx) {
const catsHtml = ALL_CATEGORIES.map(c => {
const checked = r.subject_categories.includes(c) ? 'checked' : '';
return `<label class="cat-chip"><input type="checkbox" ${checked} onchange="toggleCat(${idx}, '${c}')"> ${c}</label>`;
}).join('');
return `
<div class="training-review" data-idx="${idx}">
<div class="review-header">
<strong>${esc(r._originalName)}</strong>
<span class="review-meta">${r.chars.toLocaleString('he-IL')} תווים · ${esc(r.stats.source_type)}</span>
<button class="btn-icon" onclick="removeTrainingReview(${idx})" title="הסר">✕</button>
</div>
<div class="review-fields">
<label>מספר החלטה
<input type="text" value="${esc(r.decision_number)}"
onchange="_trainingReviews[${idx}].decision_number=this.value"
placeholder="NNNN/YY">
</label>
<label>תאריך
<input type="date" value="${esc(r.decision_date)}"
onchange="_trainingReviews[${idx}].decision_date=this.value">
</label>
</div>
<div class="review-cats">
<div class="review-cats-label">קטגוריות:</div>
<div class="review-cats-list">${catsHtml}</div>
</div>
<details class="review-preview">
<summary>תצוגה מקדימה של טקסט מנוקה (500 תווים ראשונים)</summary>
<pre>${esc(r.preview)}</pre>
</details>
</div>
`;
}
function toggleCat(idx, cat) {
const r = _trainingReviews[idx];
const i = r.subject_categories.indexOf(cat);
if (i >= 0) r.subject_categories.splice(i, 1);
else r.subject_categories.push(cat);
}
function removeTrainingReview(idx) {
const r = _trainingReviews[idx];
// Clean up the uploaded pending file
if (r._pendingName) {
fetch(API + '/uploads/' + encodeURIComponent(r._pendingName), { method: 'DELETE' })
.catch(() => {});
}
_trainingReviews.splice(idx, 1);
renderTrainingReview();
}
function cancelTrainingReview() {
// Delete all pending uploads
for (const r of _trainingReviews) {
if (r._pendingName) {
fetch(API + '/uploads/' + encodeURIComponent(r._pendingName), { method: 'DELETE' })
.catch(() => {});
}
}
_trainingReviews = [];
renderTrainingReview();
}
async function uploadAllTraining() {
const btn = document.getElementById('trainingUploadBtn');
btn.disabled = true;
const tasksCard = document.getElementById('trainingTasksCard');
const tasksList = document.getElementById('trainingTasksList');
tasksCard.style.display = '';
tasksList.innerHTML = '';
for (let i = 0; i < _trainingReviews.length; i++) {
const r = _trainingReviews[i];
const row = document.createElement('div');
row.className = 'training-task';
row.innerHTML = `<span class="mini-spinner"></span> ${esc(r._originalName)}${esc(r.decision_number || '—')}`;
tasksList.appendChild(row);
try {
const res = await fetch(API + '/training/upload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: r._pendingName,
decision_number: r.decision_number,
decision_date: r.decision_date,
subject_categories: r.subject_categories,
title: r._originalName.replace(/\.[^.]+$/, ''),
}),
});
if (!res.ok) {
const err = await res.text();
throw new Error(err);
}
const { task_id } = await res.json();
const result = await pollTrainingProgress(task_id, row, r._originalName);
row.innerHTML = `<span style="color:#0a0">✓</span> ${esc(r._originalName)}${result.chars.toLocaleString('he-IL')} תווים, ${result.chunks} קטעים`;
} catch (e) {
row.innerHTML = `<span style="color:#c00">✗</span> ${esc(r._originalName)}${esc(e.message.substring(0, 200))}`;
}
}
_trainingReviews = [];
renderTrainingReview();
btn.disabled = false;
loadCorpusList();
toast('ההעלאה הושלמה', 'success');
}
const TRAINING_STEP_LABELS = {
queued: 'בתור',
proofreading: 'הגהה',
saving: 'שמירה',
corpus: 'קליטה לקורפוס',
chunking: 'פיצול לקטעים',
embedding: 'יצירת embeddings',
completed: 'הושלם',
failed: 'נכשל',
};
function pollTrainingProgress(taskId, row, name) {
return new Promise((resolve, reject) => {
const es = new EventSource(API + '/progress/' + taskId);
es.onmessage = (e) => {
const data = JSON.parse(e.data);
const label = TRAINING_STEP_LABELS[data.step] || TRAINING_STEP_LABELS[data.status] || data.status;
row.innerHTML = `<span class="mini-spinner"></span> ${esc(name)}${esc(label)}...`;
if (data.status === 'completed') {
es.close();
resolve(data.result);
} else if (data.status === 'failed') {
es.close();
reject(new Error(data.error || 'Processing failed'));
}
};
es.onerror = () => {
es.close();
reject(new Error('connection lost'));
};
});
}
// ── Style Analysis (patterns) ────────────────────────────
const PATTERN_TYPE_LABELS = {
opening_formula: 'נוסחאות פתיחה',
closing_formula: 'נוסחאות סיום',
transition: 'ביטויי מעבר',
characteristic_phrase: 'ביטויים אופייניים',
argument_flow: 'זרימת טיעון',
analysis_structure: 'מבנה ניתוח',
evidence_handling: 'טיפול בראיות',
citation_style: 'סגנון ציטוט',
};
async function loadStylePatterns() {
const container = document.getElementById('patternsList');
const count = document.getElementById('patternsCount');
try {
const res = await fetch(API + '/training/patterns');
const data = await res.json();
count.textContent = `${data.total} דפוסים`;
if (!data.total) {
container.innerHTML = '<div class="empty">אין דפוסים עדיין. לחץ "נתח קורפוס" כדי לחלץ דפוסים מההחלטות הקיימות.</div>';
return;
}
const typeOrder = [
'opening_formula', 'transition', 'characteristic_phrase',
'argument_flow', 'analysis_structure', 'evidence_handling',
'citation_style', 'closing_formula',
];
const types = typeOrder.filter(t => data.by_type[t]);
Object.keys(data.by_type).forEach(t => { if (!types.includes(t)) types.push(t); });
container.innerHTML = types.map(type => `
<details class="pattern-group" open>
<summary>
<strong>${esc(PATTERN_TYPE_LABELS[type] || type)}</strong>
<span class="pattern-count">${data.by_type[type].length}</span>
</summary>
<div class="pattern-list">
${data.by_type[type].map(p => `
<div class="pattern-item">
<div class="pattern-text">${esc(p.pattern_text)}</div>
${p.context ? `<div class="pattern-context">${esc(p.context)}</div>` : ''}
<div class="pattern-meta">
<span>תדירות: ${p.frequency}</span>
${p.examples && p.examples.length ? `<span>· ${p.examples.length} דוגמאות</span>` : ''}
</div>
</div>
`).join('')}
</div>
</details>
`).join('');
} catch (e) {
container.innerHTML = `<div class="empty">שגיאה בטעינה: ${esc(e.message)}</div>`;
}
}
async function runStyleAnalysis() {
const btn = document.getElementById('analyzeStyleBtn');
btn.disabled = true;
try {
const res = await fetch(API + '/training/analyze-style', { method: 'POST' });
if (res.status === 409) {
toast('ניתוח כבר רץ ברקע', 'warn');
} else if (!res.ok) {
throw new Error(await res.text());
} else {
toast('ניתוח סגנון התחיל — 2-5 דקות', 'success');
}
pollStyleAnalysisStatus();
} catch (e) {
toast('שגיאה: ' + e.message, 'error');
btn.disabled = false;
}
}
async function pollStyleAnalysisStatus() {
const btn = document.getElementById('analyzeStyleBtn');
try {
const res = await fetch(API + '/training/analyze-style/status');
const state = await res.json();
if (state.running) {
btn.disabled = true;
btn.innerHTML = `<span class="mini-spinner"></span> מנתח... ${state.elapsed || 0}s`;
setTimeout(pollStyleAnalysisStatus, 3000);
} else {
btn.disabled = false;
btn.textContent = 'נתח קורפוס';
if (state.error) {
toast('ניתוח נכשל: ' + state.error.substring(0, 150), 'error');
} else if (state.result) {
toast('הניתוח הושלם — הדפוסים עודכנו', 'success');
loadStylePatterns();
}
}
} catch (e) {
btn.disabled = false;
}
}
// ── Style Report Page ────────────────────────────────────
const PATTERN_TYPE_COLORS = {
opening_formula: '#5e9a6e',
closing_formula: '#c87533',
transition: '#4e7cb3',
characteristic_phrase: '#a7547c',
argument_flow: '#7e5c9a',
analysis_structure: '#3e8583',
evidence_handling: '#b8894a',
citation_style: '#5f6b8c',
};
const SECTION_COLORS = {
intro: '#4e7cb3',
facts: '#5e9a6e',
appellant_claims: '#a7547c',
respondent_claims: '#c87533',
legal_analysis: '#7e5c9a',
ruling: '#3e8583',
conclusion: '#b8894a',
};
const DONUT_COLORS = ['#e94560', '#5e9a6e', '#4e7cb3', '#a7547c', '#c87533', '#7e5c9a', '#b8894a'];
let _styleReportData = null;
let _activeFilter = 'all';
async function loadStyleReport() {
document.getElementById('styleReportLoading').style.display = '';
document.getElementById('styleReportContent').style.display = 'none';
try {
const res = await fetch(API + '/training/style-report');
if (!res.ok) throw new Error('Failed to load report');
_styleReportData = await res.json();
renderHero(_styleReportData.corpus);
renderAnatomy(_styleReportData.anatomy);
renderPhrases(_styleReportData.signature_phrases);
renderContribution(_styleReportData.contribution);
document.getElementById('styleReportLoading').style.display = 'none';
document.getElementById('styleReportContent').style.display = '';
} catch (e) {
document.getElementById('styleReportLoading').innerHTML = `<div>שגיאה: ${esc(e.message)}</div>`;
}
}
function renderHero(corpus) {
document.getElementById('heroHeadline').textContent = '★ ' + corpus.headline;
document.getElementById('heroStats').innerHTML = `
<div class="hero-stat">
<div class="hero-stat-value">${corpus.decision_count}</div>
<div class="hero-stat-label">החלטות בקורפוס</div>
</div>
<div class="hero-stat">
<div class="hero-stat-value">${(corpus.total_chars / 1000).toFixed(0)}K</div>
<div class="hero-stat-label">סך תווים</div>
</div>
<div class="hero-stat">
<div class="hero-stat-value">${(corpus.avg_chars / 1000).toFixed(0)}K</div>
<div class="hero-stat-label">ממוצע להחלטה</div>
</div>
`;
// Donut (CSS conic-gradient)
const total = corpus.subject_distribution.reduce((a, b) => a + b.count, 0);
let pct = 0;
const segments = corpus.subject_distribution.map((s, i) => {
const start = (pct / total) * 360;
pct += s.count;
const end = (pct / total) * 360;
const color = DONUT_COLORS[i % DONUT_COLORS.length];
return `${color} ${start}deg ${end}deg`;
}).join(', ');
const donut = document.getElementById('heroDonut');
donut.style.background = `conic-gradient(${segments})`;
donut.innerHTML = `<div class="donut-center">${corpus.decision_count} החלטות</div>`;
document.getElementById('heroDonutLegend').innerHTML = corpus.subject_distribution.map((s, i) => `
<div class="donut-legend-item">
<span class="donut-legend-dot" style="background:${DONUT_COLORS[i % DONUT_COLORS.length]}"></span>
<span>${esc(s.label)} · ${s.count}</span>
</div>
`).join('');
// Timeline
const tl = document.getElementById('heroTimeline');
const dated = corpus.decisions.filter(d => d.date);
if (dated.length < 2) {
tl.innerHTML = '';
return;
}
const dates = dated.map(d => new Date(d.date).getTime());
const minT = Math.min(...dates);
const maxT = Math.max(...dates);
const range = maxT - minT || 1;
let html = '<div class="hero-timeline-line"></div>';
dated.forEach(d => {
const t = new Date(d.date).getTime();
const pct = ((t - minT) / range) * 100;
html += `<div class="hero-timeline-dot" style="right:${pct}%" title="${esc(d.number)} · ${esc(d.date)}"></div>`;
});
tl.innerHTML = html;
}
function renderAnatomy(anatomy) {
document.getElementById('anatomyHeadline').textContent = '★ ' + anatomy.headline;
if (!anatomy.sections || !anatomy.sections.length) {
document.getElementById('anatomyBar').innerHTML = '<div class="empty" style="width:100%">אין עדיין נתונים</div>';
return;
}
const bar = document.getElementById('anatomyBar');
bar.innerHTML = anatomy.sections.map(s => {
const color = SECTION_COLORS[s.type] || '#888';
const pctDisplay = Math.round(s.pct * 100);
return `
<div class="anatomy-segment" style="flex:${s.pct}; background:${color}"
title="${esc(s.label)}: ממוצע ${s.avg_chars.toLocaleString('he-IL')} תווים, ${s.coverage} החלטות">
<div>
<div>${esc(s.label)}</div>
<small>${pctDisplay}%</small>
</div>
</div>
`;
}).join('');
document.getElementById('anatomyLegend').innerHTML = anatomy.sections.map(s => `
<div class="anatomy-legend-item">
<span class="anatomy-legend-dot" style="background:${SECTION_COLORS[s.type] || '#888'}"></span>
<span>${esc(s.label)} · ממוצע ${s.avg_chars.toLocaleString('he-IL')} תווים · ${s.coverage} החלטות</span>
</div>
`).join('');
}
function renderPhrases(phrases) {
document.getElementById('phrasesHeadline').textContent = '★ ' + phrases.headline;
// Build filter chips — one per pattern_type that appears
const types = [...new Set(phrases.items.map(p => p.type))];
const typeLabels = {
opening_formula: 'פתיחה',
closing_formula: 'סיום',
transition: 'מעברים',
characteristic_phrase: 'ביטויים',
argument_flow: 'טיעון',
analysis_structure: 'מבנה',
evidence_handling: 'ראיות',
citation_style: 'ציטוט',
};
const filters = [{ id: 'all', label: 'הכל' }]
.concat(types.map(t => ({ id: t, label: typeLabels[t] || t })));
document.getElementById('phraseFilters').innerHTML = filters.map(f => `
<div class="phrase-filter ${f.id === _activeFilter ? 'active' : ''}"
data-filter="${f.id}" onclick="setPhraseFilter('${f.id}')">${esc(f.label)}</div>
`).join('');
renderPhraseWall(phrases.items);
}
function setPhraseFilter(filterId) {
_activeFilter = filterId;
document.querySelectorAll('.phrase-filter').forEach(el => {
el.classList.toggle('active', el.dataset.filter === filterId);
});
renderPhraseWall(_styleReportData.signature_phrases.items);
}
function renderPhraseWall(items) {
const filtered = _activeFilter === 'all'
? items
: items.filter(p => p.type === _activeFilter);
document.getElementById('phraseWall').innerHTML = filtered.map((p, idx) => {
// Clean display text — first alternative, strip placeholders
let display = p.text.replace(/\[[^\]]*\]/g, '').replace(/\s+/g, ' ').trim();
display = display.split(' / ')[0].split(' או ')[0].trim();
if (display.length > 80) display = display.substring(0, 77) + '...';
const color = PATTERN_TYPE_COLORS[p.type] || '#888';
const origIdx = items.indexOf(p);
return `
<div class="phrase-card" style="border-right-color:${color}" onclick="showPhraseModal(${origIdx})">
<div class="phrase-card-text">${esc(display)}</div>
<div class="phrase-card-meta">
<span>${esc(p.context.substring(0, 40))}</span>
<span class="phrase-card-freq">${p.frequency}/24</span>
</div>
</div>
`;
}).join('');
}
function showPhraseModal(idx) {
const p = _styleReportData.signature_phrases.items[idx];
if (!p) return;
const typeLabels = {
opening_formula: 'נוסחת פתיחה',
closing_formula: 'נוסחת סיום',
transition: 'ביטוי מעבר',
characteristic_phrase: 'ביטוי אופייני',
argument_flow: 'זרימת טיעון',
analysis_structure: 'מבנה ניתוח',
evidence_handling: 'טיפול בראיות',
citation_style: 'סגנון ציטוט',
};
document.getElementById('phraseModalType').textContent =
(typeLabels[p.type] || p.type) + ` · ${p.frequency}/24 החלטות`;
document.getElementById('phraseModalText').textContent = p.text;
document.getElementById('phraseModalContext').textContent = p.context || '';
const examples = (p.examples || []).filter(e => e && e.length > 0);
document.getElementById('phraseModalExamples').innerHTML = examples.length
? examples.map(e => `<div class="phrase-modal-example">${esc(e)}</div>`).join('')
: '<div class="empty">אין דוגמאות שמורות</div>';
document.getElementById('phraseModal').showModal();
}
function renderContribution(contrib) {
document.getElementById('contributionHeadline').textContent = '★ ' + contrib.headline;
// Growth curve — SVG polyline
const points = contrib.growth_curve;
if (points.length >= 2) {
const w = 800, h = 160, pad = 20;
const maxY = Math.max(...points.map(p => p.cumulative)) || 1;
const step = (w - pad * 2) / (points.length - 1);
// RTL: right = start, so reverse X
const coords = points.map((p, i) => {
const x = w - pad - i * step;
const y = h - pad - (p.cumulative / maxY) * (h - pad * 2);
return { x, y, ...p };
});
const path = coords.map((c, i) => `${i === 0 ? 'M' : 'L'} ${c.x.toFixed(1)} ${c.y.toFixed(1)}`).join(' ');
const areaPath = path + ` L ${coords[coords.length - 1].x} ${h - pad} L ${coords[0].x} ${h - pad} Z`;
const svg = document.getElementById('growthCurve');
svg.innerHTML = `
<path class="growth-curve-area" d="${areaPath}"/>
<path class="growth-curve-path" d="${path}"/>
${coords.map(c => `
<circle class="growth-curve-dot" cx="${c.x}" cy="${c.y}" r="4">
<title>${esc(c.decision_number || 'ללא מספר')}: ${c.cumulative} דפוסים מצטברים</title>
</circle>
`).join('')}
`;
}
// Contribution cards — sort by date
const cards = contrib.decision_contributions;
document.getElementById('contributionGrid').innerHTML = cards.map(d => {
const highlight = d.highlight;
let highlightDisplay = '';
if (highlight) {
let text = highlight.text.replace(/\[[^\]]*\]/g, '').replace(/\s+/g, ' ').trim();
text = text.split(' / ')[0].split(' או ')[0].trim();
if (text.length > 80) text = text.substring(0, 77) + '...';
highlightDisplay = `
<div class="contribution-highlight">
▸ תרומה בולטת: <strong>"${esc(text)}"</strong>
</div>
`;
}
return `
<div class="contribution-card">
<div class="contribution-card-header">
<span class="contribution-card-number">${esc(d.decision_number || 'ללא מספר')}</span>
<span>${esc(d.decision_date || '—')}</span>
</div>
<div class="contribution-badges">
<span class="contribution-badge new">🟢 ${d.new_count} חדשים</span>
<span class="contribution-badge confirmed">🔵 ${d.confirmed_count} חיזקה</span>
</div>
${highlightDisplay}
</div>
`;
}).join('');
}
async function loadCorpusList() {
const container = document.getElementById('corpusList');
const count = document.getElementById('corpusCount');
try {
const res = await fetch(API + '/training/corpus');
const rows = await res.json();
count.textContent = `${rows.length} החלטות`;
if (!rows.length) {
container.innerHTML = '<div class="empty">הקורפוס ריק</div>';
return;
}
container.innerHTML = `
<table class="corpus-table">
<thead>
<tr><th>מספר</th><th>תאריך</th><th>קטגוריות</th><th>תווים</th><th>נוצר</th></tr>
</thead>
<tbody>
${rows.map(r => `
<tr>
<td>${esc(r.decision_number || '—')}</td>
<td>${esc(r.decision_date || '—')}</td>
<td>${(r.subject_categories || []).map(c => `<span class="cat-tag">${esc(c)}</span>`).join('')}</td>
<td>${r.chars.toLocaleString('he-IL')}</td>
<td>${esc(r.created_at ? r.created_at.substring(0, 10) : '—')}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
} catch (e) {
container.innerHTML = `<div class="empty">שגיאה בטעינה: ${esc(e.message)}</div>`;
}
}
</script>
</body>
</html>