Management UI: corpus delete, process panel, activity feed, diagnostics
- DELETE /api/training/corpus/{id} + delete button on training page,
with confirmation dialog and recompute hint
- /api/system/tasks + floating process panel (bottom-left) showing
active background tasks with live 3s polling
- /api/system/recent-activity derives a feed from cases, style_corpus,
and last style_patterns run; sidebar on home page renders with
relative timestamps
- /api/system/diagnostics + /#/diagnostics page showing DB health,
row counts per table, active tasks, stuck documents (>10 min),
failed extractions
- Cosmetic: signature phrase headline now prefers clean phrases over
bracket-heavy templates for display
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -396,6 +396,71 @@ header nav a.active::after {
|
||||
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); }
|
||||
@@ -556,6 +621,140 @@ header nav a.active::after {
|
||||
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); }
|
||||
|
||||
/* ── 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; }
|
||||
@@ -696,7 +895,15 @@ header nav a.active::after {
|
||||
background: #f7f7f7; font-weight: 600; color: #555;
|
||||
font-size: 0.78em; text-transform: uppercase;
|
||||
}
|
||||
.corpus-table tr:hover td { background: #fafafa; }
|
||||
.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;
|
||||
@@ -1019,6 +1226,7 @@ header nav a.active::after {
|
||||
<a href="#/training" id="navTraining">אימון סגנון</a>
|
||||
<a href="#/style-report" id="navStyleReport">הסגנון שלי</a>
|
||||
<a href="#/skills" id="navSkills">Skills</a>
|
||||
<a href="#/diagnostics" id="navDiagnostics">מצב מערכת</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@@ -1058,13 +1266,21 @@ header nav a.active::after {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider-gold"></div>
|
||||
|
||||
<div class="page-header">
|
||||
<h2>תיקים פעילים</h2>
|
||||
</div>
|
||||
<div class="case-grid" id="caseGrid">
|
||||
<div class="empty">טוען תיקים...</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>
|
||||
|
||||
@@ -1434,6 +1650,17 @@ header nav a.active::after {
|
||||
|
||||
</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 -->
|
||||
@@ -1456,6 +1683,16 @@ header nav a.active::after {
|
||||
|
||||
<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 = '';
|
||||
@@ -1518,13 +1755,22 @@ function handleRoute() {
|
||||
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();
|
||||
}
|
||||
|
||||
document.getElementById('pageSubtitle').textContent = subtitle;
|
||||
}
|
||||
|
||||
window.addEventListener('hashchange', handleRoute);
|
||||
window.addEventListener('load', () => { handleRoute(); loadStatus(); });
|
||||
window.addEventListener('load', () => {
|
||||
handleRoute();
|
||||
loadStatus();
|
||||
startProcessPanelPolling();
|
||||
});
|
||||
|
||||
// ── Case List ────────────────────────────────────────────
|
||||
async function loadCaseList() {
|
||||
@@ -2268,31 +2514,215 @@ async function loadStatus() {
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// ── 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 ──────────────────────
|
||||
|
||||
let _processPollTimer = null;
|
||||
|
||||
function startProcessPanelPolling() {
|
||||
if (_processPollTimer) return;
|
||||
pollProcessPanel();
|
||||
_processPollTimer = setInterval(pollProcessPanel, 3000);
|
||||
}
|
||||
|
||||
async function pollProcessPanel() {
|
||||
try {
|
||||
const res = await fetch(API + '/system/tasks');
|
||||
const data = await res.json();
|
||||
renderProcessPanel(data.active || []);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
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
|
||||
// Home dashboard KPI tiles + activity feed
|
||||
const casesEl = document.getElementById('kpiCases');
|
||||
const corpusEl = document.getElementById('kpiCorpus');
|
||||
const patternsEl = document.getElementById('kpiPatterns');
|
||||
const procEl = document.getElementById('kpiProcessing');
|
||||
if (!casesEl) return;
|
||||
|
||||
try {
|
||||
const [statusRes, corpusRes, patternsRes] = await Promise.all([
|
||||
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: []})),
|
||||
]);
|
||||
casesEl.textContent = statusRes.cases ?? '0';
|
||||
corpusEl.textContent = corpusRes.length || '0';
|
||||
patternsEl.textContent = patternsRes.total || '0';
|
||||
// Count decisions currently being processed
|
||||
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);
|
||||
procEl.textContent = procCount || '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 '';
|
||||
@@ -3108,6 +3538,24 @@ function renderContribution(contrib) {
|
||||
}).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');
|
||||
@@ -3122,7 +3570,7 @@ async function loadCorpusList() {
|
||||
container.innerHTML = `
|
||||
<table class="corpus-table">
|
||||
<thead>
|
||||
<tr><th>מספר</th><th>תאריך</th><th>קטגוריות</th><th>תווים</th><th>נוצר</th></tr>
|
||||
<tr><th>מספר</th><th>תאריך</th><th>קטגוריות</th><th>תווים</th><th>נוצר</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${rows.map(r => `
|
||||
@@ -3132,6 +3580,7 @@ async function loadCorpusList() {
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user