Files
ewoooc/templates/ai_intelligence.html
OoO 939ed5eef5
All checks were successful
CD Pipeline / deploy (push) Successful in 2m18s
feat(ai): move intelligence page to v2 shell
2026-05-01 21:03:19 +08:00

723 lines
29 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends 'ewoooc_base.html' %}
{% block title %}AI 競情中樞 - WOOO TECH{% endblock %}
{% block extra_css %}
<style>
.ai-intel-page {
display: flex;
flex-direction: column;
gap: 18px;
}
.ai-intel-hero {
position: relative;
overflow: hidden;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 18px;
align-items: center;
padding: 22px;
border: 1px solid var(--momo-border-strong);
border-radius: 8px;
background:
radial-gradient(circle at 18px 18px, rgba(42, 37, 32, 0.12) 1px, transparent 1px),
linear-gradient(135deg, rgba(242, 178, 90, 0.28), rgba(255, 255, 255, 0.92) 42%, rgba(172, 92, 58, 0.12));
background-size: 18px 18px, auto;
box-shadow: var(--momo-shadow-soft);
}
.ai-intel-hero::after {
content: "";
position: absolute;
inset: auto 20px 18px auto;
width: 132px;
height: 132px;
border: 1px solid rgba(42, 37, 32, 0.12);
border-radius: 50%;
background: repeating-linear-gradient(
90deg,
rgba(42, 37, 32, 0.08) 0,
rgba(42, 37, 32, 0.08) 1px,
transparent 1px,
transparent 8px
);
opacity: 0.72;
pointer-events: none;
}
.ai-intel-title {
position: relative;
z-index: 1;
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
margin: 0;
color: var(--momo-text-strong);
font-family: var(--momo-font-display);
font-size: clamp(1.45rem, 2vw, 2.15rem);
font-weight: 800;
letter-spacing: 0;
}
.ai-intel-title i {
color: var(--momo-warm-rust);
}
.ai-intel-badge,
.ai-status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid rgba(42, 37, 32, 0.14);
border-radius: 999px;
background: rgba(255, 255, 255, 0.68);
color: var(--momo-text-strong);
font-family: var(--momo-font-mono);
font-size: 0.78rem;
font-weight: 800;
padding: 5px 10px;
}
.ai-status-badge.is-success {
border-color: rgba(40, 128, 80, 0.24);
background: rgba(232, 247, 238, 0.88);
color: #216542;
}
.ai-status-badge.is-error {
border-color: rgba(188, 75, 49, 0.26);
background: rgba(255, 241, 237, 0.9);
color: #9b3d2b;
}
.ai-intel-subtitle {
position: relative;
z-index: 1;
margin: 8px 0 0;
color: var(--momo-text-muted);
font-size: 0.93rem;
}
.ai-intel-actions {
position: relative;
z-index: 1;
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
max-width: 620px;
}
.ai-action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
min-height: 38px;
border-radius: 8px;
font-weight: 800;
white-space: nowrap;
}
.ai-action-btn.btn-outline-danger {
color: var(--momo-warm-rust);
border-color: rgba(172, 92, 58, 0.44);
}
.ai-action-btn.btn-outline-primary {
color: var(--momo-accent-strong);
border-color: rgba(42, 37, 32, 0.24);
}
.ai-action-btn.btn-outline-warning {
color: #805313;
border-color: rgba(242, 178, 90, 0.66);
}
.ai-intel-page #kpiRow .card,
.ai-panel {
border: 1px solid var(--momo-border-subtle) !important;
border-radius: 8px;
background: rgba(255, 255, 255, 0.84);
box-shadow: var(--momo-shadow-soft);
}
.ai-intel-page #kpiRow .card-body {
display: flex;
flex-direction: column;
justify-content: center;
min-height: 116px;
}
.ai-intel-page #kpiRow .fs-2 {
color: var(--momo-text-strong) !important;
font-family: var(--momo-font-mono);
font-size: 2rem !important;
line-height: 1.05;
}
.ai-intel-page #kpiRow .small {
color: var(--momo-text-muted) !important;
font-weight: 700;
}
.ai-intel-page #kpiHighRiskCard.border-danger {
background: linear-gradient(160deg, rgba(255, 245, 240, 0.98), rgba(255, 255, 255, 0.9));
border-color: rgba(188, 75, 49, 0.48) !important;
}
.ai-panel .card-header,
.ai-panel .card-footer {
border-color: var(--momo-border-subtle) !important;
background: rgba(255, 255, 255, 0.78) !important;
}
.ai-panel-title {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--momo-text-strong);
font-family: var(--momo-font-display);
font-size: 0.95rem;
font-weight: 800;
}
.ai-panel-title i {
color: var(--momo-warm-caramel) !important;
}
.ai-panel .form-select,
.ai-panel .form-control {
border-color: var(--momo-border-subtle);
border-radius: 8px;
color: var(--momo-text-strong);
font-size: 0.82rem;
}
.ai-legend {
border-bottom: 1px solid var(--momo-border-subtle);
}
.ai-table-scroll {
overflow-x: auto;
overflow-y: auto;
max-height: 520px;
}
.ai-intel-page .table {
--bs-table-hover-bg: rgba(242, 178, 90, 0.12);
color: var(--momo-text-strong);
}
.ai-intel-page .table thead th {
border-bottom: 1px solid var(--momo-border-strong);
background: rgba(250, 247, 240, 0.96) !important;
color: var(--momo-text-muted);
font-size: 0.76rem;
font-weight: 800;
}
.ai-intel-page .table tbody td {
border-color: rgba(42, 37, 32, 0.08);
}
.ai-recs-scroll {
overflow-y: auto;
max-height: 568px;
}
.ai-intel-page #aiRecsList > .border {
border-color: var(--momo-border-subtle) !important;
border-radius: 8px !important;
background: rgba(250, 247, 240, 0.54);
}
@media (max-width: 992px) {
.ai-intel-hero {
grid-template-columns: 1fr;
}
.ai-intel-actions {
justify-content: flex-start;
max-width: none;
}
}
@media (max-width: 576px) {
.ai-intel-hero {
padding: 18px;
}
.ai-action-btn {
width: 100%;
}
}
</style>
{% endblock %}
{% block ewooo_content %}
<div class="ai-intel-page">
<!-- ── 頁首 ── -->
<section class="ai-intel-hero">
<div>
<h1 class="ai-intel-title">
<i class="fas fa-brain"></i>
AI 競情中樞
<span class="ai-intel-badge">ICAIM</span>
</h1>
<p class="ai-intel-subtitle">Hermes 3 分析師 × NemoTron 派發器使用資料庫內的競品價格、AI 決策與 PChome 比對記錄進行監控。</p>
</div>
<div class="ai-intel-actions">
<span id="lastUpdateBadge" class="ai-status-badge">
<i class="fas fa-sync me-1"></i>載入中...
</span>
<button class="btn btn-outline-danger btn-sm ai-action-btn" id="btnTrigger" onclick="triggerAnalysis()">
<i class="fas fa-bolt me-1"></i>立即分析
</button>
<button class="btn btn-outline-primary btn-sm ai-action-btn" id="btnPickList" onclick="generatePickList()">
<i class="fas fa-wand-magic-sparkles me-1"></i>產生挑品清單
</button>
<button class="btn btn-outline-warning btn-sm ai-action-btn" id="btnBackfill" onclick="backfillPchomeMatches()">
<i class="fas fa-magnifying-glass-chart me-1"></i>補抓待比對
</button>
<button class="btn btn-outline-secondary btn-sm ai-action-btn" onclick="loadDashboard()">
<i class="fas fa-redo me-1"></i>重新整理
</button>
</div>
</section>
<!-- ── KPI 卡片 ── -->
<div class="row g-3 mb-4" id="kpiRow">
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body text-center py-3">
<div class="fs-2 fw-bold text-primary" id="kpiSkus"></div>
<div class="small text-muted mt-1"><i class="fas fa-box me-1"></i>監控商品數</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body text-center py-3">
<div class="fs-2 fw-bold text-success" id="kpiCompetitors"></div>
<div class="small text-muted mt-1">
<i class="fas fa-store me-1"></i>有效競品比價
<span id="kpiMatchRate" class="text-muted" style="font-size:0.7rem"></span>
</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<!-- 高風險卡 — 數值來自全量 CTE非前端截斷的 200 筆 -->
<div class="card border-0 shadow-sm h-100" id="kpiHighRiskCard">
<div class="card-body text-center py-3">
<div class="fs-2 fw-bold text-danger" id="kpiHighRisk"></div>
<div class="small text-muted mt-1">
<i class="fas fa-exclamation-triangle me-1"></i>高風險商品
<span class="text-muted" style="font-size:0.7rem">(貴&gt;15%)</span>
</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body text-center py-3">
<div class="fs-2 fw-bold text-info" id="kpiAiRecs"></div>
<div class="small text-muted mt-1"><i class="fas fa-robot me-1"></i>AI 挑品/決策記錄</div>
</div>
</div>
</div>
</div>
<!-- ── 主體分兩欄(競品比價 + AI 決策) ── -->
<div class="row g-3">
<!-- ── 左PChome 競品比價 ── -->
<div class="col-xl-7">
<div class="card shadow-sm h-100 ai-panel">
<div class="card-header d-flex justify-content-between align-items-center py-2 bg-white border-bottom">
<span class="ai-panel-title">
<i class="fas fa-balance-scale text-warning me-2"></i>PChome 競品比價監控
</span>
<div class="d-flex gap-2 align-items-center">
<select class="form-select form-select-sm" id="riskFilter" onchange="filterTable()" style="width:100px">
<option value="all">全部</option>
<option value="HIGH">高風險</option>
<option value="MED">中風險</option>
<option value="LOW">低風險</option>
</select>
<input type="text" class="form-control form-control-sm" id="searchInput"
placeholder="搜尋商品..." oninput="filterTable()" style="width:130px">
</div>
</div>
<!-- 熱力圖圖例 -->
<div class="px-3 pt-2 pb-1 d-flex gap-3 small text-muted ai-legend" style="font-size:0.73rem">
<span><span style="display:inline-block;width:12px;height:12px;background:#fee2e2;border-radius:2px" class="me-1"></span>&gt;20%</span>
<span><span style="display:inline-block;width:12px;height:12px;background:#fef9c3;border-radius:2px" class="me-1"></span>貴 10~20%</span>
<span><span style="display:inline-block;width:12px;height:12px;background:#dcfce7;border-radius:2px" class="me-1"></span>我便宜</span>
</div>
<div class="card-body p-0 ai-table-scroll">
<table class="table table-sm table-hover mb-0 align-middle" id="competitorTable">
<thead class="table-light sticky-top" style="font-size:0.78rem;">
<tr>
<th class="ps-3" style="min-width:200px">商品</th>
<th class="text-end" style="min-width:75px">MOMO</th>
<th class="text-end" style="min-width:75px">PChome</th>
<th class="text-end" style="min-width:70px">價差</th>
<th style="min-width:90px">競品標籤</th>
<th class="text-center" style="min-width:55px">分數</th>
<th class="text-muted" style="min-width:80px">更新</th>
</tr>
</thead>
<tbody id="competitorTbody">
<tr><td colspan="7" class="text-center py-5 text-muted">
<div class="spinner-border spinner-border-sm me-2"></div>載入中...
</td></tr>
</tbody>
</table>
</div>
<div class="card-footer bg-white py-2 d-flex justify-content-between small text-muted">
<span id="compCount"></span>
<span>TTL: 6h | 比對門檻: 0.45</span>
</div>
</div>
</div>
<!-- ── 右AI 決策日誌 ── -->
<div class="col-xl-5">
<div class="card shadow-sm h-100 ai-panel">
<div class="card-header py-2 bg-white border-bottom">
<span class="ai-panel-title">
<i class="fas fa-robot text-danger me-2"></i>AI 決策日誌
<small class="text-muted fw-normal ms-2">挑品 Agent × Hermes × NemoTron</small>
</span>
</div>
<div class="card-body p-0 ai-recs-scroll">
<div id="aiRecsList" class="p-2">
<div class="text-center py-5 text-muted">
<div class="spinner-border spinner-border-sm me-2"></div>載入中...
</div>
</div>
</div>
<div class="card-footer bg-white py-2 d-flex justify-content-between small text-muted">
<span id="aiRecsCount"></span>
<span>可手動產生挑品清單</span>
</div>
</div>
</div>
</div><!-- /row -->
<!-- ── Trigger 進度 Toast ── -->
<div class="position-fixed bottom-0 end-0 p-3" style="z-index:9999">
<div id="triggerToast" class="toast align-items-center text-white border-0" role="alert">
<div class="d-flex">
<div class="toast-body" id="triggerToastMsg">
<i class="fas fa-bolt me-1"></i>分析已啟動...
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
</div>
</div><!-- /ai-intel-page -->
<script>
// ── 全域資料 ────────────────────────────────────────
let allCompetitors = [];
// ── 頁面載入 ────────────────────────────────────────
document.addEventListener('DOMContentLoaded', loadDashboard);
async function loadDashboard() {
try {
const res = await fetch('/api/ai/icaim/dashboard');
const data = await res.json();
if (!data.success) throw new Error(data.error || '載入失敗');
renderKPIs(data.stats);
allCompetitors = data.competitors;
renderCompetitorTable(allCompetitors);
renderAiRecs(data.ai_recs);
document.getElementById('lastUpdateBadge').innerHTML =
'<i class="fas fa-check-circle me-1"></i>上次更新 ' + new Date().toLocaleTimeString('zh-TW');
document.getElementById('lastUpdateBadge').className = 'ai-status-badge is-success';
} catch (e) {
document.getElementById('lastUpdateBadge').innerHTML =
'<i class="fas fa-exclamation-circle me-1"></i>載入失敗';
document.getElementById('lastUpdateBadge').className = 'ai-status-badge is-error';
console.error(e);
}
}
// ── KPIhigh_risk_count 來自後端全量 CTE─────────
function renderKPIs(stats) {
document.getElementById('kpiSkus').textContent = (stats.total_skus || 0).toLocaleString();
document.getElementById('kpiCompetitors').textContent = (stats.valid_competitor_prices || 0).toLocaleString();
document.getElementById('kpiAiRecs').textContent = (stats.total_ai_recs || 0).toLocaleString();
document.getElementById('kpiMatchRate').textContent = stats.match_rate ? `(${stats.match_rate}%)` : '';
const hr = stats.high_risk_count || 0;
document.getElementById('kpiHighRisk').textContent = hr;
// 高風險卡:數值 > 0 加紅底強調
document.getElementById('kpiHighRiskCard').className =
hr > 0
? 'card border-2 border-danger shadow-sm h-100'
: 'card border-0 shadow-sm h-100';
}
// ── 競品比價表格(熱力圖底色)──────────────────────
function renderCompetitorTable(rows) {
const tbody = document.getElementById('competitorTbody');
if (!rows.length) {
tbody.innerHTML = `<tr><td colspan="7" class="text-center py-5 text-muted">
<i class="fas fa-info-circle me-2"></i>暫無競品比價資料
</td></tr>`;
document.getElementById('compCount').textContent = '0 筆';
return;
}
tbody.innerHTML = rows.map(r => {
// 熱力圖底色(行層級)
let rowBg = '';
if (r.gap_pct > 20) rowBg = 'background:#fee2e2'; // 淺紅 — 嚴重貴
else if (r.gap_pct > 10) rowBg = 'background:#fef9c3'; // 淺黃 — 有風險
else if (r.gap_pct < 0) rowBg = 'background:#f0fdf4'; // 淺綠 — 我便宜
// 價差文字顏色
const gapClass = r.gap_pct > 15 ? 'text-danger fw-bold'
: r.gap_pct > 5 ? 'text-warning fw-bold'
: r.gap_pct < 0 ? 'text-success fw-bold'
: 'text-muted';
const gapSign = r.gap_pct > 0 ? '+' : '';
const tagHtml = (r.tags || []).map(t => {
const tagMap = {
'on_sale': ['bg-info text-dark', '促銷中'],
'discount_30pct': ['bg-danger text-white', '折30%+'],
'discount_20pct': ['bg-warning text-dark', '折20%+'],
'discount_10pct': ['bg-secondary text-white','折10%+'],
'low_stock': ['bg-dark text-white', '低庫存'],
'high_rating': ['bg-primary text-white', '高評分'],
};
const [cls, label] = tagMap[t] || ['bg-light text-dark', t];
return `<span class="badge ${cls} me-1" style="font-size:0.68rem">${label}</span>`;
}).join('');
const scoreColor = r.match_score >= 0.7 ? 'text-success'
: r.match_score >= 0.55 ? 'text-warning'
: 'text-danger';
return `<tr data-risk="${r.risk}" data-name="${r.name.toLowerCase()}" style="${rowBg}">
<td class="ps-3">
<div style="font-size:0.82rem;font-weight:500" title="${r.name}">${r.name}</div>
<small class="text-muted">${r.category} · ${r.sku}</small>
</td>
<td class="text-end text-dark fw-bold">$${r.momo_price.toLocaleString()}</td>
<td class="text-end text-secondary">$${r.pchome_price.toLocaleString()}</td>
<td class="text-end ${gapClass}">${gapSign}${r.gap_pct}%</td>
<td>${tagHtml || '<span class="text-muted small">—</span>'}</td>
<td class="text-center ${scoreColor}" style="font-size:0.8rem">${r.match_score.toFixed(2)}</td>
<td class="text-muted" style="font-size:0.75rem">${r.crawled_at}</td>
</tr>`;
}).join('');
document.getElementById('compCount').textContent = `${rows.length}`;
}
// ── 篩選 ─────────────────────────────────────────────
function filterTable() {
const risk = document.getElementById('riskFilter').value;
const search = document.getElementById('searchInput').value.toLowerCase().trim();
const filtered = allCompetitors.filter(r => {
const riskOk = risk === 'all' || r.risk === risk;
const searchOk = !search || r.name.toLowerCase().includes(search) || r.sku.includes(search);
return riskOk && searchOk;
});
renderCompetitorTable(filtered);
}
// ── AI 決策日誌(含推理足跡)───────────────────────
function renderAiRecs(recs) {
const container = document.getElementById('aiRecsList');
document.getElementById('aiRecsCount').textContent =
recs.length ? `${recs.length} 筆決策記錄` : '尚無決策記錄';
if (!recs.length) {
container.innerHTML = `
<div class="text-center py-5">
<i class="fas fa-brain fa-3x text-muted mb-3 d-block"></i>
<p class="text-muted mb-1">AI 決策日誌尚為空</p>
<p class="small text-muted mb-3">
排程每 6 小時執行一次 Hermes 分析 + NemoTron 派發<br>
或點擊「立即分析」手動觸發
</p>
<button class="btn btn-sm btn-outline-danger" onclick="triggerAnalysis()">
<i class="fas fa-bolt me-1"></i>立即觸發分析
</button>
</div>`;
return;
}
const strategyMap = {
'price_cut': ['bg-danger', '降價'],
'promote': ['bg-primary', '主推'],
'product_pick':['bg-success', 'AI挑品'],
'monitor': ['bg-secondary', '觀察'],
'flag': ['bg-warning text-dark', '覆核'],
};
container.innerHTML = recs.map(r => {
const [sBg, sLabel] = strategyMap[r.strategy] || ['bg-secondary', r.strategy];
const confPct = Math.round(r.confidence * 100);
const confColor = confPct >= 80 ? 'bg-success' : confPct >= 60 ? 'bg-warning' : 'bg-danger';
const gapSign = r.gap_pct > 0 ? '+' : '';
// 推理足跡
const hermesInfo = r.hermes_duration
? `${r.analyst} ${r.hermes_duration}s${r.hermes_tokens ? ' / ' + r.hermes_tokens + 'tok' : ''}`
: r.analyst;
const nimInfo = r.nim_tokens
? `${r.dispatcher} ${r.nim_tokens}tok`
: r.dispatcher;
return `<div class="border rounded mb-2 p-2" style="font-size:0.83rem">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="fw-bold text-truncate me-2" style="max-width:200px" title="${r.name}">${r.name}</span>
<span class="badge ${sBg} flex-shrink-0">${sLabel}</span>
</div>
<div class="d-flex gap-3 mb-1 text-muted small">
<span>MOMO <strong class="text-dark">$${r.momo_price.toLocaleString()}</strong></span>
<span>PChome <strong class="text-secondary">$${r.pchome_price.toLocaleString()}</strong></span>
<span class="${r.gap_pct > 10 ? 'text-danger fw-bold' : 'text-muted'}">${gapSign}${r.gap_pct}%</span>
</div>
<div class="mb-1 text-muted" style="font-size:0.78rem;line-height:1.4">
${r.reason ? r.reason.substring(0, 90) + (r.reason.length > 90 ? '…' : '') : ''}
</div>
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-1">
<div class="progress" style="width:60px;height:6px">
<div class="progress-bar ${confColor}" style="width:${confPct}%"></div>
</div>
<small class="text-muted">${confPct}%</small>
</div>
<small class="text-muted" title="推理足跡">
<i class="fas fa-microchip me-1" style="font-size:0.68rem"></i>${hermesInfo}${nimInfo}
· ${r.created_at}
</small>
</div>
</div>`;
}).join('');
}
// ── 產生 AI 建議挑品清單 ───────────────────────────
async function generatePickList() {
const btn = document.getElementById('btnPickList');
const toast = document.getElementById('triggerToast');
const msg = document.getElementById('triggerToastMsg');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>挑品中...';
try {
const res = await fetch('/api/ai/product-picks/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ limit: 50 })
});
const data = await res.json();
msg.innerHTML = data.success
? `<i class="fas fa-check-circle me-1"></i>${data.message}`
: `<i class="fas fa-times-circle me-1"></i>${data.error || '產生失敗'}`;
toast.className = 'toast align-items-center text-white border-0 ' +
(data.success ? 'bg-success' : 'bg-danger');
new bootstrap.Toast(toast, { delay: 6000 }).show();
if (data.success) loadDashboard();
} catch (e) {
msg.innerHTML = '<i class="fas fa-times-circle me-1"></i>挑品失敗:' + e.message;
toast.className = 'toast align-items-center text-white border-0 bg-danger';
new bootstrap.Toast(toast, { delay: 4000 }).show();
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-wand-magic-sparkles me-1"></i>產生挑品清單';
}
}
// ── 補抓 PChome 待比對商品 ─────────────────────────
async function backfillPchomeMatches() {
const btn = document.getElementById('btnBackfill');
const toast = document.getElementById('triggerToast');
const msg = document.getElementById('triggerToastMsg');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>補抓中...';
try {
const res = await fetch('/api/ai/pchome-match/backfill', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ limit: 60 })
});
const data = await res.json();
msg.innerHTML = data.success
? `<i class="fas fa-check-circle me-1"></i>${data.message}`
: `<i class="fas fa-times-circle me-1"></i>${data.error || '補抓啟動失敗'}`;
toast.className = 'toast align-items-center text-white border-0 ' +
(data.success ? 'bg-success' : 'bg-danger');
new bootstrap.Toast(toast, { delay: 6000 }).show();
if (data.success) setTimeout(loadDashboard, 90000);
} catch (e) {
msg.innerHTML = '<i class="fas fa-times-circle me-1"></i>補抓失敗:' + e.message;
toast.className = 'toast align-items-center text-white border-0 bg-danger';
new bootstrap.Toast(toast, { delay: 4000 }).show();
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-magnifying-glass-chart me-1"></i>補抓待比對';
}
}
// ── 手動觸發分析 ────────────────────────────────────
async function triggerAnalysis() {
const btn = document.getElementById('btnTrigger');
const toast = document.getElementById('triggerToast');
const msg = document.getElementById('triggerToastMsg');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>啟動中...';
try {
const res = await fetch('/api/ai/icaim/trigger', { method: 'POST' });
const data = await res.json();
msg.innerHTML = data.success
? `<i class="fas fa-check-circle me-1"></i>${data.message}`
: `<i class="fas fa-times-circle me-1"></i>${data.error}`;
toast.className = 'toast align-items-center text-white border-0 ' +
(data.success ? 'bg-success' : 'bg-danger');
new bootstrap.Toast(toast, { delay: 6000 }).show();
if (data.success) {
// 60 秒後自動重新整理儀表板
setTimeout(loadDashboard, 60000);
}
} catch (e) {
msg.innerHTML = '<i class="fas fa-times-circle me-1"></i>觸發失敗:' + e.message;
toast.className = 'toast align-items-center text-white border-0 bg-danger';
new bootstrap.Toast(toast, { delay: 4000 }).show();
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-bolt me-1"></i>立即分析';
}
}
</script>
{% endblock %}