diff --git a/config.py b/config.py index af1d4b5..fc91a9c 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.641" +SYSTEM_VERSION = "V10.642" 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 2c2355e..802f825 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -77,6 +77,7 @@ - V10.639 起待確認候選排序必須容忍缺少單位數量;沒有 `momo_total_quantity` / `competitor_total_quantity` 時仍可保存為 `needs_review`,不得中斷 PChome 導向 MOMO 回填。 - V10.640 起 `/ai_intelligence` 必須提供 MOMO 待確認候選操作佇列;使用者可直接確認同款或排除候選。確認後 `external_offers` 會轉為 `verified/verified` 並進入作戰清單,排除後轉為 `rejected/rejected`,兩者都必須清掉 PChome 成長作戰清單快取。 - V10.641 起 `/ai_intelligence` 的摘要數字不可只是靜態文字;第一屏 KPI、商品處理進度、待確認數字都必須可點擊並導向對應明細。今日清單若已有 MOMO 待確認候選,下一步必須顯示「確認候選」並跳到候選面板,不得再只顯示「補齊比價」。 +- V10.642 起 `/ai_intelligence` 的摘要卡與商品處理數字不可只跳到大區塊;點擊後必須開啟商品明細面板,列出商品名稱、分類、近 7 天業績、業績變化、MOMO 比價狀態與下一步按鈕。明細需至少支援全部、價格壓力、價格優勢、待確認、缺比價與有外部價切換;外部價格風險分佈也必須能一鍵篩選下方表格。 ## 零之一、12 Agent 決策信封(2026-05-24) diff --git a/templates/ai_intelligence.html b/templates/ai_intelligence.html index a281cfb..d55f25a 100644 --- a/templates/ai_intelligence.html +++ b/templates/ai_intelligence.html @@ -473,6 +473,20 @@ gap: 5px; } + .price-risk-row[role="button"] { + border-radius: 8px; + cursor: pointer; + padding: 6px; + transition: background-color 0.16s ease, transform 0.16s ease; + } + + .price-risk-row[role="button"]:hover, + .price-risk-row[role="button"]:focus { + background: rgba(255, 255, 255, 0.82); + outline: 0; + transform: translateY(-1px); + } + .ops-funnel-meta, .ops-source-meta, .price-risk-meta { @@ -624,6 +638,98 @@ padding: 8px 10px; } + .growth-detail-panel { + margin-top: 12px; + } + + .growth-detail-toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 10px; + } + + .growth-detail-title { + margin: 0; + color: var(--momo-text-strong); + font-size: 0.95rem; + font-weight: 900; + line-height: 1.3; + } + + .growth-detail-meta { + color: var(--momo-text-muted); + font-size: 0.76rem; + font-weight: 800; + } + + .growth-detail-tabs { + display: flex; + flex-wrap: wrap; + gap: 7px; + } + + .growth-detail-tab { + border: 1px solid rgba(42, 37, 32, 0.12); + border-radius: 999px; + background: rgba(250, 247, 240, 0.66); + color: var(--momo-text-muted); + font-size: 0.74rem; + font-weight: 900; + padding: 6px 10px; + } + + .growth-detail-tab.is-active { + border-color: rgba(172, 92, 58, 0.34); + background: rgba(242, 178, 90, 0.18); + color: var(--momo-text-strong); + } + + .growth-detail-result { + border: 1px solid rgba(42, 37, 32, 0.1); + border-radius: 8px; + background: rgba(255, 255, 255, 0.76); + min-height: 190px; + max-height: 390px; + overflow: auto; + } + + .growth-detail-row { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(118px, auto); + gap: 12px; + border-bottom: 1px solid rgba(42, 37, 32, 0.08); + padding: 11px 12px; + } + + .growth-detail-row:last-child { + border-bottom: 0; + } + + .growth-detail-name { + margin: 0; + color: var(--momo-text-strong); + font-size: 0.86rem; + font-weight: 900; + line-height: 1.35; + } + + .growth-detail-line { + margin: 4px 0 0; + color: var(--momo-text-muted); + font-size: 0.74rem; + line-height: 1.4; + } + + .growth-detail-action { + display: grid; + gap: 6px; + align-content: start; + justify-items: end; + } + .growth-source-list { display: grid; gap: 8px; @@ -1111,9 +1217,14 @@ } .growth-item, + .growth-detail-row, .offer-dryrun-row { grid-template-columns: 1fr; } + + .growth-detail-action { + justify-items: stretch; + } } {% endblock %} @@ -1151,7 +1262,7 @@
-
+
今日任務 @@ -1159,7 +1270,7 @@
整理中
正在讀取 PChome 業績與 MOMO 外部價格。看明細
-
+
可立即處理 @@ -1167,7 +1278,7 @@
已有可用比價資料看清單
-
+
待補比價 @@ -1175,7 +1286,7 @@
有業績但缺外部參考看商品
-
+
最新業績日 @@ -1204,7 +1315,7 @@
-
+
商品處理進度 —% @@ -1224,7 +1335,7 @@
-
+
外部價格來源 @@ -1263,6 +1374,31 @@
+ +
+
+
+
+

