Files
ewoooc/templates/admin/promotion_review.html
ogt aadbce73e5
All checks were successful
CD Pipeline / deploy (push) Successful in 1m12s
fix: sanitize observability and review UI copy
2026-06-25 15:18:15 +08:00

106 lines
10 KiB
HTML
Raw Permalink 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 %}知識晉升審核{% 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: var(--momo-font-display, "Inter", "Noto Sans TC", system-ui, sans-serif); font-size:var(--obs-title-size); letter-spacing: 0; 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: 0; }
.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: 0; }
.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: 0; }
.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>
{% import "admin/_observability_labels.html" as obs_label %}
{% 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> 知識晉升審核 · 人工審核 / 去重 / 防污染</div>
<h1 class="gate-title">知識晉升審核</h1>
<p class="gate-subtitle">先審核高權重學習片段,避免錯誤知識污染業績建議。</p>
<div class="gate-command">
<div class="gate-signal"><div class="gate-label">待審核</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">知識庫</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">30 日通過率</div><span class="gate-value status-blue">{{ "%.0f"|format(approval_rate) }}%</span><small class="text-muted">{{ approved_30d }}/{{ total_dist }} 個片段</small></div>
<div class="gate-signal"><div class="gate-label">30 日拒絕</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">審核佇列</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>權重 ≥ 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">{{ obs_label.insight(ep.episode_type) }}</span>{% if ep.source_table %}<span class="badge bg-light text-dark ms-1">{{ obs_label.source(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">{{ obs_label.insight(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>知識庫無相似度 ≥ 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">蒸餾池</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">策略權重</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">{{ obs_label.strategy(s.strategy_key) }}</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">知識庫</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>知識晉升審核</small></p>
</div>
<template id="obs-promotion-review-data">{{ episode_distribution_30d | default({}) | tojson }}</template>
<script src="{{ url_for('static', filename='js/analysis-chart-theme.js') }}"></script>
<script src="{{ url_for('static', filename='js/observability-charts.js') }}"></script>
{% endblock %}