feat: add growth action summary
All checks were successful
CD Pipeline / deploy (push) Successful in 1m16s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m16s
This commit is contained in:
@@ -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 # 用於模板顯示
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user