diff --git a/web/ai-debug.css b/web/ai-debug.css index d9b2679..e7066be 100644 --- a/web/ai-debug.css +++ b/web/ai-debug.css @@ -154,6 +154,107 @@ margin-bottom: 10px; } +.ai-debug-inline-metric { + display: flex; + flex-direction: column; + gap: 4px; +} + +.ai-debug-inline-bar { + display: block; + width: 100%; + height: 6px; + border-radius: 999px; + background: #e2e8f0; + overflow: hidden; +} + +.ai-debug-inline-bar > span { + display: block; + height: 100%; + border-radius: 999px; + background: linear-gradient(90deg, #0ea5e9 0%, #0284c7 100%); +} + +.ai-debug-viz { + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 10px; +} + +.ai-debug-viz-empty { + color: #64748b; + font-size: 13px; +} + +.ai-debug-viz-group { + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 10px; + background: #f8fafc; +} + +.ai-debug-viz-group h4 { + margin: 0 0 8px; + font-size: 13px; + color: #1e293b; +} + +.ai-debug-bars { + margin: 0; + padding: 0; + list-style: none; + display: flex; + flex-direction: column; + gap: 7px; +} + +.ai-debug-bar-row { + display: grid; + grid-template-columns: minmax(120px, 180px) minmax(60px, 1fr) auto; + align-items: center; + gap: 8px; +} + +.ai-debug-bar-label { + font-size: 12px; + color: #334155; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.ai-debug-bar-track { + display: block; + width: 100%; + height: 10px; + border-radius: 999px; + background: #dbeafe; + overflow: hidden; +} + +.ai-debug-bar-fill { + display: block; + height: 100%; + border-radius: 999px; + background: linear-gradient(90deg, #38bdf8 0%, #2563eb 100%); +} + +.ai-debug-bar-value { + font-size: 12px; + color: #0f172a; + white-space: nowrap; +} + +.ai-debug-bar-row--peak .ai-debug-bar-track { + background: #fee2e2; +} + +.ai-debug-bar-row--peak .ai-debug-bar-fill { + background: linear-gradient(90deg, #fb7185 0%, #dc2626 100%); +} + .ai-debug-json { margin: 0; max-height: 60vh; @@ -177,4 +278,8 @@ .ai-debug-json { max-height: 42vh; } + + .ai-debug-bar-row { + grid-template-columns: minmax(100px, 140px) minmax(40px, 1fr) auto; + } } diff --git a/web/ai-debug.js b/web/ai-debug.js index d863493..07d7214 100644 --- a/web/ai-debug.js +++ b/web/ai-debug.js @@ -21,6 +21,7 @@ const tableBody = document.getElementById('aiDebugTableBody'); const statusEl = document.getElementById('aiDebugStatus'); const detailMeta = document.getElementById('aiDebugDetailMeta'); + const detailViz = document.getElementById('aiDebugDetailViz'); const detailJson = document.getElementById('aiDebugDetailJson'); const refreshBtn = document.getElementById('aiDebugRefreshBtn'); const statusFilter = document.getElementById('aiDebugStatusFilter'); @@ -90,6 +91,14 @@ return `${value.slice(0, 8)}…${value.slice(-6)}`; } + function toPositiveNumber(value) { + const numeric = Number(value); + if (!Number.isFinite(numeric) || numeric < 0) { + return null; + } + return numeric; + } + function getBackendTotalMs(item) { return item && item.backend_timings @@ -117,6 +126,11 @@ return; } + const totals = state.traces + .map((item) => toPositiveNumber(item.total_duration_ms)) + .filter((value) => value !== null); + const maxTotal = totals.length ? Math.max(...totals) : 0; + state.traces.forEach((item) => { const tr = document.createElement('tr'); tr.dataset.traceId = item.trace_id; @@ -124,10 +138,20 @@ tr.classList.add('is-selected'); } + const totalMs = toPositiveNumber(item.total_duration_ms); + const totalBarPercent = maxTotal > 0 && totalMs !== null + ? Math.max(4, Math.round((totalMs / maxTotal) * 100)) + : 0; + tr.innerHTML = ` ${formatDate(item.created_at)} ${item.status || '—'} - ${formatMs(item.total_duration_ms)} + +
+ ${formatMs(item.total_duration_ms)} + +
+ ${formatMs(getBackendTotalMs(item))} ${formatMs(getAiRequestMs(item))} ${compactId(item.flow_id)} / ${compactId(item.trace_id)} @@ -137,17 +161,148 @@ }); } + function collectFrontendBars(trace) { + const front = trace && trace.frontend_timings && typeof trace.frontend_timings === 'object' + ? trace.frontend_timings + : {}; + + const mapping = [ + ['extractPostTextMs', 'Text-Extraktion'], + ['profileLookupMs', 'Profil-Lookup'], + ['aiRequestMs', 'AI-Request'], + ['waitForCommentInputMs', 'Wartezeit Kommentarfeld'], + ['setCommentTextMs', 'Kommentar einfügen'], + ['confirmParticipationMs', 'Auto-Bestätigung'], + ['clipboardWriteMs', 'Clipboard-Fallback'], + ['totalMs', 'Frontend Total'] + ]; + + return mapping + .map(([key, label]) => ({ + key, + label, + value: toPositiveNumber(front[key]) + })) + .filter((entry) => entry.value !== null); + } + + function collectBackendBars(trace) { + const back = trace && trace.backend_timings && typeof trace.backend_timings === 'object' + ? trace.backend_timings + : {}; + + const mapping = [ + ['loadSettingsMs', 'Settings laden'], + ['reactivateCredentialsMs', 'Credentials reaktivieren'], + ['loadCredentialsMs', 'Credentials laden'], + ['buildPromptMs', 'Prompt bauen'], + ['credentialLoopMs', 'Provider-/Credential-Loop'], + ['totalMs', 'Backend Total'] + ]; + + return mapping + .map(([key, label]) => ({ + key, + label, + value: toPositiveNumber(back[key]) + })) + .filter((entry) => entry.value !== null); + } + + function collectCredentialBars(trace) { + const attempts = trace && Array.isArray(trace.backend_attempts) + ? trace.backend_attempts + : []; + + return attempts + .map((attempt, index) => { + const credential = attempt && attempt.credentialName ? attempt.credentialName : `Credential ${index + 1}`; + const status = attempt && attempt.status ? attempt.status : 'unknown'; + return { + key: `credential_${index + 1}`, + label: `${credential} (${status})`, + value: toPositiveNumber(attempt && attempt.duration_ms) + }; + }) + .filter((entry) => entry.value !== null); + } + + function renderBarGroup(title, rows) { + if (!Array.isArray(rows) || !rows.length) { + return ''; + } + + const max = Math.max(...rows.map((entry) => entry.value)); + const sorted = [...rows].sort((a, b) => b.value - a.value); + const peakKey = sorted[0] ? sorted[0].key : null; + + const items = rows.map((entry) => { + const width = max > 0 + ? Math.max(4, Math.round((entry.value / max) * 100)) + : 0; + const peakClass = entry.key === peakKey ? ' ai-debug-bar-row--peak' : ''; + return ` +
  • + ${entry.label} + + ${formatMs(entry.value)} +
  • + `; + }).join(''); + + return ` +
    +

    ${title}

    + +
    + `; + } + + function renderDetailVisualization(trace) { + if (!detailViz) { + return; + } + + if (!trace) { + detailViz.innerHTML = ''; + return; + } + + const frontendBars = collectFrontendBars(trace); + const backendBars = collectBackendBars(trace); + const credentialBars = collectCredentialBars(trace); + + const sections = [ + renderBarGroup('Frontend-Phasen', frontendBars), + renderBarGroup('Backend-Phasen', backendBars), + renderBarGroup('Credential-Attempts', credentialBars) + ].filter(Boolean); + + if (!sections.length) { + detailViz.innerHTML = '
    Keine Timing-Daten für diesen Lauf verfügbar.
    '; + return; + } + + detailViz.innerHTML = sections.join(''); + } + function renderDetail(trace) { if (!detailMeta || !detailJson) { return; } if (!trace) { detailMeta.textContent = 'Bitte einen Eintrag auswählen.'; + renderDetailVisualization(null); detailJson.textContent = ''; return; } detailMeta.textContent = `${trace.status || '—'} · ${formatDate(trace.created_at)} · Trace ${trace.trace_id || '—'}`; + renderDetailVisualization(trace); detailJson.textContent = JSON.stringify(trace, null, 2); } diff --git a/web/index.html b/web/index.html index 6e706a4..702e9de 100644 --- a/web/index.html +++ b/web/index.html @@ -1453,6 +1453,7 @@

    Ablaufdetails

    Bitte einen Eintrag auswählen.
    +