Files
ewoooc/templates/admin/quality_trend.html
OoO 2a3ea6f581
All checks were successful
CD Pipeline / deploy (push) Successful in 2m30s
feat(p52): topbar 觀測台健康指示燈 + RAG 反饋圓餅圖
P-1: topbar AI 觀測台 indicator(全頁可見)
- ewoooc_base.html topbar 加「🛰 AI 觀測台」icon button
- 紅色 badge 顯示告警數量(4 維度任一觸發即計數):
  • 三主機任一掛掉
  • 待審 episode > 0
  • 過去 1h 錯誤率 ≥ 30%
  • 預算任一 ≥ 90%
- 新 GET /observability/api/health_indicator
  輕量 JSON API(4 query 跨 host_health_probes/learning_episodes/
  ai_calls/ai_call_budgets)
- topbar polling 每 60s 自動刷新 + tooltip 顯示具體告警內容
- 全部頁面(包括 / 商品看板、所有觀測頁)topbar 都看得到健康狀態

P-2: quality_trend RAG 反饋圓餅圖(doughnut)
- 取代原本卡片網格佈局
- 1-5 星依綠→紅漸層著色(5=綠、3=黃、1=紅)
- 圓餅 + 右側表格雙視角(chart 配對 raw 數字)
- chart.js doughnut + tooltip 顯示筆數+佔比

效益:
- 統帥從任何頁面(不限觀測台)都能瞄一眼右上角看當前 AI 健康
- 快樂路徑:「正常」綠色 icon · 異常路徑:「紅色 badge + 數字」立即吸睛
- 圓餅圖比原網格更直觀「分布」感

Phase 38→52 累計 17 commits / 10 觀測頁 / DB 100% / 4 chart.js / 全頁 indicator。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 20:20:34 +08:00

