Files
ewoooc/templates/admin/ai_calls_dashboard.html
ogt aadbce73e5
All checks were successful
CD Pipeline / deploy (push) Successful in 1m12s
fix: sanitize observability and review UI copy
2026-06-25 15:18:15 +08:00

144 lines
13 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: var(--momo-font-display, "Inter", "Noto Sans TC", system-ui, sans-serif); font-size:var(--obs-title-size); letter-spacing: 0; 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: 0; }
.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: 0; }
.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: 0; }
.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>
{% import "admin/_observability_labels.html" as obs_label %}
{% 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 流量管制 · {{ hours }} 小時視窗</div>
<h1 class="calls-title">AI 流量控制塔</h1>
<p class="calls-subtitle">看成本、錯誤率與知識命中,確保 AI 建議穩定支援業績判斷。</p>
<div class="calls-actions">
<button class="btn btn-warning btn-sm" onclick="triggerCodeReview()"><i class="fas fa-microscope me-1"></i>觸發程式碼審查管線</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 %}>{{ obs_label.provider(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">呼叫總量</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">用量</div><span class="calls-value">{{ "{:,}".format(summary.total_tokens or 0) }}</span><div class="calls-note">{{ avg_tokens }} 單位/次</div></div>
<div class="calls-signal"><div class="calls-label">成本</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">延遲</div><span class="calls-value">{{ summary.avg_duration or 0 }}ms</span><div class="calls-note">{{ summary.cache_hits or 0 }} 次快取命中</div></div>
<div class="calls-signal"><div class="calls-label">知識命中</div><span class="calls-value status-blue">{{ "%.1f"|format(rag_rate) }}%</span><div class="calls-note">{{ summary.rag_hits or 0 }} 次命中</div></div>
<div class="calls-signal"><div class="calls-label">錯誤</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">每小時流量</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">呼叫端編排</div><h3>呼叫端 × 知識與工具編排矩陣</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">知識命中</th><th class="text-end">工具編排</th><th class="text-end">知識反饋</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">供應商分布</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">{{ obs_label.provider(row.provider) }}</span><strong>{{ "{:,}".format(row.calls) }}</strong><small class="text-muted">${{ "%.2f"|format(row.cost) }} · {{ "{:,}".format(row.tokens) }} 用量</small></div>
{% else %}<div class="text-muted small">尚無供應商資料</div>{% endfor %}
</div>
</div>
</article>
{% if recent_contexts %}
<article class="calls-panel">
<div class="calls-panel-head"><div><div class="calls-label">AI 上下文</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">模型成本</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">用量</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">{{ obs_label.provider(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">最近呼叫</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_display or r.caller }}</code>{% if r.caller_display and r.caller_display != r.caller %}<br><small class="text-muted">原始:{{ r.caller }}</small>{% endif %}</td><td><small>{{ obs_label.provider(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>{{ obs_label.status(r.status, '-') }}</small></td><td class="text-end">${{ "%.4f"|format(r.cost) }}</td><td>{% for badge in r.route_badges %}<span class="badge bg-secondary me-1">{{ badge }}</span>{% endfor %}{% if r.cache_hit %}<span class="badge bg-success">快取</span>{% endif %}{% if r.rag_hit %}<span class="badge bg-info">知識命中</span>{% endif %}</td></tr>{% endfor %}</tbody></table></div>
</section>
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>AI 流量控制塔</small></p>
</div>
{% set ai_calls_payload = {
'labels': hourly_trend | map(attribute='hour') | list,
'calls': hourly_trend | map(attribute='calls') | list,
'costs': hourly_trend | map(attribute='cost') | list,
'errors': hourly_trend | map(attribute='errors') | list
} %}
<template id="obs-ai-calls-data">{{ ai_calls_payload | tojson }}</template>
<script src="{{ url_for('static', filename='js/analysis-chart-theme.js') }}"></script>
<script src="{{ url_for('static', filename='js/observability-charts.js') }}"></script>
{% endblock %}