From 2d9acfdc5c9b7a4ae2f5cf9934d38110121e12fe Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 24 Jun 2026 19:59:46 +0800 Subject: [PATCH] feat: show price evidence in growth details --- config.py | 2 +- docs/AI_INTELLIGENCE_MODULE_SOT.md | 1 + templates/ai_intelligence.html | 149 ++++++++++++++++++-- tests/test_pchome_revenue_growth_service.py | 4 + 4 files changed, 147 insertions(+), 9 deletions(-) diff --git a/config.py b/config.py index 4c1dafa..d8a7d61 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.643" +SYSTEM_VERSION = "V10.644" 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 e72d85d..727f760 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -79,6 +79,7 @@ - V10.641 起 `/ai_intelligence` 的摘要數字不可只是靜態文字;第一屏 KPI、商品處理進度、待確認數字都必須可點擊並導向對應明細。今日清單若已有 MOMO 待確認候選,下一步必須顯示「確認候選」並跳到候選面板,不得再只顯示「補齊比價」。 - V10.642 起 `/ai_intelligence` 的摘要卡與商品處理數字不可只跳到大區塊;點擊後必須開啟商品明細面板,列出商品名稱、分類、近 7 天業績、業績變化、MOMO 比價狀態與下一步按鈕。明細需至少支援全部、價格壓力、價格優勢、待確認、缺比價與有外部價切換;外部價格風險分佈也必須能一鍵篩選下方表格。 - V10.643 起 `/ai_intelligence` 的商品明細上方必須提供「商品策略分流」視覺摘要,至少包含價格壓力、價格優勢、待確認、缺比價四類;每一類需顯示件數、近 7 天業績與比例條,且可點擊切換明細。舊 KPI 卡也不得是靜態數字,需可導向全部商品、可處理商品、高風險比價或處理紀錄。 +- V10.644 起 `/ai_intelligence` 的商品明細列不得只用句子描述比價;每列必須顯示 PChome 價格、MOMO 參考價、差距、可信度四格價格證據,並保留下一步按鈕。單位價候選需顯示單位價與單位,候選待確認或缺資料則以「待補 / 候選待確認」呈現,不得捏造價格。 ## 零之一、12 Agent 決策信封(2026-05-24) diff --git a/templates/ai_intelligence.html b/templates/ai_intelligence.html index 4f329ca..bbfa910 100644 --- a/templates/ai_intelligence.html +++ b/templates/ai_intelligence.html @@ -798,7 +798,7 @@ .growth-detail-row { display: grid; - grid-template-columns: minmax(0, 1fr) minmax(118px, auto); + grid-template-columns: minmax(0, 1.1fr) minmax(300px, 0.9fr); gap: 12px; border-bottom: 1px solid rgba(42, 37, 32, 0.08); padding: 11px 12px; @@ -827,7 +827,61 @@ display: grid; gap: 6px; align-content: start; - justify-items: end; + justify-items: stretch; + } + + .growth-detail-action .table-row-action { + justify-self: end; + } + + .growth-detail-price-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 7px; + } + + .growth-price-chip { + border: 1px solid rgba(42, 37, 32, 0.1); + border-radius: 8px; + background: rgba(250, 247, 240, 0.62); + padding: 7px 8px; + } + + .growth-price-chip.is-risk { + border-color: rgba(185, 79, 58, 0.22); + background: rgba(255, 244, 239, 0.86); + } + + .growth-price-chip.is-good { + border-color: rgba(47, 143, 102, 0.2); + background: rgba(238, 249, 241, 0.88); + } + + .growth-price-label { + display: block; + color: var(--momo-text-muted); + font-size: 0.68rem; + font-weight: 900; + line-height: 1.2; + } + + .growth-price-value { + display: block; + margin-top: 3px; + color: var(--momo-text-strong); + font-family: var(--momo-font-mono); + font-size: 0.9rem; + font-weight: 900; + line-height: 1.15; + } + + .growth-price-note { + display: block; + margin-top: 2px; + color: var(--momo-text-muted); + font-size: 0.66rem; + font-weight: 760; + line-height: 1.25; } .growth-source-list { @@ -1326,6 +1380,14 @@ .growth-detail-action { justify-items: stretch; } + + .growth-detail-action .table-row-action { + justify-self: stretch; + } + + .growth-detail-price-grid { + grid-template-columns: 1fr; + } } {% endblock %} @@ -1924,6 +1986,37 @@ function formatMoney(value) { return 'NT$ ' + Math.round(num).toLocaleString(); } +function formatPriceAmount(value, options = {}) { + if (value === null || value === undefined || value === '') return '待補'; + const num = Number(value); + if (!Number.isFinite(num) || num <= 0) return '待補'; + const decimals = options.decimals ?? (num < 100 ? 2 : 0); + return 'NT$ ' + num.toLocaleString('zh-TW', { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + }); +} + +function formatGrowthDetailPrice(price, side) { + if (!price) return '待補'; + const isUnit = price.price_basis === 'unit_price'; + const unit = price.unit_label ? ` / ${price.unit_label}` : ''; + if (isUnit) { + const value = side === 'pchome' ? price.pchome_unit_price : price.momo_unit_price; + return `${formatPriceAmount(value, { decimals: 2 })}${unit}`; + } + const value = side === 'pchome' ? price.pchome_price : price.momo_price; + return formatPriceAmount(value); +} + +function formatGapDisplay(gap) { + if (gap === null || gap === undefined || !Number.isFinite(Number(gap))) return '待判斷'; + const num = Number(gap); + if (num < 0) return `PChome 貴 ${Math.abs(num).toFixed(1)}%`; + if (num > 0) return `PChome 便宜 ${num.toFixed(1)}%`; + return '價格接近'; +} + function escapeHtml(value) { return String(value ?? '').replace(/[&<>"']/g, (ch) => ({ '&': '&', @@ -2457,14 +2550,33 @@ function renderGrowthDetail(kind = activeGrowthDetailKind) { ? '候選待確認' : gap === null ? '缺 MOMO 參考' - : gap < 0 - ? `PChome 貴 ${Math.abs(gap).toFixed(1)}%` - : gap > 0 - ? `PChome 便宜 ${gap.toFixed(1)}%` - : '價格差不多'; + : formatGapDisplay(gap); const delta = row.sales_delta_pct === null || row.sales_delta_pct === undefined ? '前期不足' : `${Number(row.sales_delta_pct).toFixed(1)}%`; + const qualityScore = row.data_quality?.score !== null && row.data_quality?.score !== undefined + ? Math.round(Number(row.data_quality.score || 0)) + : reviewCandidate?.quality_score !== null && reviewCandidate?.quality_score !== undefined + ? Math.round(Number(reviewCandidate.quality_score || 0)) + : price?.match_score + ? Math.round(Number(price.match_score || 0) * 100) + : 0; + const pchomeDisplay = price + ? formatGrowthDetailPrice(price, 'pchome') + : reviewCandidate + ? formatPriceAmount(reviewCandidate.pchome_price) + : '待補'; + const momoDisplay = price + ? formatGrowthDetailPrice(price, 'momo') + : reviewCandidate + ? formatPriceAmount(reviewCandidate.momo_price) + : '待補'; + const priceBasis = price?.price_basis_label || (reviewCandidate ? '候選價' : '待補資料'); + const gapChipClass = gap !== null && gap < 0 + ? ' is-risk' + : gap !== null && gap > 0 + ? ' is-good' + : ''; const productKey = escapeHtml(row.pchome_product_id || row.product_name || ''); const nextAction = action.code === 'review_external_candidate' ? 'review-candidate' @@ -2487,7 +2599,28 @@ function renderGrowthDetail(kind = activeGrowthDetailKind) {

