feat: add growth action summary
All checks were successful
CD Pipeline / deploy (push) Successful in 1m16s

This commit is contained in:
ogt
2026-06-24 20:05:58 +08:00
parent 2d9acfdc5c
commit 7180c0f817
4 changed files with 191 additions and 1 deletions

View File

@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.644"
SYSTEM_VERSION = "V10.645"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -80,6 +80,7 @@
- V10.642 起 `/ai_intelligence` 的摘要卡與商品處理數字不可只跳到大區塊;點擊後必須開啟商品明細面板,列出商品名稱、分類、近 7 天業績、業績變化、MOMO 比價狀態與下一步按鈕。明細需至少支援全部、價格壓力、價格優勢、待確認、缺比價與有外部價切換;外部價格風險分佈也必須能一鍵篩選下方表格。
- V10.643 起 `/ai_intelligence` 的商品明細上方必須提供「商品策略分流」視覺摘要,至少包含價格壓力、價格優勢、待確認、缺比價四類;每一類需顯示件數、近 7 天業績與比例條,且可點擊切換明細。舊 KPI 卡也不得是靜態數字,需可導向全部商品、可處理商品、高風險比價或處理紀錄。
- V10.644 起 `/ai_intelligence` 的商品明細列不得只用句子描述比價;每列必須顯示 PChome 價格、MOMO 參考價、差距、可信度四格價格證據,並保留下一步按鈕。單位價候選需顯示單位價與單位,候選待確認或缺資料則以「待補 / 候選待確認」呈現,不得捏造價格。
- V10.645 起 `/ai_intelligence` 的商品明細分流切換後,必須顯示「這類商品怎麼處理」的行動摘要,包含件數、近 7 天業績、平均可信度、最大價差、代表商品與主按鈕;使用者不得只能看到商品列表而不知道下一步。
## 零之一、12 Agent 決策信封2026-05-24

View File