今日商品明細

+
整理中
+
+
+ + + + + + +
+
+
+
+
整理商品明細中... +
+
+
+
+
@@ -1278,19 +1414,19 @@
-
+
追蹤商品
-
+
可立即處理
-
+
無法比價
-
+
待確認
@@ -1418,15 +1554,15 @@
-
+
需檢查價格
-
+
留意價差
-
+
價格有利
@@ -1557,6 +1693,8 @@ // ── 全域資料 ──────────────────────────────────────── let allCompetitors = []; let latestGrowthStats = {}; +let latestGrowthRows = []; +let activeGrowthDetailKind = 'all'; // ── 頁面載入 ──────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { @@ -1690,6 +1828,18 @@ function handleDrilldownKey(event, panelId) { scrollToPanel(panelId); } +function handleGrowthDetailKey(event, kind) { + if (event.key !== 'Enter' && event.key !== ' ') return; + event.preventDefault(); + showGrowthDetail(kind); +} + +function handlePriceRiskKey(event, risk) { + if (event.key !== 'Enter' && event.key !== ' ') return; + event.preventDefault(); + showPriceRiskDetail(risk); +} + function clampPercent(value) { return Math.max(0, Math.min(100, Number(value || 0))); } @@ -1912,7 +2062,7 @@ async function loadGrowthOps(forceRefresh = false) { } try { - const url = '/api/ai/pchome-growth/opportunities?limit=8' + (forceRefresh ? '&refresh=1' : ''); + const url = '/api/ai/pchome-growth/opportunities?limit=50' + (forceRefresh ? '&refresh=1' : ''); const res = await fetch(url); const data = await readJsonResponse(res); if (!data.success) throw new Error(data.error || '讀取失敗'); @@ -1933,13 +2083,17 @@ async function loadGrowthOps(forceRefresh = false) { `業績:${scope.primary_sales_source || 'PChome 後台業績'} · 外部:${active} · 暫停:${paused}`; renderGrowthSourceReadiness((scope.source_readiness || {}).sources || []); - renderGrowthOps(data.opportunities || []); + latestGrowthRows = data.opportunities || []; + renderGrowthOps(latestGrowthRows); + renderGrowthDetail(activeGrowthDetailKind); loadGrowthReviewCandidates(); } catch (error) { console.error(error); + latestGrowthRows = []; renderOpsCommandDashboard({}, {}); renderGrowthActionHint({ candidate_count: 0, mapped_count: 0, needs_mapping_count: 0 }); renderGrowthDataSourceSummary({}); + renderGrowthDetail('all'); renderGrowthReviewCandidates([]); list.innerHTML = `
@@ -2041,6 +2195,131 @@ function renderGrowthSourceReadiness(sources) { }).join(''); } +function resolveGrowthDetailKind(kind) { + if (kind !== 'task') return kind || 'all'; + const reviewCandidateCount = Number(latestGrowthStats.review_candidate_count || 0); + const needsMapping = Number(latestGrowthStats.needs_mapping_count || 0); + const mappedCount = Number(latestGrowthStats.mapped_count || 0); + if (reviewCandidateCount > 0) return 'review'; + if (needsMapping > mappedCount) return 'needs'; + return 'risk'; +} + +function growthDetailConfig(kind) { + const topCategory = latestGrowthStats.top_category || ''; + const configs = { + all: ['今日商品明細', '依優先級排序,先看高業績、下滑或價格有壓力的商品。'], + ready: ['可立即處理商品', '已有 MOMO 參考價,可以直接檢查售價、活動或曝光。'], + needs: ['缺比價商品', '有 PChome 業績,但還沒有可用 MOMO 參考。'], + review: ['MOMO 候選待確認', '候選已找到,確認同款後才會進入價格判斷。'], + risk: ['MOMO 更便宜商品', 'MOMO 參考價較低,優先檢查 PChome 售價、券或組合。'], + advantage: ['PChome 價格優勢商品', 'PChome 目前較有價格優勢,適合檢查曝光與主推位置。'], + source: ['外部價格來源明細', '只列出已接到 MOMO 外部參考價的商品。'], + decline: ['業績下滑商品', '近 7 天比前 7 天下滑的商品。'], + category: [topCategory ? `${topCategory} 商品明細` : '最大業績分類明細', '目前最大業績分類內的商品。'], + }; + return configs[kind] || configs.all; +} + +function growthDetailRows(kind) { + const rows = Array.isArray(latestGrowthRows) ? [...latestGrowthRows] : []; + const topCategory = latestGrowthStats.top_category || ''; + const filtered = rows.filter((row) => { + const actionCode = row.recommended_action?.code || ''; + const price = row.external_price || null; + const gap = price && price.gap_pct !== null && price.gap_pct !== undefined ? Number(price.gap_pct) : null; + if (kind === 'ready') return Boolean(price); + if (kind === 'needs') return !price && !row.review_candidate; + if (kind === 'review') return Boolean(row.review_candidate) || actionCode === 'review_external_candidate'; + if (kind === 'risk') return Boolean(price) && (actionCode === 'review_price_or_promo' || (gap !== null && gap < -5)); + if (kind === 'advantage') return Boolean(price) && (actionCode === 'amplify_price_advantage' || (gap !== null && gap > 5)); + if (kind === 'source') return Boolean(price); + if (kind === 'decline') return Number(row.sales_delta_pct || 0) < 0; + if (kind === 'category') return topCategory && row.category === topCategory; + return true; + }); + + return filtered.sort((a, b) => Number(b.priority_score || 0) - Number(a.priority_score || 0)); +} + +function showGrowthDetail(kind, shouldScroll = true) { + activeGrowthDetailKind = resolveGrowthDetailKind(kind); + renderGrowthDetail(activeGrowthDetailKind); + if (shouldScroll) scrollToPanel('growthDrilldownPanel'); +} + +function renderGrowthDetail(kind = activeGrowthDetailKind) { + const titleBox = document.getElementById('growthDrilldownTitle'); + const metaBox = document.getElementById('growthDrilldownMeta'); + const resultBox = document.getElementById('growthDrilldownResult'); + if (!titleBox || !metaBox || !resultBox) return; + + activeGrowthDetailKind = resolveGrowthDetailKind(kind); + document.querySelectorAll('.growth-detail-tab').forEach((tab) => { + tab.classList.toggle('is-active', tab.dataset.detailKind === activeGrowthDetailKind); + }); + + const [title, subtitle] = growthDetailConfig(activeGrowthDetailKind); + const rows = growthDetailRows(activeGrowthDetailKind); + const visibleRows = rows.slice(0, 50); + 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()} 件`; + + if (!rows.length) { + resultBox.innerHTML = `
+ + 目前沒有符合這個條件的商品。 +
`; + return; + } + + resultBox.innerHTML = visibleRows.map((row) => { + const action = row.recommended_action || {}; + const price = row.external_price || null; + const reviewCandidate = row.review_candidate || null; + const gap = price && price.gap_pct !== null && price.gap_pct !== undefined ? Number(price.gap_pct) : null; + const gapText = reviewCandidate + ? '候選待確認' + : gap === null + ? '缺 MOMO 參考' + : gap < 0 + ? `PChome 貴 ${Math.abs(gap).toFixed(1)}%` + : gap > 0 + ? `PChome 便宜 ${gap.toFixed(1)}%` + : '價格差不多'; + const delta = row.sales_delta_pct === null || row.sales_delta_pct === undefined + ? '前期不足' + : `${Number(row.sales_delta_pct).toFixed(1)}%`; + const productKey = escapeHtml(row.pchome_product_id || row.product_name || ''); + const nextAction = action.code === 'review_external_candidate' + ? 'review-candidate' + : action.code === 'map_external_product' + ? 'backfill' + : 'focus-price'; + const nextLabel = action.code === 'review_external_candidate' + ? '確認候選' + : action.code === 'map_external_product' + ? '補齊比價' + : '看價格'; + return `
+
+

