142 lines
13 KiB
HTML
142 lines
13 KiB
HTML
{% 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 %}
|