- ${escapeHtml(action.label || '待判斷')} +
+ + PChome + ${escapeHtml(pchomeDisplay)} + ${escapeHtml(priceBasis)} + + + MOMO + ${escapeHtml(momoDisplay)} + ${escapeHtml(price?.data_source_label || (reviewCandidate ? '候選待確認' : '等待補抓'))} + + + 差距 + ${escapeHtml(gapText)} + ${escapeHtml(action.label || '待判斷')} + + + 可信度 + ${qualityScore ? `${qualityScore}%` : '待補'} + ${escapeHtml(row.data_quality?.label || '資料待確認')} + +
`; diff --git a/tests/test_pchome_revenue_growth_service.py b/tests/test_pchome_revenue_growth_service.py index 35c7dc1..c9efcf5 100644 --- a/tests/test_pchome_revenue_growth_service.py +++ b/tests/test_pchome_revenue_growth_service.py @@ -457,6 +457,10 @@ def test_ai_intelligence_template_uses_pchome_growth_name_and_endpoint(): assert "商品策略分流" in template assert "growth-strategy-card" in template assert "aiRecsPanel" in template + assert "growth-detail-price-grid" in template + assert "growth-price-chip" in template + assert "formatGrowthDetailPrice" in template + assert "formatGapDisplay" in template assert "scrollToPanel('externalPricePanel')" in template assert "備援資料檢查" in template assert "外部報價預檢" not in template