Files
ewoooc/templates/admin/business_intel.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

739 lines
24 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" %}
{% set active_page = 'obs_business_intel' %}
{% block title %}商業 AI 戰果室{% endblock %}
{% block extra_css %}
<style>
.biz-warroom {
--biz-ink: #2f211b;
--biz-muted: #8a6b5c;
--biz-card: rgba(255, 252, 247, 0.95);
--biz-line: rgba(201, 100, 66, 0.16);
--biz-alert: #b8442f;
--biz-good: #2f8f6b;
--biz-warn: #c98a2e;
position: relative;
overflow: hidden;
}
.biz-warroom::before {
content: "";
position: absolute;
inset: 0 0 auto auto;
width: min(560px, 58vw);
height: min(560px, 58vw);
background: radial-gradient(circle, rgba(201, 100, 66, 0.16), transparent 62%);
pointer-events: none;
transform: translate(26%, -34%);
}
.biz-hero {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1.45fr) minmax(280px, 0.55fr);
gap: 1.15rem;
margin-bottom: 1.25rem;
}
.biz-command {
background:
linear-gradient(135deg, rgba(53, 35, 27, 0.96), rgba(126, 64, 42, 0.92)),
radial-gradient(circle at top right, rgba(255, 194, 115, 0.22), transparent 42%);
color: #fff8ee;
border-radius: 28px;
padding: clamp(1.35rem, 2.5vw, 2.25rem);
box-shadow: 0 28px 60px rgba(78, 45, 31, 0.24);
min-height: 320px;
}
.biz-kicker {
display: inline-flex;
align-items: center;
gap: .5rem;
padding: .35rem .65rem;
border-radius: 999px;
background: rgba(255, 255, 255, 0.12);
color: #ffd7a2;
font-size: .82rem;
font-weight: 800;
letter-spacing: .08em;
text-transform: uppercase;
}
.biz-command h1 {
font-size: clamp(2.05rem, 4vw, 3.25rem);
line-height: 1.05;
letter-spacing: -.06em;
margin: 1rem 0 .85rem;
font-weight: 900;
}
.biz-command p {
max-width: 760px;
color: rgba(255, 248, 238, 0.78);
font-size: 1.02rem;
line-height: 1.75;
}
.biz-meta-row {
display: flex;
flex-wrap: wrap;
gap: .7rem;
margin-top: 1.4rem;
}
.biz-meta-pill {
display: inline-flex;
align-items: center;
gap: .45rem;
padding: .65rem .85rem;
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
color: rgba(255, 248, 238, 0.9);
font-weight: 800;
}
.biz-filter-card {
background: var(--biz-card);
border: 1px solid var(--biz-line);
border-radius: 28px;
padding: 1.2rem;
box-shadow: 0 18px 46px rgba(93, 57, 42, 0.12);
align-self: stretch;
}
.biz-filter-card label {
font-size: .78rem;
text-transform: uppercase;
letter-spacing: .08em;
color: var(--biz-muted);
font-weight: 900;
}
.biz-filter-card .form-control {
border-radius: 16px;
border-color: rgba(201, 100, 66, 0.18);
min-height: 48px;
}
.biz-signal-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: .9rem;
margin-bottom: 1.1rem;
}
.biz-signal {
position: relative;
overflow: hidden;
background: var(--biz-card);
border: 1px solid var(--biz-line);
border-radius: 24px;
padding: 1.05rem;
box-shadow: 0 14px 34px rgba(93, 57, 42, 0.08);
}
.biz-signal::after {
content: "";
position: absolute;
inset: auto 0 0 0;
height: 4px;
background: linear-gradient(90deg, #c96442, #f1b45a, #2f8f6b);
opacity: .82;
}
.biz-signal .label {
color: var(--biz-muted);
font-size: .8rem;
font-weight: 900;
letter-spacing: .04em;
}
.biz-signal .value {
color: var(--biz-ink);
font-size: clamp(1.7rem, 3vw, 2.45rem);
font-weight: 950;
letter-spacing: -.05em;
margin: .3rem 0 .05rem;
}
.biz-signal .note {
color: var(--biz-muted);
font-size: .86rem;
}
.biz-alert-strip {
display: flex;
align-items: center;
justify-content: space-between;
gap: .9rem;
padding: 1rem 1.15rem;
border-radius: 22px;
border: 1px solid rgba(184, 68, 47, 0.2);
background: linear-gradient(135deg, rgba(184, 68, 47, 0.12), rgba(255, 248, 238, 0.92));
color: var(--biz-alert);
margin-bottom: 1.1rem;
font-weight: 850;
}
.biz-layout {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(340px, .9fr);
gap: 1rem;
align-items: start;
}
.biz-panel {
background: var(--biz-card);
border: 1px solid var(--biz-line);
border-radius: 26px;
box-shadow: 0 14px 38px rgba(93, 57, 42, 0.08);
overflow: hidden;
margin-bottom: 1rem;
}
.biz-panel-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
padding: 1.05rem 1.15rem .75rem;
border-bottom: 1px solid rgba(201, 100, 66, 0.1);
}
.biz-panel-head h3 {
margin: 0;
color: var(--biz-ink);
font-weight: 950;
letter-spacing: -.035em;
}
.biz-panel-head p {
margin: .25rem 0 0;
color: var(--biz-muted);
font-size: .88rem;
}
.biz-panel-body {
padding: 1rem 1.15rem 1.15rem;
}
.biz-strategy-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: .75rem;
}
.biz-strategy-card {
border: 1px solid rgba(201, 100, 66, 0.14);
border-radius: 20px;
padding: .95rem;
background: linear-gradient(135deg, #fffaf3, #fffdf9);
}
.biz-strategy-card .strategy {
color: var(--biz-ink);
font-weight: 950;
font-size: 1.02rem;
}
.biz-strategy-card .metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: .45rem;
margin-top: .75rem;
}
.biz-mini-metric {
padding: .55rem;
border-radius: 14px;
background: rgba(201, 100, 66, 0.07);
}
.biz-mini-metric b {
display: block;
color: var(--biz-ink);
font-size: 1rem;
}
.biz-mini-metric span {
color: var(--biz-muted);
font-size: .72rem;
font-weight: 800;
}
.biz-table {
width: 100%;
border-collapse: separate;
border-spacing: 0 .55rem;
}
.biz-table th {
color: var(--biz-muted);
font-size: .76rem;
letter-spacing: .04em;
text-transform: uppercase;
border: 0;
padding: 0 .7rem .15rem;
}
.biz-table td {
border: 0;
padding: .78rem .7rem;
background: rgba(255, 250, 243, 0.82);
vertical-align: middle;
}
.biz-table tr td:first-child {
border-radius: 16px 0 0 16px;
}
.biz-table tr td:last-child {
border-radius: 0 16px 16px 0;
}
.biz-decision-list {
display: grid;
gap: .75rem;
}
.biz-decision-card {
display: grid;
grid-template-columns: minmax(82px, .36fr) minmax(240px, 1.15fr) minmax(130px, .46fr);
gap: .8rem;
align-items: center;
padding: .95rem;
border: 1px solid rgba(201, 100, 66, 0.14);
border-radius: 18px;
background:
linear-gradient(135deg, rgba(255, 252, 247, 0.96), rgba(255, 248, 241, 0.86)),
radial-gradient(circle, rgba(201, 100, 66, 0.12) 1px, transparent 1.2px);
background-size: auto, 13px 13px;
}
.biz-decision-time {
color: var(--biz-muted);
font-size: .82rem;
font-weight: 850;
}
.biz-decision-sku,
.biz-decision-name {
color: var(--biz-ink);
font-weight: 900;
}
.biz-decision-name {
line-height: 1.45;
}
.biz-decision-reason {
grid-column: 2 / 4;
color: var(--biz-muted);
font-size: .82rem;
line-height: 1.5;
padding-top: .55rem;
border-top: 1px dashed rgba(201, 100, 66, 0.18);
}
.biz-price-stack {
color: var(--biz-ink);
font-weight: 850;
}
.biz-price-stack small {
display: block;
color: var(--biz-muted);
font-weight: 700;
}
.biz-badge {
display: inline-flex;
align-items: center;
padding: .28rem .55rem;
border-radius: 999px;
font-size: .76rem;
font-weight: 900;
background: rgba(201, 100, 66, 0.11);
color: #8e3d28;
}
.biz-badge.good {
background: rgba(47, 143, 107, 0.12);
color: var(--biz-good);
}
.biz-badge.warn {
background: rgba(201, 138, 46, 0.14);
color: var(--biz-warn);
}
.biz-chart-shell {
height: 250px;
padding: .4rem;
}
.biz-empty {
padding: 1rem;
border-radius: 18px;
background: rgba(201, 100, 66, 0.06);
color: var(--biz-muted);
font-weight: 800;
}
@media (max-width: 1180px) {
.biz-hero,
.biz-layout {
grid-template-columns: 1fr;
}
.biz-signal-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 720px) {
.biz-command h1 {
font-size: 2.45rem;
}
.biz-signal-grid,
.biz-strategy-grid,
.biz-decision-card {
grid-template-columns: 1fr;
}
.biz-decision-reason {
grid-column: auto;
}
.biz-alert-strip {
align-items: flex-start;
flex-direction: column;
}
}
</style>
{% endblock %}
{% block ewooo_content %}
{% 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 %}
{% set ns.conf_total = ns.conf_total + ((r.avg_confidence or 0) * (r.count or 0)) %}
{% endfor %}
{% for v in verdict_stats %}
{% set label = v.verdict or '未分類' %}
{% set ns.verdict_total = ns.verdict_total + (v.count or 0) %}
{% if label == 'effective' or label == 'success' or label == 'positive' %}
{% set ns.effective = ns.effective + (v.count or 0) %}
{% elif label == 'backfired' or label == 'negative' or label == 'failed' %}
{% set ns.backfired = ns.backfired + (v.count or 0) %}
{% else %}
{% set ns.neutral = ns.neutral + (v.count or 0) %}
{% endif %}
{% endfor %}
{% set avg_conf = (ns.conf_total / rec_total) if rec_total else 0 %}
{% set effective_rate = ((ns.effective / ns.verdict_total) * 100) if ns.verdict_total else 0 %}
<div class="biz-warroom">
<div class="biz-hero">
<section class="biz-command">
<span class="biz-kicker"><i class="fas fa-store"></i> Business Intelligence</span>
<h1>商業 AI 戰果室</h1>
<p>
這一頁不再只是資料列表,而是把價格建議、未跟進警示、閉環學習與競品監測收成一個商業決策控制台。
先看 AI 是否真的推動結果,再往下追每一筆策略與市場訊號。
</p>
<div class="biz-meta-row">
<span class="biz-meta-pill"><i class="fas fa-calendar-day"></i> 近 {{ days }} 天</span>
<span class="biz-meta-pill"><i class="fas fa-brain"></i> {{ rec_total }} 筆價格建議</span>
<span class="biz-meta-pill"><i class="fas fa-rotate"></i> {{ ns.verdict_total }} 筆閉環結果</span>
</div>
</section>
<aside class="biz-filter-card">
<form method="get">
<label for="days">觀測窗口</label>
<div class="d-flex gap-2 mt-2">
<input class="form-control" id="days" name="days" type="number" min="1" max="90" value="{{ days }}">
<button class="btn btn-primary" type="submit">更新</button>
</div>
</form>
<div class="mt-4">
<div class="text-muted small fw-bold text-uppercase">決策節奏</div>
<div class="h4 fw-black mb-1">先警示,再追因</div>
<p class="text-muted mb-0">未跟進高信心建議會被拉到第一層;其餘資訊按策略、閉環、競品三條線拆解。</p>
</div>
</aside>
</div>
{% if error %}
<div class="alert alert-danger"><i class="fas fa-triangle-exclamation me-2"></i>{{ error }}</div>
{% endif %}
<section class="biz-signal-grid">
<article class="biz-signal">
<div class="label">高信心未跟進</div>
<div class="value">{{ unfollowed_count }}</div>
<div class="note">confidence >= 0.8 且仍未轉 action_plan</div>
</article>
<article class="biz-signal">
<div class="label">平均信心分</div>
<div class="value">{{ '%.0f'|format(avg_conf * 100) }}%</div>
<div class="note">依策略建議量加權</div>
</article>
<article class="biz-signal">
<div class="label">有效率</div>
<div class="value">{{ '%.0f'|format(effective_rate) }}%</div>
<div class="note">effective / 已回收 verdict</div>
</article>
<article class="biz-signal">
<div class="label">競品監測</div>
<div class="value">{{ recent_competitor_prices|length }}</div>
<div class="note">近 24h 價格變動樣本</div>
</article>
</section>
{% if unfollowed_count > 0 %}
<section class="biz-alert-strip">
<div><i class="fas fa-bell me-2"></i>{{ unfollowed_count }} 筆高信心 AI 價格建議尚未跟進,建議優先轉為 action_plan 或標記原因。</div>
<span class="biz-badge warn">需人工決策</span>
</section>
{% endif %}
<div class="biz-layout">
<main>
<section class="biz-panel">
<div class="biz-panel-head">
<div>
<h3>策略族群雷達</h3>
<p>把 AI 價格建議依 strategy 聚合,快速判斷目前主攻降價、防守或毛利修復。</p>
</div>
<span class="biz-badge">{{ rec_by_strategy|length }} 類策略</span>
</div>
<div class="biz-panel-body">
{% if rec_by_strategy %}
<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="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>
<div class="biz-mini-metric"><b>{{ '%.1f'|format(r.avg_gap_pct or 0) }}%</b><span>價差</span></div>
</div>
<div class="mt-2 text-muted small">平均銷量變化 {{ '%.1f'|format(r.avg_sales_delta or 0) }}</div>
</article>
{% endfor %}
</div>
{% else %}
<div class="biz-empty">目前觀測窗口沒有策略建議。</div>
{% endif %}
</div>
</section>
<section class="biz-panel">
<div class="biz-panel-head">
<div>
<h3>最近 AI 價格建議</h3>
<p>保留決策原因與競品價差,方便直接追到 SKU 層級。</p>
</div>
<span class="biz-badge">Latest 20</span>
</div>
<div class="biz-panel-body">
{% if latest_recommendations %}
<div class="biz-decision-list">
{% for r in latest_recommendations %}
<article class="biz-decision-card">
<div>
<div class="biz-decision-time">{{ r.created_at or '-' }}</div>
<div class="biz-decision-sku">{{ r.sku }}</div>
</div>
<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 {% if (r.confidence or 0) >= 0.8 %}good{% endif %}">{{ '%.0f'|format((r.confidence or 0) * 100) }}%</span>
</div>
</div>
<div class="biz-price-stack">
{{ r.momo_price or '-' }} / {{ r.pchome_price or '-' }}
<small>gap {{ '%.1f'|format(r.gap_pct or 0) }}%</small>
</div>
<div class="biz-decision-reason">{{ r.reason or '尚無原因摘要' }}</div>
</article>
{% endfor %}
</div>
{% else %}
<div class="biz-empty">目前沒有最新價格建議。</div>
{% endif %}
</div>
</section>
<section class="biz-panel">
<div class="biz-panel-head">
<div>
<h3>閉環學習紀錄</h3>
<p>追蹤 action_plan 到 outcome 的真實效果,這是 AI 能不能變聰明的核心證據。</p>
</div>
<span class="biz-badge good">{{ loop_records|length }} records</span>
</div>
<div class="biz-panel-body table-responsive">
{% if loop_records %}
<table class="biz-table">
<thead>
<tr>
<th>Plan</th>
<th>SKU</th>
<th>狀態</th>
<th>建立 / 執行</th>
<th>Verdict</th>
<th>指標</th>
<th>變化</th>
</tr>
</thead>
<tbody>
{% for r in loop_records %}
<tr>
<td>#{{ r.plan_id }}<br><span class="text-muted small">{{ r.plan_type or '-' }}</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>{{ 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><strong>{{ '%.1f'|format(r.change_pct or 0) }}%</strong></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="biz-empty">尚未形成 action_plan → outcome 閉環紀錄。</div>
{% endif %}
</div>
</section>
</main>
<aside>
<section class="biz-panel">
<div class="biz-panel-head">
<div>
<h3>Verdict 戰果分布</h3>
<p>用結果反校正 AI 建議,不讓漂亮信心分掩蓋真實成效。</p>
</div>
</div>
<div class="biz-panel-body">
<div class="biz-chart-shell"><canvas id="verdictPieChart"></canvas></div>
{% if verdict_stats %}
<table class="biz-table mt-2">
<thead><tr><th>Verdict</th><th>Count</th><th>Avg Δ</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>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="biz-empty">尚無 verdict 統計。</div>
{% endif %}
</div>
</section>
<section class="biz-panel">
<div class="biz-panel-head">
<div>
<h3>競品比對品質</h3>
<p>先看比對是否可靠,再拿價格差做策略判斷。</p>
</div>
</div>
<div class="biz-panel-body table-responsive">
{% if match_stats %}
<table class="biz-table">
<thead><tr><th>Status</th><th>Count</th><th>Candidates</th><th>Score</th></tr></thead>
<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>{{ m.count }}</td>
<td>{{ '%.1f'|format(m.avg_candidates or 0) }}</td>
<td>{{ '%.2f'|format(m.avg_score or 0) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="biz-empty">尚無競品比對統計。</div>
{% endif %}
</div>
</section>
<section class="biz-panel">
<div class="biz-panel-head">
<div>
<h3>競品 24h 價格脈動</h3>
<p>保留最新市場訊號,支援人工快速複核。</p>
</div>
</div>
<div class="biz-panel-body table-responsive">
{% if recent_competitor_prices %}
<table class="biz-table">
<thead><tr><th>時間</th><th>SKU</th><th>PChome / momo</th><th>折扣</th></tr></thead>
<tbody>
{% for r in recent_competitor_prices %}
<tr>
<td>{{ r.crawled_at or '-' }}</td>
<td><strong>{{ r.sku }}</strong><br><span class="text-muted small">{{ r.product_name or '-' }}</span></td>
<td>{{ r.pchome_price or '-' }} / {{ r.momo_price or '-' }}<br><span class="text-muted small">gap {{ r.gap or '-' }}</span></td>
<td>{{ '%.1f'|format(r.discount_pct or 0) }}%<br><span class="text-muted small">score {{ '%.2f'|format(r.match_score or 0) }}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="biz-empty">近 24h 尚無競品價格樣本。</div>
{% endif %}
</div>
</section>
</aside>
</div>
<div class="text-muted small mt-3">資料來源ai_price_recommendations / action_plans / action_outcomes / competitor_match_attempts / competitor_price_history</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
(function() {
const verdictRows = {{ verdict_stats|tojson }};
const canvas = document.getElementById('verdictPieChart');
if (!canvas || !verdictRows || verdictRows.length === 0) return;
new Chart(canvas, {
type: 'doughnut',
data: {
labels: verdictRows.map(row => row.verdict || '未分類'),
datasets: [{
data: verdictRows.map(row => row.count || 0),
backgroundColor: ['#2f8f6b', '#c96442', '#f1b45a', '#6d4b3f', '#d9a06f'],
borderColor: '#fff8ee',
borderWidth: 4,
hoverOffset: 10
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: { usePointStyle: true, boxWidth: 8, color: '#6f564b', font: { weight: 700 } }
}
},
cutout: '66%'
}
});
})();
</script>
{% endblock %}