${escapeHtml(row.product_name || '未命名商品')}

+

+ ${escapeHtml(row.category || '未分類')} · 近 7 天 ${escapeHtml(formatMoney(row.sales_7d))} · 變化 ${escapeHtml(delta)} +

+

+ ${escapeHtml(action.label || '待判斷')} · ${escapeHtml(gapText)} +

+
+
+ ${escapeHtml(action.label || '待判斷')} + +
+
`; + }).join(''); +} + function renderGrowthOps(rows) { const list = document.getElementById('growthOpsList'); if (!rows.length) { @@ -2051,7 +2330,8 @@ function renderGrowthOps(rows) { return; } - const body = rows.map((row, index) => { + const visibleRows = rows.slice(0, 12); + const body = visibleRows.map((row, index) => { const action = row.recommended_action || {}; const reason = (row.reason_lines || []).slice(0, 2).join(' '); const price = row.external_price; @@ -2351,6 +2631,15 @@ function renderPriceRiskBoard(rows) { setWidth('priceRiskLowBar', (counts.low / total) * 100); } +function showPriceRiskDetail(risk) { + const select = document.getElementById('riskFilter'); + if (select) { + select.value = risk || 'all'; + filterTable(); + } + scrollToPanel('externalPricePanel'); +} + function renderCompetitorTable(rows) { rows = Array.isArray(rows) ? rows : []; const tbody = document.getElementById('competitorTbody'); diff --git a/tests/test_pchome_revenue_growth_service.py b/tests/test_pchome_revenue_growth_service.py index e105549..15b3cfa 100644 --- a/tests/test_pchome_revenue_growth_service.py +++ b/tests/test_pchome_revenue_growth_service.py @@ -441,6 +441,17 @@ def test_ai_intelligence_template_uses_pchome_growth_name_and_endpoint(): assert "growthDataSourceSummary" in template assert "external_data_source_counts" in template assert "compSourceSummary" in template + assert "growthDrilldownPanel" in template + assert "showGrowthDetail" in template + assert "growthDetailRows" in template + assert "growth-detail-tab" in template + assert "今日商品明細" in template + assert "價格壓力" in template + assert "價格優勢" in template + assert "有外部價" in template + assert "showPriceRiskDetail" in template + assert "handlePriceRiskKey" in template + assert "opportunities?limit=50" in template assert "scrollToPanel('externalPricePanel')" in template assert "備援資料檢查" in template assert "外部報價預檢" not in template