The home page already has a prominent '+ תיק חדש' button in the hero. Cases are always opened by clicking a case card on the home list, so the top-nav link is noise. Keyboard shortcut 'n' still opens the wizard directly for power users. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4923 lines
174 KiB
HTML
4923 lines
174 KiB
HTML
<!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>
|
||
<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">
|
||
<label>סוג ערר</label>
|
||
<select id="wiz-committee-type">
|
||
<option value="רישוי">רישוי ובניה</option>
|
||
<option value="היטל השבחה">היטל השבחה</option>
|
||
<option value="פיצויים">פיצויים (ס' 197)</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>כתובת נכס</label>
|
||
<input type="text" id="wiz-address" placeholder="רח' אבינדב 23, קריית יערים">
|
||
</div>
|
||
<div class="form-group" style="max-width:180px">
|
||
<label>מספר היתר/תכנית</label>
|
||
<input type="text" id="wiz-permit" placeholder="152-1257682" dir="ltr">
|
||
</div>
|
||
</div>
|
||
</div></div>
|
||
<div class="wizard-nav">
|
||
<span></span>
|
||
<button class="btn btn-primary" onclick="wizNext()">הבא ←</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)">×</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)">×</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()">→ הקודם</button>
|
||
<button class="btn btn-primary" onclick="wizNext()">הבא ←</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()">→ הקודם</button>
|
||
<button class="btn btn-primary" onclick="wizNext()">הבא ←</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">◯</span> יצירת תיק במסד הנתונים</li>
|
||
<li class="pending" id="cs-gitea"><span class="step-icon">◯</span> יצירת Repository ב-Gitea</li>
|
||
<li class="pending" id="cs-paperclip"><span class="step-icon">◯</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()">→ הקודם</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">🔌</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">📄</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">📚</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>
|
||
</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();
|
||
}
|
||
|
||
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 = '◯';
|
||
});
|
||
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)">×</button>`;
|
||
list.appendChild(div);
|
||
div.querySelector('input').focus();
|
||
}
|
||
|
||
function removeItem(btn) {
|
||
const list = btn.closest('.dynamic-list');
|
||
btn.closest('.item').remove();
|
||
if (!list.children.length) addItem(list.id);
|
||
}
|
||
|
||
function getListValues(listId) {
|
||
return Array.from(document.querySelectorAll(`#${listId} .item input`))
|
||
.map(i => i.value.trim()).filter(Boolean);
|
||
}
|
||
|
||
function buildSummary() {
|
||
const data = getWizardData();
|
||
const OUTCOME_LABELS = { rejection: 'דחייה', partial_acceptance: 'קבלה חלקית', full_acceptance: 'קבלה מלאה', betterment_levy: 'היטל השבחה' };
|
||
document.getElementById('wizSummary').innerHTML = `
|
||
<table style="width:100%;font-size:0.88em;border-collapse:collapse">
|
||
<tr><td style="padding:6px;color:#888;width:120px">מספר תיק</td><td style="padding:6px;font-weight:600">${esc(data.case_number)}</td></tr>
|
||
<tr><td style="padding:6px;color:#888">כותרת</td><td style="padding:6px">${esc(data.title)}</td></tr>
|
||
<tr><td style="padding:6px;color:#888">סוג</td><td style="padding:6px">${esc(data.committee_type)}</td></tr>
|
||
<tr><td style="padding:6px;color:#888">כתובת</td><td style="padding:6px">${esc(data.property_address || '—')}</td></tr>
|
||
<tr><td style="padding:6px;color:#888">עוררים</td><td style="padding:6px">${data.appellants.length ? data.appellants.map(esc).join(', ') : '—'}</td></tr>
|
||
<tr><td style="padding:6px;color:#888">משיבים</td><td style="padding:6px">${data.respondents.length ? data.respondents.map(esc).join(', ') : '—'}</td></tr>
|
||
<tr><td style="padding:6px;color:#888">תוצאה צפויה</td><td style="padding:6px">${OUTCOME_LABELS[data.expected_outcome] || '—'}</td></tr>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
function getWizardData() {
|
||
return {
|
||
case_number: document.getElementById('wiz-case-number').value.trim(),
|
||
title: document.getElementById('wiz-title').value.trim(),
|
||
committee_type: document.getElementById('wiz-committee-type').value,
|
||
property_address: document.getElementById('wiz-address').value.trim(),
|
||
permit_number: document.getElementById('wiz-permit').value.trim(),
|
||
appellants: getListValues('appellantsList'),
|
||
respondents: getListValues('respondentsList'),
|
||
hearing_date: document.getElementById('wiz-hearing-date').value,
|
||
expected_outcome: document.getElementById('wiz-outcome').value,
|
||
notes: document.getElementById('wiz-notes').value.trim(),
|
||
};
|
||
}
|
||
|
||
async function createCase() {
|
||
const data = getWizardData();
|
||
const btn = document.getElementById('createCaseBtn');
|
||
btn.disabled = true;
|
||
btn.textContent = 'יוצר...';
|
||
|
||
const setStep = (id, state, icon) => {
|
||
const li = document.getElementById(id);
|
||
li.className = state;
|
||
li.querySelector('.step-icon').innerHTML = icon;
|
||
};
|
||
|
||
try {
|
||
// Step 1: Create in DB
|
||
setStep('cs-db', 'running', '⚙');
|
||
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', '✓');
|
||
|
||
// Step 2: Gitea repo
|
||
setStep('cs-gitea', 'running', '⚙');
|
||
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', '✓');
|
||
} else {
|
||
setStep('cs-gitea', 'error', '✗');
|
||
}
|
||
} catch (e) {
|
||
setStep('cs-gitea', 'error', '✗');
|
||
}
|
||
|
||
// Step 3: Paperclip project
|
||
setStep('cs-paperclip', 'running', '⚙');
|
||
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', '✓');
|
||
} else {
|
||
setStep('cs-paperclip', 'error', '✗');
|
||
}
|
||
} catch (e) {
|
||
setStep('cs-paperclip', 'error', '✗');
|
||
}
|
||
|
||
toast('התיק נוצר בהצלחה!', 'success');
|
||
setTimeout(() => navigate('/case/' + data.case_number), 1000);
|
||
|
||
} catch (err) {
|
||
setStep('cs-db', 'error', '✗');
|
||
toast(err.message, 'error');
|
||
btn.disabled = false;
|
||
btn.textContent = 'צור תיק';
|
||
}
|
||
}
|
||
|
||
// ── Case View ────────────────────────────────────────────
|
||
const DOC_TYPE_LABELS = {
|
||
appeal: 'כתבי ערר', response: 'כתבי תשובה', protocol: 'פרוטוקולים',
|
||
plan: 'תכניות', decision: 'החלטות', court_decision: 'פסקי דין',
|
||
permit: 'היתרים', appraisal: 'שומות', exhibit: 'נספחים',
|
||
objection: 'התנגדויות', reference: 'מסמכי עזר', auto: 'לא מסווג',
|
||
};
|
||
|
||
async function loadCaseView(caseNumber) {
|
||
try {
|
||
const res = await fetch(API + '/cases/' + encodeURIComponent(caseNumber) + '/details');
|
||
if (!res.ok) throw new Error('Case not found');
|
||
const data = await res.json();
|
||
|
||
document.getElementById('caseViewTitle').textContent = `ערר ${caseNumber} — ${data.title || ''}`;
|
||
|
||
const STATUS_LABELS = {
|
||
new: 'חדש', in_progress: 'בתהליך', documents_ready: 'מסמכים מוכנים',
|
||
drafted: 'טיוטה', final: 'סופי',
|
||
};
|
||
const meta = [];
|
||
meta.push(`<span class="badge ${data.status}">${STATUS_LABELS[data.status] || data.status}</span>`);
|
||
if (data.committee_type) meta.push(data.committee_type);
|
||
if (data.property_address) meta.push(data.property_address);
|
||
if (data.appellants?.length) meta.push('עוררים: ' + data.appellants.join(', '));
|
||
document.getElementById('caseViewMeta').innerHTML = meta.map(m => `<span>${m}</span>`).join('');
|
||
|
||
// Links
|
||
const links = [];
|
||
links.push(`<a href="https://gitea.nautilus.marcusgroup.org/cases/${caseNumber}" target="_blank">Gitea</a>`);
|
||
links.push(`<a href="#" onclick="openPaperclip('${esc(caseNumber)}');return false">Paperclip</a>`);
|
||
document.getElementById('caseViewLinks').innerHTML = links.join('');
|
||
|
||
// Documents grouped by type
|
||
const docs = data.documents || [];
|
||
if (!docs.length) {
|
||
document.getElementById('caseDocsList').innerHTML = '<div class="empty">אין מסמכים עדיין — העלה קבצים למעלה</div>';
|
||
} else {
|
||
const groups = {};
|
||
docs.forEach(d => {
|
||
const t = d.doc_type || 'reference';
|
||
if (!groups[t]) groups[t] = [];
|
||
groups[t].push(d);
|
||
});
|
||
const order = ['appeal','response','protocol','plan','decision','court_decision','permit','appraisal','exhibit','objection','reference'];
|
||
let html = '';
|
||
for (const type of order) {
|
||
if (!groups[type]) continue;
|
||
html += `<div class="doc-group">
|
||
<div class="doc-group-header">
|
||
<span>${DOC_TYPE_LABELS[type] || type}</span>
|
||
<span class="count">${groups[type].length}</span>
|
||
</div>`;
|
||
for (const doc of groups[type]) {
|
||
let statusHtml;
|
||
if (doc.extraction_status === 'completed' || doc.extraction_status === 'proofread') {
|
||
statusHtml = '<span class="doc-status completed">✓</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">✗</span><button class="btn-retry" onclick="retryDoc('${esc(caseNumber)}','${esc(doc.id)}')">נסה שוב</button>`;
|
||
} else {
|
||
statusHtml = '<span class="doc-status pending">◯</span>';
|
||
}
|
||
html += `<div class="doc-item">
|
||
<span class="doc-icon">📄</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: '📚', proofread: '✅', drafts: '📝' };
|
||
|
||
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] || '📄'}</span>
|
||
<span class="lf-name">${esc(f.filename)}</span>
|
||
<span class="lf-meta">${dateStr} · ${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)}')">×</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 ? '✅' : '📄'}</span>
|
||
<div class="export-info">
|
||
<div class="export-name">${esc(f.filename)}</div>
|
||
<div class="export-meta">${dateStr} · ${formatSize(f.size)}${isFinal ? ' · <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 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(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(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: 'חילוץ טקסט',
|
||
};
|
||
|
||
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">🔌</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 · ${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>`;
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|