From 1d1a7f6e949891aa7b7d7f1d71faeed61a93f98e Mon Sep 17 00:00:00 2001 From: OoO Date: Fri, 1 May 2026 15:22:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(dashboard):=20=E5=BC=B7=E5=8C=96=20AI=20?= =?UTF-8?q?=E6=8C=91=E5=93=81=E6=B8=85=E5=96=AE=E6=B1=BA=E7=AD=96=E8=B3=87?= =?UTF-8?q?=E8=A8=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONSTITUTION.md | 2 +- app.py | 4 +- config.py | 2 +- docs/AI_INTELLIGENCE_MODULE_SOT.md | 2 +- routes/dashboard_routes.py | 38 +++++++ templates/dashboard_v2.html | 157 ++++++++++++++++++++++++++++- tests/test_frontend_v2_assets.py | 4 + 7 files changed, 201 insertions(+), 8 deletions(-) diff --git a/CONSTITUTION.md b/CONSTITUTION.md index ce3b85a..2937c5b 100644 --- a/CONSTITUTION.md +++ b/CONSTITUTION.md @@ -2,7 +2,7 @@ > 本文件定義專案開發的核心準則與不可違反的規範 > **建立日期**: 2026-01-12 -> **當前版本**: V10.57 (Dashboard AI product pick list supports 50 items) +> **當前版本**: V10.58 (Dashboard AI pick list adds decision summary and reason column) > **最後更新**: 2026-05-01 --- diff --git a/app.py b/app.py index d09d780..872a30a 100644 --- a/app.py +++ b/app.py @@ -95,8 +95,8 @@ except Exception as e: sys_log.error(f"無法檢測磁碟空間: {e}") # 🚩 系統版本定義 (備份與顯示用) -# 🚩 2026-05-01 V10.57: Dashboard AI product pick list supports 50 items -SYSTEM_VERSION = "V10.57" +# 🚩 2026-05-01 V10.58: Dashboard AI pick list adds decision summary and reason column +SYSTEM_VERSION = "V10.58" # ========================================== # 🔒 SQL Injection 防護函數 diff --git a/config.py b/config.py index 39b9f30..1124444 100644 --- a/config.py +++ b/config.py @@ -254,7 +254,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.57" +SYSTEM_VERSION = "V10.58" 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 98aea4e..fcec923 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -37,7 +37,7 @@ SQL漏斗(~300筆) - 配對來源仍以 PChome crawler 真實搜尋結果為準;無競品資料時不生成挑品。 - 比對覆蓋率補強入口:`POST /api/ai/pchome-match/backfill`,優先補抓仍無有效 PChome 配對的高價 ACTIVE 商品,完成後自動重算 AI 挑品清單。 - 排程閉環:`run_pchome_match_backfill_task` 每日 10:30 執行,補抓 PChome 待比對商品、寫入歷史價格,再重算 `strategy='product_pick'` 清單。 -- 商品看板第一屏:`/` 的 V2 看板直接以 `products`、`price_records`、`competitor_prices`、`ai_price_recommendations` 顯示比對覆蓋率、PChome 優勢、MOMO 威脅、AI 挑品與待比對優先清單;`filter=ai_picks` 可查看 50 品 AI 挑品列表。 +- 商品看板第一屏:`/` 的 V2 看板直接以 `products`、`price_records`、`competitor_prices`、`ai_price_recommendations` 顯示比對覆蓋率、PChome 優勢、MOMO 威脅、AI 挑品與待比對優先清單;`filter=ai_picks` 可查看 50 品 AI 挑品列表,並在列表上方顯示平均信心、平均價差、最大價差與估算總價差空間,列表列內顯示 AI 排名與建議理由。 | 角色 | 模型 | 主機 | 成本 | 每日限額 | |------|------|------|------|---------| diff --git a/routes/dashboard_routes.py b/routes/dashboard_routes.py index f04bd0d..9a55c9f 100644 --- a/routes/dashboard_routes.py +++ b/routes/dashboard_routes.py @@ -441,6 +441,41 @@ def _load_ai_pick_selection(session, limit=PRODUCT_PICK_LIST_LIMIT): return skus, pick_map +def _summarize_ai_pick_selection(ai_pick_map): + """彙整目前 AI 挑品清單的可操作摘要,全部來自 ai_price_recommendations。""" + picks = list(ai_pick_map.values()) + if not picks: + return { + 'count': 0, + 'avg_confidence': 0, + 'avg_gap_pct': 0, + 'max_gap_pct': 0, + 'total_gap_amount': 0, + 'high_confidence_count': 0, + 'generated_at': None, + } + + confidence_values = [pick.get('confidence', 0) for pick in picks] + gap_values = [pick.get('gap_pct', 0) for pick in picks] + total_gap_amount = sum( + max((pick.get('momo_price') or 0) - (pick.get('pchome_price') or 0), 0) + for pick in picks + ) + + return { + 'count': len(picks), + 'avg_confidence': round(sum(confidence_values) / len(confidence_values), 3), + 'avg_gap_pct': round(sum(gap_values) / len(gap_values), 1), + 'max_gap_pct': round(max(gap_values), 1), + 'total_gap_amount': round(total_gap_amount), + 'high_confidence_count': sum(1 for value in confidence_values if value >= 0.65), + 'generated_at': max( + (pick.get('created_at') for pick in picks if pick.get('created_at')), + default=None, + ), + } + + # ========================================== # 快取與監控變數 # ========================================== @@ -945,8 +980,10 @@ def index(): filtered_items = [] ai_pick_skus = [] ai_pick_map = {} + ai_pick_summary = None if filter_type == 'ai_picks': ai_pick_skus, ai_pick_map = _load_ai_pick_selection(session, PRODUCT_PICK_LIST_LIMIT) + ai_pick_summary = _summarize_ai_pick_selection(ai_pick_map) # 先處理搜尋 if search_query: @@ -1081,6 +1118,7 @@ def index(): search_query=search_query, current_sort=sort_by, current_order=order, + ai_pick_summary=ai_pick_summary, scheduler_stats=scheduler_stats, avg_increase=avg_increase, avg_decrease=avg_decrease, diff --git a/templates/dashboard_v2.html b/templates/dashboard_v2.html index fc4bab6..04a4878 100644 --- a/templates/dashboard_v2.html +++ b/templates/dashboard_v2.html @@ -357,6 +357,45 @@ font-size: 11px; } + .dashboard-ai-summary-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 0; + border-bottom: 1px solid var(--momo-border-light); + } + + .dashboard-ai-summary-item { + min-width: 0; + padding: 14px 18px; + border-right: 1px solid var(--momo-border-light); + } + + .dashboard-ai-summary-item:last-child { + border-right: 0; + } + + .dashboard-ai-summary-label { + margin-bottom: 5px; + color: var(--momo-text-tertiary); + font-family: var(--momo-font-family-mono); + font-size: 10px; + font-weight: 800; + letter-spacing: 0.08em; + } + + .dashboard-ai-summary-value { + color: var(--momo-text-primary); + font-size: 18px; + font-weight: 800; + line-height: 1.15; + } + + .dashboard-ai-summary-sub { + margin-top: 4px; + color: var(--momo-text-secondary); + font-size: 11px; + } + .dashboard-table-wrap { overflow-x: auto; } @@ -368,6 +407,10 @@ font-size: var(--momo-font-size-sm); } + .dashboard-table.is-ai-picks { + min-width: 1460px; + } + .dashboard-table th { padding: 11px 14px; color: var(--momo-text-tertiary); @@ -559,6 +602,48 @@ line-height: 1.5; } + .dashboard-ai-pick-card { + display: grid; + min-width: 170px; + gap: 6px; + } + + .dashboard-ai-pick-head { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; + } + + .dashboard-ai-pick-rank { + display: inline-flex; + align-items: center; + padding: 3px 8px; + color: var(--momo-text-inverse); + background: var(--momo-ink); + border-radius: var(--momo-radius-pill); + font-family: var(--momo-font-family-mono); + font-size: 10px; + font-weight: 800; + } + + .dashboard-ai-pick-confidence { + color: var(--momo-success); + font-family: var(--momo-font-family-mono); + font-size: 11px; + font-weight: 800; + } + + .dashboard-ai-pick-reason { + display: -webkit-box; + overflow: hidden; + color: var(--momo-text-secondary); + font-size: 11px; + line-height: 1.45; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + } + .dashboard-history-button { display: inline-flex; align-items: center; @@ -693,6 +778,14 @@ grid-template-columns: repeat(3, minmax(0, 1fr)); } + .dashboard-ai-summary-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .dashboard-ai-summary-item:nth-child(2n) { + border-right: 0; + } + .dashboard-focus-grid { grid-template-columns: 1fr 1fr; } @@ -705,10 +798,20 @@ @media (max-width: 640px) { .dashboard-kpi-grid, - .dashboard-focus-grid { + .dashboard-focus-grid, + .dashboard-ai-summary-grid { grid-template-columns: 1fr; } + .dashboard-ai-summary-item { + border-right: 0; + border-bottom: 1px solid var(--momo-border-light); + } + + .dashboard-ai-summary-item:last-child { + border-bottom: 0; + } + .dashboard-kpi { border-right: 0; border-bottom: 1px solid var(--momo-border-light); @@ -926,8 +1029,38 @@ + {% if current_filter == 'ai_picks' and ai_pick_summary %} +
+
+
PICK COUNT
+
{{ ai_pick_summary.count | number_format }}
+
目前清單上限 {{ ai_pick_list_limit }} 品
+
+
+
AVG CONFIDENCE
+
{{ (ai_pick_summary.avg_confidence * 100) | round(0) | int }}%
+
高信心 {{ ai_pick_summary.high_confidence_count | number_format }} 品
+
+
+
AVG GAP
+
+{{ ai_pick_summary.avg_gap_pct | round(1) }}%
+
PChome 相對 MOMO 價差
+
+
+
BEST GAP
+
+{{ ai_pick_summary.max_gap_pct | round(1) }}%
+
清單內最大價格優勢
+
+
+
PRICE ROOM
+
${{ ai_pick_summary.total_gap_amount | int | number_format }}
+
50 品估算總價差空間
+
+
+ {% endif %} +
- +
@@ -937,6 +1070,9 @@ + {% if current_filter == 'ai_picks' %} + + {% endif %} @@ -1024,6 +1160,21 @@ {{ decision.summary }} + {% if current_filter == 'ai_picks' %} + + {% endif %} {% else %} -
分類 PChome 價格 競價判讀AI 建議 昨日漲跌 + {% if item.ai_pick %} +
+
+ #{{ item.ai_pick.rank }} + 信心 {{ (item.ai_pick.confidence * 100) | round(0) | int }}% +
+
{{ item.ai_pick.reason }}
+
+ {% else %} + 尚無建議理由 + {% endif %} +
{% if item.yesterday_diff > 0 %} ▲ +{{ item.yesterday_diff | abs | int | number_format }} @@ -1052,7 +1203,7 @@
+
{% if search_query %} 找不到與「{{ search_query }}」相關的商品 diff --git a/tests/test_frontend_v2_assets.py b/tests/test_frontend_v2_assets.py index 13fdd50..dc6bfd5 100644 --- a/tests/test_frontend_v2_assets.py +++ b/tests/test_frontend_v2_assets.py @@ -60,6 +60,10 @@ def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data(): assert "overview.pending_priority" in dashboard assert "filter='ai_picks'" in dashboard assert "AI 挑品清單" in dashboard + assert "dashboard-ai-summary-grid" in dashboard + assert "AI 建議" in dashboard + assert "item.ai_pick.reason" in dashboard + assert "_summarize_ai_pick_selection(ai_pick_map)" in route_source assert "{{ ai_pick_list_limit }} 品" in dashboard assert "_load_ai_pick_selection(session, PRODUCT_PICK_LIST_LIMIT)" in route_source assert "PRODUCT_PICK_LIST_LIMIT = 50" in route_source