Add style report dashboard — Dafna's style portrait
Visual dashboard at #/style-report with 4 sections: - Hero: 24 decisions, char counts, subject donut, timeline - Anatomy: average section-length breakdown (intro → ruling → conclusion) - Signature Phrases Wall: pattern cards with real corpus frequencies, filter chips by type, click → modal with examples - Contribution: per-decision "new vs confirmed" patterns, growth curve SVG Backend: - /api/training/style-report endpoint computes all 4 sections in one call - Headlines in Hebrew are computed server-side from real data - Backfill script for style_patterns.frequency using _strip_nikud + pattern-variant extraction (templates with [placeholders], / alternatives, ellipsis all handled) Real findings from the 24-decision corpus: - דיון משפטי = 49% of avg decision (the focus) - 23/24 use "לפנינו ערר" opening formula - 21/24 use "ניתנה פה אחד" closing - After 7 decisions we already learned 85% of her style patterns Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -397,6 +397,206 @@ header nav a:hover, header nav a.active { color: #fff; background: rgba(255,255,
|
||||
display: flex; gap: 10px;
|
||||
}
|
||||
|
||||
/* ── Style Report (Dafna's Portrait) ────────────────── */
|
||||
.style-report-header { text-align: center; margin-bottom: 32px; padding-top: 16px; }
|
||||
.style-report-header h1 { font-size: 2em; font-weight: 600; color: #1a1a2e; margin-bottom: 6px; }
|
||||
.style-report-header .subtitle-muted { color: #888; font-size: 0.95em; }
|
||||
|
||||
.portrait-card {
|
||||
background: #fff; border-radius: 12px; padding: 28px 32px;
|
||||
margin-bottom: 20px; box-shadow: 0 1px 4px rgba(0,0,0,0.06), 0 0 1px rgba(0,0,0,0.08);
|
||||
}
|
||||
.portrait-section-title {
|
||||
font-size: 1.3em; font-weight: 600; color: #1a1a2e;
|
||||
margin-bottom: 8px; padding-bottom: 10px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
.portrait-headline {
|
||||
font-size: 1.05em; color: #555; line-height: 1.6;
|
||||
margin-bottom: 20px; padding: 12px 16px;
|
||||
background: #fff9ed; border-right: 3px solid #e9a13f;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Hero section */
|
||||
.portrait-hero .hero-body {
|
||||
display: grid; grid-template-columns: 1fr auto; gap: 32px; align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.hero-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
|
||||
.hero-stat { text-align: center; padding: 14px; background: #fafafa; border-radius: 8px; }
|
||||
.hero-stat-value { font-size: 1.9em; font-weight: 700; color: #1a1a2e; line-height: 1; }
|
||||
.hero-stat-label { font-size: 0.8em; color: #888; margin-top: 6px; }
|
||||
|
||||
.hero-donut-wrap { display: flex; align-items: center; gap: 20px; }
|
||||
.donut {
|
||||
width: 160px; height: 160px; border-radius: 50%;
|
||||
position: relative; flex-shrink: 0;
|
||||
}
|
||||
.donut::after {
|
||||
content: ''; position: absolute; inset: 24%;
|
||||
background: #fff; border-radius: 50%;
|
||||
}
|
||||
.donut-center {
|
||||
position: absolute; inset: 0; display: flex;
|
||||
align-items: center; justify-content: center;
|
||||
font-size: 0.85em; color: #666; z-index: 1; font-weight: 600;
|
||||
}
|
||||
.donut-legend {
|
||||
display: flex; flex-direction: column; gap: 6px; font-size: 0.82em;
|
||||
}
|
||||
.donut-legend-item {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.donut-legend-dot {
|
||||
width: 11px; height: 11px; border-radius: 50%; flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hero-timeline {
|
||||
position: relative; height: 44px; margin-top: 8px;
|
||||
background: linear-gradient(to left, #fafafa, #fff, #fafafa);
|
||||
border-radius: 6px; padding: 0 16px;
|
||||
}
|
||||
.hero-timeline-line {
|
||||
position: absolute; top: 50%; right: 16px; left: 16px;
|
||||
height: 2px; background: #e5e5e5; transform: translateY(-50%);
|
||||
}
|
||||
.hero-timeline-dot {
|
||||
position: absolute; top: 50%; width: 10px; height: 10px;
|
||||
border-radius: 50%; background: #e94560; transform: translate(50%, -50%);
|
||||
cursor: pointer; transition: transform 0.15s;
|
||||
box-shadow: 0 0 0 2px #fff;
|
||||
}
|
||||
.hero-timeline-dot:hover { transform: translate(50%, -50%) scale(1.4); z-index: 1; }
|
||||
|
||||
/* Anatomy section */
|
||||
.anatomy-bar {
|
||||
display: flex; width: 100%; height: 56px;
|
||||
border-radius: 8px; overflow: hidden;
|
||||
box-shadow: inset 0 1px 3px rgba(0,0,0,0.06);
|
||||
}
|
||||
.anatomy-segment {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 0.82em; font-weight: 600; color: #fff;
|
||||
transition: filter 0.15s; position: relative; cursor: help;
|
||||
text-align: center; padding: 0 4px; overflow: hidden;
|
||||
}
|
||||
.anatomy-segment:hover { filter: brightness(1.08); }
|
||||
.anatomy-segment small {
|
||||
display: block; font-size: 0.72em; font-weight: 400; opacity: 0.85;
|
||||
}
|
||||
.anatomy-legend {
|
||||
display: flex; flex-wrap: wrap; gap: 14px;
|
||||
margin-top: 14px; font-size: 0.8em; color: #666;
|
||||
}
|
||||
.anatomy-legend-item { display: flex; align-items: center; gap: 6px; }
|
||||
.anatomy-legend-dot {
|
||||
width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Phrase wall */
|
||||
.phrase-filters {
|
||||
display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px;
|
||||
}
|
||||
.phrase-filter {
|
||||
padding: 6px 14px; border-radius: 18px;
|
||||
border: 1px solid #ddd; background: #fff;
|
||||
font-size: 0.82em; cursor: pointer; transition: all 0.12s;
|
||||
}
|
||||
.phrase-filter:hover { background: #f5f5f5; }
|
||||
.phrase-filter.active {
|
||||
background: #1a1a2e; color: #fff; border-color: #1a1a2e;
|
||||
}
|
||||
.phrase-wall {
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.phrase-card {
|
||||
padding: 14px 16px; border-radius: 8px; background: #fafafa;
|
||||
border-right: 3px solid; cursor: pointer; transition: all 0.15s;
|
||||
display: flex; flex-direction: column; gap: 6px;
|
||||
}
|
||||
.phrase-card:hover { background: #fff; transform: translateY(-1px); box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
||||
.phrase-card-text { font-weight: 500; color: #1a1a2e; line-height: 1.4; }
|
||||
.phrase-card-meta {
|
||||
display: flex; justify-content: space-between; font-size: 0.75em; color: #999;
|
||||
margin-top: auto;
|
||||
}
|
||||
.phrase-card-freq {
|
||||
background: #fff; padding: 2px 8px; border-radius: 10px; font-weight: 600;
|
||||
}
|
||||
|
||||
/* Growth curve + Contribution */
|
||||
.growth-curve-wrap { margin-bottom: 24px; }
|
||||
.growth-curve-label { font-size: 0.82em; color: #888; margin-bottom: 8px; }
|
||||
.growth-curve {
|
||||
width: 100%; height: 160px;
|
||||
background: linear-gradient(to bottom, #fafafa, #fff);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.growth-curve-path { fill: none; stroke: #e94560; stroke-width: 2.5; }
|
||||
.growth-curve-area { fill: #fce4e9; opacity: 0.6; }
|
||||
.growth-curve-dot { fill: #e94560; cursor: pointer; transition: r 0.15s; }
|
||||
.growth-curve-dot:hover { r: 6; }
|
||||
|
||||
.contribution-grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.contribution-card {
|
||||
padding: 14px 16px; background: #fafafa; border-radius: 8px;
|
||||
border: 1px solid #eee;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.contribution-card:hover { background: #fff; box-shadow: 0 2px 6px rgba(0,0,0,0.05); }
|
||||
.contribution-card-header {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
margin-bottom: 8px; font-size: 0.82em; color: #555;
|
||||
}
|
||||
.contribution-card-number { font-weight: 600; color: #1a1a2e; font-size: 1em; }
|
||||
.contribution-badges { display: flex; gap: 8px; margin: 10px 0; }
|
||||
.contribution-badge {
|
||||
padding: 4px 10px; border-radius: 10px; font-size: 0.78em; font-weight: 600;
|
||||
}
|
||||
.contribution-badge.new { background: #e8f5e9; color: #2e7d32; }
|
||||
.contribution-badge.confirmed { background: #e3f2fd; color: #1565c0; }
|
||||
.contribution-highlight {
|
||||
font-size: 0.82em; color: #666; margin-top: 8px; padding-top: 8px;
|
||||
border-top: 1px dashed #e5e5e5; line-height: 1.5;
|
||||
}
|
||||
.contribution-highlight strong { color: #1a1a2e; }
|
||||
|
||||
/* Modal */
|
||||
.phrase-modal {
|
||||
border: none; border-radius: 12px; padding: 24px 28px;
|
||||
max-width: 640px; width: 90%; direction: rtl;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.2);
|
||||
}
|
||||
.phrase-modal::backdrop { background: rgba(0,0,0,0.4); }
|
||||
.phrase-modal-header {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
margin-bottom: 14px; padding-bottom: 12px; border-bottom: 1px solid #eee;
|
||||
}
|
||||
.phrase-modal-type {
|
||||
font-size: 0.78em; color: #888; text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.phrase-modal-text {
|
||||
font-size: 1.1em; font-weight: 600; color: #1a1a2e;
|
||||
margin-bottom: 12px; line-height: 1.5;
|
||||
}
|
||||
.phrase-modal-context {
|
||||
font-size: 0.88em; color: #666; margin-bottom: 16px; line-height: 1.6;
|
||||
}
|
||||
.phrase-modal-examples {
|
||||
display: flex; flex-direction: column; gap: 10px;
|
||||
max-height: 300px; overflow-y: auto;
|
||||
}
|
||||
.phrase-modal-example {
|
||||
padding: 10px 14px; background: #fafafa; border-right: 3px solid #e94560;
|
||||
font-size: 0.86em; line-height: 1.5; color: #333; border-radius: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.main { padding: 16px; }
|
||||
header { padding: 14px 16px; }
|
||||
@@ -417,6 +617,7 @@ header nav a:hover, header nav a.active { color: #fff; background: rgba(255,255,
|
||||
<a href="#/new" id="navNew">+ תיק חדש</a>
|
||||
<a href="#/upload" id="navUpload">העלאה</a>
|
||||
<a href="#/training" id="navTraining">אימון סגנון</a>
|
||||
<a href="#/style-report" id="navStyleReport">הסגנון שלי</a>
|
||||
<a href="#/skills" id="navSkills">Skills</a>
|
||||
</nav>
|
||||
</header>
|
||||
@@ -735,9 +936,83 @@ header nav a:hover, header nav a.active { color: #fff; background: rgba(255,255,
|
||||
<div class="empty">טוען...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:24px;text-align:center">
|
||||
<a href="#/style-report" class="btn btn-primary" style="font-size:1em;padding:12px 28px">
|
||||
← צפי בפורטרט הסגנון המלא
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ Page: Style Report (Dafna's Portrait) ══ -->
|
||||
<div class="page" id="page-style-report">
|
||||
<div class="style-report-header">
|
||||
<h1>פורטרט הסגנון שלך</h1>
|
||||
<p class="subtitle-muted">דוח ויזואלי על סמך הקורפוס שלמדתי ממך</p>
|
||||
</div>
|
||||
|
||||
<div id="styleReportLoading" class="empty" style="padding:60px 20px">
|
||||
<div class="mini-spinner" style="width:24px;height:24px"></div>
|
||||
<div style="margin-top:12px">טוען את הפורטרט...</div>
|
||||
</div>
|
||||
|
||||
<div id="styleReportContent" style="display:none">
|
||||
|
||||
<!-- Section 1: Hero -->
|
||||
<section class="portrait-card portrait-hero">
|
||||
<div class="portrait-headline" id="heroHeadline"></div>
|
||||
<div class="hero-body">
|
||||
<div class="hero-stats" id="heroStats"></div>
|
||||
<div class="hero-donut-wrap">
|
||||
<div class="donut" id="heroDonut"></div>
|
||||
<div class="donut-legend" id="heroDonutLegend"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-timeline" id="heroTimeline"></div>
|
||||
</section>
|
||||
|
||||
<!-- Section 2: Anatomy -->
|
||||
<section class="portrait-card portrait-anatomy">
|
||||
<h2 class="portrait-section-title">איך את בונה החלטה</h2>
|
||||
<div class="portrait-headline" id="anatomyHeadline"></div>
|
||||
<div class="anatomy-bar" id="anatomyBar"></div>
|
||||
<div class="anatomy-legend" id="anatomyLegend"></div>
|
||||
</section>
|
||||
|
||||
<!-- Section 3: Signature Phrases Wall -->
|
||||
<section class="portrait-card portrait-phrases">
|
||||
<h2 class="portrait-section-title">הביטויים שחוזרים אצלך</h2>
|
||||
<div class="portrait-headline" id="phrasesHeadline"></div>
|
||||
<div class="phrase-filters" id="phraseFilters"></div>
|
||||
<div class="phrase-wall" id="phraseWall"></div>
|
||||
</section>
|
||||
|
||||
<!-- Section 4: Contribution -->
|
||||
<section class="portrait-card portrait-contribution">
|
||||
<h2 class="portrait-section-title">מה תרמה כל החלטה</h2>
|
||||
<div class="portrait-headline" id="contributionHeadline"></div>
|
||||
<div class="growth-curve-wrap">
|
||||
<div class="growth-curve-label">עקומת הלמידה — כמה ידע חדש כל החלטה הביאה</div>
|
||||
<svg class="growth-curve" id="growthCurve" viewBox="0 0 800 160" preserveAspectRatio="none"></svg>
|
||||
</div>
|
||||
<div class="contribution-grid" id="contributionGrid"></div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal for pattern examples -->
|
||||
<dialog id="phraseModal" class="phrase-modal">
|
||||
<div class="phrase-modal-header">
|
||||
<span class="phrase-modal-type" id="phraseModalType"></span>
|
||||
<button class="btn-icon" onclick="document.getElementById('phraseModal').close()">✕</button>
|
||||
</div>
|
||||
<div class="phrase-modal-text" id="phraseModalText"></div>
|
||||
<div class="phrase-modal-context" id="phraseModalContext"></div>
|
||||
<div class="phrase-modal-examples" id="phraseModalExamples"></div>
|
||||
</dialog>
|
||||
|
||||
<!-- Status Bar -->
|
||||
<div class="status-bar">
|
||||
<div class="stat">תיקים: <span class="stat-value" id="statCases">—</span></div>
|
||||
@@ -803,6 +1078,11 @@ function handleRoute() {
|
||||
document.getElementById('navTraining').classList.add('active');
|
||||
subtitle = 'אימון סגנון';
|
||||
initTrainingPage();
|
||||
} else if (hash === '#/style-report') {
|
||||
document.getElementById('page-style-report').classList.add('active');
|
||||
document.getElementById('navStyleReport').classList.add('active');
|
||||
subtitle = 'פורטרט הסגנון שלי';
|
||||
loadStyleReport();
|
||||
}
|
||||
|
||||
document.getElementById('pageSubtitle').textContent = subtitle;
|
||||
@@ -2086,6 +2366,288 @@ async function pollStyleAnalysisStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Style Report Page ────────────────────────────────────
|
||||
|
||||
const PATTERN_TYPE_COLORS = {
|
||||
opening_formula: '#5e9a6e',
|
||||
closing_formula: '#c87533',
|
||||
transition: '#4e7cb3',
|
||||
characteristic_phrase: '#a7547c',
|
||||
argument_flow: '#7e5c9a',
|
||||
analysis_structure: '#3e8583',
|
||||
evidence_handling: '#b8894a',
|
||||
citation_style: '#5f6b8c',
|
||||
};
|
||||
|
||||
const SECTION_COLORS = {
|
||||
intro: '#4e7cb3',
|
||||
facts: '#5e9a6e',
|
||||
appellant_claims: '#a7547c',
|
||||
respondent_claims: '#c87533',
|
||||
legal_analysis: '#7e5c9a',
|
||||
ruling: '#3e8583',
|
||||
conclusion: '#b8894a',
|
||||
};
|
||||
|
||||
const DONUT_COLORS = ['#e94560', '#5e9a6e', '#4e7cb3', '#a7547c', '#c87533', '#7e5c9a', '#b8894a'];
|
||||
|
||||
let _styleReportData = null;
|
||||
let _activeFilter = 'all';
|
||||
|
||||
async function loadStyleReport() {
|
||||
document.getElementById('styleReportLoading').style.display = '';
|
||||
document.getElementById('styleReportContent').style.display = 'none';
|
||||
try {
|
||||
const res = await fetch(API + '/training/style-report');
|
||||
if (!res.ok) throw new Error('Failed to load report');
|
||||
_styleReportData = await res.json();
|
||||
renderHero(_styleReportData.corpus);
|
||||
renderAnatomy(_styleReportData.anatomy);
|
||||
renderPhrases(_styleReportData.signature_phrases);
|
||||
renderContribution(_styleReportData.contribution);
|
||||
document.getElementById('styleReportLoading').style.display = 'none';
|
||||
document.getElementById('styleReportContent').style.display = '';
|
||||
} catch (e) {
|
||||
document.getElementById('styleReportLoading').innerHTML = `<div>שגיאה: ${esc(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderHero(corpus) {
|
||||
document.getElementById('heroHeadline').textContent = '★ ' + corpus.headline;
|
||||
|
||||
document.getElementById('heroStats').innerHTML = `
|
||||
<div class="hero-stat">
|
||||
<div class="hero-stat-value">${corpus.decision_count}</div>
|
||||
<div class="hero-stat-label">החלטות בקורפוס</div>
|
||||
</div>
|
||||
<div class="hero-stat">
|
||||
<div class="hero-stat-value">${(corpus.total_chars / 1000).toFixed(0)}K</div>
|
||||
<div class="hero-stat-label">סך תווים</div>
|
||||
</div>
|
||||
<div class="hero-stat">
|
||||
<div class="hero-stat-value">${(corpus.avg_chars / 1000).toFixed(0)}K</div>
|
||||
<div class="hero-stat-label">ממוצע להחלטה</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Donut (CSS conic-gradient)
|
||||
const total = corpus.subject_distribution.reduce((a, b) => a + b.count, 0);
|
||||
let pct = 0;
|
||||
const segments = corpus.subject_distribution.map((s, i) => {
|
||||
const start = (pct / total) * 360;
|
||||
pct += s.count;
|
||||
const end = (pct / total) * 360;
|
||||
const color = DONUT_COLORS[i % DONUT_COLORS.length];
|
||||
return `${color} ${start}deg ${end}deg`;
|
||||
}).join(', ');
|
||||
|
||||
const donut = document.getElementById('heroDonut');
|
||||
donut.style.background = `conic-gradient(${segments})`;
|
||||
donut.innerHTML = `<div class="donut-center">${corpus.decision_count} החלטות</div>`;
|
||||
|
||||
document.getElementById('heroDonutLegend').innerHTML = corpus.subject_distribution.map((s, i) => `
|
||||
<div class="donut-legend-item">
|
||||
<span class="donut-legend-dot" style="background:${DONUT_COLORS[i % DONUT_COLORS.length]}"></span>
|
||||
<span>${esc(s.label)} · ${s.count}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Timeline
|
||||
const tl = document.getElementById('heroTimeline');
|
||||
const dated = corpus.decisions.filter(d => d.date);
|
||||
if (dated.length < 2) {
|
||||
tl.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
const dates = dated.map(d => new Date(d.date).getTime());
|
||||
const minT = Math.min(...dates);
|
||||
const maxT = Math.max(...dates);
|
||||
const range = maxT - minT || 1;
|
||||
|
||||
let html = '<div class="hero-timeline-line"></div>';
|
||||
dated.forEach(d => {
|
||||
const t = new Date(d.date).getTime();
|
||||
const pct = ((t - minT) / range) * 100;
|
||||
html += `<div class="hero-timeline-dot" style="right:${pct}%" title="${esc(d.number)} · ${esc(d.date)}"></div>`;
|
||||
});
|
||||
tl.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderAnatomy(anatomy) {
|
||||
document.getElementById('anatomyHeadline').textContent = '★ ' + anatomy.headline;
|
||||
|
||||
if (!anatomy.sections || !anatomy.sections.length) {
|
||||
document.getElementById('anatomyBar').innerHTML = '<div class="empty" style="width:100%">אין עדיין נתונים</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const bar = document.getElementById('anatomyBar');
|
||||
bar.innerHTML = anatomy.sections.map(s => {
|
||||
const color = SECTION_COLORS[s.type] || '#888';
|
||||
const pctDisplay = Math.round(s.pct * 100);
|
||||
return `
|
||||
<div class="anatomy-segment" style="flex:${s.pct}; background:${color}"
|
||||
title="${esc(s.label)}: ממוצע ${s.avg_chars.toLocaleString('he-IL')} תווים, ${s.coverage} החלטות">
|
||||
<div>
|
||||
<div>${esc(s.label)}</div>
|
||||
<small>${pctDisplay}%</small>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
document.getElementById('anatomyLegend').innerHTML = anatomy.sections.map(s => `
|
||||
<div class="anatomy-legend-item">
|
||||
<span class="anatomy-legend-dot" style="background:${SECTION_COLORS[s.type] || '#888'}"></span>
|
||||
<span>${esc(s.label)} · ממוצע ${s.avg_chars.toLocaleString('he-IL')} תווים · ${s.coverage} החלטות</span>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderPhrases(phrases) {
|
||||
document.getElementById('phrasesHeadline').textContent = '★ ' + phrases.headline;
|
||||
|
||||
// Build filter chips — one per pattern_type that appears
|
||||
const types = [...new Set(phrases.items.map(p => p.type))];
|
||||
const typeLabels = {
|
||||
opening_formula: 'פתיחה',
|
||||
closing_formula: 'סיום',
|
||||
transition: 'מעברים',
|
||||
characteristic_phrase: 'ביטויים',
|
||||
argument_flow: 'טיעון',
|
||||
analysis_structure: 'מבנה',
|
||||
evidence_handling: 'ראיות',
|
||||
citation_style: 'ציטוט',
|
||||
};
|
||||
|
||||
const filters = [{ id: 'all', label: 'הכל' }]
|
||||
.concat(types.map(t => ({ id: t, label: typeLabels[t] || t })));
|
||||
document.getElementById('phraseFilters').innerHTML = filters.map(f => `
|
||||
<div class="phrase-filter ${f.id === _activeFilter ? 'active' : ''}"
|
||||
data-filter="${f.id}" onclick="setPhraseFilter('${f.id}')">${esc(f.label)}</div>
|
||||
`).join('');
|
||||
|
||||
renderPhraseWall(phrases.items);
|
||||
}
|
||||
|
||||
function setPhraseFilter(filterId) {
|
||||
_activeFilter = filterId;
|
||||
document.querySelectorAll('.phrase-filter').forEach(el => {
|
||||
el.classList.toggle('active', el.dataset.filter === filterId);
|
||||
});
|
||||
renderPhraseWall(_styleReportData.signature_phrases.items);
|
||||
}
|
||||
|
||||
function renderPhraseWall(items) {
|
||||
const filtered = _activeFilter === 'all'
|
||||
? items
|
||||
: items.filter(p => p.type === _activeFilter);
|
||||
|
||||
document.getElementById('phraseWall').innerHTML = filtered.map((p, idx) => {
|
||||
// Clean display text — first alternative, strip placeholders
|
||||
let display = p.text.replace(/\[[^\]]*\]/g, '').replace(/\s+/g, ' ').trim();
|
||||
display = display.split(' / ')[0].split(' או ')[0].trim();
|
||||
if (display.length > 80) display = display.substring(0, 77) + '...';
|
||||
|
||||
const color = PATTERN_TYPE_COLORS[p.type] || '#888';
|
||||
const origIdx = items.indexOf(p);
|
||||
return `
|
||||
<div class="phrase-card" style="border-right-color:${color}" onclick="showPhraseModal(${origIdx})">
|
||||
<div class="phrase-card-text">${esc(display)}</div>
|
||||
<div class="phrase-card-meta">
|
||||
<span>${esc(p.context.substring(0, 40))}</span>
|
||||
<span class="phrase-card-freq">${p.frequency}/24</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function showPhraseModal(idx) {
|
||||
const p = _styleReportData.signature_phrases.items[idx];
|
||||
if (!p) return;
|
||||
const typeLabels = {
|
||||
opening_formula: 'נוסחת פתיחה',
|
||||
closing_formula: 'נוסחת סיום',
|
||||
transition: 'ביטוי מעבר',
|
||||
characteristic_phrase: 'ביטוי אופייני',
|
||||
argument_flow: 'זרימת טיעון',
|
||||
analysis_structure: 'מבנה ניתוח',
|
||||
evidence_handling: 'טיפול בראיות',
|
||||
citation_style: 'סגנון ציטוט',
|
||||
};
|
||||
document.getElementById('phraseModalType').textContent =
|
||||
(typeLabels[p.type] || p.type) + ` · ${p.frequency}/24 החלטות`;
|
||||
document.getElementById('phraseModalText').textContent = p.text;
|
||||
document.getElementById('phraseModalContext').textContent = p.context || '';
|
||||
const examples = (p.examples || []).filter(e => e && e.length > 0);
|
||||
document.getElementById('phraseModalExamples').innerHTML = examples.length
|
||||
? examples.map(e => `<div class="phrase-modal-example">${esc(e)}</div>`).join('')
|
||||
: '<div class="empty">אין דוגמאות שמורות</div>';
|
||||
document.getElementById('phraseModal').showModal();
|
||||
}
|
||||
|
||||
function renderContribution(contrib) {
|
||||
document.getElementById('contributionHeadline').textContent = '★ ' + contrib.headline;
|
||||
|
||||
// Growth curve — SVG polyline
|
||||
const points = contrib.growth_curve;
|
||||
if (points.length >= 2) {
|
||||
const w = 800, h = 160, pad = 20;
|
||||
const maxY = Math.max(...points.map(p => p.cumulative)) || 1;
|
||||
const step = (w - pad * 2) / (points.length - 1);
|
||||
// RTL: right = start, so reverse X
|
||||
const coords = points.map((p, i) => {
|
||||
const x = w - pad - i * step;
|
||||
const y = h - pad - (p.cumulative / maxY) * (h - pad * 2);
|
||||
return { x, y, ...p };
|
||||
});
|
||||
const path = coords.map((c, i) => `${i === 0 ? 'M' : 'L'} ${c.x.toFixed(1)} ${c.y.toFixed(1)}`).join(' ');
|
||||
const areaPath = path + ` L ${coords[coords.length - 1].x} ${h - pad} L ${coords[0].x} ${h - pad} Z`;
|
||||
|
||||
const svg = document.getElementById('growthCurve');
|
||||
svg.innerHTML = `
|
||||
<path class="growth-curve-area" d="${areaPath}"/>
|
||||
<path class="growth-curve-path" d="${path}"/>
|
||||
${coords.map(c => `
|
||||
<circle class="growth-curve-dot" cx="${c.x}" cy="${c.y}" r="4">
|
||||
<title>${esc(c.decision_number || 'ללא מספר')}: ${c.cumulative} דפוסים מצטברים</title>
|
||||
</circle>
|
||||
`).join('')}
|
||||
`;
|
||||
}
|
||||
|
||||
// Contribution cards — sort by date
|
||||
const cards = contrib.decision_contributions;
|
||||
document.getElementById('contributionGrid').innerHTML = cards.map(d => {
|
||||
const highlight = d.highlight;
|
||||
let highlightDisplay = '';
|
||||
if (highlight) {
|
||||
let text = highlight.text.replace(/\[[^\]]*\]/g, '').replace(/\s+/g, ' ').trim();
|
||||
text = text.split(' / ')[0].split(' או ')[0].trim();
|
||||
if (text.length > 80) text = text.substring(0, 77) + '...';
|
||||
highlightDisplay = `
|
||||
<div class="contribution-highlight">
|
||||
▸ תרומה בולטת: <strong>"${esc(text)}"</strong>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return `
|
||||
<div class="contribution-card">
|
||||
<div class="contribution-card-header">
|
||||
<span class="contribution-card-number">${esc(d.decision_number || 'ללא מספר')}</span>
|
||||
<span>${esc(d.decision_date || '—')}</span>
|
||||
</div>
|
||||
<div class="contribution-badges">
|
||||
<span class="contribution-badge new">🟢 ${d.new_count} חדשים</span>
|
||||
<span class="contribution-badge confirmed">🔵 ${d.confirmed_count} חיזקה</span>
|
||||
</div>
|
||||
${highlightDisplay}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function loadCorpusList() {
|
||||
const container = document.getElementById('corpusList');
|
||||
const count = document.getElementById('corpusCount');
|
||||
|
||||
Reference in New Issue
Block a user