402 lines
18 KiB
HTML
402 lines
18 KiB
HTML
{% extends "ewoooc_base.html" %}
|
||
|
||
{% block title %}PPT 視覺 QA 產線{% endblock %}
|
||
|
||
{% block ewooo_content %}
|
||
<style>
|
||
.ppt-hero, .ppt-panel, .ppt-table-shell {
|
||
border: 1px solid var(--obs-line);
|
||
border-radius: 26px;
|
||
background: var(--obs-card);
|
||
box-shadow: 0 16px 38px rgba(70, 46, 28, .08);
|
||
}
|
||
.ppt-hero {
|
||
padding: 1.4rem;
|
||
background: radial-gradient(circle at 12% 14%, rgba(201,100,66,.18), transparent 24rem),
|
||
radial-gradient(circle at 88% 8%, rgba(79,111,143,.14), transparent 22rem),
|
||
linear-gradient(135deg, rgba(255,248,239,.98), rgba(255,255,255,.74));
|
||
}
|
||
.ppt-kicker {
|
||
color: var(--obs-accent);
|
||
font-size: .76rem;
|
||
letter-spacing: .13em;
|
||
text-transform: uppercase;
|
||
font-weight: 850;
|
||
}
|
||
.ppt-title {
|
||
margin: .45rem 0 .25rem;
|
||
font-family: 'Noto Sans TC', 'Inter', sans-serif;
|
||
font-size: var(--obs-title-size);
|
||
letter-spacing: -0.055em;
|
||
line-height: .98;
|
||
}
|
||
.ppt-subtitle {
|
||
color: var(--obs-muted);
|
||
max-width: 860px;
|
||
line-height: 1.7;
|
||
}
|
||
.ppt-command {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0,1fr));
|
||
gap: .75rem;
|
||
margin-top: 1rem;
|
||
}
|
||
.ppt-signal {
|
||
padding: .95rem;
|
||
border: 1px solid var(--obs-line);
|
||
border-radius: 20px;
|
||
background: rgba(255,255,255,.62);
|
||
}
|
||
.ppt-label {
|
||
color: var(--obs-muted);
|
||
font-size: .72rem;
|
||
letter-spacing: .1em;
|
||
text-transform: uppercase;
|
||
}
|
||
.ppt-value {
|
||
display: block;
|
||
margin-top: .28rem;
|
||
font-size: var(--obs-value-size);
|
||
font-weight: 880;
|
||
letter-spacing: -.045em;
|
||
}
|
||
.ppt-toolbar {
|
||
margin-top: 1rem;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: .75rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
.ppt-type-tabs {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: .45rem;
|
||
}
|
||
.ppt-type-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: .35rem;
|
||
}
|
||
.ppt-grid {
|
||
display: grid;
|
||
grid-template-columns: minmax(0,1.2fr) minmax(330px,.8fr);
|
||
gap: 1rem;
|
||
margin-top: 1rem;
|
||
}
|
||
.ppt-stack { display: grid; gap: 1rem; }
|
||
.ppt-panel-head, .ppt-table-title {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
gap: 1rem;
|
||
padding: 1.05rem 1.1rem .25rem;
|
||
}
|
||
.ppt-panel-title, .ppt-table-title h3 {
|
||
margin: .15rem 0 0;
|
||
font-size: 1.1rem;
|
||
font-weight: 850;
|
||
letter-spacing: -.025em;
|
||
}
|
||
.ppt-panel-body {
|
||
padding: 1rem 1.1rem 1.1rem;
|
||
}
|
||
.ppt-table-shell {
|
||
overflow: hidden;
|
||
margin-top: 1rem;
|
||
}
|
||
.ppt-table-shell .table {
|
||
min-width: 760px;
|
||
}
|
||
.ppt-mini-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0,1fr));
|
||
gap: .7rem;
|
||
}
|
||
.ppt-mini {
|
||
padding: .85rem;
|
||
border: 1px solid var(--obs-line);
|
||
border-radius: 18px;
|
||
background: rgba(255,255,255,.58);
|
||
}
|
||
.ppt-mini strong {
|
||
display: block;
|
||
margin-top: .24rem;
|
||
font-size: 1.35rem;
|
||
letter-spacing: -.04em;
|
||
}
|
||
.fix-card {
|
||
padding: .85rem;
|
||
border: 1px solid var(--obs-line);
|
||
border-radius: 18px;
|
||
background: rgba(255,255,255,.58);
|
||
margin-bottom: .7rem;
|
||
}
|
||
.status-good { color: var(--obs-green); }
|
||
.status-warn { color: var(--obs-amber); }
|
||
.status-bad { color: var(--obs-red); }
|
||
.status-blue { color: var(--obs-blue); }
|
||
.ppt-file-actions {
|
||
display: flex;
|
||
gap: .4rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
.ppt-file-actions .btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: .3rem;
|
||
}
|
||
@media(max-width:1100px) {
|
||
.ppt-command, .ppt-mini-grid { grid-template-columns: repeat(2,minmax(0,1fr)); }
|
||
.ppt-grid { grid-template-columns: 1fr; }
|
||
}
|
||
@media(max-width:720px) {
|
||
.ppt-command { grid-template-columns: 1fr; }
|
||
}
|
||
</style>
|
||
|
||
{% import "admin/_observability_labels.html" as obs_label %}
|
||
{% set report_is_daily = report_type == 'daily' %}
|
||
|
||
<div class="container-fluid mt-3">
|
||
<section class="ppt-hero">
|
||
<div class="ppt-kicker"><i class="fas fa-search me-1"></i> PPT 視覺 QA 產線 · minicpm-v / AiderHeal / RAG 修法</div>
|
||
<h1 class="ppt-title">PPT 視覺 QA 產線</h1>
|
||
<p class="ppt-subtitle">這頁追蹤每份自動簡報是否通過視覺審核:檔案產出、minicpm-v 審核、Telegram 推送、RAG 修法建議與 AiderHeal 自動修產生器。</p>
|
||
<div class="ppt-command">
|
||
<div class="ppt-signal">
|
||
<div class="ppt-label">視覺模型</div>
|
||
<span class="ppt-value {% if vision_enabled %}status-good{% else %}status-warn{% endif %}">{{ '啟用' if vision_enabled else '停用' }}</span>
|
||
<small class="text-muted">PPT_VISION_ENABLED</small>
|
||
</div>
|
||
<div class="ppt-signal">
|
||
<div class="ppt-label">{{ report_month }} {{ selected_report_type.label }}</div>
|
||
<span class="ppt-value">{{ files|length }}</span>
|
||
<small class="text-muted">檔案數</small>
|
||
</div>
|
||
<div class="ppt-signal">
|
||
<div class="ppt-label">審核紀錄</div>
|
||
<span class="ppt-value {% if report_is_daily and audit_30d_stats and audit_30d_stats.pass_rate >= 80 %}status-good{% elif report_is_daily and audit_30d_stats and audit_30d_stats.pass_rate >= 60 %}status-warn{% elif report_is_daily and audit_30d_stats %}status-bad{% else %}status-blue{% endif %}">
|
||
{% if report_is_daily %}{{ audit_30d_stats.total if audit_30d_stats else '—' }}{% else %}—{% endif %}
|
||
</span>
|
||
<small class="text-muted">{{ report_is_daily and '僅 daily' or '切到 daily 可查看' }}</small>
|
||
</div>
|
||
<div class="ppt-signal">
|
||
<div class="ppt-label">問題數</div>
|
||
<span class="ppt-value {% if report_is_daily and audit_30d_stats and audit_30d_stats.total_issues > 0 %}status-warn{% elif report_is_daily and audit_30d_stats %}status-good{% else %}status-blue{% endif %}">
|
||
{% if report_is_daily %}{{ audit_30d_stats.total_issues if audit_30d_stats else '—' }}{% else %}—{% endif %}
|
||
</span>
|
||
<small class="text-muted">視覺問題數</small>
|
||
</div>
|
||
</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="ppt-toolbar">
|
||
<div class="d-flex align-items-center gap-2">
|
||
<a class="btn btn-sm btn-outline-secondary" href="{{ url_for('admin_observability.ppt_audit_history', month=prev_month_label, report_type=report_type) }}">
|
||
<i class="fas fa-angle-left me-1"></i>上個月
|
||
</a>
|
||
<span class="badge bg-light text-dark border py-2 px-3">{{ report_month }}</span>
|
||
{% if show_next_month %}
|
||
<a class="btn btn-sm btn-outline-secondary" href="{{ url_for('admin_observability.ppt_audit_history', month=next_month_label, report_type=report_type) }}">
|
||
下個月<i class="fas fa-angle-right ms-1"></i>
|
||
</a>
|
||
{% else %}
|
||
<button class="btn btn-sm btn-outline-secondary" type="button" disabled>下個月<i class="fas fa-angle-right ms-1"></i></button>
|
||
{% endif %}
|
||
</div>
|
||
<div class="ppt-type-tabs">
|
||
{% for opt in report_type_options %}
|
||
<a class="btn btn-sm {% if report_type == opt.key %}btn-primary{% else %}btn-outline-primary{% endif %} ppt-type-chip" href="{{ url_for('admin_observability.ppt_audit_history', month=report_month, report_type=opt.key) }}">
|
||
{{ opt.label }}
|
||
</a>
|
||
{% endfor %}
|
||
</div>
|
||
</section>
|
||
<section class="ppt-grid">
|
||
<div class="ppt-stack">
|
||
<article class="ppt-table-shell">
|
||
<div class="ppt-table-title">
|
||
<div>
|
||
<div class="ppt-label">審核歷史</div>
|
||
<h3>視覺審核歷史({{ report_month }})</h3>
|
||
</div>
|
||
</div>
|
||
<div class="table-responsive">
|
||
{% if report_is_daily %}
|
||
<table class="table table-sm mb-0">
|
||
<thead class="table-light">
|
||
<tr><th>時間</th><th>檔名</th><th>結果</th><th class="text-end">問題</th><th class="text-end">信心</th><th class="text-end">耗時</th><th>錯誤</th><th>動作</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for r in audit_records %}
|
||
<tr>
|
||
<td><small>{{ r.audited_at }}</small></td>
|
||
<td><code>{{ r.pptx_filename }}</code></td>
|
||
<td>
|
||
{% if r.audit_status == 'passed' %}<span class="badge bg-success">通過</span>
|
||
{% elif r.audit_status == 'failed' %}<span class="badge bg-warning">有問題</span>
|
||
{% elif r.audit_status == 'error' %}<span class="badge bg-danger">錯誤</span>
|
||
{% elif r.audit_status == 'skipped' %}<span class="badge bg-secondary">跳過</span>
|
||
{% else %}<span class="badge bg-light text-dark">{{ r.audit_status }}</span>{% endif %}
|
||
</td>
|
||
<td class="text-end">{{ r.issues_count }}</td>
|
||
<td class="text-end">{{ "%.2f"|format(r.confidence) }}</td>
|
||
<td class="text-end">{{ r.duration_ms }}</td>
|
||
<td><small class="text-muted">{{ (r.error_msg or '')[:80] }}</small></td>
|
||
<td>{% if r.audit_status in ('failed','error') %}<button class="btn btn-sm btn-outline-warning" onclick="triggerAiderHeal({{ r.pptx_filename|tojson }}, {{ (r.error_msg or '')|tojson }})"><i class="fas fa-wrench me-1"></i>AiderHeal</button>{% endif %}</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="8" class="text-center text-muted">目前無 daily 審核歷史;請確認 {{ report_month }} 是否已完成 22:00 排程。</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<div class="p-3">
|
||
<div class="ppt-empty p-3">
|
||
<div class="mb-2"><span class="badge bg-info text-dark">非每日型資料</span></div>
|
||
<p class="mb-2"><strong>只有「每日日報」會進入視覺審核流程。</strong></p>
|
||
<p class="mb-2">目前此頁只顯示每日以外的簡報檔案;若要追蹤視覺結果,請切到「每日日報」。</p>
|
||
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('admin_observability.ppt_audit_history', month=report_month, report_type='daily') }}">
|
||
<i class="fas fa-calendar-day me-1"></i>切到每日日報
|
||
</a>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</article>
|
||
<article class="ppt-table-shell">
|
||
<div class="ppt-table-title">
|
||
<div>
|
||
<div class="ppt-label">已產檔案</div>
|
||
<h3>{{ report_month }} {{ selected_report_type.label }}</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">KB</th><th>修改時間</th><th>狀態</th><th>操作</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for f in files %}
|
||
<tr>
|
||
<td><code>{{ f.name }}</code></td>
|
||
<td class="text-end">{{ f.size_kb }}</td>
|
||
<td><small>{{ f.mtime }}</small></td>
|
||
<td><small class="text-muted">22:00 排程掃描</small></td>
|
||
<td>
|
||
<div class="ppt-file-actions">
|
||
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('admin_observability.ppt_audit_file', filename=f.name) }}" target="_blank" rel="noopener">
|
||
<i class="fas fa-file-powerpoint me-1"></i>開啟
|
||
</a>
|
||
<a class="btn btn-outline-secondary btn-sm" href="{{ url_for('admin_observability.ppt_audit_file', filename=f.name, action='download') }}">
|
||
<i class="fas fa-download me-1"></i>下載
|
||
</a>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="5" class="text-center text-muted">本月無 {{ selected_report_type.label }} 簡報</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
<aside class="ppt-stack">
|
||
{% if report_is_daily %}
|
||
{% if audit_30d_stats and audit_30d_stats.total > 0 %}
|
||
<article class="ppt-panel">
|
||
<div class="ppt-panel-head">
|
||
<div><div class="ppt-label">{{ report_month }} 審核分布</div><h2 class="ppt-panel-title">審核結果分布</h2></div>
|
||
</div>
|
||
<div class="ppt-panel-body">
|
||
<div class="obs-chart-frame"><canvas id="pptAuditPieChart"></canvas></div>
|
||
<div class="ppt-mini-grid mt-3">
|
||
<div class="ppt-mini"><span class="ppt-label">通過</span><strong class="status-good">{{ audit_30d_stats.passed }}</strong></div>
|
||
<div class="ppt-mini"><span class="ppt-label">失敗</span><strong class="{% if audit_30d_stats.failed > 0 %}status-warn{% endif %}">{{ audit_30d_stats.failed }}</strong></div>
|
||
<div class="ppt-mini"><span class="ppt-label">錯誤</span><strong class="{% if audit_30d_stats.error > 0 %}status-bad{% endif %}">{{ audit_30d_stats.error }}</strong></div>
|
||
<div class="ppt-mini"><span class="ppt-label">信心分</span><strong>{{ "%.2f"|format(audit_30d_stats.avg_confidence) }}</strong></div>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
{% endif %}
|
||
{% if top_failure_files %}
|
||
<article class="ppt-panel">
|
||
<div class="ppt-panel-head">
|
||
<div><div class="ppt-label">失敗熱點</div><h2 class="ppt-panel-title">Top 失敗檔案</h2></div>
|
||
</div>
|
||
<div class="ppt-panel-body">
|
||
{% for f in top_failure_files %}<div class="fix-card"><code>{{ f.filename }}</code><div class="d-flex justify-content-between mt-1"><small class="text-muted">{{ f.last_audit }}</small><span class="badge bg-warning">{{ f.attempts }} 次</span></div><small class="text-muted">問題 {{ f.total_issues }}</small></div>{% endfor %}
|
||
</div>
|
||
</article>
|
||
{% endif %}
|
||
{% if not (audit_30d_stats and audit_30d_stats.total > 0) %}
|
||
<article class="ppt-panel">
|
||
<div class="ppt-panel-head">
|
||
<div><div class="ppt-label">審核摘要</div><h2 class="ppt-panel-title">本月審核狀態</h2></div>
|
||
</div>
|
||
<div class="ppt-panel-body">
|
||
<div class="ppt-empty p-3">
|
||
<p class="mb-1"><strong>尚未形成 daily 審核統計。</strong></p>
|
||
<p class="mb-0">排程完成後,這裡會顯示本月通過率、失敗檔案與修復建議。</p>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
{% endif %}
|
||
{% endif %}
|
||
</aside>
|
||
</section>
|
||
|
||
{% if rag_fixes %}
|
||
<section class="ppt-panel mt-3">
|
||
<div class="ppt-panel-head">
|
||
<div><div class="ppt-label">RAG 修法建議</div><h2 class="ppt-panel-title">RAG 自動修法建議</h2></div>
|
||
</div>
|
||
<div class="ppt-panel-body">
|
||
{% for fix in rag_fixes %}
|
||
<div class="fix-card">
|
||
<strong><code>{{ fix.pptx_filename }}</code></strong><small class="text-muted ms-2">{{ fix.audited_at }}</small>
|
||
<div class="small status-bad mt-1">{{ fix.error_msg }}</div>
|
||
<ul class="list-unstyled mt-2 mb-0 small">
|
||
{% for h in fix.hits %}
|
||
<li class="mb-1">
|
||
<span class="badge bg-info me-1">{{ obs_label.insight(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>
|
||
</section>
|
||
{% endif %}
|
||
{% if not vision_enabled %}
|
||
<div class="alert alert-info mt-3">
|
||
<strong>為什麼這頁空?</strong>
|
||
<ul class="mb-0 small mt-2">
|
||
<li>PPT_VISION_ENABLED=false</li>
|
||
<li>188 主機需安裝 LibreOffice</li>
|
||
<li>需 Ollama 拉取 minicpm-v 模型</li>
|
||
<li>啟用後每日 22:00 排程寫入 ppt_audit_results</li>
|
||
</ul>
|
||
</div>
|
||
{% elif files|length == 0 %}
|
||
<div class="alert alert-warning mt-3">
|
||
<strong>本月無資料</strong>
|
||
<ul class="mb-0 small mt-2">
|
||
<li>若為「每日日報」,檢查 {{ report_month }} 是否由 188 排程成功落盤。</li>
|
||
<li>若為「週報 / 月報 / 策略 / 競品 / 促銷」,請確認 Telegram 任務是否有對應產出。</li>
|
||
<li>可回到上方月份切換器看先前月份,或調整報表類型重新查詢。</li>
|
||
</ul>
|
||
</div>
|
||
{% endif %}
|
||
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Ollama 優先策略 v5.0 — PPT 視覺 QA 產線</small></p>
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script><script>(function(){const stats={{ audit_30d_stats | default({}) | tojson }};const el=document.getElementById('pptAuditPieChart');if(!el||!stats.total)return;const data=[{label:'通過',value:stats.passed||0,color:'#4f8a5b'},{label:'失敗',value:stats.failed||0,color:'#b8792f'},{label:'錯誤',value:stats.error||0,color:'#b94b45'},{label:'跳過',value:stats.skipped||0,color:'#8b8077'}].filter(d=>d.value>0);if(!data.length)return;new Chart(el,{type:'doughnut',data:{labels:data.map(d=>d.label),datasets:[{data:data.map(d=>d.value),backgroundColor:data.map(d=>d.color),borderWidth:1,borderColor:'#fff'}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{font:{size:12}}}}}});})();
|
||
async function triggerAiderHeal(pptxFilename,errorMsg){if(!confirm(`觸發 AiderHeal 自動修復?\n\n檔案:${pptxFilename}\n錯誤:${(errorMsg||'').substring(0,200)}`))return;try{const r=await fetch('/observability/ppt_audit/trigger_aider_heal',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({pptx_filename:pptxFilename,error_msg:errorMsg||''})});const d=await r.json();if(d.ok){alert(`✅ AiderHeal 已派出\n動作:${d.action||'—'}\n訊息:${d.message||''}`);}else{alert('❌ '+(d.error||d.message||'觸發失敗'));}}catch(e){console.warn('ppt_audit_trigger_aider_heal_failed',e);alert('操作暫時無法完成,請稍後再試或查看系統日誌。');}}
|
||
</script>
|
||
{% endblock %}
|