From 7180c0f817860dd18287247de1e4cd4f26251405 Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 24 Jun 2026 20:05:58 +0800 Subject: [PATCH] feat: add growth action summary --- config.py | 2 +- docs/AI_INTELLIGENCE_MODULE_SOT.md | 1 + templates/ai_intelligence.html | 185 ++++++++++++++++++++ tests/test_pchome_revenue_growth_service.py | 4 + 4 files changed, 191 insertions(+), 1 deletion(-) diff --git a/config.py b/config.py index d8a7d61..cf6ee11 100644 --- a/config.py +++ b/config.py @@ -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 # 用於模板顯示 diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md index 727f760..3b608ac 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -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) diff --git a/templates/ai_intelligence.html b/templates/ai_intelligence.html index bbfa910..1d76004 100644 --- a/templates/ai_intelligence.html +++ b/templates/ai_intelligence.html @@ -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 @@ +
+
+

正在整理處理建議

+

讀取商品、業績與比價狀態後,這裡會顯示最適合的下一步。

+
+ +
整理商品明細中... @@ -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 = `
+

${escapeHtml(config.title)}

+

目前沒有符合這個條件的商品。

+
+ 商品 0 + 近 7 天 NT$ 0 + 可信度 待補 + 最大價差 待判斷 +
+
+ `; + 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 = `
+

${escapeHtml(config.title)}

+

${escapeHtml(config.copy)} ${escapeHtml(topName)}

+
+ 商品 ${formatCount(rows.length)} + 近 7 天 ${escapeHtml(formatMoney(sales))} + 可信度 ${avgQuality ? avgQuality + '%' : '待補'} + 最大價差 ${escapeHtml(largestGap === null ? '待判斷' : formatGapDisplay(largestGap))} +
+
+ `; +} + 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 = `
diff --git a/tests/test_pchome_revenue_growth_service.py b/tests/test_pchome_revenue_growth_service.py index c9efcf5..ffd59af 100644 --- a/tests/test_pchome_revenue_growth_service.py +++ b/tests/test_pchome_revenue_growth_service.py @@ -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