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:
2026-04-11 11:34:37 +00:00
parent 32f18de049
commit 858333b386
3 changed files with 1088 additions and 0 deletions

View File

@@ -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');