108 lines
13 KiB
HTML
108 lines
13 KiB
HTML
{% 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 4:weight ≥ 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 %}
|