/* page-ai-recommend.js — Turn C (extracted from inline) */ (function () { 'use strict'; // 台灣節日資料 const taiwanHolidays = [ { name: '農曆新年', month: 1, day: 29, keywords: ['過年', '春節', '紅包'] }, { name: '元宵節', month: 2, day: 12, keywords: ['元宵', '湯圓'] }, { name: '228連假', month: 2, day: 28, keywords: ['連假', '旅遊'] }, { name: '婦女節', month: 3, day: 8, keywords: ['女神節', '寵愛'] }, { name: '白色情人節', month: 3, day: 14, keywords: ['情人節', '送禮'] }, { name: '兒童節', month: 4, day: 4, keywords: ['兒童', '親子'] }, { name: '母親節', month: 5, day: 11, keywords: ['母親節', '感恩'] }, { name: '端午節', month: 5, day: 31, keywords: ['端午', '粽子'] }, { name: '七夕', month: 8, day: 4, keywords: ['七夕', '浪漫'] }, { name: '父親節', month: 8, day: 8, keywords: ['父親節', '88節'] }, { name: '中秋節', month: 9, day: 17, keywords: ['中秋', '月餅'] }, { name: '雙11', month: 11, day: 11, keywords: ['雙11', '購物節'] }, { name: '聖誕節', month: 12, day: 25, keywords: ['聖誕', '禮物'] } ]; // 計算即將到來的假期 function getUpcomingHolidays() { const today = new Date(); const upcoming = []; taiwanHolidays.forEach(h => { let date = new Date(today.getFullYear(), h.month - 1, h.day); if (date < today) date = new Date(today.getFullYear() + 1, h.month - 1, h.day); const days = Math.ceil((date - today) / (1000 * 60 * 60 * 24)); if (days <= 45) upcoming.push({ ...h, date, daysUntil: days }); }); return upcoming.sort((a, b) => a.daysUntil - b.daysUntil).slice(0, 3); } // 渲染近期節日 function renderUpcomingHolidays() { const holidays = getUpcomingHolidays(); const container = document.getElementById('upcomingHolidays'); if (!holidays.length) { container.innerHTML = '近期無重大節日'; return; } container.innerHTML = holidays.map(h => { const cls = h.daysUntil <= 7 ? 'bg-danger' : h.daysUntil <= 14 ? 'bg-warning text-dark' : 'bg-info'; return `${h.name} (${h.daysUntil}天後)`; }).join(''); } // 載入熱銷商品 function loadBestsellers() { const category = document.getElementById('bestsellersCategory').value; const platform = document.querySelector('input[name="platform"]:checked')?.value || 'pchome'; const container = document.getElementById('bestsellersCard'); container.innerHTML = '
'; fetch(`/api/ai/bestsellers?category=${encodeURIComponent(category)}&limit=5&platform=${platform}`) .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }) .then(data => { if (data.success && data.data.products?.length) { container.innerHTML = data.data.products.map((p, i) => renderBestsellerCard(p, i, data.data.platform)).join('') + `
${escapeHtml(data.data.source || '')}
`; } else { container.innerHTML = '

無法載入熱銷商品

'; } }) .catch(e => container.innerHTML = '

載入失敗

'); } function normalizeBestsellerProduct(product, platform) { const p = product || {}; return { name: String(p.name || '').trim(), productId: String(p.product_id || p.id || p.i_code || p.goodsCode || '').trim(), platform: String(p.platform || platform || '').toLowerCase(), price: Number(p.price || 0), url: String(p.product_url || p.url || '').trim(), imageUrl: String(p.image_url || p.image || '').trim() }; } function platformLabel(platform) { return String(platform || '').toLowerCase() === 'momo' ? 'MOMO' : 'PChome'; } function renderProductThumb(product) { const image = product.imageUrl; if (image && /^https?:\/\//i.test(image)) { return `${escapeHtml(product.name)}`; } return `
待補圖片
`; } function renderBestsellerCard(product, index, platform) { const p = normalizeBestsellerProduct(product, platform); const label = platformLabel(p.platform); const price = p.price > 0 ? `NT$ ${p.price.toLocaleString()}` : '待補價格'; const productId = p.productId || '待補'; const storeAction = p.url ? `開賣場` : '待補連結'; return `
${index + 1}
${renderProductThumb(p)}
${label} 商品 ID ${escapeHtml(productId)}
${escapeHtml(p.name || '待補商品名稱')}
${price}
${storeAction}
`; } // 載入 COSME 排行榜 function loadCosmeRankings() { const category = document.getElementById('cosmeCategory').value; const container = document.getElementById('cosmeCard'); container.innerHTML = '
'; fetch(`/api/ai/cosme_rankings?category=${encodeURIComponent(category)}&limit=5`) .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }) .then(data => { if (data.success && data.data.products?.length) { container.innerHTML = data.data.products.map((p, i) => `
${p.rank}
${p.brand} ${p.name} 評分: ${p.rating || 'N/A'}
`).join('') + `
${data.data.source}
`; } else { container.innerHTML = '

暫無資料

'; } }) .catch(e => container.innerHTML = '

載入失敗

'); } // 載入 mybest 推薦文章 function loadMybestArticles() { const category = document.getElementById('mybestCategory').value; const container = document.getElementById('mybestCard'); container.innerHTML = '
'; fetch(`/api/ai/mybest_articles?category=${encodeURIComponent(category)}&limit=5`) .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }) .then(data => { if (data.success && data.data.articles?.length) { container.innerHTML = data.data.articles.map(a => `
${a.title} ${a.product_count ? a.product_count + '款推薦' : ''}
${a.article_url ? `` : ''}
`).join('') + `
${data.data.source}
`; } else { container.innerHTML = '

暫無資料

'; } }) .catch(e => container.innerHTML = '

載入失敗

'); } // 趨勢資料快取 let trendDataCache = null; let currentTrendType = 'news'; // 載入趨勢新聞 function loadTrends() { const container = document.getElementById('newsCard'); container.innerHTML = '
'; fetch('/api/ai/trends?time_range=week') .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }) .then(data => { if (data.success) { trendDataCache = data.data; renderTrendContent(currentTrendType); } else { container.innerHTML = '

無法載入趨勢資料

'; } }) .catch(e => container.innerHTML = '

載入失敗

'); } // 切換趨勢分類頁籤 function switchTrendTab(el, type) { event.preventDefault(); currentTrendType = type; // 更新頁籤狀態 document.querySelectorAll('#trendTabs .nav-link').forEach(tab => tab.classList.remove('active')); el.classList.add('active'); // 重新渲染內容 renderTrendContent(type); } // 渲染趨勢內容 function renderTrendContent(type) { const container = document.getElementById('newsCard'); if (!trendDataCache) { container.innerHTML = '

請先載入趨勢資料

'; return; } let items = []; let icon = 'fa-newspaper'; let iconClass = 'text-info'; switch(type) { case 'news': items = trendDataCache.news || []; icon = 'fa-newspaper'; iconClass = 'text-info'; break; case 'social': items = trendDataCache.social || []; icon = 'fa-hashtag'; iconClass = 'text-primary'; break; case 'search': // 搜尋趨勢使用社群資料或關鍵字作為備用 items = trendDataCache.search || trendDataCache.social || []; icon = 'fa-search'; iconClass = 'text-success'; break; } if (items.length === 0) { container.innerHTML = `

暫無${type === 'news' ? '新聞' : type === 'social' ? '社群' : '搜尋'}資料

`; return; } container.innerHTML = items.slice(0, 20).map(n => `
${n.title || n.query || ''} ${n.source || ''} ${formatDate(n.published)}
`).join(''); } // 格式化日期 function formatDate(dateStr) { if (!dateStr) return ''; try { const date = new Date(dateStr); const hours = Math.floor((new Date() - date) / 3600000); if (hours < 1) return '剛剛'; if (hours < 24) return `${hours}小時前`; const days = Math.floor(hours / 24); if (days < 7) return `${days}天前`; return `${date.getMonth() + 1}/${date.getDate()}`; } catch { return ''; } } // ====== 建議引擎切換相關 ====== // 建議引擎切換處理 function onProviderChange() { const provider = document.getElementById('aiProvider').value; const isGemini = provider === 'gemini'; document.getElementById('ollamaModelSelect').style.display = isGemini ? 'none' : 'block'; document.getElementById('geminiModelSelect').style.display = isGemini ? 'block' : 'none'; document.getElementById('geminiUsagePanel').style.display = isGemini ? 'block' : 'none'; if (isGemini) { loadGeminiUsage(); } } // 載入 Gemini 使用量統計 function loadGeminiUsage() { fetch('/api/ai/gemini_usage?days=30') .then(r => r.json()) .then(data => { if (data.success) { const summary = data.data.summary; document.getElementById('fallbackMonthlySpend').textContent = '$' + summary.total_cost_usd.toFixed(4); document.getElementById('geminiRequestCount').textContent = summary.total_requests.toLocaleString(); document.getElementById('geminiTokenUsage').textContent = summary.total_tokens.toLocaleString(); } }) .catch(e => { console.error('載入 Gemini 使用量失敗:', e); }); } // 首屏先渲染,建議引擎狀態載入後再更新,避免健康檢查阻塞頁面 TTFB function refreshAIStatus() { fetch('/api/ai/status') .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }) .then(data => { if (!data.success || !data.data) return; const status = data.data; updateAIStatusBadge('ollamaStatus', 'fas fa-wand-magic-sparkles', '建議引擎', status.ollama?.connected, 'ar-status--ok'); updateAIStatusBadge('geminiStatus', 'fas fa-shield-alt', '備援守門', status.gemini?.connected, 'ar-status--info'); updateOllamaModels(status.ollama?.available_models || []); }) .catch(e => console.warn('建議引擎狀態刷新未完成:', e)); } function updateAIStatusBadge(id, iconClass, label, connected, okClass) { const el = document.getElementById(id); if (!el) return; el.classList.remove('ar-status--ok', 'ar-status--info', 'ar-status--off'); el.classList.add(connected ? okClass : 'ar-status--off'); el.innerHTML = ` ${label} ${connected ? '可用' : '待確認'}`; } function updateOllamaModels(models) { const select = document.getElementById('ollamaModelSelect'); if (!select || !models.length) return; const current = select.value; select.innerHTML = models.map(model => { const selected = model === current || (!current && model.includes('gemma3:4b')) ? ' selected' : ''; return ``; }).join(''); } // 初始化建議引擎選擇(頁面載入時) function initAIProvider() { const provider = document.getElementById('aiProvider').value; onProviderChange(); } // ====== 其他功能 ====== // 設定商品名稱 function setProduct(name) { document.getElementById('productName').value = name.substring(0, 100); } function setProductFromCard(card) { const name = card?.dataset?.productName || ''; if (name) setProduct(name); } // 切換關鍵字 function toggleKeyword(el) { el.classList.toggle('is-selected'); updateKeywordCount(); } // 更新已選關鍵字計數 function updateKeywordCount() { const count = document.querySelectorAll('.keyword-badge.is-selected').length; const countEl = document.getElementById('selectedKeywordCount'); if (countEl) { countEl.textContent = count + ' 個已選'; countEl.className = count > 0 ? 'badge ar-selected-count is-active ms-1' : 'badge ar-selected-count ms-1'; } } // 取得已選關鍵字 function getSelectedKeywords() { return Array.from(document.querySelectorAll('.keyword-badge.is-selected')).map(el => el.textContent); } // 取得節日資訊(用於 API) function getHolidaysForAPI() { return getUpcomingHolidays().map(h => ({ name: h.name, days_until: h.daysUntil })); } // 取得熱銷商品資訊 function getBestsellersForAPI() { const items = document.querySelectorAll('#bestsellersCard .ar-product-card'); return Array.from(items).slice(0, 3).map(el => { const price = parseInt(el.dataset.price || '0', 10) || 0; return { name: el.dataset.productName || '', price, product_id: el.dataset.productId || '', platform: el.dataset.platform || '', url: el.dataset.productUrl || '' }; }); } // 生成文案 function generateCopy() { const productName = document.getElementById('productName').value.trim(); if (!productName) { document.getElementById('productName').classList.add('is-invalid'); document.getElementById('productName').focus(); return; } document.getElementById('productName').classList.remove('is-invalid'); // 立即更新按鈕狀態為載入中 const btn = document.getElementById('generateBtn'); const originalBtnText = btn.innerHTML; btn.disabled = true; btn.innerHTML = '正在整理建議...'; btn.classList.add('btn-secondary'); btn.classList.remove('btn-primary'); // 顯示全螢幕載入動畫 showLoading('正在產生銷售建議...'); // 取得幕後建議設定 const provider = document.getElementById('aiProvider').value; const model = provider === 'gemini' ? document.getElementById('geminiModelSelect').value : document.getElementById('ollamaModelSelect').value; fetch('/api/ai/generate_copy', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, body: JSON.stringify({ product_name: productName, trend_keywords: getSelectedKeywords(), style: document.getElementById('copyStyle').value, provider: provider, model: model, upcoming_holidays: getHolidaysForAPI(), bestseller_products: getBestsellersForAPI() }) }) .then(r => { if (!r.ok) { return r.text().then(text => { throw new Error(`伺服器錯誤 (${r.status}): ${text.substring(0, 100)}`); }); } return r.json(); }) .then(data => { hideLoading(); // 恢復按鈕狀態 btn.disabled = false; btn.innerHTML = originalBtnText; btn.classList.remove('btn-secondary'); btn.classList.add('btn-primary'); if (data.success) { // 格式化顯示文案內容 const formattedCopy = formatCopyResult(data.data.copy); document.getElementById('generatedCopy').innerHTML = formattedCopy; // 組合元資料顯示 let metaHtml = `建議已完成`; metaHtml += ` | 分析耗時:${data.data.duration}秒`; if (data.data.provider === 'gemini' && data.data.cost) { loadGeminiUsage(); } document.getElementById('copyMeta').innerHTML = metaHtml; document.getElementById('resultArea').style.display = 'block'; document.getElementById('resultArea').scrollIntoView({ behavior: 'smooth' }); } else { alert('建議沒有完成:' + data.error); } }) .catch(e => { hideLoading(); // 恢復按鈕狀態 btn.disabled = false; btn.innerHTML = originalBtnText; btn.classList.remove('btn-secondary'); btn.classList.add('btn-primary'); alert('建議暫時無法產生:' + e.message); }); } // 格式化文案結果 function formatCopyResult(copy) { if (!copy) return ''; // 將【標題】格式化為漂亮的樣式 let formatted = escapeHtml(copy) .replace(/【大標題】/g, '
大標題
') .replace(/【中標題】/g, '
中標題
') .replace(/【小標題】/g, '
小標題
') .replace(/【詳細文案】/g, '
詳細文案
') .replace(/【推廣建議】/g, '
推廣建議
') .replace(/• 社群推廣:/g, '
社群推廣:') .replace(/• 影音內容:/g, '
影音內容:') .replace(/• 其他建議:/g, '
其他建議:
') .replace(/\n/g, '
'); // 確保最後一個 div 閉合 if (formatted.includes('其他建議')) { formatted += '
'; } return formatted; } // 複製文案(純文字版本) function copyCopyText() { // 取得原始文案文字(不含 HTML 格式) const copyEl = document.getElementById('generatedCopy'); const text = copyEl.innerText || copyEl.textContent; navigator.clipboard.writeText(text) .then(() => { // 顯示成功提示 const btn = event.target.closest('button'); const originalText = btn.innerHTML; btn.innerHTML = '已複製'; btn.classList.add('btn-success'); btn.classList.remove('btn-light'); setTimeout(() => { btn.innerHTML = originalText; btn.classList.remove('btn-success'); btn.classList.add('btn-light'); }, 2000); }); } // 轉義 HTML function escapeHtml(text) { if (!text) return ''; return String(text).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); } // 顯示/隱藏載入 function showLoading(text) { document.getElementById('loadingText').textContent = text; document.getElementById('loadingOverlay').classList.remove('d-none'); } function hideLoading() { document.getElementById('loadingOverlay').classList.add('d-none'); } // ===== 網路搜尋功能 ===== // 快速搜尋 function quickWebSearch(query) { document.getElementById('webSearchQuery').value = query; doWebSearch(); } // 市場訊號搜尋 function doWebSearch() { const query = document.getElementById('webSearchQuery').value.trim(); if (!query) { document.getElementById('webSearchQuery').classList.add('is-invalid'); return; } document.getElementById('webSearchQuery').classList.remove('is-invalid'); const searchType = document.getElementById('webSearchType').value; const btn = document.getElementById('webSearchBtn'); const resultArea = document.getElementById('webSearchResult'); const contentArea = document.getElementById('webSearchContent'); // 更新按鈕狀態 btn.disabled = true; btn.innerHTML = '整理中...'; // 顯示結果區 resultArea.style.display = 'block'; contentArea.innerHTML = '

正在整理市場訊號...約需 30-60 秒
'; // 設定前端超時 (3 分鐘) const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 180000); fetch('/api/ai/web_search', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, body: JSON.stringify({ query: query, search_type: searchType, num_results: 5 }), signal: controller.signal }) .then(r => { clearTimeout(timeoutId); if (r.redirected || r.url.includes('/login')) { window.location.href = '/login'; throw new Error('登入已過期,請重新登入'); } if (!r.ok) { throw new Error(`伺服器錯誤 (${r.status})`); } return r.json(); }) .then(data => { btn.disabled = false; btn.innerHTML = '整理訊號'; if (data.success) { renderWebSearchResult(data.data); } else { contentArea.innerHTML = `
市場訊號暫時不可用,請稍後重試。
`; } }) .catch(e => { clearTimeout(timeoutId); btn.disabled = false; btn.innerHTML = '整理訊號'; if (e.name === 'AbortError') { contentArea.innerHTML = `
市場訊號整理較慢,請稍後再試。
`; } else { contentArea.innerHTML = `
市場訊號暫時不可用,請稍後重試。
`; } }); } // 渲染市場訊號結果 function renderWebSearchResult(data) { const contentArea = document.getElementById('webSearchContent'); let html = ''; if (data.parsed) { const p = data.parsed; if (p.summary) { html += `
市場摘要

${escapeHtml(p.summary)}

`; } if (p.results && p.results.length > 0) { html += `
可用線索 (${p.results.length})
`; p.results.forEach((r, idx) => { html += `
${idx + 1}
${escapeHtml(r.title || '')}

${escapeHtml(r.description || '')}

`; }); html += `
`; } if ((p.insights && p.insights.length > 0) || (p.recommended_actions && p.recommended_actions.length > 0)) { html += '
'; if (p.insights && p.insights.length > 0) { const colClass = (p.recommended_actions && p.recommended_actions.length > 0) ? 'col-md-6' : 'col-12'; html += `
關鍵洞察
${p.insights.map(i => `${escapeHtml(i)}`).join('')}
`; } if (p.recommended_actions && p.recommended_actions.length > 0) { const colClass = (p.insights && p.insights.length > 0) ? 'col-md-6' : 'col-12'; html += `
可採取動作
${p.recommended_actions.map(a => `${escapeHtml(a)}`).join('')}
`; } html += '
'; } } else { html = `
外部訊號已取得,但尚未整理成可直接判斷的摘要;請重新整理訊號後再產生下一步。
`; } html += `
完成於 ${data.duration || '?'} 秒
`; contentArea.innerHTML = html; } // 商品洞察分析 - 整合網路搜尋 function doProductInsights() { const productName = document.getElementById('productName').value.trim(); if (!productName) { alert('請先輸入商品名稱'); document.getElementById('productName').focus(); return; } const btn = document.getElementById('insightsBtn'); const resultArea = document.getElementById('productInsightsResult'); const placeholder = document.getElementById('productInsightsPlaceholder'); // 更新按鈕狀態 btn.disabled = true; btn.innerHTML = '整理中...'; // 顯示結果區 placeholder.style.display = 'none'; resultArea.style.display = 'block'; resultArea.innerHTML = `

步驟 1/2:整理外部訊號...

約需 60-90 秒
`; // 設定前端超時 (4 分鐘,因為需要兩步) const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 240000); // 步驟 1: 先進行網路搜尋取得最新資訊 fetch('/api/ai/web_search', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, body: JSON.stringify({ query: `${productName} 市場分析 競品比較 價格 評價 ${new Date().getFullYear()}`, search_type: 'shopping', num_results: 5 }), signal: controller.signal }) .then(r => { if (r.redirected || r.url.includes('/login')) { window.location.href = '/login'; throw new Error('登入已過期,請重新登入'); } if (!r.ok) throw new Error(`搜尋失敗 (${r.status})`); return r.json(); }) .then(searchData => { // 更新進度 document.getElementById('insightProgress').style.width = '60%'; document.getElementById('insightStatus').textContent = '步驟 2/2:產生下一步...'; btn.innerHTML = '判斷中...'; // 準備外部訊號摘要 let webContext = ''; if (searchData.success && searchData.data) { const parsed = searchData.data.parsed; if (parsed) { webContext = `\n\n【外部訊號摘要】\n${parsed.summary || ''}\n`; if (parsed.results && parsed.results.length > 0) { webContext += '\n相關訊號:\n'; parsed.results.slice(0, 3).forEach((r, i) => { webContext += `${i+1}. ${r.title}: ${r.description}\n`; }); } } } // 步驟 2: 進行商品洞察分析,傳入網路搜尋結果 return fetch('/api/ai/product_insights', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, body: JSON.stringify({ product_name: productName, include_competitors: true, include_trends: true, web_context: webContext }), signal: controller.signal }); }) .then(r => { clearTimeout(timeoutId); if (!r.ok) throw new Error(`分析失敗 (${r.status})`); return r.json(); }) .then(data => { btn.disabled = false; btn.innerHTML = '判斷下一步'; if (data.success) { renderProductInsights(data.data, productName); } else { resultArea.innerHTML = `
商品判斷暫時不可用,請稍後重試。
`; } }) .catch(e => { clearTimeout(timeoutId); btn.disabled = false; btn.innerHTML = '判斷下一步'; if (e.name === 'AbortError') { resultArea.innerHTML = `
商品判斷較慢,請稍後再試。
`; } else { resultArea.innerHTML = `
商品判斷暫時不可用,請稍後重試。
`; } }); } // 渲染商品洞察結果 function renderProductInsights(data, productName) { const resultArea = document.getElementById('productInsightsResult'); let html = ''; // 標題 html += `
${escapeHtml(productName || '商品')} 下一步判斷
含外部訊號
`; if (data.insights) { const ins = data.insights; if (ins.market_position) { const mp = ins.market_position; html += `
商品定位 ${mp.price_range ? `${escapeHtml(mp.price_range)}` : ''}

${escapeHtml(mp.positioning || mp.target_audience || mp.description || '')}

`; } if (ins.competitors && ins.competitors.length > 0) { html += `
競品重點
`; ins.competitors.slice(0, 5).forEach(c => { html += ``; }); html += `
競品可借鏡風險
${escapeHtml(c.name || c.brand || '')} ${escapeHtml(c.strength || c.advantage || '-')} ${escapeHtml(c.weakness || c.disadvantage || '-')}
`; } if (ins.trends) { html += `
市場趨勢

${escapeHtml(ins.trends.current || ins.trends.description || '')}

${ins.trends.forecast ? `

預測:${escapeHtml(ins.trends.forecast)}

` : ''}
`; } if (ins.recommendations && ins.recommendations.length > 0) { html += `
下一步
`; } if (ins.keywords && ins.keywords.length > 0) { html += `
可用關鍵字 點擊可加入文案
`; ins.keywords.forEach(k => { html += `${escapeHtml(k)}`; }); html += `
`; } } else { html += `
商品判斷尚未整理成可執行摘要;請重新判斷或先補商品名稱與賣場線索。
`; } html += `
完成於 ${data.duration || '?'} 秒
`; resultArea.innerHTML = html; } // 從洞察結果添加關鍵字 function addKeywordFromInsight(keyword) { // 檢查是否已存在此關鍵字 const existingBadges = document.querySelectorAll('#keywordsArea .keyword-badge'); for (let badge of existingBadges) { if (badge.textContent === keyword) { // 如果已存在,選中它 if (!badge.classList.contains('is-selected')) { toggleKeyword(badge); } return; } } // 如果不存在,新增一個選中的關鍵字 const newBadge = document.createElement('span'); newBadge.className = 'badge ar-keyword-badge is-selected border me-1 mb-1 keyword-badge'; newBadge.style.cursor = 'pointer'; newBadge.textContent = keyword; newBadge.onclick = function() { toggleKeyword(this); }; newBadge.title = '點選移除此關鍵字'; document.getElementById('keywordsArea').appendChild(newBadge); } // ===== 即時趨勢洞察功能 ===== // 刷新趨勢資料 function refreshTrends() { const source = document.getElementById('trendSource').value; const category = document.getElementById('trendCategory').value; // 並行載入關鍵字和趨勢記錄 Promise.all([ fetch(`/api/trends/keywords?source=${source}&category=${category}&days=7&limit=15`).then(r => r.json()), fetch(`/api/trends/records?source=${source}&category=${category}&days=7&limit=10`).then(r => r.json()) ]) .then(([keywordsData, recordsData]) => { // 渲染關鍵字標籤雲 if (keywordsData.success && keywordsData.data.length > 0) { renderTrendKeywordCloud(keywordsData.data); } else { document.getElementById('trendKeywordCloud').innerHTML = '暫無關鍵字資料'; } // 渲染趨勢列表 if (recordsData.success && recordsData.data.length > 0) { renderTrendList(recordsData.data); } else { document.getElementById('trendListArea').innerHTML = '

暫無趨勢資料,請先觸發爬取

'; } }) .catch(e => { console.error('載入趨勢資料失敗:', e); document.getElementById('trendKeywordCloud').innerHTML = '載入失敗'; document.getElementById('trendListArea').innerHTML = '

載入失敗

'; }); } // 渲染趨勢關鍵字標籤雲 function renderTrendKeywordCloud(keywords) { const container = document.getElementById('trendKeywordCloud'); const html = keywords.map(kw => { const size = Math.min(Math.max(10 + kw.total_mentions, 12), 16); return `${escapeHtml(kw.keyword)}`; }).join(''); container.innerHTML = html; } // 渲染趨勢列表 function renderTrendList(records) { const container = document.getElementById('trendListArea'); const sourceIcons = { 'ptt': 'PTT', 'dcard': 'Dcard', 'google_news': '新聞', 'youtube': 'YT', 'ollama_web_search': '搜尋' }; const html = records.map(r => `
${sourceIcons[r.source] || '其他'}
${escapeHtml(r.title.substring(0, 40))}${r.title.length > 40 ? '...' : ''} ${r.category || ''} · 熱度 ${r.popularity_score || 0}
`).join(''); container.innerHTML = html; } // 使用趨勢關鍵字 function useTrendKeyword(keyword) { addKeywordFromInsight(keyword); } // 使用趨勢作為商品名稱 function useTrendForProduct(title) { document.getElementById('productName').value = title; } Object.assign(window, { addKeywordFromInsight, copyCopyText, doProductInsights, doWebSearch, generateCopy, loadBestsellers, loadCosmeRankings, loadMybestArticles, loadTrends, onProviderChange, quickWebSearch, refreshTrends, setProduct, setProductFromCard, switchTrendTab, toggleKeyword, useTrendForProduct, useTrendKeyword }); // 初始化 document.addEventListener('DOMContentLoaded', function() { initAIProvider(); refreshAIStatus(); renderUpcomingHolidays(); refreshTrends(); // 載入即時趨勢(預設頁籤) updateKeywordCount(); // 監聽平台切換 document.querySelectorAll('input[name="platform"]').forEach(r => r.addEventListener('change', loadBestsellers)); // 監聽 Enter 鍵搜尋 document.getElementById('webSearchQuery').addEventListener('keypress', function(e) { if (e.key === 'Enter') doWebSearch(); }); // 頁籤切換時延遲載入數據 const tabEl = document.querySelectorAll('#marketInfoTabs button[data-bs-toggle="pill"]'); tabEl.forEach(tab => { tab.addEventListener('shown.bs.tab', function(event) { const targetId = event.target.getAttribute('data-bs-target'); if (targetId === '#bestsellers-panel') { // 檢查是否已載入 const container = document.getElementById('bestsellersCard'); if (container.querySelector('.spinner-border')) { loadBestsellers(); } } else if (targetId === '#rankings-panel') { const cosmeContainer = document.getElementById('cosmeCard'); const mybestContainer = document.getElementById('mybestCard'); if (cosmeContainer.querySelector('.spinner-border')) { loadCosmeRankings(); } if (mybestContainer.querySelector('.spinner-border')) { loadMybestArticles(); } } else if (targetId === '#news-panel') { const newsContainer = document.getElementById('newsCard'); if (newsContainer.querySelector('.spinner-border')) { loadTrends(); } } }); }); }); })();