290 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 %}Caller 反饋趨勢{% endblock %}
{% block ewooo_content %}
<div class="container-fluid mt-3">
<h2 class="mb-3"><i class="fas fa-comments me-2"></i>Caller 反饋趨勢
<small class="text-muted">過去 {{ days }} 日</small>
</h2>
{% if error %}
<div class="alert alert-warning"><strong><i class="fas fa-exclamation-triangle me-1"></i></strong> {{ error }}</div>
{% endif %}
<form method="get" class="row g-2 mb-3">
<div class="col-auto">
<select name="days" class="form-select form-select-sm">
{% for d in [7, 14, 30, 90] %}
<option value="{{ d }}" {% if days == d %}selected{% endif %}>{{ d }} 日</option>
{% endfor %}
</select>
</div>
<div class="col-auto"><button class="btn btn-primary btn-sm">查詢</button></div>
</form>
{% if episode_distribution %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-flask me-2"></i>蒸餾池狀態learning_episodes 過去 {{ days }} 日)</strong>
<small class="text-muted">資料來源learning_episodes — 展現 RAG 學習鏈路飽和度</small>
</div>
<div class="card-body">
<div class="row g-2">
{% for status, cnt in episode_distribution.items() %}
<div class="col-md-2 col-sm-4">
<div class="border rounded p-2 text-center">
<small class="text-muted d-block">
{% if status == 'pending' %}<i class="fas fa-hourglass-start"></i> 待處理
{% elif status == 'awaiting_review' %}<i class="fas fa-user-clock"></i> 待審核
{% elif status == 'approved' %}<i class="fas fa-check-circle text-success"></i> 已晉升
{% elif status == 'rejected_quality' %}<i class="fas fa-times text-danger"></i> 品質拒
{% elif status == 'rejected_hallucination' %}<i class="fas fa-times text-danger"></i> 幻覺拒
{% elif status == 'rejected_duplicate' %}<i class="fas fa-clone text-warning"></i> 重複拒
{% elif status == 'rejected_human' %}<i class="fas fa-user-times text-danger"></i> 人工拒
{% elif status == 'expired' %}<i class="fas fa-clock text-muted"></i> 已過期
{% else %}{{ status }}{% endif %}
</small>
<strong style="font-size: 1.4em;">{{ cnt }}</strong>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% if rag_root_causes %}
<div class="card mb-3" style="border-left: 4px solid #6f42c1;">
<div class="card-header bg-light">
<strong><i class="fas fa-stethoscope me-2"></i>RAG 自動根因建議</strong>
<small class="text-muted">— 對最差 3 名 caller 自動從 ai_insights 召回相似案例</small>
</div>
<div class="card-body p-2">
{% for rc in rag_root_causes %}
<div class="mb-3 p-2" style="background: #fafafa; border-radius: 6px;">
<strong><code>{{ rc.caller }}</code></strong>
<span class="badge bg-danger ms-1">{{ "%.2f"|format(rc.avg_score) }}/5</span>
<span class="badge bg-secondary ms-1">{{ rc.feedback_n }} 筆反饋</span>
<ul class="list-unstyled mt-2 mb-0 small">
{% for h in rc.hits %}
<li class="mb-1">
<span class="badge bg-info text-dark me-1">{{ h.insight_type }}</span>
<span class="badge bg-light text-dark me-1">相似度 {{ "%.2f"|format(h.similarity) }}</span>
{{ h.content }}{% if h.content|length >= 200 %}…{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if recommendations %}
<div class="card mb-3">
<div class="card-header bg-warning"><strong><i class="fas fa-lightbulb me-2"></i>智能建議</strong></div>
<div class="card-body">
<ul class="mb-0">
{% for rec in recommendations %}
<li>
{% if rec.action == 'review' %}<i class="fas fa-exclamation-triangle text-warning me-1"></i>{% else %}<i class="fas fa-check text-success me-1"></i>{% endif %}
<code>{{ rec.caller }}</code>{{ rec.reason }}
</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
<div class="card">
<div class="card-header"><strong>呼叫端 × 反饋分佈</strong>
<small class="text-muted">(平均分數升序排列,最差先看)</small>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0">
<thead class="table-light">
<tr>
<th>呼叫端</th><th class="text-end">平均</th>
<th class="text-end"><i class="fas fa-thumbs-up text-success"></i></th>
<th class="text-end"><i class="fas fa-thumbs-down text-danger"></i></th>
<th class="text-end">總數</th><th>趨勢</th><th>分布</th>
</tr>
</thead>
<tbody>
{% for caller, info in trends %}
<tr>
<td><code>{{ caller }}</code></td>
<td class="text-end">
<strong>{{ "%.2f"|format(info.avg_score) }}</strong>/5
</td>
<td class="text-end text-success">{{ info.thumbs_up }}</td>
<td class="text-end text-danger">{{ info.thumbs_down }}</td>
<td class="text-end">{{ info.total_feedback }}</td>
<td>
{% if info.trend == 'positive' %}
<span class="badge bg-success"><i class="fas fa-arrow-up me-1"></i>正向</span>
{% elif info.trend == 'negative' %}
<span class="badge bg-danger"><i class="fas fa-arrow-down me-1"></i>負向</span>
{% elif info.trend == 'neutral' %}
<span class="badge bg-secondary"><i class="fas fa-minus me-1"></i>中性</span>
{% else %}
<span class="badge bg-light text-dark"><i class="fas fa-question me-1"></i>無資料</span>
{% endif %}
</td>
<td style="width: 200px;">
<div class="progress" style="height: 8px;">
{% set pct = (info.avg_score / 5 * 100)|int %}
<div class="progress-bar
{% if pct >= 80 %}bg-success
{% elif pct >= 50 %}bg-info
{% else %}bg-danger{% endif %}"
style="width: {{ pct }}%"></div>
</div>
</td>
</tr>
{% else %}
<tr><td colspan="7" class="text-center text-muted">無反饋資料</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Phase 47 K-5 + Phase 52 P-2: RAG 整體 feedback 圓餅圖 -->
{% if rag_overall_dist %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-poll me-2"></i>RAG 整體反饋分布(過去 {{ days }} 日)</strong>
<small class="text-muted">資料來源rag_query_log.feedback_score含全 caller1-5 分)</small>
</div>
<div class="card-body">
<div class="row g-2 align-items-center">
<div class="col-md-5">
<canvas id="ragFeedbackPieChart" height="180"></canvas>
</div>
<div class="col-md-7">
<table class="table table-sm mb-0" style="font-size: 0.9em;">
<thead class="table-light">
<tr><th>星等</th><th class="text-end">筆數</th><th class="text-end">佔比</th></tr>
</thead>
<tbody>
{% set total_fb = (rag_overall_dist | sum(attribute='count')) or 1 %}
{% for r in rag_overall_dist %}
<tr>
<td>
{% for _ in range(r.score) %}<i class="fas fa-star text-warning"></i>{% endfor %}
{% for _ in range(5 - r.score) %}<i class="far fa-star text-muted"></i>{% endfor %}
<small class="ms-1 text-muted">{{ r.score }} 分</small>
</td>
<td class="text-end"><strong>{{ r.count }}</strong></td>
<td class="text-end">{{ "%.1f"|format(r.count / total_fb * 100) }}%</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Phase 47 K-5: Action Plans status 分布 -->
{% if action_plans_status %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-tasks me-2"></i>Action Plans 狀態分布(過去 {{ days }} 日)</strong>
<small class="text-muted">資料來源action_plansNemoTron/OpenClaw 的計畫產出 + 審核狀態)</small>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0" style="font-size: 0.9em;">
<thead class="table-light">
<tr><th>狀態</th><th>計畫類型</th><th class="text-end">數量</th></tr>
</thead>
<tbody>
{% for a in action_plans_status %}
<tr>
<td>
{% if a.status == 'pending' %}<span class="badge bg-warning text-dark">{{ a.status }}</span>
{% elif a.status == 'approved' %}<span class="badge bg-success">{{ a.status }}</span>
{% elif a.status == 'executed' %}<span class="badge bg-primary">{{ a.status }}</span>
{% elif a.status == 'rejected' %}<span class="badge bg-danger">{{ a.status }}</span>
{% else %}<span class="badge bg-secondary">{{ a.status }}</span>{% endif %}
</td>
<td><code>{{ a.plan_type }}</code></td>
<td class="text-end">{{ a.count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- Phase 47 K-5: Action Outcomes verdict 分布 -->
{% if action_outcomes_stats %}
<div class="card mb-3" style="border-left: 4px solid #198754;">
<div class="card-header bg-light"><strong><i class="fas fa-trophy me-2"></i>Action Outcomes 成效(過去 {{ days }} 日)</strong>
<small class="text-muted">資料來源action_outcomesADR-012 閉環學習:實際動作有效嗎?)</small>
</div>
<div class="card-body">
<div class="row g-2">
{% set total_ao = (action_outcomes_stats | sum(attribute='count')) or 1 %}
{% for r in action_outcomes_stats %}
<div class="col-md-3 col-sm-6">
<div class="border rounded p-2 text-center"
style="border-left-width: 4px !important;
border-left-color: {% if r.verdict == 'effective' %}#198754
{% elif r.verdict == 'backfired' %}#dc3545
{% else %}#6c757d{% endif %} !important;">
<small class="text-muted d-block">
{% if r.verdict == 'effective' %}<i class="fas fa-check-circle text-success"></i> 有效
{% elif r.verdict == 'backfired' %}<i class="fas fa-times-circle text-danger"></i> 適得其反
{% elif r.verdict == 'neutral' %}<i class="fas fa-minus text-secondary"></i> 無顯著效果
{% else %}{{ r.verdict }}{% endif %}
</small>
<strong style="font-size: 1.4em;">{{ r.count }}</strong>
<small class="d-block text-muted">{{ "%.1f"|format(r.count / total_ao * 100) }}%</small>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<p class="text-muted mt-3"><small>
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 52 — Caller 反饋趨勢(含 RAG 圓餅圖)
6 表深挖rag_query_log / learning_episodes / ai_insights / action_plans / action_outcomes / agent_strategy_weights
</small></p>
</div>
{% if rag_overall_dist %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
(function() {
const data = {{ rag_overall_dist | tojson }};
const el = document.getElementById('ragFeedbackPieChart');
if (!el || !data.length) return;
// 1-5 分對應綠→紅漸層
const colorMap = {1: '#dc3545', 2: '#fd7e14', 3: '#ffc107', 4: '#84c454', 5: '#198754'};
new Chart(el, {
type: 'doughnut',
data: {
labels: data.map(r => `${r.score}`),
datasets: [{
data: data.map(r => r.count),
backgroundColor: data.map(r => colorMap[r.score] || '#6c757d'),
borderWidth: 1, borderColor: '#fff',
}]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: {
legend: { position: 'right', labels: { font: { size: 12 } } },
tooltip: { callbacks: { label: c => `${c.label}: ${c.parsed} 筆 (${(c.parsed / data.reduce((a,r)=>a+r.count,0) * 100).toFixed(1)}%)` } }
}
}
});
})();
</script>
{% endif %}
{% endblock %}