Files
ewoooc/templates/admin/ai_calls_dashboard.html
ogt 0808063133
Some checks failed
CD Pipeline / deploy (push) Failing after 1m7s
fix: keep ai call context matrix visible
2026-06-26 07:47:54 +08:00

142 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 %}>{{ obs_label.caller(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 %}
<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><span>{{ obs_label.caller(c.caller) }}</span></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>{% else %}<tr><td colspan="6" class="text-muted py-3">近期尚無足夠情境資料,先看上方錯誤、成本與知識命中。</td></tr>{% endfor %}</tbody></table></div>
</article>
</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">建議素材</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">{{ obs_label.caller(c.agent_name) }}</span><div class="text-muted small mt-1">已納入近期業績判斷素材</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 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><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 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><span>{{ r.caller_display or '營運建議流程' }}</span></td><td><small>{{ obs_label.provider(r.provider) }}</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 %}