Files
ewoooc/templates/admin/promotion_review.html
OoO b21b40cae2
All checks were successful
CD Pipeline / deploy (push) Successful in 1m2s
fix(observability): soften frontend error copy
2026-05-05 21:58:49 +08:00

108 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 %}RAG Promotion Gate{% endblock %}
{% block ewooo_content %}
<style>
.gate-hero, .gate-panel, .gate-table-shell, .episode-card { border:1px solid var(--obs-line); border-radius:26px; background:var(--obs-card); box-shadow:0 16px 38px rgba(70,46,28,.08); }
.gate-hero { padding:clamp(1.2rem,2.4vw,2rem); 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)); }
.gate-kicker { color:var(--obs-accent); font-size:.76rem; letter-spacing:.13em; text-transform:uppercase; font-weight:850; }
.gate-title { margin:.45rem 0 .25rem; font-family:'Noto Sans TC','Inter',sans-serif; font-size:var(--obs-title-size); letter-spacing:-.055em; line-height:.98; }
.gate-subtitle { color:var(--obs-muted); max-width:880px; line-height:1.7; }
.gate-command { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:.75rem; margin-top:1rem; }
.gate-signal { padding:.95rem; border:1px solid var(--obs-line); border-radius:20px; background:rgba(255,255,255,.62); }
.gate-label { color:var(--obs-muted); font-size:.72rem; letter-spacing:.1em; text-transform:uppercase; }
.gate-value { display:block; margin-top:.28rem; font-size:var(--obs-value-size); font-weight:880; letter-spacing:-.045em; }
.gate-grid { display:grid; grid-template-columns:minmax(0,1.16fr) minmax(330px,.84fr); gap:1rem; margin-top:1rem; }
.gate-stack { display:grid; gap:1rem; }
.gate-panel-head, .gate-table-title { display:flex; justify-content:space-between; align-items:flex-start; gap:1rem; padding:1.05rem 1.1rem .25rem; }
.gate-panel-title, .gate-table-title h3 { margin:.15rem 0 0; font-size:1.1rem; font-weight:850; letter-spacing:-.025em; }
.gate-panel-body { padding:1rem 1.1rem 1.1rem; }
.gate-mini-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.7rem; }
.gate-mini { padding:.85rem; border:1px solid var(--obs-line); border-radius:18px; background:rgba(255,255,255,.58); }
.gate-mini strong { display:block; margin-top:.24rem; font-size:1.35rem; letter-spacing:-.04em; }
.episode-card { overflow:hidden; margin-bottom:1rem; }
.episode-head { display:flex; justify-content:space-between; gap:1rem; padding:1rem 1.1rem .65rem; border-bottom:1px solid var(--obs-line); background:linear-gradient(90deg,rgba(255,248,239,.92),rgba(255,255,255,.72)); }
.episode-body { padding:1rem 1.1rem; }
.episode-text { white-space:pre-wrap; max-height:220px; overflow:auto; padding:1rem; border:1px solid var(--obs-line); border-radius:18px; background:rgba(255,255,255,.6); font-size:.92rem; line-height:1.65; }
.similar-box { margin-top:1rem; padding:.9rem; border:1px solid rgba(79,111,143,.18); border-left:5px solid var(--obs-blue); border-radius:18px; background:rgba(79,111,143,.08); }
.gate-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:1100px){ .gate-command{grid-template-columns:repeat(2,minmax(0,1fr));}.gate-grid{grid-template-columns:1fr;} }
@media (max-width:720px){ .gate-command,.gate-mini-grid{grid-template-columns:1fr;} .episode-head{display:block;} }
</style>
{% set total_dist = (episode_distribution_30d.values() | sum) if episode_distribution_30d else 0 %}
{% set approved_30d = episode_distribution_30d.get('approved', 0) if episode_distribution_30d else 0 %}
{% set rejected_30d = namespace(value=0) %}
{% if episode_distribution_30d %}{% for status, cnt in episode_distribution_30d.items() %}{% if status.startswith('rejected') %}{% set rejected_30d.value = rejected_30d.value + cnt %}{% endif %}{% endfor %}{% endif %}
{% set approval_rate = (approved_30d / total_dist * 100) if total_dist > 0 else 0 %}
<div class="container-fluid mt-3">
<section class="gate-hero">
<div class="gate-kicker"><i class="fas fa-brain me-1"></i> RAG Promotion Gate · Human Review / Dedup / Anti-pollution</div>
<h1 class="gate-title">RAG 知識晉升閘</h1>
<p class="gate-subtitle">這頁是 RAG 不被污染的最後關卡。高權重 learning episode 不能直接進知識庫,必須先看品質、相似知識、人工拒絕與晉升分布,再決定是否寫入 ai_insights。</p>
<div class="gate-command">
<div class="gate-signal"><div class="gate-label">Awaiting Review</div><span class="gate-value {% if episodes|length > 0 %}status-warn{% else %}status-good{% endif %}">{{ episodes|length }}</span><small class="text-muted">高權重待審片段</small></div>
<div class="gate-signal"><div class="gate-label">Knowledge Base</div><span class="gate-value">{{ kb_size or 0 }}</span><small class="text-muted">ai_insights 已晉升</small></div>
<div class="gate-signal"><div class="gate-label">30d Approval</div><span class="gate-value status-blue">{{ "%.0f"|format(approval_rate) }}%</span><small class="text-muted">{{ approved_30d }}/{{ total_dist }} episodes</small></div>
<div class="gate-signal"><div class="gate-label">Rejected 30d</div><span class="gate-value {% if rejected_30d.value > 0 %}status-bad{% else %}status-good{% endif %}">{{ rejected_30d.value }}</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="gate-grid">
<div class="gate-stack">
<article class="gate-panel">
<div class="gate-panel-head"><div><div class="gate-label">Review Queue</div><h2 class="gate-panel-title">待審核片段</h2></div><span class="badge {% if episodes %}bg-warning{% else %}bg-success{% endif %}">{{ episodes|length }} 筆</span></div>
<div class="gate-panel-body">
<p class="text-muted small mb-0"><i class="fas fa-shield-halved me-1"></i>PromotionGate Stage 4weight ≥ 0.8 必經統帥審核24 小時無回應自動降權,不直接污染知識庫。</p>
</div>
</article>
{% if episodes %}
{% for ep in episodes %}
<article class="episode-card" data-episode-id="{{ ep.id }}">
<div class="episode-head">
<div><strong>學習片段 #{{ ep.id }}</strong> <span class="badge bg-secondary ms-1">{{ ep.episode_type }}</span>{% if ep.source_table %}<span class="badge bg-light text-dark ms-1">{{ ep.source_table }}#{{ ep.source_id }}</span>{% endif %}<span class="badge bg-info ms-1">權重 {{ "%.2f"|format(ep.weight) }}</span><span class="badge bg-info ms-1">品質 {{ "%.2f"|format(ep.quality_score) }}</span></div>
<small class="text-muted">{{ ep.created_at }}</small>
</div>
<div class="episode-body">
<div class="episode-text">{{ ep.distilled_text }}</div>
{% if ep.similar_insights %}<div class="similar-box"><small class="text-muted d-block mb-2"><i class="fas fa-search me-1"></i><strong>Top 3 相似已晉升知識</strong>(用來判斷是否重複)</small><ul class="list-unstyled mb-0 small">{% for sim in ep.similar_insights %}<li class="mb-2"><span class="badge bg-light text-dark me-1">#{{ sim.id }}</span><span class="badge bg-info me-1">{{ sim.insight_type }}</span><span class="badge bg-secondary me-1">相似度 {{ "%.2f"|format(sim.similarity) }}</span><span>{{ sim.content }}{% if sim.content|length >= 180 %}…{% endif %}</span></li>{% endfor %}</ul></div>{% else %}<div class="similar-box"><small><i class="fas fa-seedling me-1"></i>知識庫無 cosine ≥ 0.7 相似內容,可能是新領域知識。</small></div>{% endif %}
</div>
<div class="card-footer text-end"><button class="btn btn-success btn-sm me-2" onclick="approveEpisode({{ ep.id }}, this)"><i class="fas fa-check me-1"></i>通過晉升</button><button class="btn btn-outline-danger btn-sm" onclick="rejectEpisode({{ ep.id }}, this)"><i class="fas fa-times me-1"></i>拒絕</button></div>
</article>
{% endfor %}
{% else %}
<div class="alert alert-info"><i class="fas fa-sparkles me-1"></i>目前無待審核片段。</div>
{% endif %}
</div>
<aside class="gate-stack">
{% if episode_distribution_30d %}
<article class="gate-panel"><div class="gate-panel-head"><div><div class="gate-label">Distillation Pool</div><h2 class="gate-panel-title">30 日狀態分布</h2></div></div><div class="gate-panel-body"><div class="obs-chart-frame obs-chart-frame-tall"><canvas id="episodeDistChart"></canvas></div></div></article>
{% endif %}
{% if strategy_weights %}
<article class="gate-panel"><div class="gate-panel-head"><div><div class="gate-label">OpenClaw Weights</div><h2 class="gate-panel-title">策略權重 Top</h2></div></div><div class="gate-panel-body"><div class="gate-mini-grid">{% for s in strategy_weights[:6] %}<div class="gate-mini"><span class="gate-label">{{ s.strategy_key[:22] }}</span><strong>{{ "%.2f"|format(s.weight) }}</strong><small class="text-muted">成功 {{ s.success }} · 失敗 {{ s.fail }}</small></div>{% endfor %}</div></div></article>
{% endif %}
</aside>
</section>
{% if latest_insights %}
<section class="gate-table-shell"><div class="gate-table-title"><div><div class="gate-label">Knowledge Base</div><h3>最近 10 筆 ai_insights</h3></div></div><div class="table-responsive"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>#</th><th>類型</th><th>期間</th><th>SKU</th><th>建立時間</th><th>預覽</th></tr></thead><tbody>{% for i in latest_insights %}<tr><td><code>#{{ i.id }}</code></td><td><span class="badge bg-info">{{ i.insight_type }}</span></td><td><small>{{ i.period or '—' }}</small></td><td><small>{{ i.product_sku or '—' }}</small></td><td><small>{{ i.created_at }}</small></td><td><small class="text-muted">{{ i.preview }}{% if i.preview|length >= 160 %}…{% endif %}</small></td></tr>{% endfor %}</tbody></table></div></section>
{% endif %}
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 — RAG 知識晉升閘</small></p>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
(function(){const dist={{ episode_distribution_30d | default({}) | tojson }};const el=document.getElementById('episodeDistChart');if(!el||!Object.keys(dist).length)return;const colorMap={pending:'#8b8077',awaiting_review:'#b8792f',approved:'#4f8a5b',rejected_quality:'#b94b45',rejected_hallucination:'#9f3330',rejected_duplicate:'#c96442',rejected_human:'#8f2925',expired:'#b8aea5'};const labelMap={pending:'待處理',awaiting_review:'待審核',approved:'已晉升',rejected_quality:'品質拒',rejected_hallucination:'幻覺拒',rejected_duplicate:'重複拒',rejected_human:'人工拒',expired:'已過期'};const keys=Object.keys(dist);new Chart(el,{type:'doughnut',data:{labels:keys.map(k=>labelMap[k]||k),datasets:[{data:keys.map(k=>dist[k]),backgroundColor:keys.map(k=>colorMap[k]||'#999'),borderWidth:1,borderColor:'#fff'}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{font:{size:11}}}}}});})();
async function approveEpisode(id,btn){btn.disabled=true;btn.innerHTML='<i class="fas fa-spinner fa-spin"></i> 處理中...';try{const r=await fetch(`/observability/promotion_review/approve/${id}`,{method:'POST'});const d=await r.json();if(d.ok){const card=document.querySelector(`.episode-card[data-episode-id="${id}"]`);card.classList.add('border-success');card.querySelector('.card-footer').innerHTML=`<span class="text-success"><i class="fas fa-check me-1"></i>已晉升 → ai_insights #${d.insight_id}(審核者:${d.approver}</span>`;}else{alert('晉升失敗:'+(d.error||'請稍後再試'));btn.disabled=false;btn.innerHTML='<i class="fas fa-check me-1"></i>通過晉升';}}catch(e){console.warn('promotion_approve_failed',e);alert('操作暫時無法完成,請稍後再試或查看系統日誌。');btn.disabled=false;btn.innerHTML='<i class="fas fa-check me-1"></i>通過晉升';}}
async function rejectEpisode(id,btn){if(!confirm(`拒絕學習片段 #${id}?此筆將永不晉升。`))return;btn.disabled=true;btn.innerHTML='<i class="fas fa-spinner fa-spin"></i> 處理中...';try{const r=await fetch(`/observability/promotion_review/reject/${id}`,{method:'POST'});const d=await r.json();if(d.ok){const card=document.querySelector(`.episode-card[data-episode-id="${id}"]`);card.classList.add('border-danger');card.querySelector('.card-footer').innerHTML='<span class="text-danger"><i class="fas fa-times me-1"></i>已拒絕rejected_human</span>';}else{alert('拒絕失敗:'+(d.error||'請稍後再試'));btn.disabled=false;btn.innerHTML='<i class="fas fa-times me-1"></i>拒絕';}}catch(e){console.warn('promotion_reject_failed',e);alert('操作暫時無法完成,請稍後再試或查看系統日誌。');btn.disabled=false;btn.innerHTML='<i class="fas fa-times me-1"></i>拒絕';}}
</script>
{% endblock %}