/* 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 ` `;
}
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 `${escapeHtml(model)} `;
}).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 = '';
// 設定前端超時 (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 += `
`;
}
if (p.results && p.results.length > 0) {
html += `
`;
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 += `
${escapeHtml(c.name || c.brand || '')}
${escapeHtml(c.strength || c.advantage || '-')}
${escapeHtml(c.weakness || c.disadvantage || '-')}
`;
});
html += `
`;
}
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 += `
下一步
`;
ins.recommendations.forEach(r => {
html += `${escapeHtml(r)} `;
});
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();
}
}
});
});
});
})();