diff --git a/config.py b/config.py index cf6ee11..8b018c6 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.645" +SYSTEM_VERSION = "V10.646" 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 3b608ac..d3d7156 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -81,6 +81,7 @@ - V10.643 起 `/ai_intelligence` 的商品明細上方必須提供「商品策略分流」視覺摘要,至少包含價格壓力、價格優勢、待確認、缺比價四類;每一類需顯示件數、近 7 天業績與比例條,且可點擊切換明細。舊 KPI 卡也不得是靜態數字,需可導向全部商品、可處理商品、高風險比價或處理紀錄。 - V10.644 起 `/ai_intelligence` 的商品明細列不得只用句子描述比價;每列必須顯示 PChome 價格、MOMO 參考價、差距、可信度四格價格證據,並保留下一步按鈕。單位價候選需顯示單位價與單位,候選待確認或缺資料則以「待補 / 候選待確認」呈現,不得捏造價格。 - V10.645 起 `/ai_intelligence` 的商品明細分流切換後,必須顯示「這類商品怎麼處理」的行動摘要,包含件數、近 7 天業績、平均可信度、最大價差、代表商品與主按鈕;使用者不得只能看到商品列表而不知道下一步。 +- V10.646 起 `/ai_intelligence` 的商品明細必須提供搜尋與排序;搜尋至少涵蓋商品、分類、商品編號與 MOMO 候選資訊,排序至少支援優先級、近 7 天業績、價差、下滑幅度與可信度。搜尋/排序後的行動摘要與明細列表必須使用同一批結果。 ## 零之一、12 Agent 決策信封(2026-05-24) diff --git a/templates/ai_intelligence.html b/templates/ai_intelligence.html index 1d76004..fc8e447 100644 --- a/templates/ai_intelligence.html +++ b/templates/ai_intelligence.html @@ -837,6 +837,43 @@ font-family: var(--momo-font-mono); } + .growth-detail-controls { + display: grid; + grid-template-columns: minmax(180px, 1fr) minmax(150px, 0.42fr) auto; + gap: 8px; + align-items: end; + margin-bottom: 10px; + } + + .growth-detail-field { + display: grid; + gap: 5px; + } + + .growth-detail-field span { + color: var(--momo-text-muted); + font-size: 0.68rem; + font-weight: 900; + } + + .growth-detail-control { + width: 100%; + border: 1px solid rgba(42, 37, 32, 0.12); + border-radius: 8px; + background: rgba(255, 255, 255, 0.86); + color: var(--momo-text-strong); + font-size: 0.78rem; + font-weight: 800; + min-height: 34px; + padding: 7px 10px; + } + + .growth-detail-control:focus { + border-color: rgba(172, 92, 58, 0.36); + box-shadow: 0 0 0 3px rgba(172, 92, 58, 0.1); + outline: 0; + } + .growth-detail-result { border: 1px solid rgba(42, 37, 32, 0.1); border-radius: 8px; @@ -1443,6 +1480,10 @@ width: 100%; } + .growth-detail-controls { + grid-template-columns: 1fr; + } + .growth-detail-price-grid { grid-template-columns: 1fr; } @@ -1645,6 +1686,23 @@ +
+ + + +
整理商品明細中... @@ -1949,6 +2007,8 @@ let allCompetitors = []; let latestGrowthStats = {}; let latestGrowthRows = []; let activeGrowthDetailKind = 'all'; +let growthDetailSearchText = ''; +let activeGrowthDetailSort = 'priority'; // ── 頁面載入 ──────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { @@ -2509,7 +2569,7 @@ function growthDetailConfig(kind) { function growthDetailRows(kind) { const rows = Array.isArray(latestGrowthRows) ? [...latestGrowthRows] : []; const topCategory = latestGrowthStats.top_category || ''; - const filtered = rows.filter((row) => { + let 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; @@ -2524,7 +2584,53 @@ function growthDetailRows(kind) { return true; }); - return filtered.sort((a, b) => Number(b.priority_score || 0) - Number(a.priority_score || 0)); + const search = String(growthDetailSearchText || '').trim().toLowerCase(); + if (search) { + filtered = filtered.filter((row) => { + const price = row.external_price || {}; + const candidate = row.review_candidate || {}; + const haystack = [ + row.product_name, + row.category, + row.vendor, + row.pchome_product_id, + row.recommended_action?.label, + price.momo_name, + price.momo_sku, + candidate.momo_name, + candidate.momo_sku, + ].filter(Boolean).join(' ').toLowerCase(); + return haystack.includes(search); + }); + } + + const gapValue = (row) => { + const gap = row.external_price?.gap_pct; + return gap === null || gap === undefined || !Number.isFinite(Number(gap)) ? null : Number(gap); + }; + const declineValue = (row) => { + const value = row.sales_delta_pct; + return value === null || value === undefined || !Number.isFinite(Number(value)) ? null : Number(value); + }; + const nullLast = (a, b, getter, direction = 'desc') => { + const av = getter(a); + const bv = getter(b); + if (av === null && bv === null) return Number(b.priority_score || 0) - Number(a.priority_score || 0); + if (av === null) return 1; + if (bv === null) return -1; + return direction === 'asc' ? av - bv : bv - av; + }; + + return filtered.sort((a, b) => { + if (activeGrowthDetailSort === 'sales') return Number(b.sales_7d || 0) - Number(a.sales_7d || 0); + if (activeGrowthDetailSort === 'gap') return nullLast(a, b, (row) => { + const gap = gapValue(row); + return gap === null ? null : Math.abs(gap); + }); + if (activeGrowthDetailSort === 'decline') return nullLast(a, b, declineValue, 'asc'); + if (activeGrowthDetailSort === 'quality') return rowQualityScore(b) - rowQualityScore(a); + return Number(b.priority_score || 0) - Number(a.priority_score || 0); + }); } function showGrowthDetail(kind, shouldScroll = true) { @@ -2533,6 +2639,27 @@ function showGrowthDetail(kind, shouldScroll = true) { if (shouldScroll) scrollToPanel('growthDrilldownPanel'); } +function setGrowthDetailSearch(value) { + growthDetailSearchText = String(value || '').trim().toLowerCase(); + renderGrowthDetail(activeGrowthDetailKind); +} + +function setGrowthDetailSort(value) { + const allowed = new Set(['priority', 'sales', 'gap', 'decline', 'quality']); + activeGrowthDetailSort = allowed.has(value) ? value : 'priority'; + renderGrowthDetail(activeGrowthDetailKind); +} + +function clearGrowthDetailFilters() { + growthDetailSearchText = ''; + activeGrowthDetailSort = 'priority'; + const search = document.getElementById('growthDetailSearch'); + const sort = document.getElementById('growthDetailSort'); + if (search) search.value = ''; + if (sort) sort.value = 'priority'; + renderGrowthDetail(activeGrowthDetailKind); +} + function renderGrowthStrategySummary() { const box = document.getElementById('growthStrategyGrid'); if (!box) return; diff --git a/tests/test_pchome_revenue_growth_service.py b/tests/test_pchome_revenue_growth_service.py index ffd59af..17438a1 100644 --- a/tests/test_pchome_revenue_growth_service.py +++ b/tests/test_pchome_revenue_growth_service.py @@ -465,6 +465,12 @@ def test_ai_intelligence_template_uses_pchome_growth_name_and_endpoint(): assert "renderGrowthDecisionSummary" in template assert "growth-decision-metric" in template assert "最大價差" in template + assert "growthDetailSearch" in template + assert "growthDetailSort" in template + assert "setGrowthDetailSearch" in template + assert "setGrowthDetailSort" in template + assert "clearGrowthDetailFilters" in template + assert "價差大到小" in template assert "scrollToPanel('externalPricePanel')" in template assert "備援資料檢查" in template assert "外部報價預檢" not in template