All checks were successful
CD Pipeline / deploy (push) Successful in 2m30s
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>
290 lines
13 KiB
HTML
290 lines
13 KiB
HTML
{% 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(含全 caller,1-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_plans(NemoTron/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_outcomes(ADR-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 %}
|