Files
ewoooc/templates/admin/quality_trend.html
OoO 849e189b60
All checks were successful
CD Pipeline / deploy (push) Successful in 2m37s
feat(p45): UI/UX 升級 ewoooc_base.html + sidebar AI 觀測 7 項 + 新增總覽頁
統帥質疑:「那六頁的視覺方格 UI/UX 搞好了嗎?還有新增頁面嗎?」
回答:沒有,從 Phase 38 開始一直推遲。本 commit 補做。

I-1: 6 頁 base.html → ewoooc_base.html
- host_health / ai_calls_dashboard / budget / promotion_review /
  quality_trend / ppt_audit_history 全改
- {% extends "base.html" %} → {% extends "ewoooc_base.html" %}
- {% block content %} → {% block ewooo_content %}
- 自動繼承:sidebar 240px / topbar 64px / fonts (Inter+JetBrains+Noto Sans TC)
  / ewoooc-tokens.css / ewoooc-shell.css / search box / 米色背景

I-2: _ewoooc_shell.html 加「AI 觀測」nav group
- 7 個項目:觀測台總覽 / 主機健康 / AI 呼叫 / 預算控管 /
  RAG 晉升審核 / 反饋趨勢 / PPT 視覺審核
- 對應 active_page='obs_*',正確高亮
- 編號 07-13(系統管理改 14)

I-3: 新增頁面 /observability/ + /observability/overview
- routes/admin_observability_routes.py::observability_overview
- 單頁聚合 8 表跨 JOIN 的 KPI:
  • 三主機 24h 在線率(host_health_probes,per host card)
  • AI 呼叫 24h(ai_calls:total/tokens/cost/error rate/RAG hit/cache hit)
  • 當月成本累計
  • 預算告警(ratio ≥ alert_pct 自動列表)
  • AIOps 7d(incidents + heal_logs:自癒成功率)
  • MCP 24h(mcp_calls:tool 呼叫 + cache 率 + cost)
  • RAG 學習 30d(learning_episodes:待審 + 晉升率)
  • PPT 視覺審核 7d(ppt_audit_results:通過率)
  • 6 大子頁入口卡(含一行說明)
- 對應 Phase 44 daily Telegram summary 的 web 版本
- 全部失敗安全(個別 query 失敗只跳過該卡,不擋整頁)

升級對應:
- UI 框架:base.html → ewoooc_base.html (sidebar + topbar + token css 已生效)
- 設計憲法:8 卡片 + 8 表跨 JOIN 全景 + 一頁式總覽
- 入口:sidebar 7 項 + 觀測台首頁
- 資料表覆蓋:4 表(Phase 38)→ 8 表(Phase 45)

注意:完整 design token 重塑(Bootstrap class → --momo-* token / 焦糖橘)
留待後續 phase;本 commit 重點是「框架升級 + 新總覽頁」。

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

158 lines
6.7 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>
<p class="text-muted mt-3"><small>
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 29 — Caller 反饋趨勢
</small></p>
</div>
{% endblock %}