Files
ewoooc/templates/admin/ai_calls_dashboard.html
OoO b21b40cae2
All checks were successful
CD Pipeline / deploy (push) Successful in 1m2s
fix(observability): soften frontend error copy
2026-05-05 21:58:49 +08:00

149 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "ewoooc_base.html" %}
{% block title %}AI 流量控制塔{% endblock %}
{% block ewooo_content %}
<style>
.calls-hero, .calls-panel, .calls-table-shell {
border: 1px solid var(--obs-line);
border-radius: 26px;
background: var(--obs-card);
box-shadow: 0 16px 38px rgba(70, 46, 28, 0.08);
}
.calls-hero {
padding: clamp(1.2rem, 2.4vw, 2rem);
background:
radial-gradient(circle at 14% 18%, rgba(201,100,66,.18), transparent 24rem),
radial-gradient(circle at 84% 10%, rgba(79,111,143,.14), transparent 22rem),
linear-gradient(135deg, rgba(255,248,239,.98), rgba(255,255,255,.72));
}
.calls-kicker { color: var(--obs-accent); font-size:.76rem; letter-spacing:.13em; text-transform:uppercase; font-weight:800; }
.calls-title { margin:.45rem 0 .25rem; font-family:'Noto Sans TC','Inter',sans-serif; font-size:var(--obs-title-size); letter-spacing:-.055em; line-height:.98; }
.calls-subtitle { color:var(--obs-muted); max-width:830px; line-height:1.7; }
.calls-actions { display:flex; flex-wrap:wrap; gap:.6rem; margin-top:1rem; align-items:center; }
.calls-filter { display:flex; flex-wrap:wrap; gap:.5rem; padding:.8rem; border:1px solid var(--obs-line); border-radius:20px; background:rgba(255,255,255,.58); }
.calls-command { display:grid; grid-template-columns:repeat(6,minmax(0,1fr)); gap:.75rem; margin-top:1rem; }
.calls-signal { padding:.9rem; border:1px solid var(--obs-line); border-radius:20px; background:rgba(255,255,255,.62); min-height:116px; }
.calls-label { color:var(--obs-muted); font-size:.72rem; letter-spacing:.1em; text-transform:uppercase; }
.calls-value { display:block; margin-top:.28rem; font-size:var(--obs-value-size); font-weight:880; letter-spacing:-.045em; }
.calls-note { color:var(--obs-muted); font-size:.8rem; margin-top:.25rem; }
.calls-grid { display:grid; grid-template-columns:minmax(0,1.25fr) minmax(330px,.75fr); gap:1rem; margin-top:1rem; }
.calls-stack { display:grid; gap:1rem; }
.calls-panel-head, .calls-table-title { display:flex; justify-content:space-between; align-items:flex-start; gap:1rem; padding:1.05rem 1.1rem .25rem; }
.calls-panel-title, .calls-table-title h3 { margin:.15rem 0 0; font-size:1.1rem; font-weight:850; letter-spacing:-.025em; }
.calls-panel-body { padding:1rem 1.1rem 1.1rem; }
.calls-chart { height:280px; }
.calls-mini-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.7rem; }
.calls-mini { padding:.85rem; border:1px solid var(--obs-line); border-radius:18px; background:rgba(255,255,255,.58); }
.calls-mini strong { display:block; margin-top:.24rem; font-size:1.4rem; letter-spacing:-.04em; }
.calls-table-shell { overflow:hidden; margin-top:1rem; }
.status-good { color:var(--obs-green); } .status-warn { color:var(--obs-amber); } .status-bad { color:var(--obs-red); } .status-blue { color:var(--obs-blue); }
@media (max-width: 1180px) { .calls-command { grid-template-columns:repeat(3,minmax(0,1fr)); } .calls-grid { grid-template-columns:1fr; } }
@media (max-width: 720px) { .calls-command, .calls-mini-grid { grid-template-columns:1fr; } }
</style>
{% set total = summary.total_calls or 0 %}
{% set errors = summary.error_calls or 0 %}
{% set error_rate = (errors / total * 100) if total > 0 else 0 %}
{% set rag_rate = ((summary.rag_hits or 0) / total * 100) if total > 0 else 0 %}
{% set avg_tokens = ((summary.total_tokens or 0) // (total or 1)) %}
<div class="container-fluid mt-3">
<section class="calls-hero">
<div class="calls-kicker"><i class="fas fa-chart-bar me-1"></i> AI Traffic Control · {{ hours }}h Window</div>
<h1 class="calls-title">AI 流量控制塔</h1>
<p class="calls-subtitle">這裡不是流水帳,而是 AI 中樞的飛航管制台看呼叫量、Token、成本、錯誤率、RAG 命中與 MCP 編排,並在異常時一鍵派出 Code Review Pipeline。</p>
<div class="calls-actions">
<button class="btn btn-warning btn-sm" onclick="triggerCodeReview()"><i class="fas fa-microscope me-1"></i>觸發 Code Review Pipeline</button>
<form method="get" class="calls-filter">
<select name="hours" class="form-select form-select-sm">{% for h in [1, 6, 24, 72, 168] %}<option value="{{ h }}" {% if hours == h %}selected{% endif %}>{% if h < 24 %}{{ h }} 小時{% else %}{{ (h//24) }} {% endif %}</option>{% endfor %}</select>
<select name="caller" class="form-select form-select-sm"><option value="">全部呼叫端</option>{% for c in callers %}<option value="{{ c }}" {% if caller_filter == c %}selected{% endif %}>{{ c }}</option>{% endfor %}</select>
<select name="provider" class="form-select form-select-sm"><option value="">全部供應商</option>{% for p in ['gcp_ollama','ollama_secondary','ollama_111','gemini','claude','nim','openrouter','nim_via_elephant'] %}<option value="{{ p }}" {% if provider_filter == p %}selected{% endif %}>{{ p }}</option>{% endfor %}</select>
<button class="btn btn-primary btn-sm">套用</button>
</form>
</div>
</section>
{% if error %}<div class="alert alert-warning mt-3"><strong><i class="fas fa-triangle-exclamation me-1"></i></strong>{{ error }}</div>{% endif %}
<section class="calls-command">
<div class="calls-signal"><div class="calls-label">Calls</div><span class="calls-value">{{ "{:,}".format(total) }}</span>{% if hourly_trend %}<canvas data-spark="calls" height="26"></canvas>{% endif %}</div>
<div class="calls-signal"><div class="calls-label">Tokens</div><span class="calls-value">{{ "{:,}".format(summary.total_tokens or 0) }}</span><div class="calls-note">{{ avg_tokens }} tk/call</div></div>
<div class="calls-signal"><div class="calls-label">Cost</div><span class="calls-value">${{ "%.2f"|format(summary.total_cost or 0) }}</span>{% if hourly_trend %}<canvas data-spark="cost" height="26"></canvas>{% endif %}</div>
<div class="calls-signal"><div class="calls-label">Latency</div><span class="calls-value">{{ summary.avg_duration or 0 }}ms</span><div class="calls-note">{{ summary.cache_hits or 0 }} cache hits</div></div>
<div class="calls-signal"><div class="calls-label">RAG Hit</div><span class="calls-value status-blue">{{ "%.1f"|format(rag_rate) }}%</span><div class="calls-note">{{ summary.rag_hits or 0 }} hits</div></div>
<div class="calls-signal"><div class="calls-label">Errors</div><span class="calls-value {% if error_rate >= 15 %}status-bad{% elif error_rate >= 5 %}status-warn{% else %}status-good{% endif %}">{{ errors }}</span>{% if hourly_trend %}<canvas data-spark="errors" height="26"></canvas>{% endif %}</div>
</section>
<section class="calls-grid">
<div class="calls-stack">
{% if hourly_trend %}
<article class="calls-panel">
<div class="calls-panel-head"><div><div class="calls-label">Hourly Traffic</div><h2 class="calls-panel-title">每小時呼叫趨勢</h2></div></div>
<div class="calls-panel-body"><div class="calls-chart"><canvas id="hourlyTrendChart"></canvas></div></div>
</article>
{% endif %}
{% if caller_richness %}
<article class="calls-table-shell">
<div class="calls-table-title"><div><div class="calls-label">Caller Orchestration</div><h3>呼叫端 × RAG × MCP 編排矩陣</h3></div></div>
<div class="table-responsive"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>呼叫端</th><th class="text-end">總呼叫</th><th class="text-end">RAG 命中</th><th class="text-end">MCP 編排</th><th class="text-end">RAG 反饋</th><th class="text-end">筆數</th></tr></thead><tbody>{% for c in caller_richness %}<tr><td><code>{{ c.caller }}</code></td><td class="text-end">{{ "{:,}".format(c.total_calls) }}</td><td class="text-end"><strong class="{% if c.rag_hit_rate >= 50 %}status-good{% elif c.rag_hit_rate >= 20 %}status-warn{% else %}text-muted{% endif %}">{{ "%.1f"|format(c.rag_hit_rate) }}%</strong> <small class="text-muted">({{ c.rag_hits }})</small></td><td class="text-end"><strong class="{% if c.mcp_rate >= 30 %}status-blue{% elif c.mcp_rate >= 10 %}status-warn{% endif %}">{{ "%.1f"|format(c.mcp_rate) }}%</strong></td><td class="text-end">{% if c.feedback_count > 0 %}<strong class="{% if c.avg_rag_feedback >= 4 %}status-good{% elif c.avg_rag_feedback >= 3 %}status-warn{% else %}status-bad{% endif %}">{{ "%.2f"|format(c.avg_rag_feedback) }}/5</strong>{% else %}<small class="text-muted"></small>{% endif %}</td><td class="text-end">{{ c.feedback_count }}</td></tr>{% endfor %}</tbody></table></div>
</article>
{% endif %}
</div>
<aside class="calls-stack">
<article class="calls-panel">
<div class="calls-panel-head"><div><div class="calls-label">Provider Split</div><h2 class="calls-panel-title">供應商分布</h2></div></div>
<div class="calls-panel-body">
<div class="calls-mini-grid">
{% for row in by_provider[:4] %}
<div class="calls-mini"><span class="calls-label">{{ row.provider }}</span><strong>{{ "{:,}".format(row.calls) }}</strong><small class="text-muted">${{ "%.2f"|format(row.cost) }} · {{ "{:,}".format(row.tokens) }} tk</small></div>
{% else %}<div class="text-muted small">尚無 provider 資料</div>{% endfor %}
</div>
</div>
</article>
{% if recent_contexts %}
<article class="calls-panel">
<div class="calls-panel-head"><div><div class="calls-label">Agent Context</div><h2 class="calls-panel-title">最近上下文</h2></div></div>
<div class="calls-panel-body">
{% for c in recent_contexts[:5] %}<div class="mb-2 pb-2 border-bottom"><span class="badge bg-info">{{ c.agent_name }}</span> <code>{{ c.context_key }}</code><div class="text-muted small mt-1">{{ c.preview }}{% if c.preview|length >= 120 %}…{% endif %}</div></div>{% endfor %}
</div>
</article>
{% endif %}
</aside>
</section>
{% if by_model %}
<section class="calls-table-shell">
<div class="calls-table-title"><div><div class="calls-label">Model Economics</div><h3>依模型細分</h3></div></div>
<div class="table-responsive"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>模型</th><th>供應商</th><th class="text-end">呼叫</th><th class="text-end">Token</th><th class="text-end">成本</th><th class="text-end">耗時</th><th class="text-end">錯誤</th></tr></thead><tbody>{% for m in by_model %}<tr><td><code>{{ m.model[:35] }}</code></td><td><span class="badge bg-secondary">{{ m.provider }}</span></td><td class="text-end">{{ "{:,}".format(m.calls) }}</td><td class="text-end">{{ "{:,}".format(m.tokens) }}</td><td class="text-end">${{ "%.4f"|format(m.cost) }}</td><td class="text-end">{{ m.avg_ms }} ms</td><td class="text-end">{% if m.errors > 0 %}<span class="status-bad">{{ m.errors }}</span>{% else %}<small class="text-muted">0</small>{% endif %}</td></tr>{% endfor %}</tbody></table></div>
</section>
{% endif %}
<section class="calls-table-shell">
<div class="calls-table-title"><div><div class="calls-label">Recent Calls</div><h3>最近呼叫 100 筆</h3></div></div>
<div class="table-responsive"><table class="table table-sm table-striped mb-0"><thead class="table-light"><tr><th>編號</th><th>時間</th><th>呼叫端</th><th>供應商</th><th>模型</th><th class="text-end">輸入</th><th class="text-end">輸出</th><th class="text-end">耗時</th><th>狀態</th><th class="text-end">成本</th><th>標記</th></tr></thead><tbody>{% for r in recent %}<tr {% if r.status not in ['ok','cache_only'] %}class="table-warning"{% endif %}><td>{{ r.id }}</td><td><small>{{ r.called_at }}</small></td><td><code>{{ r.caller }}</code></td><td><small>{{ r.provider }}</small></td><td><small>{{ r.model[:25] }}</small></td><td class="text-end">{{ r.in_tokens }}</td><td class="text-end">{{ r.out_tokens }}</td><td class="text-end">{{ r.duration_ms }}</td><td><small>{{ r.status }}</small></td><td class="text-end">${{ "%.4f"|format(r.cost) }}</td><td>{% if r.cache_hit %}<span class="badge bg-success">快取</span>{% endif %}{% if r.rag_hit %}<span class="badge bg-info">RAG</span>{% endif %}</td></tr>{% endfor %}</tbody></table></div>
</section>
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 — AI 流量控制塔</small></p>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
(function() {
const labels = {{ hourly_trend | map(attribute='hour') | list | tojson }};
const calls = {{ hourly_trend | map(attribute='calls') | list | tojson }};
const costs = {{ hourly_trend | map(attribute='cost') | list | tojson }};
const errors = {{ hourly_trend | map(attribute='errors') | list | tojson }};
const sparkColors = { calls: '#c96442', cost: '#b8792f', errors: '#b94b45' };
const sparkData = { calls, cost: costs, errors };
document.querySelectorAll('canvas[data-spark]').forEach(el => { const k = el.getAttribute('data-spark'); const data = sparkData[k]; if (!data || !data.length) return; new Chart(el, { type: 'line', data: { labels, datasets: [{ data, borderColor: sparkColors[k], backgroundColor: sparkColors[k] + '24', borderWidth: 1.4, fill: true, tension: .42, pointRadius: 0 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { enabled: false } }, scales: { x: { display: false }, y: { display: false, beginAtZero: true } } } }); });
const el = document.getElementById('hourlyTrendChart'); if (!el || !labels.length) return;
new Chart(el, { data: { labels, datasets: [ { type: 'line', label: '呼叫數', data: calls, borderColor: '#c96442', backgroundColor: 'rgba(201,100,66,.12)', tension: .35, fill: true, yAxisID: 'y' }, { type: 'line', label: '錯誤', data: errors, borderColor: '#b94b45', backgroundColor: 'rgba(185,75,69,.1)', tension: .35, yAxisID: 'y' }, { type: 'bar', label: '成本 USD', data: costs, backgroundColor: 'rgba(184,121,47,.38)', borderColor: '#b8792f', yAxisID: 'y1' } ] }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, scales: { y: { beginAtZero: true, title: { display: true, text: '次數' } }, y1: { position: 'right', beginAtZero: true, grid: { drawOnChartArea: false }, title: { display: true, text: 'USD' } } } } });
})();
async function triggerCodeReview() { if (!confirm('觸發 Code Review Pipeline\n\n會對最新 commit 跑 5 step 審查,背景執行。')) return; try { const r = await fetch('/observability/ai_calls/trigger_code_review', {method: 'POST'}); const d = await r.json(); if (d.ok) { alert(`${d.message}\n\nPipeline ID: ${d.pipeline_id}\nCommit: ${d.commit_sha}\n變更檔案: ${d.changed_files_count}`); } else { alert('❌ ' + (d.error || '觸發失敗')); } } catch (e) { console.warn('code_review_trigger_failed', e); alert('操作暫時無法完成,請稍後再試或查看系統日誌。'); } }
</script>
{% endblock %}