@@ -787,6 +787,56 @@
background: #d8a13a;
}
.growth-decision-panel {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 12px;
align-items: center;
border: 1px solid rgba(172, 92, 58, 0.18);
border-radius: 8px;
background: rgba(255, 255, 255, 0.72);
margin-bottom: 10px;
padding: 10px 12px;
}
.growth-decision-title {
margin: 0;
color: var(--momo-text-strong);
font-size: 0.86rem;
font-weight: 900;
line-height: 1.3;
}
.growth-decision-copy {
margin: 4px 0 0;
color: var(--momo-text-muted);
font-size: 0.74rem;
font-weight: 760;
line-height: 1.4;
}
.growth-decision-metrics {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.growth-decision-metric {
border: 1px solid rgba(42, 37, 32, 0.08);
border-radius: 999px;
background: rgba(250, 247, 240, 0.72);
color: var(--momo-text-muted);
font-size: 0.68rem;
font-weight: 900;
padding: 4px 8px;
}
.growth-decision-metric strong {
color: var(--momo-text-strong);
font-family: var(--momo-font-mono);
}
.growth-detail-result {
border: 1px solid rgba(42, 37, 32, 0.1);
border-radius: 8px;
@@ -1385,6 +1435,14 @@
justify-self: stretch;
}
.growth-decision-panel {
grid-template-columns: 1fr;
}
.growth-decision-panel .table-row-action {
width: 100%;
}
.growth-detail-price-grid {
grid-template-columns: 1fr;
}
@@ -1580,6 +1638,13 @@
<span class="growth-strategy-track"><span class="growth-strategy-bar"></span></span>
</button>
</div>
<div class="growth-decision-panel" id="growthDecisionSummary">
<div>
<h3 class="growth-decision-title">正在整理處理建議</h3>
<p class="growth-decision-copy">讀取商品、業績與比價狀態後,這裡會顯示最適合的下一步。</p>
</div>
<button type="button" class="btn btn-sm btn-outline-primary table-row-action" onclick="showGrowthDetail('all')">查看明細</button>
</div>
<div class="growth-detail-result" id="growthDrilldownResult">
<div class="text-center py-4 text-muted">
<div class="spinner-border spinner-border-sm me-2"></div>整理商品明細中...
@@ -2514,6 +2579,125 @@ function renderGrowthStrategySummary() {
}).join('');
}
function growthDecisionConfig(kind) {
const configs = {
all: {
title: '先處理最有機會影響業績的商品',
copy: '先看價格壓力與價格優勢商品,再補待確認與缺比價資料。',
actionLabel: '看第一筆',
},
ready: {
title: '這批商品可以直接進入銷售判斷',
copy: '已有 MOMO 參考價,先處理價差大且近 7 天業績有波動的商品。',
actionLabel: '檢查價格',
},
risk: {
title: '先檢查 PChome 價格壓力',
copy: 'MOMO 參考價較低,優先檢查售價、折扣券、組合包與頁面曝光。',
actionLabel: '看價格',
},
advantage: {
title: '放大 PChome 價格優勢',
copy: 'PChome 有價格優勢,適合排主推、加強文案與提高曝光位置。',
actionLabel: '看主推清單',
},
review: {
title: '先確認候選是否同款',
copy: '確認同款後才會進入價格判斷;色號、容量或組合不一致就排除。',
actionLabel: '確認候選',
action: 'review-candidate',
},
needs: {
title: '先補齊 MOMO 對應商品',
copy: '這批商品有 PChome 業績但缺外部參考價,先補抓候選再判斷價格。',
actionLabel: '補齊比價',
action: 'backfill',
},
source: {
title: '檢查已接到外部價的商品',
copy: '這些商品已有 MOMO 參考價,可直接比較價格與銷售變化。',
actionLabel: '檢查價格',
},
decline: {
title: '先找回下滑商品的銷售動能',
copy: '近 7 天轉弱的商品,優先檢查價格、曝光、庫存與商品頁內容。',
actionLabel: '看下滑商品',
},
};
return configs[kind] || configs.all;
}
function rowQualityScore(row) {
if (row?.data_quality?.score !== null && row?.data_quality?.score !== undefined) {
return Number(row.data_quality.score || 0);
}
if (row?.review_candidate?.quality_score !== null && row?.review_candidate?.quality_score !== undefined) {
return Number(row.review_candidate.quality_score || 0);
}
if (row?.external_price?.match_score) return Number(row.external_price.match_score || 0) * 100;
return 0;
}
function renderGrowthDecisionSummary(rows, kind) {
const box = document.getElementById('growthDecisionSummary');
if (!box) return;
rows = Array.isArray(rows) ? rows : [];
const config = growthDecisionConfig(kind);
if (!rows.length) {
box.innerHTML = `<div>
<h3 class="growth-decision-title">${escapeHtml(config.title)}</h3>
<p class="growth-decision-copy">目前沒有符合這個條件的商品。</p>
<div class="growth-decision-metrics">
<span class="growth-decision-metric">商品 <strong>0</strong> 件</span>
<span class="growth-decision-metric">近 7 天 <strong>NT$ 0</strong></span>
<span class="growth-decision-metric">可信度 <strong>待補</strong></span>
<span class="growth-decision-metric">最大價差 <strong>待判斷</strong></span>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary table-row-action" disabled>無需處理</button>`;
return;
}
const sales = rows.reduce((sum, row) => sum + Number(row.sales_7d || 0), 0);
const qualityRows = rows.map(rowQualityScore).filter((score) => Number.isFinite(score) && score > 0);
const avgQuality = qualityRows.length
? Math.round(qualityRows.reduce((sum, score) => sum + score, 0) / qualityRows.length)
: 0;
const gapValues = rows
.map((row) => row.external_price?.gap_pct)
.filter((gap) => gap !== null && gap !== undefined && Number.isFinite(Number(gap)))
.map(Number);
const largestGap = gapValues.length
? gapValues.reduce((best, gap) => Math.abs(gap) > Math.abs(best) ? gap : best, gapValues[0])
: null;
const topRow = rows[0] || {};
const topName = topRow.product_name ? `代表商品:${topRow.product_name}` : '目前沒有符合條件的商品。';
const action = config.action || (
topRow.recommended_action?.code === 'review_external_candidate'
? 'review-candidate'
: topRow.recommended_action?.code === 'map_external_product'
? 'backfill'
: 'focus-price'
);
const productKey = escapeHtml(topRow.pchome_product_id || topRow.product_name || '');
const buttonAttrs = action === 'backfill'
? 'data-growth-action="backfill"'
: action === 'review-candidate'
? `data-growth-action="review-candidate" data-product-key="${productKey}"`
: `data-growth-action="focus-price" data-product-key="${productKey}"`;
box.innerHTML = `<div>
<h3 class="growth-decision-title">${escapeHtml(config.title)}</h3>
<p class="growth-decision-copy">${escapeHtml(config.copy)} ${escapeHtml(topName)}</p>
<div class="growth-decision-metrics">
<span class="growth-decision-metric">商品 <strong>${formatCount(rows.length)}</strong> 件</span>
<span class="growth-decision-metric">近 7 天 <strong>${escapeHtml(formatMoney(sales))}</strong></span>
<span class="growth-decision-metric">可信度 <strong>${avgQuality ? avgQuality + '%' : '待補'}</strong></span>
<span class="growth-decision-metric">最大價差 <strong>${escapeHtml(largestGap === null ? '待判斷' : formatGapDisplay(largestGap))}</strong></span>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary table-row-action" ${buttonAttrs}>${escapeHtml(config.actionLabel)}</button>`;
}
function renderGrowthDetail(kind = activeGrowthDetailKind) {
const titleBox = document.getElementById('growthDrilldownTitle');
const metaBox = document.getElementById('growthDrilldownMeta');
@@ -2532,6 +2716,7 @@ function renderGrowthDetail(kind = activeGrowthDetailKind) {
const salesTotal = rows.reduce((sum, row) => sum + Number(row.sales_7d || 0), 0);
titleBox.textContent = title;
metaBox.textContent = `${rows.length.toLocaleString()} 件 · 近 7 天業績 ${formatMoney(salesTotal)} · ${subtitle} · 先列 ${visibleRows.length.toLocaleString()}`;
renderGrowthDecisionSummary(rows, activeGrowthDetailKind);
if (!rows.length) {
resultBox.innerHTML = `<div class="text-center py-4 text-muted">

View File

@@ -461,6 +461,10 @@ def test_ai_intelligence_template_uses_pchome_growth_name_and_endpoint():
assert "growth-price-chip" in template
assert "formatGrowthDetailPrice" in template
assert "formatGapDisplay" in template
assert "growthDecisionSummary" in template
assert "renderGrowthDecisionSummary" in template
assert "growth-decision-metric" in template
assert "最大價差" in template
assert "scrollToPanel('externalPricePanel')" in template
assert "備援資料檢查" in template
assert "外部報價預檢" not in template