Files
legal-ai/web/static/index.html
Chaim 5028f677f1
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 7s
Fix English statuses and labels throughout UI to Hebrew
- Complete STATUS_LABELS in case view (added outcome_set, direction_approved,
  drafting, qa_review, reviewed)
- Add DOC_STATUS_LABELS for diagnostics page (failed/stuck documents)
- Add completed/failed/pending/error to global STEP_LABELS
- Translate settings page table headers to Hebrew

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 06:32:03 +00:00

5193 lines
186 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>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="/design-system.css">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--font-body);
background: var(--color-bg);
color: var(--color-ink);
direction: rtl;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Header — editorial navy with gold accent rule */
header {
background: var(--color-navy);
color: var(--color-parchment);
padding: 18px 40px;
display: flex;
align-items: center;
gap: 16px;
border-bottom: 3px solid var(--color-gold);
box-shadow: var(--shadow-md);
position: relative;
z-index: 10;
}
header h1 {
font-family: var(--font-display);
font-size: 1.45em;
font-weight: 700;
color: var(--color-parchment);
letter-spacing: 0.02em;
cursor: pointer;
transition: color var(--t);
}
header h1:hover { color: var(--color-gold-soft); }
header .sep {
width: 1px; height: 24px;
background: var(--color-gold);
opacity: 0.5;
border-radius: 1px;
}
header .subtitle {
font-family: var(--font-display);
font-size: 0.95em;
font-style: italic;
color: var(--color-gold-soft);
opacity: 0.85;
}
header nav { margin-right: auto; display: flex; gap: 4px; }
header nav a {
color: rgba(245, 241, 232, 0.7);
text-decoration: none;
font-size: 0.88em;
font-weight: 500;
padding: 8px 16px;
border-radius: var(--radius);
transition: all var(--t);
position: relative;
}
header nav a:hover {
color: var(--color-parchment);
background: rgba(169, 125, 58, 0.15);
}
header nav a.active {
color: var(--color-gold-soft);
background: rgba(169, 125, 58, 0.2);
}
header nav a.active::after {
content: '';
position: absolute;
bottom: -19px; right: 50%;
transform: translateX(50%);
width: 30px; height: 3px;
background: var(--color-gold);
border-radius: 2px 2px 0 0;
}
.theme-toggle {
background: transparent;
border: 1px solid rgba(169, 125, 58, 0.4);
color: var(--color-gold-soft);
cursor: pointer;
font-size: 1.05em;
padding: 6px 12px;
border-radius: var(--radius);
margin-right: var(--space-3);
transition: all var(--t);
}
.theme-toggle:hover {
background: rgba(169, 125, 58, 0.15);
border-color: var(--color-gold);
}
/* Main */
.main { flex: 1; max-width: 1200px; width: 100%; margin: 0 auto; padding: var(--space-7) var(--space-6); }
.page { display: none; animation: fadeSlideIn 0.35s var(--ease-out); }
.page.active { display: block; }
@keyframes fadeSlideIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
/* Cards — editorial surfaces */
.card {
background: var(--color-surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
border: 1px solid var(--color-rule-soft);
overflow: hidden;
margin-bottom: var(--space-4);
transition: box-shadow var(--t), transform var(--t);
}
.card-header {
padding: 16px 24px;
font-family: var(--font-display);
font-size: 1.05em;
font-weight: 600;
color: var(--color-navy);
border-bottom: 1px solid var(--color-rule);
background: var(--color-parchment);
display: flex;
align-items: center;
gap: 10px;
}
.card-body { padding: 24px; }
/* Buttons — editorial with gold */
.btn {
padding: 10px 22px;
border: 1px solid transparent;
border-radius: var(--radius);
font-size: 0.9em;
font-family: var(--font-body);
cursor: pointer;
transition: all var(--t);
font-weight: 600;
letter-spacing: 0.01em;
}
.btn-primary {
background: var(--color-navy);
color: var(--color-parchment);
border-color: var(--color-navy);
}
.btn-primary:hover {
background: var(--color-navy-soft);
box-shadow: var(--shadow-md);
transform: translateY(-1px);
}
.btn-primary:disabled {
background: var(--color-ink-light);
border-color: var(--color-ink-light);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.btn-secondary {
background: var(--color-cream-deep);
color: var(--color-navy);
border-color: var(--color-rule);
}
.btn-secondary:hover {
background: var(--color-rule);
}
.btn-outline, .btn-ghost {
background: transparent;
color: var(--color-navy);
border: 1px solid var(--color-navy);
}
.btn-outline:hover, .btn-ghost:hover {
background: var(--color-gold-wash);
border-color: var(--color-gold-deep);
color: var(--color-gold-deep);
}
.btn-gold {
background: var(--color-gold);
color: var(--color-parchment);
border-color: var(--color-gold);
}
.btn-gold:hover {
background: var(--color-gold-deep);
box-shadow: var(--shadow-md);
transform: translateY(-1px);
}
.btn-sm { padding: 6px 14px; font-size: 0.8em; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.btn-success {
background: var(--color-success);
color: #fff;
border-color: var(--color-success);
}
/* Form Elements */
.form-row { display: flex; gap: var(--space-3); flex-wrap: wrap; align-items: end; margin-bottom: var(--space-4); }
.form-group { flex: 1; min-width: 160px; }
.form-group label {
display: block;
font-size: 0.78em;
color: var(--color-ink-muted);
margin-bottom: 6px;
font-weight: var(--weight-medium);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.form-group select,
.form-group input[type="text"],
.form-group input[type="date"],
.form-group textarea {
width: 100%;
padding: 10px 14px;
border: 1px solid var(--color-rule);
border-radius: var(--radius);
font-size: 0.92em;
font-family: var(--font-body);
direction: rtl;
background: var(--color-surface);
transition: border-color var(--t), box-shadow var(--t);
color: var(--color-ink);
}
.form-group textarea {
resize: vertical;
min-height: 80px;
line-height: var(--leading-body);
}
.form-group select:focus,
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--color-gold);
box-shadow: 0 0 0 3px var(--color-gold-wash);
}
/* ── Case List ─────────────────────────────────────────── */
.case-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: var(--space-5);
}
.case-card {
background: var(--color-surface);
border-radius: var(--radius-lg);
padding: var(--space-6);
cursor: pointer;
box-shadow: var(--shadow-sm);
transition: box-shadow var(--t), transform var(--t), border-color var(--t);
border: 1px solid var(--color-rule-soft);
border-right: 4px solid var(--color-gold);
position: relative;
}
.case-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 1px;
background: linear-gradient(to left, transparent, var(--color-gold-soft), transparent);
opacity: 0;
transition: opacity var(--t);
}
.case-card:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
border-right-color: var(--color-gold-deep);
}
.case-card:hover::before { opacity: 1; }
.case-card .case-number {
font-family: var(--font-display);
font-size: 1.2em;
font-weight: 700;
color: var(--color-navy);
letter-spacing: 0.01em;
}
.case-card .case-title {
font-size: 0.95em;
color: var(--color-ink-soft);
margin: 8px 0;
line-height: var(--leading-snug);
font-weight: var(--weight-medium);
}
.case-card .case-meta {
font-size: 0.78em;
color: var(--color-ink-muted);
display: flex;
gap: 14px;
flex-wrap: wrap;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--color-rule-soft);
}
.case-card .badge {
display: inline-block;
padding: 3px 10px;
border-radius: var(--radius-pill);
font-size: 0.72em;
font-weight: 600;
background: var(--color-cream-deep);
color: var(--color-ink-muted);
letter-spacing: 0.02em;
}
.case-card .badge.new { background: var(--color-info-bg); color: var(--color-info); }
.case-card .badge.in_progress { background: var(--color-warn-bg); color: var(--color-warn); }
.case-card .badge.drafted, .case-card .badge.final { background: var(--color-success-bg); color: var(--color-success); }
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-6);
padding-bottom: var(--space-4);
border-bottom: 2px solid var(--color-gold);
}
.page-header h2 {
font-family: var(--font-display);
font-size: var(--text-2xl);
font-weight: 700;
color: var(--color-navy);
}
/* ── Home Dashboard Hero ───────────────────────────── */
.home-hero {
display: flex;
justify-content: space-between;
align-items: flex-end;
padding: var(--space-8) 0 var(--space-6);
margin-bottom: var(--space-6);
border-bottom: 1px solid var(--color-rule);
}
.home-hero-eyebrow {
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.15em;
color: var(--color-gold-deep);
font-weight: 600;
margin-bottom: var(--space-2);
}
.home-hero-title {
font-family: var(--font-display);
font-size: var(--text-4xl);
font-weight: 900;
color: var(--color-navy);
line-height: 1;
letter-spacing: -0.02em;
margin-bottom: var(--space-2);
}
.home-hero-subtitle {
font-family: var(--font-display);
font-style: italic;
color: var(--color-ink-muted);
font-size: 1.05em;
text-align: right;
}
/* KPI cards */
.kpi-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-4);
margin-bottom: var(--space-7);
}
.kpi-card {
background: var(--color-surface);
border: 1px solid var(--color-rule-soft);
border-radius: var(--radius-lg);
padding: var(--space-5) var(--space-6);
position: relative;
overflow: hidden;
transition: all var(--t);
}
.kpi-card::before {
content: '';
position: absolute;
top: 0; right: 0; bottom: 0;
width: 3px;
background: var(--color-gold);
opacity: 0.7;
transition: width var(--t), opacity var(--t);
}
.kpi-card:hover::before { width: 5px; opacity: 1; }
.kpi-card:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.kpi-label {
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--color-ink-muted);
font-weight: 600;
margin-bottom: var(--space-2);
}
.kpi-value {
font-family: var(--font-display);
font-size: var(--text-4xl);
font-weight: 900;
color: var(--color-navy);
line-height: 1;
letter-spacing: -0.02em;
}
.kpi-caption {
font-size: var(--text-xs);
color: var(--color-ink-light);
margin-top: var(--space-2);
font-family: var(--font-display);
font-style: italic;
}
/* Home layout: main + activity sidebar */
.home-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) 280px;
gap: var(--space-6);
}
.home-main { min-width: 0; }
.home-sidebar {
background: var(--color-parchment);
border: 1px solid var(--color-rule);
border-radius: var(--radius-lg);
padding: var(--space-5);
height: fit-content;
position: sticky;
top: var(--space-5);
}
.home-aside-title {
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--color-gold-deep);
font-weight: 700;
margin-bottom: var(--space-4);
padding-bottom: var(--space-3);
border-bottom: 1px solid var(--color-gold);
}
/* Activity feed */
.activity-feed { display: flex; flex-direction: column; gap: var(--space-3); }
.activity-item {
padding: var(--space-3);
background: var(--color-surface);
border: 1px solid var(--color-rule-soft);
border-radius: var(--radius);
font-size: 0.82em;
transition: all var(--t);
cursor: pointer;
border-right: 3px solid var(--color-gold);
}
.activity-item:hover {
box-shadow: var(--shadow-sm);
border-right-color: var(--color-gold-deep);
}
.activity-label {
color: var(--color-navy);
font-weight: 600;
line-height: 1.4;
margin-bottom: 4px;
}
.activity-detail {
color: var(--color-ink-muted);
font-size: 0.9em;
line-height: 1.4;
margin-bottom: 4px;
}
.activity-time {
color: var(--color-ink-light);
font-size: 0.78em;
}
.activity-icon { font-size: 0.9em; margin-left: 4px; }
@media (max-width: 1000px) {
.home-grid { grid-template-columns: 1fr; }
.home-sidebar { position: static; }
}
@media (max-width: 800px) {
.kpi-row { grid-template-columns: repeat(2, 1fr); }
.home-hero { flex-direction: column; align-items: flex-start; gap: var(--space-4); }
}
/* ── Wizard ────────────────────────────────────────────── */
.wizard-steps {
display: flex;
gap: 0;
margin-bottom: var(--space-6);
background: var(--color-surface);
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
border: 1px solid var(--color-rule-soft);
}
.wizard-step {
flex: 1;
padding: 16px 18px;
text-align: center;
font-size: 0.88em;
color: var(--color-ink-light);
font-family: var(--font-display);
font-weight: 500;
border-bottom: 3px solid transparent;
transition: all var(--t);
position: relative;
background: var(--color-parchment);
}
.wizard-step:not(:last-child)::after {
content: '';
position: absolute;
left: 0; top: 25%; bottom: 25%;
width: 1px;
background: var(--color-rule);
}
.wizard-step.active {
color: var(--color-navy);
border-bottom-color: var(--color-gold);
font-weight: 700;
background: var(--color-surface);
}
.wizard-step.done {
color: var(--color-success);
border-bottom-color: var(--color-success);
background: var(--color-success-bg);
}
.wizard-panel { display: none; }
.wizard-panel.active { display: block; }
.wizard-nav {
display: flex;
justify-content: space-between;
margin-top: var(--space-6);
padding-top: var(--space-5);
border-top: 1px solid var(--color-rule);
}
/* 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: var(--color-gold); cursor: pointer; font-size: 1.1em; padding: 4px;
}
/* ── Case View ─────────────────────────────────────────── */
.case-header-bar {
background: var(--color-surface);
border-radius: var(--radius-lg);
padding: var(--space-6) var(--space-7);
margin-bottom: var(--space-5);
box-shadow: var(--shadow-sm);
border: 1px solid var(--color-rule-soft);
border-right: 4px solid var(--color-gold);
display: flex;
justify-content: space-between;
align-items: start;
gap: var(--space-6);
position: relative;
}
.case-header-bar::before {
content: '';
position: absolute;
top: 0; left: var(--space-6); right: var(--space-6);
height: 2px;
background: linear-gradient(to left, transparent, var(--color-gold-soft), transparent);
opacity: 0.5;
}
.case-header-bar .info h2 {
font-family: var(--font-display);
font-size: var(--text-2xl);
font-weight: 800;
color: var(--color-navy);
margin-bottom: var(--space-2);
letter-spacing: -0.01em;
}
.case-header-bar .info .meta {
font-size: 0.85em;
color: var(--color-ink-muted);
display: flex;
gap: var(--space-5);
flex-wrap: wrap;
}
.case-header-bar .info .meta strong {
color: var(--color-navy);
font-weight: 600;
}
.case-header-bar .links { display: flex; gap: var(--space-2); flex-shrink: 0; }
.case-header-bar .links a {
font-size: 0.82em;
padding: 8px 14px;
border-radius: var(--radius);
text-decoration: none;
border: 1px solid var(--color-rule);
color: var(--color-ink-muted);
background: var(--color-parchment);
transition: all var(--t);
font-weight: 500;
}
.case-header-bar .links a:hover {
border-color: var(--color-gold);
color: var(--color-gold-deep);
background: var(--color-gold-wash);
}
.doc-group { margin-bottom: var(--space-5); }
.doc-group-header {
font-family: var(--font-display);
font-size: 0.92em;
font-weight: 700;
color: var(--color-navy);
padding: var(--space-3) 0;
border-bottom: 1px solid var(--color-gold);
margin-bottom: var(--space-3);
display: flex;
justify-content: space-between;
letter-spacing: 0.01em;
}
.doc-group-header .count {
font-weight: 400;
color: var(--color-ink-light);
font-family: var(--font-body);
}
.doc-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
border-radius: var(--radius);
transition: all var(--t);
font-size: 0.88em;
border: 1px solid transparent;
}
.doc-item:hover {
background: var(--color-parchment);
border-color: var(--color-rule-soft);
}
.doc-item .doc-icon { color: var(--color-gold); }
.doc-item .doc-name { flex: 1; color: var(--color-ink); font-weight: 500; }
.doc-item .doc-status { font-size: 0.78em; color: var(--color-ink-light); }
/* ── Research Analysis Cards ───────────────────────── */
.research-section {
margin-bottom: var(--space-6);
}
.research-section:last-child { margin-bottom: 0; }
.research-section-title {
font-family: var(--font-body);
font-size: 1.05em;
font-weight: 700;
color: var(--color-navy);
padding: var(--space-3) var(--space-4);
background: var(--color-parchment);
border-right: 4px solid var(--color-gold);
border-radius: var(--radius) 0 0 var(--radius);
margin-bottom: var(--space-4);
display: flex;
justify-content: space-between;
align-items: center;
}
.research-section-title .count-pill {
background: var(--color-gold);
color: var(--color-parchment);
padding: 2px 12px;
border-radius: var(--radius-pill);
font-size: 0.78em;
font-weight: 600;
}
.research-prose {
padding: var(--space-4) var(--space-5);
background: var(--color-parchment);
border: 1px solid var(--color-rule-soft);
border-radius: var(--radius);
font-size: 0.92em;
line-height: var(--leading-body);
color: var(--color-ink);
white-space: pre-wrap;
text-align: justify;
text-justify: inter-word;
}
.research-item {
border: 1px solid var(--color-rule-soft);
border-radius: var(--radius-lg);
background: var(--color-surface);
margin-bottom: var(--space-3);
overflow: hidden;
transition: box-shadow var(--t);
}
.research-item[open] {
box-shadow: var(--shadow-sm);
border-color: var(--color-gold);
}
.research-item summary {
padding: var(--space-4) var(--space-5);
cursor: pointer;
font-weight: 600;
color: var(--color-navy);
font-size: 0.98em;
display: flex;
align-items: center;
gap: var(--space-3);
list-style: none;
transition: background var(--t-fast);
}
.research-item summary::-webkit-details-marker { display: none; }
.research-item summary::before {
content: '▸';
color: var(--color-gold);
font-size: 1.1em;
transition: transform var(--t);
flex-shrink: 0;
}
.research-item[open] summary::before { transform: rotate(90deg); }
.research-item summary:hover { background: var(--color-parchment); }
.research-item-number {
display: inline-block;
background: var(--color-gold-wash);
color: var(--color-gold-deep);
padding: 2px 10px;
border-radius: var(--radius-pill);
font-size: 0.78em;
font-weight: 700;
flex-shrink: 0;
}
.research-item-title {
flex: 1;
line-height: 1.4;
}
.research-item-status {
flex-shrink: 0;
font-size: 0.76em;
color: var(--color-ink-muted);
font-weight: 400;
}
.research-item-status.filled {
color: var(--color-success);
font-weight: 600;
}
.research-item-body {
padding: 0 var(--space-5) var(--space-5);
border-top: 1px solid var(--color-rule-soft);
}
.research-field {
margin-top: var(--space-4);
}
.research-field-label {
font-size: 0.78em;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-ink-muted);
font-weight: 700;
margin-bottom: var(--space-2);
padding-bottom: 4px;
border-bottom: 1px solid var(--color-rule-soft);
}
.research-field-content {
font-size: 0.92em;
line-height: var(--leading-body);
color: var(--color-ink);
white-space: pre-wrap;
text-align: justify;
text-justify: inter-word;
}
/* Chair position editor — the highlight feature */
.chair-editor {
margin-top: var(--space-5);
padding: var(--space-4) var(--space-5);
background: var(--color-gold-wash);
border: 2px solid var(--color-gold);
border-radius: var(--radius-md);
position: relative;
}
.chair-editor-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-3);
}
.chair-editor-label {
font-family: var(--font-body);
font-size: 0.88em;
font-weight: 700;
color: var(--color-navy);
display: flex;
align-items: center;
gap: 6px;
}
.chair-editor-label::before {
content: '📝';
font-size: 1.1em;
}
.chair-editor textarea {
width: 100%;
min-height: 90px;
padding: 12px 14px;
font-family: var(--font-body);
font-size: 0.95em;
line-height: var(--leading-body);
color: var(--color-ink);
background: var(--color-surface);
border: 1px solid var(--color-rule);
border-radius: var(--radius);
resize: vertical;
direction: rtl;
text-align: justify;
text-justify: inter-word;
}
.chair-editor textarea:focus {
outline: none;
border-color: var(--color-gold);
box-shadow: 0 0 0 3px rgba(169, 125, 58, 0.15);
}
.chair-editor textarea::placeholder {
color: var(--color-ink-light);
font-style: italic;
}
.save-indicator {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.78em;
font-weight: 600;
min-height: 1.2em;
transition: opacity var(--t);
}
.save-indicator.saving { color: var(--color-ink-muted); }
.save-indicator.saved { color: var(--color-success); animation: saveFlash 0.4s var(--ease-out); }
.save-indicator.error { color: var(--color-danger); }
@keyframes saveFlash {
0% { transform: scale(0.9); opacity: 0; }
50% { transform: scale(1.05); }
100% { transform: scale(1); opacity: 1; }
}
.doc-status.completed { color: var(--color-success); font-weight: 700; font-size: 1em; }
.doc-status.processing { display: inline-block; width: 14px; height: 14px; border: 2px solid var(--color-gold); border-top-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; }
.doc-status.pending { color: #ccc; }
.doc-status.failed { color: var(--color-gold); font-weight: 700; }
.btn-retry { background: none; border: 1px solid var(--color-gold); color: var(--color-gold); border-radius: 4px; padding: 2px 8px; font-size: 0.75em; cursor: pointer; margin-right: 6px; }
.btn-retry:hover { background: var(--color-gold); color: #fff; }
.processing-badge { display: inline-flex; align-items: center; gap: 4px; color: var(--color-gold); font-size: 0.78em; font-weight: 500; }
.mini-spinner { display: inline-block; width: 10px; height: 10px; border: 1.5px solid var(--color-gold); border-top-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; }
/* Upload zone (reusable) */
.upload-zone {
border: 2px dashed var(--color-rule);
border-radius: var(--radius-lg);
padding: var(--space-9) var(--space-6);
text-align: center;
cursor: pointer;
transition: all var(--t);
background: var(--color-parchment);
position: relative;
}
.upload-zone:hover,
.upload-zone.dragover {
border-color: var(--color-gold);
background: var(--color-gold-wash);
transform: translateY(-1px);
}
.upload-zone h3 {
font-family: var(--font-display);
font-size: 1.1em;
font-weight: 700;
color: var(--color-navy);
margin-bottom: 6px;
}
.upload-zone p {
font-size: 0.85em;
color: var(--color-ink-muted);
}
.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: var(--color-gold);
border-radius: 50%; animation: spin 0.8s linear infinite; flex-shrink: 0;
}
.task-item.done .spinner { border-color: var(--color-success); border-top-color: var(--color-success); animation: none; }
.task-item.failed .spinner { border-color: var(--color-gold); border-top-color: var(--color-gold); 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: var(--color-gold); }
.creation-steps li.done .step-icon { color: var(--color-success); }
.creation-steps li.error .step-icon { color: var(--color-gold); }
/* Status Bar */
.status-bar {
background: var(--color-navy);
color: rgba(245, 241, 232, 0.55);
font-size: 0.78em;
font-family: var(--font-body);
padding: 10px 40px;
display: flex;
gap: var(--space-7);
align-items: center;
border-top: 1px solid var(--color-gold-deep);
letter-spacing: 0.02em;
}
.status-bar .stat { display: flex; align-items: center; gap: 8px; }
.status-bar .stat-value {
color: var(--color-gold-soft);
font-weight: 700;
font-family: var(--font-display);
font-size: 1.1em;
}
/* Toast */
.toast {
position: fixed; bottom: 70px; left: 50%;
transform: translateX(-50%) translateY(10px);
background: var(--color-navy);
color: var(--color-parchment);
padding: 14px 28px;
border-radius: var(--radius-md);
font-size: 0.9em;
font-weight: 500;
z-index: 1000;
opacity: 0;
transition: opacity var(--t-slow), transform var(--t-slow);
pointer-events: none;
box-shadow: var(--shadow-lg);
border: 1px solid var(--color-gold);
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.toast.error {
background: var(--color-danger);
border-color: var(--color-danger);
}
.toast.success {
background: var(--color-success);
border-color: var(--color-success);
}
/* Processing visibility panel (floating bottom-right) */
.process-panel {
position: fixed;
bottom: var(--space-5);
left: var(--space-5);
width: 320px;
max-height: 60vh;
background: var(--color-surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
border: 1px solid var(--color-gold);
overflow: hidden;
z-index: 900;
animation: fadeSlideUp 0.35s var(--ease-out);
}
@keyframes fadeSlideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.process-panel.collapsed .process-panel-body { display: none; }
.process-panel-header {
padding: 12px 16px;
background: var(--color-navy);
color: var(--color-parchment);
display: flex;
align-items: center;
gap: 10px;
border-bottom: 2px solid var(--color-gold);
}
.process-panel-title {
font-weight: 600;
font-size: 0.9em;
flex: 1;
}
.process-panel-count {
background: var(--color-gold);
color: var(--color-navy);
padding: 2px 10px;
border-radius: var(--radius-pill);
font-size: 0.78em;
font-weight: 700;
min-width: 24px;
text-align: center;
}
.process-panel .btn-icon {
color: var(--color-parchment);
font-size: 1.3em;
line-height: 1;
padding: 0 8px;
}
.process-panel .btn-icon:hover {
color: var(--color-gold-soft);
background: transparent;
}
.process-panel-body {
padding: 10px 14px;
max-height: calc(60vh - 50px);
overflow-y: auto;
}
.process-item {
padding: 10px 12px;
background: var(--color-cream);
border-radius: var(--radius);
margin-bottom: 6px;
font-size: 0.82em;
border-right: 3px solid var(--color-gold);
}
.process-item:last-child { margin-bottom: 0; }
.process-item-name {
font-weight: 500;
color: var(--color-navy);
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.process-item-status {
display: flex;
align-items: center;
gap: 6px;
color: var(--color-ink-muted);
font-size: 0.92em;
}
/* Diagnostics page */
.diag-pill {
display: inline-block;
padding: 3px 12px;
border-radius: var(--radius-pill);
font-size: 0.78em;
font-weight: 600;
margin-right: auto;
}
.diag-pill.ok { background: var(--color-success-bg); color: var(--color-success); }
.diag-pill.error { background: var(--color-danger-bg); color: var(--color-danger); }
.diag-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: var(--space-4);
}
.diag-stat {
padding: var(--space-4);
background: var(--color-cream);
border-radius: var(--radius);
border-right: 3px solid var(--color-gold);
}
.diag-stat-label {
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--color-ink-muted);
font-weight: 600;
margin-bottom: 4px;
}
.diag-stat-value {
font-size: var(--text-2xl);
font-weight: 800;
color: var(--color-navy);
line-height: 1;
}
.diag-row {
padding: 10px 14px;
background: var(--color-cream);
border-radius: var(--radius);
margin-bottom: 6px;
font-size: 0.86em;
border-right: 3px solid var(--color-rule);
}
.diag-row:last-child { margin-bottom: 0; }
.diag-row-error { border-right-color: var(--color-danger); background: var(--color-danger-bg); }
.diag-row-warn { border-right-color: var(--color-warn); background: var(--color-warn-bg); }
.diag-row-title { font-weight: 600; color: var(--color-navy); margin-bottom: 2px; }
.diag-row-meta { font-size: 0.88em; color: var(--color-ink-muted); }
/* ── Compare Decisions ───────────────────────────────── */
.compare-pickers {
display: grid;
grid-template-columns: 1fr auto 1fr auto;
gap: var(--space-4);
align-items: end;
}
.compare-vs {
font-size: 1.5em;
color: var(--color-gold);
padding-bottom: 10px;
font-family: var(--font-display);
}
.compare-result-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-5);
margin-bottom: var(--space-5);
}
.compare-column {
background: var(--color-surface);
border: 1px solid var(--color-rule-soft);
border-radius: var(--radius-lg);
padding: var(--space-6);
border-top: 4px solid var(--color-gold);
}
.compare-col-title {
font-family: var(--font-display);
font-size: 1.2em;
font-weight: 800;
color: var(--color-navy);
margin-bottom: var(--space-2);
}
.compare-col-meta {
font-size: 0.82em;
color: var(--color-ink-muted);
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-bottom: var(--space-4);
padding-bottom: var(--space-3);
border-bottom: 1px solid var(--color-rule-soft);
}
.compare-col-stat {
display: flex;
justify-content: space-between;
padding: 6px 0;
font-size: 0.88em;
}
.compare-col-stat strong { color: var(--color-navy); }
.compare-diff-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: var(--space-4);
}
.compare-diff-col {
background: var(--color-surface);
border: 1px solid var(--color-rule-soft);
border-radius: var(--radius-lg);
padding: var(--space-5);
}
.compare-diff-title {
font-family: var(--font-display);
font-weight: 700;
color: var(--color-navy);
margin-bottom: var(--space-3);
padding-bottom: var(--space-2);
border-bottom: 2px solid var(--color-gold);
display: flex;
justify-content: space-between;
align-items: baseline;
}
.compare-diff-count {
background: var(--color-gold-wash);
color: var(--color-gold-deep);
padding: 2px 10px;
border-radius: var(--radius-pill);
font-size: 0.78em;
font-weight: 700;
font-family: var(--font-body);
}
.compare-diff-list {
display: flex;
flex-direction: column;
gap: 6px;
max-height: 400px;
overflow-y: auto;
}
.compare-diff-item {
padding: 8px 12px;
background: var(--color-cream);
border-radius: var(--radius);
font-size: 0.82em;
color: var(--color-ink);
border-right: 2px solid var(--color-gold);
line-height: 1.4;
}
/* ── Compose (writing assistant) ─────────────────────── */
.compose-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 320px;
gap: var(--space-5);
min-height: 70vh;
}
.compose-main {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.compose-toolbar {
display: flex;
gap: var(--space-3);
align-items: center;
}
.compose-title-input {
flex: 1;
padding: 12px 16px;
font-family: var(--font-display);
font-size: 1.15em;
font-weight: 600;
color: var(--color-navy);
background: var(--color-surface);
border: 1px solid var(--color-rule);
border-radius: var(--radius-md);
}
.compose-title-input:focus {
outline: none;
border-color: var(--color-gold);
box-shadow: 0 0 0 3px var(--color-gold-wash);
}
.compose-textarea {
flex: 1;
min-height: 60vh;
padding: var(--space-6) var(--space-7);
font-family: var(--font-body);
font-size: 1.02em;
line-height: 1.75;
color: var(--color-ink);
background: var(--color-surface);
border: 1px solid var(--color-rule);
border-radius: var(--radius-lg);
resize: vertical;
text-align: justify;
text-justify: inter-word;
direction: rtl;
}
.compose-textarea:focus {
outline: none;
border-color: var(--color-gold);
box-shadow: 0 0 0 3px var(--color-gold-wash);
}
.compose-stats {
font-size: 0.82em;
color: var(--color-ink-muted);
padding: 6px 14px;
background: var(--color-parchment);
border-radius: var(--radius);
border: 1px solid var(--color-rule-soft);
}
.compose-stats span { color: var(--color-navy); font-weight: 600; }
.compose-sidebar {
background: var(--color-parchment);
border: 1px solid var(--color-rule);
border-radius: var(--radius-lg);
padding: var(--space-5);
display: flex;
flex-direction: column;
gap: var(--space-3);
max-height: 80vh;
overflow: hidden;
}
.compose-sidebar-title {
font-family: var(--font-display);
font-size: 1.05em;
font-weight: 700;
color: var(--color-navy);
padding-bottom: var(--space-3);
border-bottom: 2px solid var(--color-gold);
}
.compose-filters {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.compose-filter {
padding: 4px 10px;
border-radius: var(--radius-pill);
border: 1px solid var(--color-rule);
background: var(--color-surface);
font-size: 0.76em;
cursor: pointer;
transition: all var(--t-fast);
}
.compose-filter:hover { background: var(--color-gold-wash); }
.compose-filter.active {
background: var(--color-navy);
color: var(--color-parchment);
border-color: var(--color-navy);
}
.compose-patterns {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 6px;
padding-right: 4px;
}
.compose-pattern {
padding: 10px 12px;
background: var(--color-surface);
border: 1px solid var(--color-rule-soft);
border-radius: var(--radius);
font-size: 0.84em;
cursor: pointer;
transition: all var(--t-fast);
border-right: 3px solid var(--color-gold);
line-height: 1.4;
}
.compose-pattern:hover {
background: var(--color-gold-wash);
transform: translateX(-2px);
box-shadow: var(--shadow-sm);
}
.compose-pattern-text {
color: var(--color-navy);
font-weight: 500;
margin-bottom: 2px;
}
.compose-pattern-meta {
font-size: 0.78em;
color: var(--color-ink-muted);
}
@media (max-width: 1000px) {
.compose-layout { grid-template-columns: 1fr; }
.compare-result-grid, .compare-diff-row { grid-template-columns: 1fr; }
}
/* ── 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: var(--color-success); background: #f0faf3; }
.export-item.final .export-icon { color: var(--color-success); }
.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: var(--color-gold); background: #fff5f7; }
.export-upload-zone input[type="file"] { display: none; }
.empty {
text-align: center;
color: var(--color-ink-light);
padding: var(--space-8) var(--space-5);
font-size: 0.9em;
line-height: var(--leading-body);
font-family: var(--font-display);
font-style: italic;
}
/* ── Skills Management ───────────────────────────────── */
.skill-list { display: flex; flex-direction: column; gap: var(--space-3); }
.skill-item {
display: flex;
align-items: center;
gap: var(--space-4);
padding: 16px 20px;
border: 1px solid var(--color-rule-soft);
border-radius: var(--radius-lg);
background: var(--color-parchment);
transition: all var(--t);
}
.skill-item:hover {
background: var(--color-surface);
border-color: var(--color-gold);
box-shadow: var(--shadow-sm);
}
.skill-item .skill-icon { font-size: 1.6em; flex-shrink: 0; color: var(--color-gold); }
.skill-item .skill-info { flex: 1; min-width: 0; }
.skill-item .skill-name {
font-family: var(--font-display);
font-size: 1em;
font-weight: 700;
color: var(--color-navy);
}
.skill-item .skill-meta {
font-size: 0.78em;
color: var(--color-ink-muted);
margin-top: 4px;
display: flex;
gap: var(--space-4);
flex-wrap: wrap;
}
.skill-item .skill-badges { display: flex; gap: 6px; flex-shrink: 0; }
.skill-item .badge-ok {
background: var(--color-success-bg);
color: var(--color-success);
padding: 3px 10px;
border-radius: var(--radius-pill);
font-size: 0.72em;
font-weight: 700;
letter-spacing: 0.02em;
}
.skill-item .badge-warn {
background: var(--color-warn-bg);
color: var(--color-warn);
padding: 3px 10px;
border-radius: var(--radius-pill);
font-size: 0.72em;
font-weight: 700;
letter-spacing: 0.02em;
}
.skill-install-result {
margin-top: var(--space-4);
padding: var(--space-4) var(--space-5);
border-radius: var(--radius-md);
background: var(--color-success-bg);
border: 1px solid var(--color-success);
border-right: 4px solid var(--color-success);
font-size: 0.9em;
}
.skill-install-result.error {
background: var(--color-danger-bg);
border-color: var(--color-danger);
}
/* ── 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: var(--color-ink); 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: var(--color-gold);
}
.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: var(--color-gold); 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: var(--color-cream); }
.btn-icon {
background: transparent; border: none; cursor: pointer;
color: var(--color-ink-light); font-size: 1.05em;
padding: 6px 8px; border-radius: var(--radius);
transition: all var(--t);
}
.btn-icon:hover { color: var(--color-navy); background: var(--color-cream-deep); }
.btn-icon-danger:hover { color: var(--color-danger); background: var(--color-danger-bg); }
.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: var(--color-ink); 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: var(--space-9);
padding-top: var(--space-5);
position: relative;
}
.style-report-header::after {
content: '';
display: block;
width: 80px;
height: 3px;
background: linear-gradient(to left, transparent, var(--color-gold), transparent);
margin: var(--space-4) auto 0;
}
.style-report-header h1 {
font-family: var(--font-display);
font-size: var(--text-4xl);
font-weight: 900;
color: var(--color-navy);
margin-bottom: var(--space-2);
letter-spacing: -0.02em;
}
.style-report-header .subtitle-muted {
color: var(--color-ink-muted);
font-size: 1.05em;
font-family: var(--font-display);
font-style: italic;
}
.portrait-card {
background: var(--color-surface);
border-radius: var(--radius-lg);
padding: var(--space-8) var(--space-9);
margin-bottom: var(--space-6);
box-shadow: var(--shadow-sm);
border: 1px solid var(--color-rule-soft);
position: relative;
}
.portrait-card::before {
content: '';
position: absolute;
top: 0; right: var(--space-9); left: var(--space-9);
height: 3px;
background: linear-gradient(to left, transparent, var(--color-gold-soft), transparent);
border-radius: 0 0 2px 2px;
opacity: 0.6;
}
.portrait-section-title {
font-family: var(--font-display);
font-size: var(--text-2xl);
font-weight: 700;
color: var(--color-navy);
margin-bottom: var(--space-3);
padding-bottom: var(--space-3);
border-bottom: 1px solid var(--color-rule);
letter-spacing: -0.01em;
}
.portrait-headline {
font-family: var(--font-display);
font-size: 1.15em;
font-style: italic;
color: var(--color-navy);
line-height: 1.65;
margin-bottom: 24px;
padding: 16px 22px;
background: var(--color-gold-wash);
border-right: 4px solid var(--color-gold);
border-radius: var(--radius);
position: relative;
}
.portrait-headline::before {
content: '"';
font-size: 3em;
font-family: var(--font-display);
color: var(--color-gold-soft);
position: absolute;
top: -8px; right: 16px;
opacity: 0.3;
font-weight: 700;
}
/* 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: var(--color-ink); 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: var(--color-gold); 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: var(--color-ink); color: #fff; border-color: var(--color-ink);
}
.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: var(--color-ink); 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: var(--color-gold); stroke-width: 2.5; }
.growth-curve-area { fill: #fce4e9; opacity: 0.6; }
.growth-curve-dot { fill: var(--color-gold); 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: var(--color-ink); 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: var(--color-ink); }
/* 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: var(--color-ink);
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: var(--color-cream); border-right: 3px solid var(--color-gold);
font-size: 0.86em; line-height: 1.5; color: var(--color-ink); border-radius: var(--radius);
}
/* Shortcuts help dialog */
.shortcuts-dialog { max-width: 460px; }
.shortcuts-table {
width: 100%;
border-collapse: collapse;
font-size: 0.88em;
}
.shortcuts-table tr { border-bottom: 1px solid var(--color-rule-soft); }
.shortcuts-table tr:last-child { border-bottom: none; }
.shortcuts-table td {
padding: 10px 8px;
color: var(--color-ink);
}
.shortcut-keys {
text-align: left;
direction: ltr;
unicode-bidi: embed;
white-space: nowrap;
width: 120px;
}
kbd {
display: inline-block;
padding: 2px 8px;
background: var(--color-parchment);
border: 1px solid var(--color-rule);
border-bottom-width: 2px;
border-radius: var(--radius-sm);
font-family: var(--font-mono);
font-size: 0.85em;
color: var(--color-navy);
margin: 0 2px;
min-width: 22px;
text-align: center;
}
@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="#/upload" id="navUpload">העלאה</a>
<a href="#/training" id="navTraining">אימון סגנון</a>
<a href="#/style-report" id="navStyleReport">הסגנון שלי</a>
<a href="#/compare" id="navCompare">השוואה</a>
<a href="#/compose" id="navCompose">כתיבה</a>
<a href="#/skills" id="navSkills">Skills</a>
<a href="#/diagnostics" id="navDiagnostics">מצב מערכת</a>
<a href="#/settings" id="navSettings">הגדרות</a>
<button class="theme-toggle" id="themeToggle" onclick="toggleTheme()" title="החלף ערכת צבעים (Shift+D)">
<span id="themeIcon">🌙</span>
</button>
</nav>
</header>
<div class="main">
<!-- ══ Page: Case List (Dashboard) ══ -->
<div class="page" id="page-home">
<div class="home-hero">
<div class="home-hero-text">
<p class="home-hero-eyebrow">עוזר משפטי · ועדת ערר ירושלים</p>
<h1 class="home-hero-title">לוח הבית</h1>
<p class="home-hero-subtitle">ניהול תיקי ערר, קורפוס הסגנון והחלטות בתהליך</p>
</div>
<div class="home-hero-actions">
<button class="btn btn-primary" onclick="navigate('/new')">+ תיק חדש</button>
</div>
</div>
<div class="kpi-row" id="kpiRow">
<div class="kpi-card">
<div class="kpi-label">תיקי ערר</div>
<div class="kpi-value" id="kpiCases"></div>
</div>
<div class="kpi-card">
<div class="kpi-label">קורפוס סגנון</div>
<div class="kpi-value" id="kpiCorpus"></div>
<div class="kpi-caption">החלטות שנלמדו</div>
</div>
<div class="kpi-card">
<div class="kpi-label">דפוסי סגנון</div>
<div class="kpi-value" id="kpiPatterns"></div>
<div class="kpi-caption">ביטויים שחולצו</div>
</div>
<div class="kpi-card">
<div class="kpi-label">בעיבוד</div>
<div class="kpi-value" id="kpiProcessing"></div>
<div class="kpi-caption">מסמכים פעילים</div>
</div>
</div>
<div class="home-grid">
<div class="home-main">
<div class="page-header">
<h2>תיקים פעילים</h2>
</div>
<div class="case-grid" id="caseGrid">
<div class="empty">טוען תיקים...</div>
</div>
</div>
<aside class="home-sidebar">
<h3 class="home-aside-title">פעילות אחרונה</h3>
<div class="activity-feed" id="activityFeed">
<div class="empty" style="font-size:0.82em;padding:20px">טוען...</div>
</div>
</aside>
</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" style="max-width:200px">
<label>תחום משפטי</label>
<select id="wiz-practice-area">
<option value="appeals_committee">ועדת ערר</option>
<option value="national_insurance" disabled>ביטוח לאומי (בקרוב)</option>
<option value="labor_law" disabled>דיני עבודה (בקרוב)</option>
</select>
</div>
<div class="form-group">
<label>סוג ערר <span style="color:#888;font-size:0.85em">(מוסק אוטומטית ממספר התיק)</span></label>
<select id="wiz-appeal-subtype">
<option value="building_permit">רישוי ובנייה</option>
<option value="betterment_levy">היטל השבחה</option>
<option value="compensation_197">פיצויים (ס' 197)</option>
<option value="unknown">לא ידוע</option>
</select>
</div>
<input type="hidden" id="wiz-committee-type" value="רישוי">
</div>
<div class="form-row">
<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>
<!-- Research Analysis Cards (parsed analysis-and-research.md) -->
<div class="card" id="caseResearchCard" style="display:none">
<div class="card-header">
<span>ניתוח משפטי ומחקר — תצוגה מובנית</span>
<span id="caseResearchMeta" style="float:left;font-size:0.82em;color:var(--color-ink-muted);font-weight:400;font-family:var(--font-body)"></span>
</div>
<div class="card-body" id="caseResearchBody">
<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" style="padding: 20px 0">
<div class="portrait-card" style="padding:40px">
<div class="skeleton skeleton-line medium" style="height:2em;margin-bottom:16px"></div>
<div class="skeleton skeleton-line" style="height:3em;margin-bottom:20px"></div>
<div style="display:flex;gap:20px;margin-top:30px">
<div class="skeleton" style="width:160px;height:160px;border-radius:50%"></div>
<div style="flex:1">
<div class="skeleton skeleton-line"></div>
<div class="skeleton skeleton-line"></div>
<div class="skeleton skeleton-line short"></div>
</div>
</div>
</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>
<!-- ══ Page: Compare Decisions ══ -->
<div class="page" id="page-compare">
<div class="page-header">
<h2>השוואת שתי החלטות</h2>
</div>
<div class="card">
<div class="card-body">
<div class="compare-pickers">
<div class="form-group">
<label>החלטה א'</label>
<select id="compareSelectA"><option value="">בחרי החלטה...</option></select>
</div>
<div class="compare-vs"></div>
<div class="form-group">
<label>החלטה ב'</label>
<select id="compareSelectB"><option value="">בחרי החלטה...</option></select>
</div>
<button class="btn btn-primary" onclick="runCompare()">השווה</button>
</div>
</div>
</div>
<div id="compareResult"></div>
</div>
<!-- ══ Page: Compose (writing with live suggestions) ══ -->
<div class="page" id="page-compose">
<div class="page-header">
<h2>כתיבת החלטה — עם הצעות סגנון</h2>
</div>
<div class="compose-layout">
<div class="compose-main">
<div class="compose-toolbar">
<input type="text" id="composeTitle" placeholder="כותרת ההחלטה (למשל: ערר 1045/25 — יעקב כהן)" class="compose-title-input">
<button class="btn btn-ghost btn-sm" onclick="copyComposeText()">העתק טקסט</button>
<button class="btn btn-ghost btn-sm" onclick="saveComposeDraft()">שמור טיוטה</button>
</div>
<textarea id="composeTextarea" class="compose-textarea"
placeholder="התחילי לכתוב כאן. הצעות סגנון יופיעו בצד בהתאם לתוכן..."></textarea>
<div class="compose-stats">
<span id="composeCharCount">0</span> תווים ·
<span id="composeWordCount">0</span> מילים ·
<span id="composeSectionGuess"></span>
</div>
</div>
<aside class="compose-sidebar">
<h3 class="compose-sidebar-title">ביטויים של דפנה</h3>
<div class="compose-filters" id="composeFilters"></div>
<div class="compose-patterns" id="composePatterns">
<div class="empty" style="padding:20px">טוען...</div>
</div>
</aside>
</div>
</div>
<!-- ══ Page: Diagnostics ══ -->
<div class="page" id="page-diagnostics">
<div class="page-header">
<h2>מצב מערכת</h2>
<button class="btn btn-ghost btn-sm" onclick="loadDiagnostics()">רענן</button>
</div>
<div id="diagnosticsContent">
<div class="empty">טוען...</div>
</div>
</div>
<!-- ══ Page: Settings ══ -->
<div class="page" id="page-settings">
<div class="page-header">
<h2>הגדרות</h2>
</div>
<!-- Tag → Company Mappings -->
<div class="card">
<div class="card-header">שיוך תגי תיקים לחברות Paperclip</div>
<div class="card-body">
<p style="color:#888;font-size:0.85em;margin-bottom:16px">כל תג ערר (סוג תיק) משויך לחברה ב-Paperclip. כשנפתח תיק חדש, הפרויקט נוצר אוטומטית בחברה המתאימה לפי התג.</p>
<!-- Add new mapping -->
<div style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;margin-bottom:24px;padding:16px;background:var(--bg-secondary,#f8f9fa);border-radius:8px">
<div style="flex:1;min-width:160px">
<label style="display:block;font-size:0.8em;color:#888;margin-bottom:4px">תג ערר</label>
<input type="text" id="settingsNewTag" placeholder="לדוגמה: building_permit" style="width:100%;padding:8px;border:1px solid var(--border,#ddd);border-radius:6px;background:var(--bg-primary,#fff);color:var(--text-primary)">
</div>
<div style="flex:1;min-width:160px">
<label style="display:block;font-size:0.8em;color:#888;margin-bottom:4px">תיאור בעברית</label>
<input type="text" id="settingsNewTagLabel" placeholder="לדוגמה: רישוי ובנייה" style="width:100%;padding:8px;border:1px solid var(--border,#ddd);border-radius:6px;background:var(--bg-primary,#fff);color:var(--text-primary)">
</div>
<div style="flex:1.5;min-width:200px">
<label style="display:block;font-size:0.8em;color:#888;margin-bottom:4px">חברה ב-Paperclip</label>
<select id="settingsCompanySelect" style="width:100%;padding:8px;border:1px solid var(--border,#ddd);border-radius:6px;background:var(--bg-primary,#fff);color:var(--text-primary)">
<option value="">טוען חברות...</option>
</select>
</div>
<button class="btn btn-primary" onclick="addTagMapping()" style="white-space:nowrap">הוסף שיוך</button>
</div>
<!-- Existing mappings table -->
<div id="tagMappingsTable">
<div class="empty">טוען...</div>
</div>
</div>
</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>
<!-- Processing visibility panel (floating) -->
<div class="process-panel" id="processPanel" style="display:none">
<div class="process-panel-header">
<span class="process-panel-title">עיבוד פעיל</span>
<span class="process-panel-count" id="processPanelCount">0</span>
<button class="btn-icon" onclick="toggleProcessPanel()" title="הסתר"></button>
</div>
<div class="process-panel-body" id="processPanelBody"></div>
</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();
loadKPIs();
} 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();
} else if (hash === '#/diagnostics') {
document.getElementById('page-diagnostics').classList.add('active');
document.getElementById('navDiagnostics').classList.add('active');
subtitle = 'מצב מערכת';
loadDiagnostics();
} else if (hash === '#/compare') {
document.getElementById('page-compare').classList.add('active');
document.getElementById('navCompare').classList.add('active');
subtitle = 'השוואת החלטות';
initComparePage();
} else if (hash === '#/compose') {
document.getElementById('page-compose').classList.add('active');
document.getElementById('navCompose').classList.add('active');
subtitle = 'כתיבת החלטה';
initComposePage();
} else if (hash === '#/settings') {
document.getElementById('page-settings').classList.add('active');
document.getElementById('navSettings').classList.add('active');
subtitle = 'הגדרות';
loadSettingsPage();
}
document.getElementById('pageSubtitle').textContent = subtitle;
}
window.addEventListener('hashchange', handleRoute);
// ── Keyboard shortcuts ──────────────────────────────
const SHORTCUTS = [
{ keys: ['g', 'h'], label: 'דף הבית', action: () => location.hash = '#/' },
{ keys: ['g', 'n'], label: 'תיק חדש', action: () => location.hash = '#/new' },
{ keys: ['g', 'u'], label: 'העלאה', action: () => location.hash = '#/upload' },
{ keys: ['g', 't'], label: 'אימון סגנון', action: () => location.hash = '#/training' },
{ keys: ['g', 's'], label: 'הסגנון שלי', action: () => location.hash = '#/style-report' },
{ keys: ['g', 'c'], label: 'השוואה', action: () => location.hash = '#/compare' },
{ keys: ['g', 'w'], label: 'כתיבת החלטה', action: () => location.hash = '#/compose' },
{ keys: ['g', 'd'], label: 'מצב מערכת', action: () => location.hash = '#/diagnostics' },
{ keys: ['g', 'k'], label: 'Skills', action: () => location.hash = '#/skills' },
{ keys: ['n'], label: 'תיק חדש', action: () => location.hash = '#/new' },
{ keys: ['D'], label: 'Dark mode', shiftKey: true, action: toggleTheme },
{ keys: ['?'], label: 'עזרה', shiftKey: true, action: () => showShortcutsHelp() },
{ keys: ['Escape'], label: 'סגור', action: () => {
document.querySelectorAll('dialog[open]').forEach(d => d.close());
}},
];
let _pendingKey = null;
let _pendingTimer = null;
function setupKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
// Ignore when typing in inputs
const tag = (e.target.tagName || '').toLowerCase();
if (tag === 'input' || tag === 'textarea' || tag === 'select' || e.target.isContentEditable) {
if (e.key === 'Escape') e.target.blur();
return;
}
// Handle immediate-single shortcuts
for (const s of SHORTCUTS) {
if (s.keys.length === 1 && s.keys[0] === e.key) {
if (s.shiftKey !== undefined && e.shiftKey !== s.shiftKey) continue;
e.preventDefault();
s.action();
return;
}
}
// Handle 2-key chords starting with 'g'
if (e.key === 'g' && !_pendingKey) {
_pendingKey = 'g';
_pendingTimer = setTimeout(() => { _pendingKey = null; }, 1500);
return;
}
if (_pendingKey === 'g') {
clearTimeout(_pendingTimer);
_pendingKey = null;
for (const s of SHORTCUTS) {
if (s.keys.length === 2 && s.keys[0] === 'g' && s.keys[1] === e.key) {
e.preventDefault();
s.action();
return;
}
}
}
});
}
function showShortcutsHelp() {
let dialog = document.getElementById('shortcutsDialog');
if (!dialog) {
dialog = document.createElement('dialog');
dialog.id = 'shortcutsDialog';
dialog.className = 'phrase-modal shortcuts-dialog';
document.body.appendChild(dialog);
}
const rows = SHORTCUTS.map(s => {
const keysDisplay = s.keys.map(k => `<kbd>${k}</kbd>`).join(' ');
return `<tr><td class="shortcut-keys">${keysDisplay}</td><td>${esc(s.label)}</td></tr>`;
}).join('');
dialog.innerHTML = `
<div class="phrase-modal-header">
<span class="phrase-modal-type">קיצורי מקלדת</span>
<button class="btn-icon" onclick="document.getElementById('shortcutsDialog').close()">✕</button>
</div>
<table class="shortcuts-table">${rows}</table>
`;
dialog.showModal();
}
// ── Theme (dark/light) ──────────────────────────────
function applyTheme(dark) {
document.body.classList.toggle('dark', dark);
const icon = document.getElementById('themeIcon');
if (icon) icon.textContent = dark ? '☀️' : '🌙';
}
function toggleTheme() {
const dark = !document.body.classList.contains('dark');
applyTheme(dark);
try { localStorage.setItem('theme', dark ? 'dark' : 'light'); } catch (e) {}
}
// Apply saved theme immediately (before onload to prevent flash)
(function initTheme() {
try {
const saved = localStorage.getItem('theme');
if (saved === 'dark') applyTheme(true);
} catch (e) {}
})();
window.addEventListener('load', () => {
handleRoute();
loadStatus();
startProcessPanelPolling();
setupKeyboardShortcuts();
});
// ── Case List ────────────────────────────────────────────
async function loadCaseList() {
const grid = document.getElementById('caseGrid');
// Show skeleton while loading
grid.innerHTML = `
<div class="case-card"><div class="skeleton skeleton-line medium"></div><div class="skeleton skeleton-line"></div><div class="skeleton skeleton-line short"></div></div>
<div class="case-card"><div class="skeleton skeleton-line medium"></div><div class="skeleton skeleton-line"></div><div class="skeleton skeleton-line short"></div></div>
<div class="case-card"><div class="skeleton skeleton-line medium"></div><div class="skeleton skeleton-line"></div><div class="skeleton skeleton-line short"></div></div>
`;
try {
const res = await fetch(API + '/cases?detail=true');
const cases = await res.json();
if (!cases.length) {
grid.innerHTML = `
<div class="empty" style="grid-column:1/-1">
<div style="font-size:2.2em;color:var(--color-gold);margin-bottom:12px;font-family:var(--font-display)">❦</div>
<div style="font-size:1.1em;color:var(--color-navy);margin-bottom:6px;font-family:var(--font-display)">עדיין אין תיקי ערר</div>
<div>לחץ <strong>"+ תיק חדש"</strong> כדי להתחיל</div>
</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" style="grid-column:1/-1">שגיאה בטעינת תיקים<br><small style="color:var(--color-ink-light)">' + esc(e.message) + '</small></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: 'היטל השבחה' };
const PRACTICE_AREA_LABELS = { appeals_committee: 'ועדת ערר', national_insurance: 'ביטוח לאומי', labor_law: 'דיני עבודה' };
const SUBTYPE_LABELS = { building_permit: 'רישוי ובנייה', betterment_levy: 'היטל השבחה', compensation_197: "פיצויים (ס' 197)", unknown: 'לא ידוע' };
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(PRACTICE_AREA_LABELS[data.practice_area] || data.practice_area)}</td></tr>
<tr><td style="padding:6px;color:#888">סוג ערר</td><td style="padding:6px">${esc(SUBTYPE_LABELS[data.appeal_subtype] || data.appeal_subtype)}</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>
`;
}
// 1xxx → building_permit, 8xxx → betterment_levy, 9xxx → compensation_197
function deriveSubtypeFromCaseNumber(caseNumber) {
const m = (caseNumber || '').trim().match(/^(\d)/);
if (!m) return 'unknown';
return ({1: 'building_permit', 8: 'betterment_levy', 9: 'compensation_197'})[m[1]] || 'unknown';
}
// Auto-fill subtype + committee_type when the user types/edits the case number.
// User can override the dropdown manually afterwards.
function wireSubtypeAutofill() {
const cnInput = document.getElementById('wiz-case-number');
const subtypeSel = document.getElementById('wiz-appeal-subtype');
const committeeHidden = document.getElementById('wiz-committee-type');
if (!cnInput || !subtypeSel) return;
const SUBTYPE_TO_COMMITTEE = {
building_permit: 'רישוי',
betterment_levy: 'היטל השבחה',
compensation_197: 'פיצויים',
unknown: 'רישוי',
};
let userOverrode = false;
subtypeSel.addEventListener('change', () => { userOverrode = true; });
cnInput.addEventListener('input', () => {
if (userOverrode) return;
const derived = deriveSubtypeFromCaseNumber(cnInput.value);
subtypeSel.value = derived;
if (committeeHidden) committeeHidden.value = SUBTYPE_TO_COMMITTEE[derived];
});
}
document.addEventListener('DOMContentLoaded', wireSubtypeAutofill);
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,
practice_area: document.getElementById('wiz-practice-area').value,
appeal_subtype: document.getElementById('wiz-appeal-subtype').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: 'מסמכים מוכנים',
outcome_set: 'תוצאה נקבעה', direction_approved: 'כיוון אושר',
drafting: 'בכתיבה', drafted: 'טיוטה', qa_review: 'בבדיקת QA', reviewed: 'נבדק', final: 'סופי',
};
const PRACTICE_AREA_LABELS = { appeals_committee: 'ועדת ערר', national_insurance: 'ביטוח לאומי', labor_law: 'דיני עבודה' };
const SUBTYPE_LABELS = { building_permit: 'רישוי ובנייה', betterment_levy: 'היטל השבחה', compensation_197: "פיצויים (ס' 197)", unknown: 'לא ידוע' };
const meta = [];
meta.push(`<span class="badge ${data.status}">${STATUS_LABELS[data.status] || data.status}</span>`);
if (data.practice_area || data.appeal_subtype) {
const parts = [];
if (data.practice_area) parts.push(PRACTICE_AREA_LABELS[data.practice_area] || data.practice_area);
if (data.appeal_subtype) parts.push(SUBTYPE_LABELS[data.appeal_subtype] || data.appeal_subtype);
meta.push(`<span class="badge" style="background:#e8f0fe;color:#1a56db" title="תחום משפטי / סוג ערר">${parts.join(' · ')}</span>`);
} else 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);
loadResearchAnalysis(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>';
}
}
// ── Research Analysis Cards (analysis-and-research.md) ─────
let _currentResearchCase = null;
async function loadResearchAnalysis(caseNumber) {
_currentResearchCase = caseNumber;
const card = document.getElementById('caseResearchCard');
const body = document.getElementById('caseResearchBody');
const meta = document.getElementById('caseResearchMeta');
try {
const res = await fetch(API + '/cases/' + encodeURIComponent(caseNumber) + '/research/analysis');
if (res.status === 404) {
card.style.display = 'none';
return;
}
if (!res.ok) throw new Error(await res.text());
const data = await res.json();
card.style.display = '';
renderResearchCards(data, body, meta);
} catch (e) {
card.style.display = '';
body.innerHTML = `<div class="empty">שגיאה בטעינת הניתוח: ${esc(e.message)}</div>`;
}
}
function renderResearchCards(data, container, metaEl) {
const h = data.header || {};
if (metaEl) {
const date = h.date || '';
const modDate = h.modified_at ? new Date(h.modified_at).toLocaleDateString('he-IL') : '';
metaEl.textContent = (date ? `תאריך ניתוח: ${date}` : '') +
(modDate ? ` · עודכן: ${modDate}` : '');
}
const parts = [];
// Intro/prose sections
const proseSection = (title, content) => {
if (!content || !content.trim()) return '';
return `
<div class="research-section">
<div class="research-section-title">${esc(title)}</div>
<div class="research-prose">${esc(content.trim())}</div>
</div>
`;
};
parts.push(proseSection('צד מיוצג', data.represented_party));
parts.push(proseSection('רקע דיוני', data.procedural_background));
parts.push(proseSection('עובדות מוסכמות', data.agreed_facts));
parts.push(proseSection('עובדות שנויות במחלוקת', data.disputed_facts));
// Threshold claims
if (data.threshold_claims && data.threshold_claims.length) {
parts.push(`
<div class="research-section">
<div class="research-section-title">
<span>טענות סף</span>
<span class="count-pill">${data.threshold_claims.length}</span>
</div>
${data.threshold_claims.map((tc, i) => renderSubsection(tc, i === 0)).join('')}
</div>
`);
}
// Issues
if (data.issues && data.issues.length) {
parts.push(`
<div class="research-section">
<div class="research-section-title">
<span>סוגיות להכרעה</span>
<span class="count-pill">${data.issues.length}</span>
</div>
${data.issues.map((iss, i) => renderSubsection(iss, false)).join('')}
</div>
`);
}
// Conclusions
parts.push(proseSection('מסקנות', data.conclusions));
// Other sections (if any unexpected)
if (data.other_sections && data.other_sections.length) {
data.other_sections.forEach(s => {
parts.push(proseSection(s.title, s.body));
});
}
container.innerHTML = parts.join('') || '<div class="empty">הקובץ ריק</div>';
}
function renderSubsection(item, openByDefault) {
const isFilled = item.chair_position && item.chair_position.trim().length > 0;
const statusClass = isFilled ? 'filled' : '';
const statusText = isFilled ? '✓ עמדה נקבעה' : 'ממתין לעמדה';
const fieldsHtml = (item.fields || []).map(f => `
<div class="research-field">
<div class="research-field-label">${esc(f.label)}</div>
<div class="research-field-content">${esc(f.content)}</div>
</div>
`).join('');
const textareaContent = esc(item.chair_position || '');
return `
<details class="research-item" ${openByDefault ? 'open' : ''} data-section-id="${esc(item.id)}">
<summary>
<span class="research-item-number">${item.number}</span>
<span class="research-item-title">${esc(item.title)}</span>
<span class="research-item-status ${statusClass}" data-status>${esc(statusText)}</span>
</summary>
<div class="research-item-body">
${fieldsHtml}
<div class="chair-editor">
<div class="chair-editor-header">
<span class="chair-editor-label">עמדת ועדת הערר</span>
<span class="save-indicator" data-save-indicator></span>
</div>
<textarea
data-chair-editor
data-section-id="${esc(item.id)}"
placeholder="כתבי כאן את עמדתך לגבי סוגיה זו. הטקסט נשמר אוטומטית כשעוזבת את השדה."
onblur="saveChairPosition(this)">${textareaContent}</textarea>
</div>
</div>
</details>
`;
}
async function saveChairPosition(textarea) {
const sectionId = textarea.dataset.sectionId;
const caseNumber = _currentResearchCase;
if (!sectionId || !caseNumber) return;
const item = textarea.closest('.research-item');
const indicator = item.querySelector('[data-save-indicator]');
const statusEl = item.querySelector('[data-status]');
const newText = textarea.value.trim();
// Track the last-saved value on the element to avoid redundant saves
if (textarea._lastSaved === undefined) textarea._lastSaved = textarea.defaultValue;
if (newText === textarea._lastSaved) return;
indicator.className = 'save-indicator saving';
indicator.textContent = '⏳ שומר...';
try {
const res = await fetch(
API + '/cases/' + encodeURIComponent(caseNumber) + '/research/analysis/chair-position',
{
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ section_id: sectionId, position: newText }),
}
);
if (!res.ok) throw new Error(await res.text());
const data = await res.json();
textarea._lastSaved = newText;
indicator.className = 'save-indicator saved';
const timeStr = new Date().toLocaleTimeString('he-IL', { hour: '2-digit', minute: '2-digit' });
indicator.textContent = `✓ נשמר ${timeStr}`;
// Update status pill
if (statusEl) {
if (newText) {
statusEl.className = 'research-item-status filled';
statusEl.textContent = '✓ עמדה נקבעה';
} else {
statusEl.className = 'research-item-status';
statusEl.textContent = 'ממתין לעמדה';
}
}
// Fade the saved indicator after 3 seconds
setTimeout(() => {
if (indicator.className.includes('saved')) {
indicator.style.opacity = '0.5';
}
}, 3000);
indicator.style.opacity = '1';
} catch (e) {
indicator.className = 'save-indicator error';
indicator.textContent = '✗ שגיאה — ' + (e.message || '').substring(0, 60);
}
}
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 = '#a97d3a'; });
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) {}
}
// ── Compare Decisions ────────────────────────────────
async function initComparePage() {
const selA = document.getElementById('compareSelectA');
const selB = document.getElementById('compareSelectB');
if (selA.options.length > 1) return; // already loaded
try {
const res = await fetch(API + '/training/corpus');
const rows = await res.json();
const options = rows.map(r => {
const label = `${r.decision_number || '(ללא מספר)'} · ${r.decision_date || '—'}`;
return `<option value="${esc(r.id)}">${esc(label)}</option>`;
}).join('');
selA.innerHTML = '<option value="">בחרי החלטה...</option>' + options;
selB.innerHTML = '<option value="">בחרי החלטה...</option>' + options;
} catch (e) {
toast('שגיאה בטעינת רשימת החלטות', 'error');
}
}
async function runCompare() {
const a = document.getElementById('compareSelectA').value;
const b = document.getElementById('compareSelectB').value;
if (!a || !b) { toast('בחרי שתי החלטות', 'error'); return; }
if (a === b) { toast('יש לבחור שתי החלטות שונות', 'error'); return; }
const result = document.getElementById('compareResult');
result.innerHTML = '<div class="card"><div class="card-body"><div class="empty">משווה...</div></div></div>';
try {
const res = await fetch(API + '/training/compare?a=' + encodeURIComponent(a) + '&b=' + encodeURIComponent(b));
if (!res.ok) throw new Error(await res.text());
const data = await res.json();
renderCompareResult(data);
} catch (e) {
result.innerHTML = `<div class="card"><div class="card-body"><div class="empty">שגיאה: ${esc(e.message)}</div></div></div>`;
}
}
function renderCompareResult(data) {
const container = document.getElementById('compareResult');
const a = data.a, b = data.b;
const headerCard = (row, label) => {
const secTotal = row.sections.reduce((sum, s) => sum + s.chars, 0) || 1;
const sectionsHtml = row.sections.map(s => {
const pct = Math.round((s.chars / secTotal) * 100);
const labelHeb = ({
intro: 'פתיחה', facts: 'רקע', appellant_claims: 'טענות עורר',
respondent_claims: 'טענות משיב', legal_analysis: 'דיון משפטי',
ruling: 'הכרעה', conclusion: 'סוף דבר',
})[s.type] || s.type;
return `<div class="compare-col-stat"><span>${esc(labelHeb)}</span><strong>${s.chars.toLocaleString('he-IL')} (${pct}%)</strong></div>`;
}).join('');
return `
<div class="compare-column">
<div class="compare-col-title">${esc(row.decision_number || '(ללא מספר)')}</div>
<div class="compare-col-meta">
<span>${esc(row.decision_date || '—')}</span>
<span>${row.chars.toLocaleString('he-IL')} תווים</span>
</div>
<div class="compare-col-stat"><span>דפוסים שזוהו</span><strong>${row.patterns_count}</strong></div>
${sectionsHtml}
</div>
`;
};
const diffColumn = (title, items, colorClass) => {
if (!items.length) {
return `
<div class="compare-diff-col">
<div class="compare-diff-title">${esc(title)} <span class="compare-diff-count">0</span></div>
<div class="empty" style="padding:16px">—</div>
</div>
`;
}
const itemsHtml = items.map(p => {
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) + '...';
return `<div class="compare-diff-item">${esc(display)}</div>`;
}).join('');
return `
<div class="compare-diff-col">
<div class="compare-diff-title">${esc(title)} <span class="compare-diff-count">${items.length}</span></div>
<div class="compare-diff-list">${itemsHtml}</div>
</div>
`;
};
container.innerHTML = `
<div class="compare-result-grid">
${headerCard(a)}
${headerCard(b)}
</div>
<div class="compare-diff-row">
${diffColumn('רק ב-' + (a.decision_number || 'א'), data.only_a)}
${diffColumn('משותפים', data.shared)}
${diffColumn('רק ב-' + (b.decision_number || 'ב'), data.only_b)}
</div>
`;
}
// ── Compose (writing with suggestions) ───────────────
let _composePatterns = [];
let _composeFilter = 'all';
async function initComposePage() {
// Restore draft if any
try {
const saved = localStorage.getItem('composeDraft');
const title = localStorage.getItem('composeTitle');
if (saved) document.getElementById('composeTextarea').value = saved;
if (title) document.getElementById('composeTitle').value = title;
} catch (e) {}
updateComposeStats();
document.getElementById('composeTextarea').addEventListener('input', onComposeInput);
if (_composePatterns.length) {
renderComposePatterns();
return;
}
try {
const res = await fetch(API + '/training/patterns');
const data = await res.json();
_composePatterns = [];
Object.entries(data.by_type || {}).forEach(([type, items]) => {
items.forEach(p => _composePatterns.push({ type, ...p }));
});
// Sort by frequency desc
_composePatterns.sort((a, b) => (b.frequency || 0) - (a.frequency || 0));
renderComposeFilters();
renderComposePatterns();
} catch (e) {
document.getElementById('composePatterns').innerHTML = '<div class="empty">שגיאה בטעינה</div>';
}
}
function renderComposeFilters() {
const types = [...new Set(_composePatterns.map(p => p.type))];
const typeLabels = {
opening_formula: 'פתיחה',
closing_formula: 'סיום',
transition: 'מעברים',
characteristic_phrase: 'ביטויים',
argument_flow: 'טיעון',
analysis_structure: 'מבנה',
evidence_handling: 'ראיות',
citation_style: 'ציטוט',
};
const all = [{ id: 'all', label: 'הכל' }]
.concat(types.map(t => ({ id: t, label: typeLabels[t] || t })));
document.getElementById('composeFilters').innerHTML = all.map(f => `
<div class="compose-filter ${f.id === _composeFilter ? 'active' : ''}"
onclick="setComposeFilter('${f.id}')">${esc(f.label)}</div>
`).join('');
}
function setComposeFilter(f) {
_composeFilter = f;
renderComposeFilters();
renderComposePatterns();
}
function renderComposePatterns() {
const filtered = _composeFilter === 'all'
? _composePatterns
: _composePatterns.filter(p => p.type === _composeFilter);
const typeLabels = {
opening_formula: 'פתיחה', closing_formula: 'סיום', transition: 'מעבר',
characteristic_phrase: 'ביטוי', argument_flow: 'טיעון',
analysis_structure: 'מבנה', evidence_handling: 'ראיות', citation_style: 'ציטוט',
};
document.getElementById('composePatterns').innerHTML = filtered.map((p, idx) => {
let display = p.pattern_text || p.text || '';
let clean = display.replace(/\[[^\]]*\]/g, '').replace(/\s+/g, ' ').trim();
clean = clean.split(' / ')[0].split(' או ')[0].trim();
if (clean.length > 90) clean = clean.substring(0, 87) + '...';
const origIdx = _composePatterns.indexOf(p);
return `
<div class="compose-pattern" onclick="insertComposePattern(${origIdx})">
<div class="compose-pattern-text">${esc(clean)}</div>
<div class="compose-pattern-meta">${esc(typeLabels[p.type] || p.type)} · ${p.frequency || 0}/24</div>
</div>
`;
}).join('') || '<div class="empty" style="padding:20px">אין דפוסים בקטגוריה זו</div>';
}
function insertComposePattern(idx) {
const p = _composePatterns[idx];
if (!p) return;
let text = (p.pattern_text || p.text || '').replace(/\[[^\]]*\]/g, '___').replace(/\s+/g, ' ').trim();
text = text.split(' / ')[0].split(' או ')[0].trim();
const ta = document.getElementById('composeTextarea');
const start = ta.selectionStart;
const end = ta.selectionEnd;
const before = ta.value.substring(0, start);
const after = ta.value.substring(end);
// Add space before if needed
const prefix = (before && !/\s$/.test(before)) ? ' ' : '';
const suffix = (after && !/^\s/.test(after)) ? ' ' : '';
const insert = prefix + text + suffix;
ta.value = before + insert + after;
const newPos = start + insert.length;
ta.selectionStart = ta.selectionEnd = newPos;
ta.focus();
onComposeInput();
}
function onComposeInput() {
updateComposeStats();
// Auto-save draft every second (debounced)
clearTimeout(onComposeInput._t);
onComposeInput._t = setTimeout(() => {
try {
localStorage.setItem('composeDraft', document.getElementById('composeTextarea').value);
localStorage.setItem('composeTitle', document.getElementById('composeTitle').value);
} catch (e) {}
}, 1000);
}
function updateComposeStats() {
const ta = document.getElementById('composeTextarea');
if (!ta) return;
const text = ta.value || '';
const chars = text.length;
const words = (text.match(/\S+/g) || []).length;
document.getElementById('composeCharCount').textContent = chars.toLocaleString('he-IL');
document.getElementById('composeWordCount').textContent = words.toLocaleString('he-IL');
// Guess section from recent text
const recent = text.slice(-400);
let section = '—';
if (/בפנינו|לפנינו|עניינו|עסקינן/.test(recent)) section = 'פתיחה';
else if (/רקע|העובדות|תכנית/.test(recent)) section = 'רקע';
else if (/טענות העורר|טוען העורר|המערער/.test(recent)) section = 'טענות עורר';
else if (/טענות המשיב|טוענת המשיבה|טוענים המשיבים/.test(recent)) section = 'טענות משיב';
else if (/דיון|הכרעה|לאחר שבחנו|נחדד|נזכיר/.test(recent)) section = 'דיון והכרעה';
else if (/סוף דבר|לאור כל האמור|הערר נדחה|הערר מתקבל|ניתנה פה/.test(recent)) section = 'סוף דבר';
document.getElementById('composeSectionGuess').textContent = section;
}
function copyComposeText() {
const ta = document.getElementById('composeTextarea');
const title = document.getElementById('composeTitle').value;
const text = (title ? title + '\n\n' : '') + ta.value;
navigator.clipboard.writeText(text).then(() => toast('הועתק ללוח', 'success'));
}
function saveComposeDraft() {
try {
localStorage.setItem('composeDraft', document.getElementById('composeTextarea').value);
localStorage.setItem('composeTitle', document.getElementById('composeTitle').value);
toast('הטיוטה נשמרה', 'success');
} catch (e) {
toast('שגיאה בשמירה', 'error');
}
}
// ── Diagnostics Page ─────────────────────────────────
async function loadDiagnostics() {
const container = document.getElementById('diagnosticsContent');
if (!container) return;
container.innerHTML = '<div class="empty">טוען...</div>';
try {
const res = await fetch(API + '/system/diagnostics');
const data = await res.json();
const dbBadge = data.db_ok
? '<span class="diag-pill ok">✓ מחובר</span>'
: '<span class="diag-pill error">✕ תקלה</span>';
const tablesHtml = Object.entries(data.tables).map(([name, count]) => {
const label = {
cases: 'תיקים',
documents: 'מסמכים',
document_chunks: 'קטעים',
style_corpus: 'קורפוס סגנון',
style_patterns: 'דפוסים',
}[name] || name;
const val = count === null ? '⚠️' : count.toLocaleString('he-IL');
return `<div class="diag-stat"><div class="diag-stat-label">${esc(label)}</div><div class="diag-stat-value">${val}</div></div>`;
}).join('');
const DOC_STATUS_LABELS = {
pending: 'ממתין', processing: 'בעיבוד', extracting: 'מחלץ טקסט',
chunking: 'מפצל', embedding: 'יוצר embeddings', completed: 'הושלם',
failed: 'נכשל', error: 'שגיאה', queued: 'בתור',
};
const failedHtml = (data.failed_documents || []).map(d => `
<div class="diag-row diag-row-error">
<div class="diag-row-title">${esc(d.title || '(ללא שם)')}</div>
<div class="diag-row-meta">
${d.case_number ? `תיק ${esc(d.case_number)} · ` : ''}
סטטוס: <strong>${esc(DOC_STATUS_LABELS[d.status] || d.status)}</strong>
</div>
</div>
`).join('') || '<div class="empty" style="padding:16px">אין כישלונות</div>';
const stuckHtml = (data.stuck_documents || []).map(d => `
<div class="diag-row diag-row-warn">
<div class="diag-row-title">${esc(d.title || '(ללא שם)')}</div>
<div class="diag-row-meta">
${d.case_number ? `תיק ${esc(d.case_number)} · ` : ''}
${esc(DOC_STATUS_LABELS[d.status] || d.status)} מאז ${formatRelativeTime(d.created_at)}
</div>
</div>
`).join('') || '<div class="empty" style="padding:16px">אין מסמכים תקועים</div>';
const activeHtml = (data.active_tasks || []).map(t => `
<div class="diag-row">
<div class="diag-row-title">${esc(t.filename || t.task_id)}</div>
<div class="diag-row-meta">${esc(STEP_LABELS[t.step] || STEP_LABELS[t.status] || t.status)}</div>
</div>
`).join('') || '<div class="empty" style="padding:16px">אין משימות פעילות</div>';
container.innerHTML = `
<div class="card">
<div class="card-header">בריאות בסיס נתונים ${dbBadge}</div>
<div class="card-body">
<div class="diag-stats-grid">${tablesHtml}</div>
</div>
</div>
<div class="card">
<div class="card-header">משימות פעילות ברקע</div>
<div class="card-body">${activeHtml}</div>
</div>
<div class="card">
<div class="card-header">מסמכים תקועים (יותר מ-10 דקות)</div>
<div class="card-body">${stuckHtml}</div>
</div>
<div class="card">
<div class="card-header">מסמכים שכשלו</div>
<div class="card-body">${failedHtml}</div>
</div>
`;
} catch (e) {
container.innerHTML = `<div class="empty">שגיאה בטעינה: ${esc(e.message)}</div>`;
}
}
// ── Processing Visibility Panel (SSE push) ───────────
let _tasksEventSource = null;
function startProcessPanelPolling() {
if (_tasksEventSource) return;
try {
const es = new EventSource(API + '/system/tasks/stream');
es.addEventListener('tasks', (e) => {
try {
const data = JSON.parse(e.data);
renderProcessPanel(data.active || []);
} catch (err) {}
});
es.onerror = () => {
// Reconnect after a delay
es.close();
_tasksEventSource = null;
setTimeout(startProcessPanelPolling, 5000);
};
_tasksEventSource = es;
} catch (e) {
// Browsers without EventSource fall back to one-shot fetch
fetch(API + '/system/tasks').then(r => r.json()).then(d => renderProcessPanel(d.active || [])).catch(() => {});
}
}
const STEP_LABELS = {
queued: 'בתור',
processing: 'מעבד',
proofreading: 'הגהה',
saving: 'שומר',
corpus: 'קורפוס',
chunking: 'פיצול',
embedding: 'embeddings',
validating: 'מאמת',
copying: 'מעתיק',
registering: 'רושם',
extracting: 'חילוץ טקסט',
completed: 'הושלם',
failed: 'נכשל',
pending: 'ממתין',
error: 'שגיאה',
};
function renderProcessPanel(items) {
const panel = document.getElementById('processPanel');
const body = document.getElementById('processPanelBody');
const count = document.getElementById('processPanelCount');
if (!panel) return;
if (!items.length) {
panel.style.display = 'none';
return;
}
panel.style.display = '';
count.textContent = items.length;
body.innerHTML = items.map(t => {
const label = STEP_LABELS[t.step] || STEP_LABELS[t.status] || t.status;
const name = t.filename || t.task_id;
return `
<div class="process-item">
<div class="process-item-name">${esc(name)}</div>
<div class="process-item-status">
<span class="mini-spinner"></span>
<span>${esc(label)}</span>
</div>
</div>
`;
}).join('');
}
function toggleProcessPanel() {
const panel = document.getElementById('processPanel');
panel.classList.toggle('collapsed');
}
async function loadKPIs() {
// Home dashboard KPI tiles + activity feed
const casesEl = document.getElementById('kpiCases');
if (!casesEl) return;
try {
const [statusRes, corpusRes, patternsRes, activityRes] = await Promise.all([
fetch(API + '/processing-status').then(r => r.json()).catch(() => ({})),
fetch(API + '/training/corpus').then(r => r.json()).catch(() => []),
fetch(API + '/training/patterns').then(r => r.json()).catch(() => ({total: 0})),
fetch(API + '/system/recent-activity').then(r => r.json()).catch(() => ({events: []})),
]);
document.getElementById('kpiCases').textContent = statusRes.cases ?? '0';
document.getElementById('kpiCorpus').textContent = corpusRes.length || '0';
document.getElementById('kpiPatterns').textContent = patternsRes.total || '0';
const procCount = (statusRes.processing_documents ?? statusRes.processing ?? 0);
document.getElementById('kpiProcessing').textContent = procCount || '0';
renderActivityFeed(activityRes.events || []);
} catch (e) {
console.error('KPI load failed', e);
}
}
const ACTIVITY_ICONS = {
case_created: '📁',
corpus_added: '📚',
analysis_run: '✨',
};
function formatRelativeTime(iso) {
if (!iso) return '';
const then = new Date(iso);
const diffMs = Date.now() - then.getTime();
const min = Math.floor(diffMs / 60000);
if (min < 1) return 'עכשיו';
if (min < 60) return `לפני ${min} דקות`;
const hr = Math.floor(min / 60);
if (hr < 24) return `לפני ${hr} שעות`;
const days = Math.floor(hr / 24);
if (days < 30) return `לפני ${days} ימים`;
return then.toLocaleDateString('he-IL');
}
function renderActivityFeed(events) {
const feed = document.getElementById('activityFeed');
if (!feed) return;
if (!events.length) {
feed.innerHTML = '<div class="empty" style="font-size:0.82em;padding:20px">עדיין אין פעילות</div>';
return;
}
feed.innerHTML = events.map(e => {
const icon = ACTIVITY_ICONS[e.type] || '•';
const target = e.target || '#/';
return `
<div class="activity-item" onclick="location.hash='${esc(target.replace(/^\/?#/, ''))}'">
<div class="activity-label"><span class="activity-icon">${icon}</span>${esc(e.label)}</div>
${e.detail ? `<div class="activity-detail">${esc(e.detail)}</div>` : ''}
<div class="activity-time">${formatRelativeTime(e.timestamp)}</div>
</div>
`;
}).join('');
}
// ── 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:#a97d3a;border-color:#a97d3a" 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 = '#a97d3a';
toast('Restart failed', 'error');
} else {
status.textContent = 'Restarted successfully';
status.style.color = 'var(--color-success)';
toast('Paperclip restarted', 'success');
}
} catch (e) {
status.textContent = 'Network error';
status.style.color = '#a97d3a';
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',
};
// Hex literals (not CSS vars) — used both in CSS strings (conic-gradient)
// and in inline dot backgrounds. Tuned to the editorial palette.
const DONUT_COLORS = ['#a97d3a', '#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 deleteCorpusItem(id, decisionNumber) {
if (!confirm(`להסיר את החלטה ${decisionNumber || '(ללא מספר)'} מהקורפוס?\n\nפעולה זו בלתי הפיכה — הדפוסים הקשורים להחלטה עצמה יישארו, אבל עקומת הלמידה תחושב מחדש בריצה הבאה של "נתח קורפוס".`)) {
return;
}
try {
const res = await fetch(API + '/training/corpus/' + encodeURIComponent(id), { method: 'DELETE' });
if (!res.ok) {
const err = await res.text();
throw new Error(err);
}
toast('ההחלטה הוסרה מהקורפוס', 'success');
loadCorpusList();
loadKPIs();
} catch (e) {
toast('שגיאה במחיקה: ' + e.message, 'error');
}
}
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><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>
<td><button class="btn-icon btn-icon-danger" title="הסר מהקורפוס" onclick="deleteCorpusItem('${esc(r.id)}', '${esc(r.decision_number || '')}')">🗑</button></td>
</tr>
`).join('')}
</tbody>
</table>
`;
} catch (e) {
container.innerHTML = `<div class="empty">שגיאה בטעינה: ${esc(e.message)}</div>`;
}
}
// ── Settings Page ─────────────────────────────────────────────────
let _settingsCompanies = [];
async function loadSettingsPage() {
await Promise.all([loadPaperclipCompanies(), loadTagMappings()]);
}
async function loadPaperclipCompanies() {
const sel = document.getElementById('settingsCompanySelect');
try {
const res = await fetch(`${API}/settings/paperclip-companies`);
if (!res.ok) throw new Error(await res.text());
_settingsCompanies = await res.json();
// Build options safely via DOM
sel.textContent = '';
const defaultOpt = document.createElement('option');
defaultOpt.value = '';
defaultOpt.textContent = '— בחר חברה —';
sel.appendChild(defaultOpt);
for (const c of _settingsCompanies) {
const opt = document.createElement('option');
opt.value = c.id;
opt.dataset.name = c.name;
opt.textContent = c.name + (c.identifier ? ` (${c.identifier})` : '');
sel.appendChild(opt);
}
} catch (e) {
sel.textContent = '';
const opt = document.createElement('option');
opt.value = '';
opt.textContent = 'שגיאה: ' + e.message;
sel.appendChild(opt);
}
}
async function loadTagMappings() {
const container = document.getElementById('tagMappingsTable');
try {
const res = await fetch(`${API}/settings/tag-mappings`);
if (!res.ok) throw new Error(await res.text());
const mappings = await res.json();
if (!mappings.length) {
container.textContent = '';
const empty = document.createElement('div');
empty.className = 'empty';
empty.textContent = 'אין שיוכים מוגדרים עדיין';
container.appendChild(empty);
return;
}
// Group by company
const byCompany = {};
for (const m of mappings) {
const key = m.company_id;
if (!byCompany[key]) byCompany[key] = { company_name: m.company_name || m.company_id, tags: [] };
byCompany[key].tags.push(m);
}
const table = document.createElement('table');
table.style.cssText = 'width:100%;border-collapse:collapse;font-size:0.9em';
const thead = table.createTHead();
const headerRow = thead.insertRow();
headerRow.style.borderBottom = '2px solid var(--border,#ddd)';
for (const label of ['חברה', 'תג', 'תיאור', '']) {
const th = document.createElement('th');
th.style.cssText = 'text-align:right;padding:8px';
if (label === '') th.style.width = '60px';
th.textContent = label;
headerRow.appendChild(th);
}
const tbody = table.createTBody();
for (const [companyId, group] of Object.entries(byCompany)) {
for (let i = 0; i < group.tags.length; i++) {
const m = group.tags[i];
const tr = tbody.insertRow();
tr.style.borderBottom = '1px solid var(--border,#eee)';
if (i === 0) {
const tdCompany = tr.insertCell();
tdCompany.style.cssText = 'padding:8px;font-weight:600;vertical-align:top';
tdCompany.rowSpan = group.tags.length;
tdCompany.textContent = group.company_name;
}
const tdTag = tr.insertCell();
tdTag.style.padding = '8px';
const code = document.createElement('code');
code.style.cssText = 'background:var(--bg-secondary,#f0f0f0);padding:2px 6px;border-radius:4px';
code.textContent = m.tag;
tdTag.appendChild(code);
const tdLabel = tr.insertCell();
tdLabel.style.padding = '8px';
tdLabel.textContent = m.tag_label || '—';
const tdAction = tr.insertCell();
tdAction.style.padding = '8px';
const btn = document.createElement('button');
btn.className = 'btn-icon btn-icon-danger';
btn.title = 'הסר שיוך';
btn.textContent = '✕';
btn.addEventListener('click', () => deleteTagMapping(m.id));
tdAction.appendChild(btn);
}
}
container.textContent = '';
container.appendChild(table);
} catch (e) {
container.textContent = '';
const empty = document.createElement('div');
empty.className = 'empty';
empty.textContent = 'שגיאה: ' + e.message;
container.appendChild(empty);
}
}
async function addTagMapping() {
const tag = document.getElementById('settingsNewTag').value.trim();
const tagLabel = document.getElementById('settingsNewTagLabel').value.trim();
const sel = document.getElementById('settingsCompanySelect');
const companyId = sel.value;
const companyName = sel.selectedOptions[0]?.dataset?.name || '';
if (!tag) { showToast('יש להזין תג', 'error'); return; }
if (!companyId) { showToast('יש לבחור חברה', 'error'); return; }
try {
const res = await fetch(`${API}/settings/tag-mappings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tag, tag_label: tagLabel, company_id: companyId, company_name: companyName }),
});
if (!res.ok) throw new Error(await res.text());
showToast('שיוך נוסף בהצלחה');
document.getElementById('settingsNewTag').value = '';
document.getElementById('settingsNewTagLabel').value = '';
sel.value = '';
await loadTagMappings();
} catch (e) {
showToast('שגיאה: ' + e.message, 'error');
}
}
async function deleteTagMapping(id) {
if (!confirm('להסיר שיוך זה?')) return;
try {
const res = await fetch(`${API}/settings/tag-mappings/${id}`, { method: 'DELETE' });
if (!res.ok) throw new Error(await res.text());
showToast('שיוך הוסר');
await loadTagMappings();
} catch (e) {
showToast('שגיאה: ' + e.message, 'error');
}
}
</script>
</body>
</html>