This commit is contained in:
@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.122"
|
||||
SYSTEM_VERSION = "V10.123"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
144
templates/admin/_observability_labels.html
Normal file
144
templates/admin/_observability_labels.html
Normal file
@@ -0,0 +1,144 @@
|
||||
{% macro strategy(value, fallback='未分類策略') -%}
|
||||
{%- set labels = {
|
||||
'product_pick': '選品攻擊',
|
||||
'price_recommendation': '價格建議',
|
||||
'price_adjustment': '價格調整',
|
||||
'price_reduction': '降價攻擊',
|
||||
'price_drop': '降價攻擊',
|
||||
'discount_attack': '降價攻擊',
|
||||
'price_increase': '漲價防守',
|
||||
'margin_repair': '毛利修復',
|
||||
'inventory_clearance': '庫存去化',
|
||||
'competitor_response': '競品回應',
|
||||
'competitor_check': '競品複核',
|
||||
'promotion': '活動促銷',
|
||||
'stockout': '缺貨處理',
|
||||
'repricing': '價格重定',
|
||||
'watch': '觀察',
|
||||
'maintain': '維持價格',
|
||||
'unknown': '未分類策略'
|
||||
} -%}
|
||||
{%- if value -%}{{ labels.get(value, value|replace('_', ' ')) }}{%- else -%}{{ fallback }}{%- endif -%}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro status(value, fallback='未分類') -%}
|
||||
{%- set labels = {
|
||||
'pending': '待處理',
|
||||
'awaiting_review': '待審核',
|
||||
'approved': '已核准',
|
||||
'rejected': '已拒絕',
|
||||
'rejected_quality': '品質拒絕',
|
||||
'rejected_hallucination': '幻覺拒絕',
|
||||
'rejected_duplicate': '重複拒絕',
|
||||
'rejected_human': '人工拒絕',
|
||||
'expired': '已過期',
|
||||
'done': '已完成',
|
||||
'completed': '已完成',
|
||||
'running': '執行中',
|
||||
'success': '成功',
|
||||
'failed': '失敗',
|
||||
'matched': '已比對',
|
||||
'unmatched': '未比對',
|
||||
'ok': '正常',
|
||||
'cache_only': '只用快取',
|
||||
'passed': '已通過',
|
||||
'error': '錯誤',
|
||||
'skipped': '已跳過',
|
||||
'unknown': '未分類'
|
||||
} -%}
|
||||
{%- if value -%}{{ labels.get(value, value|replace('_', ' ')) }}{%- else -%}{{ fallback }}{%- endif -%}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro verdict(value, fallback='未分類') -%}
|
||||
{%- set labels = {
|
||||
'effective': '有效',
|
||||
'success': '成功',
|
||||
'positive': '正向',
|
||||
'backfired': '反效果',
|
||||
'negative': '負向',
|
||||
'failed': '失敗',
|
||||
'neutral': '中性',
|
||||
'pending': '待回收',
|
||||
'inconclusive': '尚未定論',
|
||||
'no_data': '無資料',
|
||||
'unknown': '未分類'
|
||||
} -%}
|
||||
{%- if value -%}{{ labels.get(value, value|replace('_', ' ')) }}{%- else -%}{{ fallback }}{%- endif -%}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro plan_type(value, fallback='未分類計畫') -%}
|
||||
{%- set labels = {
|
||||
'action_plan': '行動計畫',
|
||||
'price_adjustment': '價格調整',
|
||||
'product_pick': '選品攻擊',
|
||||
'promotion_review': '活動複核',
|
||||
'stockout_followup': '缺貨跟進',
|
||||
'competitor_check': '競品複核',
|
||||
'margin_repair': '毛利修復',
|
||||
'strategy_review': '策略複核',
|
||||
'quality_review': '品質複核'
|
||||
} -%}
|
||||
{%- if value -%}{{ labels.get(value, value|replace('_', ' ')) }}{%- else -%}{{ fallback }}{%- endif -%}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro metric(value, fallback='未分類指標') -%}
|
||||
{%- set labels = {
|
||||
'sales': '銷售',
|
||||
'revenue': '業績',
|
||||
'margin': '毛利',
|
||||
'profit': '毛利',
|
||||
'conversion': '轉換',
|
||||
'price': '價格',
|
||||
'stock': '庫存',
|
||||
'orders': '訂單',
|
||||
'unknown': '未分類指標'
|
||||
} -%}
|
||||
{%- if value -%}{{ labels.get(value, value|replace('_', ' ')) }}{%- else -%}{{ fallback }}{%- endif -%}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro insight(value, fallback='未分類洞察') -%}
|
||||
{%- set labels = {
|
||||
'product_pick': '選品攻擊',
|
||||
'price_recommendation': '價格建議',
|
||||
'competitor_price': '競品價格',
|
||||
'sales_anomaly': '業績異常',
|
||||
'budget_strategy': '預算策略',
|
||||
'rag_feedback': 'RAG 反饋',
|
||||
'ppt_audit': 'PPT 審核',
|
||||
'quality_issue': '品質問題',
|
||||
'promotion': '活動促銷',
|
||||
'market_signal': '市場訊號',
|
||||
'strategy': '策略洞察',
|
||||
'unknown': '未分類洞察'
|
||||
} -%}
|
||||
{%- if value -%}{{ labels.get(value, value|replace('_', ' ')) }}{%- else -%}{{ fallback }}{%- endif -%}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro provider(value, fallback='未分類供應商') -%}
|
||||
{%- set labels = {
|
||||
'gcp_ollama': '主力 Ollama',
|
||||
'ollama_secondary': '備援 Ollama',
|
||||
'ollama_111': '111 Ollama',
|
||||
'nim_via_elephant': 'NIM Elephant',
|
||||
'gemini': 'Gemini',
|
||||
'claude': 'Claude',
|
||||
'nim': 'NIM',
|
||||
'openrouter': 'OpenRouter',
|
||||
'unknown': '未分類供應商'
|
||||
} -%}
|
||||
{%- if value -%}{{ labels.get(value, value|replace('_', ' ')) }}{%- else -%}{{ fallback }}{%- endif -%}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro source(value, fallback='未分類來源') -%}
|
||||
{%- set labels = {
|
||||
'ai_insights': 'AI 知識庫',
|
||||
'ai_learning_episodes': '學習片段',
|
||||
'ai_price_recommendations': 'AI 價格建議',
|
||||
'action_plans': '行動計畫',
|
||||
'action_outcomes': '實際結果',
|
||||
'competitor_match_attempts': '競品比對',
|
||||
'competitor_price_history': '競品價格歷史',
|
||||
'ppt_audit_results': 'PPT 審核結果'
|
||||
} -%}
|
||||
{%- if value -%}{{ labels.get(value, value|replace('_', ' ')) }}{%- else -%}{{ fallback }}{%- endif -%}
|
||||
{%- endmacro %}
|
||||
@@ -42,6 +42,7 @@
|
||||
@media (max-width: 720px) { .calls-command, .calls-mini-grid { grid-template-columns:1fr; } }
|
||||
</style>
|
||||
|
||||
{% import "admin/_observability_labels.html" as obs_label %}
|
||||
{% set total = summary.total_calls or 0 %}
|
||||
{% set errors = summary.error_calls or 0 %}
|
||||
{% set error_rate = (errors / total * 100) if total > 0 else 0 %}
|
||||
@@ -58,7 +59,7 @@
|
||||
<form method="get" class="calls-filter">
|
||||
<select name="hours" class="form-select form-select-sm">{% for h in [1, 6, 24, 72, 168] %}<option value="{{ h }}" {% if hours == h %}selected{% endif %}>{% if h < 24 %}{{ h }} 小時{% else %}{{ (h//24) }} 天{% endif %}</option>{% endfor %}</select>
|
||||
<select name="caller" class="form-select form-select-sm"><option value="">全部呼叫端</option>{% for c in callers %}<option value="{{ c }}" {% if caller_filter == c %}selected{% endif %}>{{ c }}</option>{% endfor %}</select>
|
||||
<select name="provider" class="form-select form-select-sm"><option value="">全部供應商</option>{% for p in ['gcp_ollama','ollama_secondary','ollama_111','gemini','claude','nim','openrouter','nim_via_elephant'] %}<option value="{{ p }}" {% if provider_filter == p %}selected{% endif %}>{{ p }}</option>{% endfor %}</select>
|
||||
<select name="provider" class="form-select form-select-sm"><option value="">全部供應商</option>{% for p in ['gcp_ollama','ollama_secondary','ollama_111','gemini','claude','nim','openrouter','nim_via_elephant'] %}<option value="{{ p }}" {% if provider_filter == p %}selected{% endif %}>{{ obs_label.provider(p) }}</option>{% endfor %}</select>
|
||||
<button class="btn btn-primary btn-sm">套用</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -98,7 +99,7 @@
|
||||
<div class="calls-panel-body">
|
||||
<div class="calls-mini-grid">
|
||||
{% for row in by_provider[:4] %}
|
||||
<div class="calls-mini"><span class="calls-label">{{ row.provider }}</span><strong>{{ "{:,}".format(row.calls) }}</strong><small class="text-muted">${{ "%.2f"|format(row.cost) }} · {{ "{:,}".format(row.tokens) }} tk</small></div>
|
||||
<div class="calls-mini"><span class="calls-label">{{ obs_label.provider(row.provider) }}</span><strong>{{ "{:,}".format(row.calls) }}</strong><small class="text-muted">${{ "%.2f"|format(row.cost) }} · {{ "{:,}".format(row.tokens) }} 權杖</small></div>
|
||||
{% else %}<div class="text-muted small">尚無供應商資料</div>{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,13 +119,13 @@
|
||||
{% if by_model %}
|
||||
<section class="calls-table-shell">
|
||||
<div class="calls-table-title"><div><div class="calls-label">模型成本</div><h3>依模型細分</h3></div></div>
|
||||
<div class="table-responsive"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>模型</th><th>供應商</th><th class="text-end">呼叫</th><th class="text-end">權杖</th><th class="text-end">成本</th><th class="text-end">耗時</th><th class="text-end">錯誤</th></tr></thead><tbody>{% for m in by_model %}<tr><td><code>{{ m.model[:35] }}</code></td><td><span class="badge bg-secondary">{{ m.provider }}</span></td><td class="text-end">{{ "{:,}".format(m.calls) }}</td><td class="text-end">{{ "{:,}".format(m.tokens) }}</td><td class="text-end">${{ "%.4f"|format(m.cost) }}</td><td class="text-end">{{ m.avg_ms }} ms</td><td class="text-end">{% if m.errors > 0 %}<span class="status-bad">{{ m.errors }}</span>{% else %}<small class="text-muted">0</small>{% endif %}</td></tr>{% endfor %}</tbody></table></div>
|
||||
<div class="table-responsive"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>模型</th><th>供應商</th><th class="text-end">呼叫</th><th class="text-end">權杖</th><th class="text-end">成本</th><th class="text-end">耗時</th><th class="text-end">錯誤</th></tr></thead><tbody>{% for m in by_model %}<tr><td><code>{{ m.model[:35] }}</code></td><td><span class="badge bg-secondary">{{ obs_label.provider(m.provider) }}</span></td><td class="text-end">{{ "{:,}".format(m.calls) }}</td><td class="text-end">{{ "{:,}".format(m.tokens) }}</td><td class="text-end">${{ "%.4f"|format(m.cost) }}</td><td class="text-end">{{ m.avg_ms }} ms</td><td class="text-end">{% if m.errors > 0 %}<span class="status-bad">{{ m.errors }}</span>{% else %}<small class="text-muted">0</small>{% endif %}</td></tr>{% endfor %}</tbody></table></div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section class="calls-table-shell">
|
||||
<div class="calls-table-title"><div><div class="calls-label">最近呼叫</div><h3>最近呼叫 100 筆</h3></div></div>
|
||||
<div class="table-responsive"><table class="table table-sm table-striped mb-0"><thead class="table-light"><tr><th>編號</th><th>時間</th><th>呼叫端</th><th>供應商</th><th>模型</th><th class="text-end">輸入</th><th class="text-end">輸出</th><th class="text-end">耗時</th><th>狀態</th><th class="text-end">成本</th><th>標記</th></tr></thead><tbody>{% for r in recent %}<tr {% if r.status not in ['ok','cache_only'] %}class="table-warning"{% endif %}><td>{{ r.id }}</td><td><small>{{ r.called_at }}</small></td><td><code>{{ r.caller }}</code></td><td><small>{{ r.provider }}</small></td><td><small>{{ r.model[:25] }}</small></td><td class="text-end">{{ r.in_tokens }}</td><td class="text-end">{{ r.out_tokens }}</td><td class="text-end">{{ r.duration_ms }}</td><td><small>{{ r.status }}</small></td><td class="text-end">${{ "%.4f"|format(r.cost) }}</td><td>{% if r.cache_hit %}<span class="badge bg-success">快取</span>{% endif %}{% if r.rag_hit %}<span class="badge bg-info">RAG</span>{% endif %}</td></tr>{% endfor %}</tbody></table></div>
|
||||
<div class="table-responsive"><table class="table table-sm table-striped mb-0"><thead class="table-light"><tr><th>編號</th><th>時間</th><th>呼叫端</th><th>供應商</th><th>模型</th><th class="text-end">輸入</th><th class="text-end">輸出</th><th class="text-end">耗時</th><th>狀態</th><th class="text-end">成本</th><th>標記</th></tr></thead><tbody>{% for r in recent %}<tr {% if r.status not in ['ok','cache_only'] %}class="table-warning"{% endif %}><td>{{ r.id }}</td><td><small>{{ r.called_at }}</small></td><td><code>{{ r.caller }}</code></td><td><small>{{ obs_label.provider(r.provider) }}</small></td><td><small>{{ r.model[:25] }}</small></td><td class="text-end">{{ r.in_tokens }}</td><td class="text-end">{{ r.out_tokens }}</td><td class="text-end">{{ r.duration_ms }}</td><td><small>{{ obs_label.status(r.status, '-') }}</small></td><td class="text-end">${{ "%.4f"|format(r.cost) }}</td><td>{% if r.cache_hit %}<span class="badge bg-success">快取</span>{% endif %}{% if r.rag_hit %}<span class="badge bg-info">RAG</span>{% endif %}</td></tr>{% endfor %}</tbody></table></div>
|
||||
</section>
|
||||
|
||||
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Ollama 優先策略 v5.0 — AI 流量控制塔</small></p>
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
@media (max-width:720px){ .gov-command,.gov-mini-grid{grid-template-columns:1fr;} }
|
||||
</style>
|
||||
|
||||
{% import "admin/_observability_labels.html" as obs_label %}
|
||||
{% set total_budget = namespace(value=0) %}{% set total_spent = namespace(value=0) %}{% set warn_count = namespace(value=0) %}{% set throttled_count = namespace(value=0) %}
|
||||
{% for r in rows %}{% set total_budget.value = total_budget.value + (r.budget_usd or 0) %}{% set total_spent.value = total_spent.value + (r.spent or 0) %}{% if r.ratio >= 0.8 %}{% set warn_count.value = warn_count.value + 1 %}{% endif %}{% if r.throttled %}{% set throttled_count.value = throttled_count.value + 1 %}{% endif %}{% endfor %}
|
||||
{% set total_ratio = (total_spent.value / total_budget.value * 100) if total_budget.value > 0 else 0 %}
|
||||
@@ -40,7 +41,7 @@
|
||||
<div class="gov-kicker"><i class="fas fa-wallet me-1"></i> AI 成本治理 · 預算 / 節流 / RAG 策略</div>
|
||||
<h1 class="gov-title">AI 成本治理艙</h1>
|
||||
<p class="gov-subtitle">這頁回答一個問題:AI 中樞花錢是否仍在治理邊界內?預算、實際支出、月底推估、節流狀態與 RAG 策略建議集中在同一個工作台。</p>
|
||||
<div class="gov-actions"><button class="btn btn-warning btn-sm" onclick="forceThrottle()"><i class="fas fa-bolt me-1"></i>立即重算節流狀態</button><span class="text-muted small">超過 110% 時不用等 排程,直接重算供應商節流。</span></div>
|
||||
<div class="gov-actions"><button class="btn btn-warning btn-sm" onclick="forceThrottle()"><i class="fas fa-bolt me-1"></i>立即重算節流狀態</button><span class="text-muted small">超過 110% 時不用等排程,直接重算供應商節流。</span></div>
|
||||
<div class="gov-command">
|
||||
<div class="gov-signal"><div class="gov-label">當月花費</div><span class="gov-value">${{ "%.2f"|format(total_spent.value) }}</span><div class="gov-note">預算 ${{ "%.2f"|format(total_budget.value) }}</div></div>
|
||||
<div class="gov-signal"><div class="gov-label">預算使用率</div><span class="gov-value {% if total_ratio >= 110 %}status-bad{% elif total_ratio >= 80 %}status-warn{% else %}status-good{% endif %}">{{ "%.0f"|format(total_ratio) }}%</span><div class="gov-note">全供應商加總</div></div>
|
||||
@@ -55,7 +56,7 @@
|
||||
<div class="gov-stack">
|
||||
<article class="gov-table-shell">
|
||||
<div class="gov-table-title"><div><div class="gov-label">預算線</div><h3>預算線與節流狀態</h3></div></div>
|
||||
<div class="table-responsive"><table class="table table-hover mb-0"><thead class="table-light"><tr><th>週期</th><th>供應商</th><th class="text-end">已花費</th><th>預算</th><th>閾值</th><th class="text-end">使用率</th><th>狀態</th><th>動作</th></tr></thead><tbody>{% for r in rows %}<tr {% if r.throttled %}class="table-danger"{% elif r.ratio >= 0.8 %}class="table-warning"{% endif %}><td><span class="badge bg-secondary">{{ r.period }}</span></td><td><code>{{ r.provider }}</code></td><td class="text-end">${{ "%.2f"|format(r.spent) }}</td><td><input type="number" step="0.01" min="0.01" value="{{ "%.2f"|format(r.budget_usd) }}" class="form-control form-control-sm budget-input" data-budget-id="{{ r.id }}" ></td><td><input type="number" min="1" max="100" value="{{ r.alert_pct }}" class="form-control form-control-sm alert-input" data-budget-id="{{ r.id }}" ></td><td class="text-end"><strong class="{% if r.ratio >= 1.10 %}status-bad{% elif r.ratio >= 0.8 %}status-warn{% else %}status-good{% endif %}">{{ "%.0f"|format(r.ratio * 100) }}%</strong></td><td>{% if r.throttled %}<span class="badge bg-danger">已節流</span>{% elif r.ratio >= 0.8 %}<span class="badge bg-warning">接近上限</span>{% else %}<span class="badge bg-success">正常</span>{% endif %}</td><td><button class="btn btn-primary btn-sm save-budget-btn" data-budget-id="{{ r.id }}" onclick="saveBudget({{ r.id }})"><i class="fas fa-save me-1"></i>儲存</button></td></tr>{% else %}<tr><td colspan="8" class="text-center text-muted">無預算資料(需先跑 migrations/025)</td></tr>{% endfor %}</tbody></table></div>
|
||||
<div class="table-responsive"><table class="table table-hover mb-0"><thead class="table-light"><tr><th>週期</th><th>供應商</th><th class="text-end">已花費</th><th>預算</th><th>閾值</th><th class="text-end">使用率</th><th>狀態</th><th>動作</th></tr></thead><tbody>{% for r in rows %}<tr {% if r.throttled %}class="table-danger"{% elif r.ratio >= 0.8 %}class="table-warning"{% endif %}><td><span class="badge bg-secondary">{{ r.period }}</span></td><td><code>{{ obs_label.provider(r.provider) }}</code></td><td class="text-end">${{ "%.2f"|format(r.spent) }}</td><td><input type="number" step="0.01" min="0.01" value="{{ "%.2f"|format(r.budget_usd) }}" class="form-control form-control-sm budget-input" data-budget-id="{{ r.id }}" ></td><td><input type="number" min="1" max="100" value="{{ r.alert_pct }}" class="form-control form-control-sm alert-input" data-budget-id="{{ r.id }}" ></td><td class="text-end"><strong class="{% if r.ratio >= 1.10 %}status-bad{% elif r.ratio >= 0.8 %}status-warn{% else %}status-good{% endif %}">{{ "%.0f"|format(r.ratio * 100) }}%</strong></td><td>{% if r.throttled %}<span class="badge bg-danger">已節流</span>{% elif r.ratio >= 0.8 %}<span class="badge bg-warning">接近上限</span>{% else %}<span class="badge bg-success">正常</span>{% endif %}</td><td><button class="btn btn-primary btn-sm save-budget-btn" data-budget-id="{{ r.id }}" onclick="saveBudget({{ r.id }})"><i class="fas fa-save me-1"></i>儲存</button></td></tr>{% else %}<tr><td colspan="8" class="text-center text-muted">無預算資料(需先跑 migrations/025)</td></tr>{% endfor %}</tbody></table></div>
|
||||
</article>
|
||||
|
||||
{% if cost_trend_30d %}
|
||||
@@ -75,11 +76,11 @@
|
||||
</section>
|
||||
|
||||
{% if budget_strategies %}
|
||||
<section class="gov-panel mt-3"><div class="gov-panel-head"><div><div class="gov-label">RAG 策略</div><h2 class="gov-panel-title">RAG 自動策略建議</h2></div></div><div class="gov-panel-body">{% for s in budget_strategies %}<div class="strategy-card"><span class="badge bg-info me-1">{{ s.insight_type }}</span><span class="badge bg-secondary me-1">相似度 {{ "%.2f"|format(s.similarity) }}</span><span>{{ s.content }}{% if s.content|length >= 240 %}…{% endif %}</span></div>{% endfor %}</div></section>
|
||||
<section class="gov-panel mt-3"><div class="gov-panel-head"><div><div class="gov-label">RAG 策略</div><h2 class="gov-panel-title">RAG 自動策略建議</h2></div></div><div class="gov-panel-body">{% for s in budget_strategies %}<div class="strategy-card"><span class="badge bg-info me-1">{{ obs_label.insight(s.insight_type) }}</span><span class="badge bg-secondary me-1">相似度 {{ "%.2f"|format(s.similarity) }}</span><span>{{ s.content }}{% if s.content|length >= 240 %}…{% endif %}</span></div>{% endfor %}</div></section>
|
||||
{% endif %}
|
||||
|
||||
{% if price_rec_7d %}
|
||||
<section class="gov-panel mt-3"><div class="gov-panel-head"><div><div class="gov-label">商業產出</div><h2 class="gov-panel-title">AI 價格決策 7 日</h2></div></div><div class="gov-panel-body"><div class="gov-mini-grid">{% for p in price_rec_7d %}<div class="gov-mini"><span class="gov-label">{{ p.strategy }}</span><strong>{{ p.count }}</strong><small class="text-muted">信心 {{ "%.2f"|format(p.avg_confidence) }}</small></div>{% endfor %}</div></div></section>
|
||||
<section class="gov-panel mt-3"><div class="gov-panel-head"><div><div class="gov-label">商業產出</div><h2 class="gov-panel-title">AI 價格決策 7 日</h2></div></div><div class="gov-panel-body"><div class="gov-mini-grid">{% for p in price_rec_7d %}<div class="gov-mini"><span class="gov-label">{{ obs_label.strategy(p.strategy) }}</span><strong>{{ p.count }}</strong><small class="text-muted">信心 {{ "%.2f"|format(p.avg_confidence) }}</small></div>{% endfor %}</div></div></section>
|
||||
{% endif %}
|
||||
|
||||
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Ollama 優先策略 v5.0 — AI 成本治理艙</small></p>
|
||||
@@ -87,8 +88,9 @@
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
<script>
|
||||
(function() { const data = {{ provider_cost_month | default([]) | tojson }}; const el = document.getElementById('providerCostPieChart'); if (!el || !data.length) return; const colors = {'gcp_ollama':'#4f8a5b','ollama_secondary':'#7aaa82','ollama_111':'#a3cfa8','gemini':'#b8792f','claude':'#4f6f8f','nim':'#6aa6a6','openrouter':'#8b8077','nim_via_elephant':'#c96442'}; new Chart(el,{type:'doughnut',data:{labels:data.map(d=>d.provider),datasets:[{data:data.map(d=>d.cost),backgroundColor:data.map((d,i)=>colors[d.provider]||`hsl(${(i*47)%360},55%,55%)`),borderWidth:1,borderColor:'#fff'}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{font:{size:11}}}}}}); })();
|
||||
(function() { const raw = {{ cost_trend_30d | tojson }}; if (!raw || !raw.length) return; const dateSet = [...new Set(raw.map(r=>r.date))].sort(); const providerSet = [...new Set(raw.map(r=>r.provider))]; const palette = ['#c96442','#b8792f','#4f8a5b','#4f6f8f','#6aa6a6','#8b8077','#a66a4a']; const datasets = providerSet.map((p,i)=>({label:p,data:dateSet.map(d=>{const row=raw.find(r=>r.date===d&&r.provider===p);return row?row.cost:0;}),backgroundColor:palette[i%palette.length]})); const el=document.getElementById('costTrend30dChart'); if(!el)return; new Chart(el,{type:'bar',data:{labels:dateSet,datasets},options:{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},scales:{x:{stacked:true},y:{stacked:true,beginAtZero:true,title:{display:true,text:'USD'}}}}}); })();
|
||||
const providerLabelMap = {gcp_ollama:'主力 Ollama',ollama_secondary:'備援 Ollama',ollama_111:'111 Ollama',gemini:'Gemini',claude:'Claude',nim:'NIM',openrouter:'OpenRouter',nim_via_elephant:'NIM Elephant'};
|
||||
(function() { const data = {{ provider_cost_month | default([]) | tojson }}; const el = document.getElementById('providerCostPieChart'); if (!el || !data.length) return; const colors = {'gcp_ollama':'#4f8a5b','ollama_secondary':'#7aaa82','ollama_111':'#a3cfa8','gemini':'#b8792f','claude':'#4f6f8f','nim':'#6aa6a6','openrouter':'#8b8077','nim_via_elephant':'#c96442'}; new Chart(el,{type:'doughnut',data:{labels:data.map(d=>providerLabelMap[d.provider]||d.provider),datasets:[{data:data.map(d=>d.cost),backgroundColor:data.map((d,i)=>colors[d.provider]||`hsl(${(i*47)%360},55%,55%)`),borderWidth:1,borderColor:'#fff'}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{font:{size:11}}}}}}); })();
|
||||
(function() { const raw = {{ cost_trend_30d | tojson }}; if (!raw || !raw.length) return; const dateSet = [...new Set(raw.map(r=>r.date))].sort(); const providerSet = [...new Set(raw.map(r=>r.provider))]; const palette = ['#c96442','#b8792f','#4f8a5b','#4f6f8f','#6aa6a6','#8b8077','#a66a4a']; const datasets = providerSet.map((p,i)=>({label:providerLabelMap[p]||p,data:dateSet.map(d=>{const row=raw.find(r=>r.date===d&&r.provider===p);return row?row.cost:0;}),backgroundColor:palette[i%palette.length]})); const el=document.getElementById('costTrend30dChart'); if(!el)return; new Chart(el,{type:'bar',data:{labels:dateSet,datasets},options:{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},scales:{x:{stacked:true},y:{stacked:true,beginAtZero:true,title:{display:true,text:'USD'}}}}}); })();
|
||||
async function forceThrottle(){if(!confirm('立即重算所有供應商的節流狀態?'))return;try{const r=await fetch('/observability/budget/force_throttle',{method:'POST'});const d=await r.json();if(d.ok){alert(`✅ 已重算:被節流的供應商 = ${(d.throttled_providers&&d.throttled_providers.length>0)?d.throttled_providers.join(', '):'(無)'}`);window.location.reload();}else{alert('❌ '+(d.error||'重算失敗'));}}catch(e){console.warn('budget_force_throttle_failed',e);alert('操作暫時無法完成,請稍後再試或查看系統日誌。');}}
|
||||
async function saveBudget(id){const budgetInput=document.querySelector(`.budget-input[data-budget-id="${id}"]`);const alertInput=document.querySelector(`.alert-input[data-budget-id="${id}"]`);const btn=document.querySelector(`.save-budget-btn[data-budget-id="${id}"]`);btn.disabled=true;btn.innerHTML='<i class="fas fa-spinner fa-spin"></i>';try{const r=await fetch(`/observability/budget/update/${id}`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({budget_usd:parseFloat(budgetInput.value),alert_pct:parseInt(alertInput.value)})});const d=await r.json();if(d.ok){btn.innerHTML='<i class="fas fa-check"></i> 已儲存';setTimeout(()=>{btn.innerHTML='<i class="fas fa-save me-1"></i>儲存';btn.disabled=false;},1500);}else{alert('更新失敗:'+(d.error||'請稍後再試'));btn.disabled=false;btn.innerHTML='<i class="fas fa-save me-1"></i>儲存';}}catch(e){console.warn('budget_save_failed',e);alert('操作暫時無法完成,請稍後再試或查看系統日誌。');btn.disabled=false;btn.innerHTML='<i class="fas fa-save me-1"></i>儲存';}}
|
||||
</script>
|
||||
|
||||
@@ -417,6 +417,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block ewooo_content %}
|
||||
{% import "admin/_observability_labels.html" as obs_label %}
|
||||
{% set rec_total = rec_by_strategy|sum(attribute='count') if rec_by_strategy else 0 %}
|
||||
{% set ns = namespace(conf_total=0, effective=0, backfired=0, neutral=0, verdict_total=0) %}
|
||||
{% for r in rec_by_strategy %}
|
||||
@@ -497,7 +498,7 @@
|
||||
|
||||
{% if unfollowed_count > 0 %}
|
||||
<section class="biz-alert-strip">
|
||||
<div><i class="fas fa-bell me-2"></i>{{ unfollowed_count }} 筆高信心 AI 價格建議尚未跟進,建議優先轉為 action_plan 或標記原因。</div>
|
||||
<div><i class="fas fa-bell me-2"></i>{{ unfollowed_count }} 筆高信心 AI 價格建議尚未跟進,建議優先轉為行動計畫或標記原因。</div>
|
||||
<span class="biz-badge warn">需人工決策</span>
|
||||
</section>
|
||||
{% endif %}
|
||||
@@ -508,7 +509,7 @@
|
||||
<div class="biz-panel-head">
|
||||
<div>
|
||||
<h3>策略族群雷達</h3>
|
||||
<p>把 AI 價格建議依 strategy 聚合,快速判斷目前主攻降價、防守或毛利修復。</p>
|
||||
<p>把 AI 價格建議依策略類型聚合,快速判斷目前主攻降價、防守或毛利修復。</p>
|
||||
</div>
|
||||
<span class="biz-badge">{{ rec_by_strategy|length }} 類策略</span>
|
||||
</div>
|
||||
@@ -517,7 +518,7 @@
|
||||
<div class="biz-strategy-grid">
|
||||
{% for r in rec_by_strategy %}
|
||||
<article class="biz-strategy-card">
|
||||
<div class="strategy">{{ r.strategy or '未分類策略' }}</div>
|
||||
<div class="strategy">{{ obs_label.strategy(r.strategy) }}</div>
|
||||
<div class="metrics">
|
||||
<div class="biz-mini-metric"><b>{{ r.count }}</b><span>建議數</span></div>
|
||||
<div class="biz-mini-metric"><b>{{ '%.0f'|format((r.avg_confidence or 0) * 100) }}%</b><span>信心</span></div>
|
||||
@@ -553,7 +554,7 @@
|
||||
<div>
|
||||
<div class="biz-decision-name">{{ r.name or '-' }}</div>
|
||||
<div class="mt-1">
|
||||
<span class="biz-badge">{{ r.strategy or '-' }}</span>
|
||||
<span class="biz-badge">{{ obs_label.strategy(r.strategy, '-') }}</span>
|
||||
<span class="biz-badge {% if (r.confidence or 0) >= 0.8 %}good{% endif %}">{{ '%.0f'|format((r.confidence or 0) * 100) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -596,12 +597,12 @@
|
||||
<tbody>
|
||||
{% for r in loop_records %}
|
||||
<tr>
|
||||
<td>#{{ r.plan_id }}<br><span class="text-muted small">{{ r.plan_type or '-' }}</span></td>
|
||||
<td>#{{ r.plan_id }}<br><span class="text-muted small">{{ obs_label.plan_type(r.plan_type, '-') }}</span></td>
|
||||
<td><strong>{{ r.sku }}</strong></td>
|
||||
<td><span class="biz-badge {% if r.status == 'done' or r.status == 'completed' %}good{% elif r.status == 'pending' %}warn{% endif %}">{{ r.status or '-' }}</span></td>
|
||||
<td><span class="biz-badge {% if r.status == 'done' or r.status == 'completed' %}good{% elif r.status == 'pending' %}warn{% endif %}">{{ obs_label.status(r.status, '-') }}</span></td>
|
||||
<td>{{ r.created_at or '-' }} / {{ r.executed_at or '-' }}</td>
|
||||
<td>{{ r.verdict or '-' }}</td>
|
||||
<td>{{ r.metric_type or '-' }}<br><span class="text-muted small">{{ r.before or '-' }} → {{ r.after or '-' }}</span></td>
|
||||
<td>{{ obs_label.verdict(r.verdict, '-') }}</td>
|
||||
<td>{{ obs_label.metric(r.metric_type, '-') }}<br><span class="text-muted small">{{ r.before or '-' }} → {{ r.after or '-' }}</span></td>
|
||||
<td><strong>{{ '%.1f'|format(r.change_pct or 0) }}%</strong></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -629,7 +630,7 @@
|
||||
<thead><tr><th>結論</th><th>數量</th><th>平均變化</th></tr></thead>
|
||||
<tbody>
|
||||
{% for v in verdict_stats %}
|
||||
<tr><td>{{ v.verdict or '未分類' }}</td><td>{{ v.count }}</td><td>{{ '%.1f'|format(v.avg_delta or 0) }}%</td></tr>
|
||||
<tr><td>{{ obs_label.verdict(v.verdict) }}</td><td>{{ v.count }}</td><td>{{ '%.1f'|format(v.avg_delta or 0) }}%</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -653,7 +654,7 @@
|
||||
<tbody>
|
||||
{% for m in match_stats %}
|
||||
<tr>
|
||||
<td><span class="biz-badge {% if m.status == 'matched' or m.status == 'success' %}good{% elif m.status == 'failed' %}warn{% endif %}">{{ m.status or '-' }}</span></td>
|
||||
<td><span class="biz-badge {% if m.status == 'matched' or m.status == 'success' %}good{% elif m.status == 'failed' %}warn{% endif %}">{{ obs_label.status(m.status, '-') }}</span></td>
|
||||
<td>{{ m.count }}</td>
|
||||
<td>{{ '%.1f'|format(m.avg_candidates or 0) }}</td>
|
||||
<td>{{ '%.2f'|format(m.avg_score or 0) }}</td>
|
||||
@@ -708,11 +709,23 @@
|
||||
const verdictRows = {{ verdict_stats|tojson }};
|
||||
const canvas = document.getElementById('verdictPieChart');
|
||||
if (!canvas || !verdictRows || verdictRows.length === 0) return;
|
||||
const verdictLabelMap = {
|
||||
effective: '有效',
|
||||
success: '成功',
|
||||
positive: '正向',
|
||||
backfired: '反效果',
|
||||
negative: '負向',
|
||||
failed: '失敗',
|
||||
neutral: '中性',
|
||||
pending: '待回收',
|
||||
inconclusive: '尚未定論',
|
||||
no_data: '無資料'
|
||||
};
|
||||
|
||||
new Chart(canvas, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: verdictRows.map(row => row.verdict || '未分類'),
|
||||
labels: verdictRows.map(row => verdictLabelMap[row.verdict] || row.verdict || '未分類'),
|
||||
datasets: [{
|
||||
data: verdictRows.map(row => row.count || 0),
|
||||
backgroundColor: ['#2f8f6b', '#c96442', '#f1b45a', '#6d4b3f', '#d9a06f'],
|
||||
|
||||
@@ -406,6 +406,7 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
{% import "admin/_observability_labels.html" as obs_label %}
|
||||
{% set ai = summary.ai_calls if summary.ai_calls else none %}
|
||||
{% set host_count = summary.hosts|length if summary.hosts else 0 %}
|
||||
{% set host_bad = namespace(value=0) %}
|
||||
@@ -507,7 +508,7 @@
|
||||
{% for b in summary.budget_alerts %}
|
||||
<tr>
|
||||
<td><span class="obs-pill">{{ b.period }}</span></td>
|
||||
<td><code>{{ b.provider }}</code></td>
|
||||
<td><code>{{ obs_label.provider(b.provider) }}</code></td>
|
||||
<td>${{ "%.2f"|format(b.spent) }}</td>
|
||||
<td>${{ "%.2f"|format(b.budget) }}</td>
|
||||
<td><strong class="{% if b.ratio >= 1.0 %}obs-status-bad{% else %}obs-status-warn{% endif %}">{{ "%.0f"|format(b.ratio * 100) }}%</strong></td>
|
||||
@@ -609,9 +610,9 @@
|
||||
<h3>RAG 與品質</h3>
|
||||
<div class="obs-route-list">
|
||||
<a class="obs-route-card" href="/observability/promotion_review"><span class="obs-route-icon"><i class="fas fa-brain"></i></span><span><span class="obs-route-title">RAG 晉升審核</span><span class="obs-route-desc">晉升守門與人工審核。</span></span><span class="obs-route-code">07</span></a>
|
||||
<a class="obs-route-card" href="/observability/rag_queries"><span class="obs-route-icon"><i class="fas fa-magnifying-glass-chart"></i></span><span><span class="obs-route-title">RAG 召回詳情</span><span class="obs-route-desc">Query hits、節省呼叫、反饋追蹤。</span></span><span class="obs-route-code">08</span></a>
|
||||
<a class="obs-route-card" href="/observability/rag_queries"><span class="obs-route-icon"><i class="fas fa-magnifying-glass-chart"></i></span><span><span class="obs-route-title">RAG 召回詳情</span><span class="obs-route-desc">查詢命中、節省呼叫、反饋追蹤。</span></span><span class="obs-route-code">08</span></a>
|
||||
<a class="obs-route-card" href="/observability/quality_trend"><span class="obs-route-icon"><i class="fas fa-comments"></i></span><span><span class="obs-route-title">反饋趨勢</span><span class="obs-route-desc">呼叫端品質、蒸餾池、根因建議。</span></span><span class="obs-route-code">09</span></a>
|
||||
<a class="obs-route-card" href="/observability/ppt_audit_history"><span class="obs-route-icon"><i class="fas fa-search"></i></span><span><span class="obs-route-title">PPT 視覺審核</span><span class="obs-route-desc">PPT audit、RAG 修法、AiderHeal。</span></span><span class="obs-route-code">10</span></a>
|
||||
<a class="obs-route-card" href="/observability/ppt_audit_history"><span class="obs-route-icon"><i class="fas fa-search"></i></span><span><span class="obs-route-title">PPT 視覺審核</span><span class="obs-route-desc">PPT 審核、RAG 修法、AiderHeal。</span></span><span class="obs-route-code">10</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -8,14 +8,15 @@
|
||||
.ppt-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))}.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:-.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-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-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)}@media(max-width:1100px){.ppt-command{grid-template-columns:repeat(2,minmax(0,1fr))}.ppt-grid{grid-template-columns:1fr}}@media(max-width:720px){.ppt-command,.ppt-mini-grid{grid-template-columns:1fr}}
|
||||
</style>
|
||||
|
||||
{% import "admin/_observability_labels.html" as obs_label %}
|
||||
<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 自動修 generator。</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">30 日總量</div><span class="ppt-value">{{ audit_30d_stats.total if audit_30d_stats else 0 }}</span><small class="text-muted">審核紀錄</small></div><div class="ppt-signal"><div class="ppt-label">通過率</div><span class="ppt-value {% if audit_30d_stats and audit_30d_stats.pass_rate >= 80 %}status-good{% elif audit_30d_stats and audit_30d_stats.pass_rate >= 60 %}status-warn{% else %}status-bad{% endif %}">{{ "%.0f"|format(audit_30d_stats.pass_rate) if audit_30d_stats else '—' }}{% if audit_30d_stats %}%{% endif %}</span><small class="text-muted">過去 30 日</small></div><div class="ppt-signal"><div class="ppt-label">問題數</div><span class="ppt-value {% if audit_30d_stats and audit_30d_stats.total_issues > 0 %}status-warn{% else %}status-good{% endif %}">{{ audit_30d_stats.total_issues if audit_30d_stats else 0 }}</span><small class="text-muted">視覺問題數</small></div></div></section>
|
||||
<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">30 日總量</div><span class="ppt-value">{{ audit_30d_stats.total if audit_30d_stats else 0 }}</span><small class="text-muted">審核紀錄</small></div><div class="ppt-signal"><div class="ppt-label">通過率</div><span class="ppt-value {% if audit_30d_stats and audit_30d_stats.pass_rate >= 80 %}status-good{% elif audit_30d_stats and audit_30d_stats.pass_rate >= 60 %}status-warn{% else %}status-bad{% endif %}">{{ "%.0f"|format(audit_30d_stats.pass_rate) if audit_30d_stats else '—' }}{% if audit_30d_stats %}%{% endif %}</span><small class="text-muted">過去 30 日</small></div><div class="ppt-signal"><div class="ppt-label">問題數</div><span class="ppt-value {% if audit_30d_stats and audit_30d_stats.total_issues > 0 %}status-warn{% else %}status-good{% endif %}">{{ audit_30d_stats.total_issues if audit_30d_stats else 0 }}</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-grid">
|
||||
<div class="ppt-stack">
|
||||
<article class="ppt-table-shell"><div class="ppt-table-title"><div><div class="ppt-label">審核歷史</div><h3>視覺審核歷史 100 筆</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 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">尚無審核紀錄</td></tr>{% endfor %}</tbody></table></div></article>
|
||||
<article class="ppt-table-shell"><div class="ppt-table-title"><div><div class="ppt-label">已產檔案</div><h3>過去 7 日 PPT 檔案</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></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 cron 自動審核</small></td></tr>{% else %}<tr><td colspan="4" class="text-center text-muted">過去 7 日無 PPT 生成</td></tr>{% endfor %}</tbody></table></div></article>
|
||||
<article class="ppt-table-shell"><div class="ppt-table-title"><div><div class="ppt-label">已產檔案</div><h3>過去 7 日 PPT 檔案</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></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></tr>{% else %}<tr><td colspan="4" class="text-center text-muted">過去 7 日無 PPT 生成</td></tr>{% endfor %}</tbody></table></div></article>
|
||||
</div>
|
||||
<aside class="ppt-stack">
|
||||
{% if audit_30d_stats and audit_30d_stats.total > 0 %}<article class="ppt-panel"><div class="ppt-panel-head"><div><div class="ppt-label">30 日審核分布</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 %}
|
||||
@@ -23,8 +24,8 @@
|
||||
</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">{{ 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 audit_30d_stats or audit_30d_stats.total == 0) and 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 cron 寫入 ppt_audit_results</li></ul></div>{% endif %}
|
||||
{% 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 audit_30d_stats or audit_30d_stats.total == 0) and 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>{% endif %}
|
||||
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Ollama 優先策略 v5.0 — PPT 視覺 QA 產線</small></p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
@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) %}
|
||||
@@ -66,12 +67,12 @@
|
||||
{% 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>
|
||||
<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">{{ 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 %}
|
||||
{% 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>
|
||||
@@ -86,7 +87,7 @@
|
||||
<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">OpenClaw 權重</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>
|
||||
<article class="gate-panel"><div class="gate-panel-head"><div><div class="gate-label">OpenClaw 權重</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>
|
||||
@@ -102,6 +103,6 @@
|
||||
<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>拒絕';}}
|
||||
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>已拒絕(人工拒絕)</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 %}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
@media(max-width:1100px){.quality-command{grid-template-columns:repeat(2,minmax(0,1fr))}.quality-grid{grid-template-columns:1fr}}@media(max-width:720px){.quality-command,.quality-mini-grid{grid-template-columns:1fr}}
|
||||
</style>
|
||||
|
||||
{% import "admin/_observability_labels.html" as obs_label %}
|
||||
{% set total_feedback = namespace(value=0) %}{% set worst_avg = namespace(value=5) %}{% for caller, info in trends %}{% set total_feedback.value = total_feedback.value + (info.total_feedback or 0) %}{% if info.avg_score < worst_avg.value %}{% set worst_avg.value = info.avg_score %}{% endif %}{% endfor %}
|
||||
{% set episode_total = (episode_distribution.values() | sum) if episode_distribution else 0 %}
|
||||
{% set rag_total = (rag_overall_dist | sum(attribute='count')) if rag_overall_dist else 0 %}
|
||||
@@ -23,17 +24,17 @@
|
||||
<section class="quality-grid">
|
||||
<div class="quality-stack">
|
||||
<article class="quality-table-shell"><div class="quality-table-title"><div><div class="quality-label">呼叫端反饋</div><h3>呼叫端 × 反饋分佈</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">平均</th><th class="text-end">讚</th><th class="text-end">倒讚</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 class="{% if info.avg_score >= 4 %}status-good{% elif info.avg_score >= 3 %}status-warn{% else %}status-bad{% endif %}">{{ "%.2f"|format(info.avg_score) }}</strong>/5</td><td class="text-end status-good">{{ info.thumbs_up }}</td><td class="text-end status-bad">{{ info.thumbs_down }}</td><td class="text-end">{{ info.total_feedback }}</td><td>{% if info.trend == 'positive' %}<span class="badge bg-success">正向</span>{% elif info.trend == 'negative' %}<span class="badge bg-danger">負向</span>{% elif info.trend == 'neutral' %}<span class="badge bg-secondary">中性</span>{% else %}<span class="badge bg-light text-dark">無資料</span>{% endif %}</td><td class="quality-distribution-cell"><div class="progress obs-progress-sm"><div class="progress-bar" style="width:{{ (info.avg_score / 5 * 100)|int }}%"></div></div></td></tr>{% else %}<tr><td colspan="7" class="text-center text-muted">無反饋資料</td></tr>{% endfor %}</tbody></table></div></article>
|
||||
{% if action_plans_status %}<article class="quality-table-shell"><div class="quality-table-title"><div><div class="quality-label">行動計畫</div><h3>行動計畫 狀態分布</h3></div></div><div class="table-responsive"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>狀態</th><th>計畫類型</th><th class="text-end">數量</th></tr></thead><tbody>{% for a in action_plans_status %}<tr><td><span class="badge {% if a.status == 'approved' %}bg-success{% elif a.status == 'pending' %}bg-warning{% elif a.status == 'rejected' %}bg-danger{% else %}bg-secondary{% endif %}">{{ a.status }}</span></td><td><code>{{ a.plan_type }}</code></td><td class="text-end">{{ a.count }}</td></tr>{% endfor %}</tbody></table></div></article>{% endif %}
|
||||
{% if action_plans_status %}<article class="quality-table-shell"><div class="quality-table-title"><div><div class="quality-label">行動計畫</div><h3>行動計畫狀態分布</h3></div></div><div class="table-responsive"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>狀態</th><th>計畫類型</th><th class="text-end">數量</th></tr></thead><tbody>{% for a in action_plans_status %}<tr><td><span class="badge {% if a.status == 'approved' %}bg-success{% elif a.status == 'pending' %}bg-warning{% elif a.status == 'rejected' %}bg-danger{% else %}bg-secondary{% endif %}">{{ obs_label.status(a.status) }}</span></td><td><code>{{ obs_label.plan_type(a.plan_type) }}</code></td><td class="text-end">{{ a.count }}</td></tr>{% endfor %}</tbody></table></div></article>{% endif %}
|
||||
</div>
|
||||
<aside class="quality-stack">
|
||||
{% if rag_overall_dist %}<article class="quality-panel"><div class="quality-panel-head"><div><div class="quality-label">RAG 反饋總量</div><h2 class="quality-panel-title">RAG 分數分布</h2></div></div><div class="quality-panel-body"><div class="obs-chart-frame"><canvas id="ragFeedbackPieChart"></canvas></div></div></article>{% endif %}
|
||||
{% if episode_distribution %}<article class="quality-panel"><div class="quality-panel-head"><div><div class="quality-label">學習池</div><h2 class="quality-panel-title">蒸餾池狀態</h2></div></div><div class="quality-panel-body"><div class="quality-mini-grid">{% for status, cnt in episode_distribution.items() %}<div class="quality-mini"><span class="quality-label">{{ status }}</span><strong>{{ cnt }}</strong></div>{% endfor %}</div></div></article>{% endif %}
|
||||
{% if episode_distribution %}<article class="quality-panel"><div class="quality-panel-head"><div><div class="quality-label">學習池</div><h2 class="quality-panel-title">蒸餾池狀態</h2></div></div><div class="quality-panel-body"><div class="quality-mini-grid">{% for status, cnt in episode_distribution.items() %}<div class="quality-mini"><span class="quality-label">{{ obs_label.status(status) }}</span><strong>{{ cnt }}</strong></div>{% endfor %}</div></div></article>{% endif %}
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
{% if rag_root_causes %}<section class="quality-panel mt-3"><div class="quality-panel-head"><div><div class="quality-label">根因分析</div><h2 class="quality-panel-title">RAG 自動根因建議</h2></div></div><div class="quality-panel-body">{% for rc in rag_root_causes %}<div class="root-card"><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 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></section>{% endif %}
|
||||
{% if rag_root_causes %}<section class="quality-panel mt-3"><div class="quality-panel-head"><div><div class="quality-label">根因分析</div><h2 class="quality-panel-title">RAG 自動根因建議</h2></div></div><div class="quality-panel-body">{% for rc in rag_root_causes %}<div class="root-card"><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 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 recommendations %}<section class="quality-panel mt-3"><div class="quality-panel-head"><div><div class="quality-label">智能建議</div><h2 class="quality-panel-title">智能建議</h2></div></div><div class="quality-panel-body"><ul class="mb-0">{% for rec in recommendations %}<li>{% if rec.action == 'review' %}<i class="fas fa-triangle-exclamation status-warn me-1"></i>{% else %}<i class="fas fa-check status-good me-1"></i>{% endif %}<code>{{ rec.caller }}</code>:{{ rec.reason }}</li>{% endfor %}</ul></div></section>{% endif %}
|
||||
{% if action_outcomes_stats %}<section class="quality-panel mt-3"><div class="quality-panel-head"><div><div class="quality-label">動作成效</div><h2 class="quality-panel-title">實際動作成效</h2></div></div><div class="quality-panel-body"><div class="quality-mini-grid">{% set total_ao = (action_outcomes_stats | sum(attribute='count')) or 1 %}{% for r in action_outcomes_stats %}<div class="quality-mini"><span class="quality-label">{{ r.verdict }}</span><strong class="{% if r.verdict == 'effective' %}status-good{% elif r.verdict == 'backfired' %}status-bad{% endif %}">{{ r.count }}</strong><small class="text-muted">{{ "%.1f"|format(r.count / total_ao * 100) }}%</small></div>{% endfor %}</div></div></section>{% endif %}
|
||||
{% if action_outcomes_stats %}<section class="quality-panel mt-3"><div class="quality-panel-head"><div><div class="quality-label">動作成效</div><h2 class="quality-panel-title">實際動作成效</h2></div></div><div class="quality-panel-body"><div class="quality-mini-grid">{% set total_ao = (action_outcomes_stats | sum(attribute='count')) or 1 %}{% for r in action_outcomes_stats %}<div class="quality-mini"><span class="quality-label">{{ obs_label.verdict(r.verdict) }}</span><strong class="{% if r.verdict == 'effective' %}status-good{% elif r.verdict == 'backfired' %}status-bad{% endif %}">{{ r.count }}</strong><small class="text-muted">{{ "%.1f"|format(r.count / total_ao * 100) }}%</small></div>{% endfor %}</div></div></section>{% endif %}
|
||||
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Ollama 優先策略 v5.0 — AI 品質診斷台</small></p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -40,7 +40,9 @@
|
||||
|
||||
<div class="modal fade" id="hitsModal" tabindex="-1"><div class="modal-dialog modal-lg"><div class="modal-content"><div class="modal-header"><h5 class="modal-title"><i class="fas fa-eye me-2"></i>RAG 命中內容</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body" id="hitsModalBody"><div class="text-center"><i class="fas fa-spinner fa-spin"></i> 載入中...</div></div></div></div></div>
|
||||
<script>
|
||||
async function showHits(queryId){const modalEl=document.getElementById('hitsModal');const body=document.getElementById('hitsModalBody');body.innerHTML='<div class="text-center"><i class="fas fa-spinner fa-spin"></i> 載入中...</div>';const modal=new bootstrap.Modal(modalEl);modal.show();try{const r=await fetch(`/observability/rag_queries/${queryId}/hits`);const d=await r.json();if(!d.ok){body.innerHTML=`<div class="alert alert-danger">❌ ${d.error||'載入失敗'}</div>`;return;}let html=`<div class="mb-3"><small class="text-muted">查詢 #${d.query_id} · 門檻 ${d.threshold} · 命中 ${d.hit_count}</small><div class="p-2 mt-1 obs-modal-preview"><small><strong>查詢內容:</strong></small><br><code>${escapeHtml(d.query_text||'')}</code></div></div>`;if(d.hits.length===0){html+='<div class="alert alert-warning">無命中詳細資料</div>';}else{html+='<h6 class="mb-2">Top 命中內容預覽:</h6>';d.hits.forEach(h=>{html+=`<div class="mb-2 p-2 obs-modal-preview"><div class="mb-1"><span class="badge bg-light text-dark me-1">#${h.id}</span><span class="badge bg-info me-1">${escapeHtml(h.insight_type||'')}</span>${h.period?`<span class="badge bg-secondary me-1">${escapeHtml(h.period)}</span>`:''}${h.product_sku?`<small class="text-muted me-1">SKU: ${escapeHtml(h.product_sku)}</small>`:''}<small class="text-muted">${h.created_at}</small></div><small>${escapeHtml(h.content||'')}${h.content&&h.content.length>=300?'…':''}</small></div>`;});}body.innerHTML=html;}catch(e){console.warn('rag_query_hits_load_failed',e);body.innerHTML='<div class="alert alert-danger">❌ 召回詳情暫時無法載入,請稍後再試或查看系統日誌。</div>';}}
|
||||
const insightLabelMap={product_pick:'選品攻擊',price_recommendation:'價格建議',competitor_price:'競品價格',sales_anomaly:'業績異常',budget_strategy:'預算策略',rag_feedback:'RAG 反饋',ppt_audit:'PPT 審核',quality_issue:'品質問題',promotion:'活動促銷',market_signal:'市場訊號',strategy:'策略洞察'};
|
||||
function insightLabel(value){return insightLabelMap[value]||String(value||'未分類洞察').replaceAll('_',' ');}
|
||||
async function showHits(queryId){const modalEl=document.getElementById('hitsModal');const body=document.getElementById('hitsModalBody');body.innerHTML='<div class="text-center"><i class="fas fa-spinner fa-spin"></i> 載入中...</div>';const modal=new bootstrap.Modal(modalEl);modal.show();try{const r=await fetch(`/observability/rag_queries/${queryId}/hits`);const d=await r.json();if(!d.ok){body.innerHTML=`<div class="alert alert-danger">❌ ${d.error||'載入失敗'}</div>`;return;}let html=`<div class="mb-3"><small class="text-muted">查詢 #${d.query_id} · 門檻 ${d.threshold} · 命中 ${d.hit_count}</small><div class="p-2 mt-1 obs-modal-preview"><small><strong>查詢內容:</strong></small><br><code>${escapeHtml(d.query_text||'')}</code></div></div>`;if(d.hits.length===0){html+='<div class="alert alert-warning">無命中詳細資料</div>';}else{html+='<h6 class="mb-2">主要命中內容預覽:</h6>';d.hits.forEach(h=>{html+=`<div class="mb-2 p-2 obs-modal-preview"><div class="mb-1"><span class="badge bg-light text-dark me-1">#${h.id}</span><span class="badge bg-info me-1">${escapeHtml(insightLabel(h.insight_type))}</span>${h.period?`<span class="badge bg-secondary me-1">${escapeHtml(h.period)}</span>`:''}${h.product_sku?`<small class="text-muted me-1">SKU: ${escapeHtml(h.product_sku)}</small>`:''}<small class="text-muted">${h.created_at}</small></div><small>${escapeHtml(h.content||'')}${h.content&&h.content.length>=300?'…':''}</small></div>`;});}body.innerHTML=html;}catch(e){console.warn('rag_query_hits_load_failed',e);body.innerHTML='<div class="alert alert-danger">❌ 召回詳情暫時無法載入,請稍後再試或查看系統日誌。</div>';}}
|
||||
function escapeHtml(s){if(!s)return'';return s.replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user