Files
ewoooc/templates/ai_recommend.html
OoO d6782ee710
All checks were successful
CD Pipeline / deploy (push) Successful in 2m13s
feat(ai): move recommendation page to v2 shell
2026-05-01 21:08:44 +08:00

1684 lines
82 KiB
HTML
Raw 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-recommend-page {
display: flex;
flex-direction: column;
gap: 18px;
}
.ai-recommend-hero {
padding: 20px;
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.22), rgba(255, 255, 255, 0.94) 46%, rgba(42, 37, 32, 0.06));
background-size: 18px 18px, auto;
box-shadow: var(--momo-shadow-soft);
}
.ai-recommend-title {
display: flex;
align-items: center;
gap: 10px;
margin: 0;
color: var(--momo-text-strong);
font-family: var(--momo-font-display);
font-size: clamp(1.35rem, 2vw, 2rem);
font-weight: 800;
letter-spacing: 0;
}
.ai-recommend-title i {
color: var(--momo-warm-caramel) !important;
}
.ai-recommend-page .card {
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-recommend-page .card-header {
border-color: var(--momo-border-subtle) !important;
background: rgba(250, 247, 240, 0.88) !important;
color: var(--momo-text-strong) !important;
}
.ai-recommend-page .card-header h6,
.ai-recommend-page .card-header span {
color: var(--momo-text-strong);
font-family: var(--momo-font-display);
font-weight: 800;
}
.ai-recommend-page .form-control,
.ai-recommend-page .form-select,
.ai-recommend-page .input-group-text {
border-color: var(--momo-border-subtle);
border-radius: 8px;
}
.ai-recommend-page .btn-primary {
background: var(--momo-text-strong);
border-color: var(--momo-text-strong);
font-weight: 800;
}
.ai-recommend-page .btn-outline-primary {
color: var(--momo-accent-strong);
border-color: rgba(42, 37, 32, 0.24);
}
.ai-recommend-page .badge {
border-radius: 999px;
font-weight: 800;
}
.ai-recommend-page .bg-light {
background-color: rgba(250, 247, 240, 0.76) !important;
}
.ai-recommend-page .alert-info {
border-color: rgba(83, 135, 154, 0.2);
background: rgba(238, 248, 251, 0.82);
}
.ai-recommend-page #marketInfoTabs .nav-link.active {
background: var(--momo-text-strong) !important;
color: #fff !important;
}
</style>
{% endblock %}
{% block ewooo_content %}
<div class="ai-recommend-page">
<!-- 頁面標題 - 更緊湊 -->
<div class="row mb-3 ai-recommend-hero">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<h1 class="ai-recommend-title">
<i class="fas fa-robot text-primary me-2"></i>AI 智慧推薦
</h1>
<small class="text-muted">根據資料庫商品分類、即時趨勢與 AI 生成紀錄,產出可追蹤的銷售文案。</small>
</div>
<div class="d-flex align-items-center gap-2">
<span id="ollamaStatus" class="badge {% if ollama_status %}bg-success{% else %}bg-secondary{% endif %}">
<i class="fas fa-server me-1"></i>
Ollama {% if ollama_status %}✓{% else %}✗{% endif %}
</span>
<span id="geminiStatus" class="badge {% if gemini_status %}bg-info{% else %}bg-secondary{% endif %}">
<i class="fab fa-google me-1"></i>
Gemini {% if gemini_status %}✓{% else %}✗{% endif %}
</span>
<!-- 快速操作提示 -->
<button class="btn btn-outline-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#helpModal" title="使用說明">
<i class="fas fa-question-circle"></i>
</button>
</div>
</div>
</div>
</div>
<div class="row">
<!-- 左側:文案生成區 (更寬) -->
<div class="col-lg-6 col-xl-5 mb-3">
<!-- 文案生成表單 - 優化後的緊湊版本 -->
<div class="card shadow-sm border-primary">
<div class="card-header bg-primary text-white py-2">
<div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0"><i class="fas fa-magic me-2"></i>文案生成</h6>
<small class="opacity-75">第 1 步:設定參數</small>
</div>
</div>
<div class="card-body py-3">
<!-- 商品名稱 - 重點突出 -->
<div class="mb-3">
<label class="form-label fw-bold mb-1">
<i class="fas fa-box text-primary me-1"></i>商品名稱 <span class="text-danger">*</span>
</label>
<div class="input-group">
<input type="text" class="form-control form-control-lg" id="productName" placeholder="輸入商品名稱...">
<button class="btn btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" title="從商品分類快速選取">
<i class="fas fa-list"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end" style="max-height: 300px; overflow-y: auto;">
<li><h6 class="dropdown-header">點選分類快速填入</h6></li>
{% for category in product_categories %}
<li><a class="dropdown-item" href="#" onclick="setProduct('{{ category }}')">{{ category }}</a></li>
{% endfor %}
</ul>
</div>
<div class="d-flex justify-content-between mt-1">
<small class="text-muted"><i class="fas fa-lightbulb me-1"></i>可從右側熱銷商品快速選取</small>
<button type="button" class="btn btn-link btn-sm p-0 text-decoration-none" onclick="document.getElementById('productName').value=''">
<small>清除</small>
</button>
</div>
</div>
<!-- 風格與 AI 引擎選擇 - 橫向緊湊排列 -->
<div class="row g-2 mb-3">
<div class="col-4">
<label class="form-label small fw-bold mb-1">文案風格</label>
<select class="form-select form-select-sm" id="copyStyle" title="選擇文案的語氣風格">
<option value="吸睛">🎯 吸睛活潑</option>
<option value="專業">🔬 專業權威</option>
<option value="溫馨">💕 溫馨感性</option>
<option value="急迫">⚡ 限時急迫</option>
</select>
</div>
<div class="col-4">
<label class="form-label small fw-bold mb-1">AI 引擎</label>
<select class="form-select form-select-sm" id="aiProvider" title="選擇 AI 提供者" onchange="onProviderChange()">
<option value="ollama" {% if default_provider == 'ollama' %}selected{% endif %}>🖥️ Ollama (本地)</option>
<option value="gemini" {% if default_provider == 'gemini' %}selected{% endif %}>☁️ Gemini (雲端)</option>
</select>
</div>
<div class="col-4">
<label class="form-label small fw-bold mb-1">AI 模型</label>
<!-- Ollama 模型選擇 -->
<select class="form-select form-select-sm" id="ollamaModelSelect" title="選擇 Ollama 模型">
{% for model in available_models %}
<option value="{{ model }}" {% if 'gemma3:4b' in model %}selected{% endif %}>{{ model }}</option>
{% endfor %}
</select>
<!-- Gemini 模型選擇 -->
<select class="form-select form-select-sm" id="geminiModelSelect" title="選擇 Gemini 模型" style="display: none;">
{% for model in gemini_models %}
<option value="{{ model.id }}" {% if loop.first %}selected{% endif %}>{{ model.name }}</option>
{% endfor %}
</select>
</div>
</div>
<!-- Gemini 使用量面板 -->
<div id="geminiUsagePanel" class="alert alert-info py-2 mb-3" style="display: none;">
<div class="d-flex justify-content-between align-items-center">
<small class="fw-bold"><i class="fab fa-google me-1"></i>Gemini 本月使用量</small>
<button type="button" class="btn btn-link btn-sm p-0" onclick="loadGeminiUsage()">
<i class="fas fa-sync-alt"></i>
</button>
</div>
<div class="d-flex justify-content-between small mt-1">
<span>費用: <strong id="geminiMonthlyCost" class="text-primary">$0.0000</strong></span>
<span>請求: <span id="geminiRequestCount">0</span></span>
<span>Token: <span id="geminiTokenUsage">0</span></span>
</div>
</div>
<!-- 可收合區域:關鍵字與節日 -->
<div class="accordion accordion-flush" id="advancedOptions">
<div class="accordion-item border-0">
<h2 class="accordion-header">
<button class="accordion-button collapsed py-2 px-0 bg-transparent" type="button" data-bs-toggle="collapse" data-bs-target="#keywordsCollapse">
<small class="fw-bold"><i class="fas fa-tags text-primary me-1"></i>關鍵字與節日 <span class="badge bg-light text-muted ms-1" id="selectedKeywordCount">0 個已選</span></small>
</button>
</h2>
<div id="keywordsCollapse" class="accordion-collapse collapse" data-bs-parent="#advancedOptions">
<div class="accordion-body p-0 pt-2">
<!-- 關鍵字選擇 -->
<div class="mb-2">
<small class="text-muted d-block mb-1">點選標籤加入/移除</small>
<div id="keywordsArea" class="p-2 bg-light rounded" style="max-height: 80px; overflow-y: auto;">
{% for category in product_categories[:12] %}
<span class="badge bg-light text-dark border me-1 mb-1 keyword-badge" style="cursor: pointer; font-size: 0.75rem;" onclick="toggleKeyword(this)" title="點選加入此關鍵字">{{ category }}</span>
{% endfor %}
</div>
</div>
<!-- 即將到來的假期 -->
<div>
<small class="text-muted"><i class="fas fa-calendar-alt text-success me-1"></i>近期節日(自動融入)</small>
<div id="upcomingHolidays" class="small mt-1"></div>
</div>
</div>
</div>
</div>
</div>
<hr class="my-2">
<!-- 生成按鈕 -->
<div class="d-grid">
<button class="btn btn-primary" onclick="generateCopy()" id="generateBtn" title="使用 AI 生成銷售文案">
<i class="fas fa-magic me-2"></i>生成文案
</button>
</div>
</div>
</div>
<!-- 生成結果 -->
<div id="resultArea" class="mt-3" style="display: none;">
<div class="card shadow-sm border-success">
<div class="card-header bg-success text-white py-2 d-flex justify-content-between align-items-center">
<span><i class="fas fa-sparkles me-2"></i>AI 生成文案</span>
<button class="btn btn-sm btn-light" onclick="copyCopyText()" title="複製全部文案到剪貼簿">
<i class="fas fa-copy me-1"></i>複製
</button>
</div>
<div class="card-body py-2" style="max-height: 400px; overflow-y: auto;">
<div id="generatedCopy" class="copy-result" style="white-space: pre-wrap; line-height: 1.6; font-size: 0.9rem;"></div>
<hr class="my-2">
<small class="text-muted" id="copyMeta"></small>
</div>
</div>
</div>
<!-- AI 智慧搜尋 - 移至左側下方 -->
<div class="card shadow-sm mt-3 border-primary">
<div class="card-header bg-primary bg-opacity-10 py-2">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-0"><i class="fas fa-search-dollar text-primary me-2"></i>AI 智慧搜尋</h6>
<small class="text-muted">輸入關鍵字AI 分析市場趨勢</small>
</div>
</div>
</div>
<div class="card-body py-2">
<div class="input-group input-group-sm mb-2">
<input type="text" class="form-control" id="webSearchQuery" placeholder="輸入搜尋關鍵字...">
<select class="form-select" id="webSearchType" style="max-width: 100px;">
<option value="general">一般</option>
<option value="shopping">商品</option>
<option value="trends">趨勢</option>
<option value="news">新聞</option>
</select>
<button class="btn btn-primary" onclick="doWebSearch()" id="webSearchBtn">
<i class="fas fa-brain"></i>
</button>
</div>
<!-- 快速搜尋標籤 -->
<div class="mb-2">
{% for category in product_categories[:4] %}
<span class="badge bg-light text-dark border me-1" style="cursor: pointer; font-size: 0.7rem;" onclick="quickWebSearch({{ category|tojson }})">{{ category }}</span>
{% endfor %}
</div>
<!-- 搜尋結果區 - 不限高度,完整顯示 -->
<div id="webSearchResult" style="display: none;">
<hr class="my-2">
<div id="webSearchContent"></div>
</div>
</div>
</div>
<!-- 商品洞察分析 - 整合 Web Search -->
<div class="card shadow-sm mt-3 border-warning">
<div class="card-header bg-warning bg-opacity-10 d-flex justify-content-between align-items-center py-2">
<div>
<h6 class="mb-0"><i class="fas fa-lightbulb text-warning me-2"></i>商品洞察分析</h6>
<small class="text-muted">結合網路搜尋的即時市場分析</small>
</div>
<button class="btn btn-sm btn-warning" onclick="doProductInsights()" id="insightsBtn">
<i class="fas fa-search-dollar me-1"></i>分析商品
</button>
</div>
<div id="productInsightsResult" class="card-body py-3" style="display: none;">
<!-- 結果將動態填入,不限高度 -->
</div>
<div id="productInsightsPlaceholder" class="card-body py-3 text-center text-muted">
<i class="fas fa-info-circle me-1"></i>輸入商品名稱後點擊「分析商品」<br>
<small>AI 會先搜尋最新網路資訊,再進行深度分析</small>
</div>
</div>
</div>
<!-- 右側:市場資訊 - 用頁籤組織 -->
<div class="col-lg-6 col-xl-7">
<!-- 主要頁籤導航 -->
<ul class="nav nav-pills nav-fill mb-3" id="marketInfoTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active py-2" id="trends-tab" data-bs-toggle="pill" data-bs-target="#trends-panel" type="button" role="tab">
<i class="fas fa-chart-line me-1"></i><span class="d-none d-md-inline">趨勢洞察</span><span class="d-md-none">趨勢</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link py-2" id="bestsellers-tab" data-bs-toggle="pill" data-bs-target="#bestsellers-panel" type="button" role="tab">
<i class="fas fa-fire-alt me-1"></i><span class="d-none d-md-inline">熱銷商品</span><span class="d-md-none">熱銷</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link py-2" id="rankings-tab" data-bs-toggle="pill" data-bs-target="#rankings-panel" type="button" role="tab">
<i class="fas fa-crown me-1"></i><span class="d-none d-md-inline">排行榜</span><span class="d-md-none">排行</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link py-2" id="news-tab" data-bs-toggle="pill" data-bs-target="#news-panel" type="button" role="tab">
<i class="fas fa-newspaper me-1"></i><span class="d-none d-md-inline">趨勢新聞</span><span class="d-md-none">新聞</span>
</button>
</li>
</ul>
<!-- 頁籤內容 -->
<div class="tab-content" id="marketInfoTabContent">
<!-- 趨勢洞察面板 -->
<div class="tab-pane fade show active" id="trends-panel" role="tabpanel">
<div class="card shadow-sm border-success">
<div class="card-header bg-white d-flex justify-content-between align-items-center py-2">
<div>
<h6 class="mb-0"><i class="fas fa-chart-line text-success me-2"></i>即時趨勢洞察</h6>
<small class="text-muted">來自 PTT、Dcard、Google News</small>
</div>
<div class="d-flex gap-1">
<select class="form-select form-select-sm" id="trendSource" style="width: 90px;" onchange="refreshTrends()">
<option value="">全部來源</option>
<option value="google_news">Google</option>
<option value="ptt">PTT</option>
<option value="dcard">Dcard</option>
<option value="youtube">YouTube</option>
</select>
<select class="form-select form-select-sm" id="trendCategory" style="width: 70px;" onchange="refreshTrends()">
<option value="">全部</option>
<option value="美妝">美妝</option>
<option value="3C">3C</option>
<option value="服飾">服飾</option>
<option value="居家">居家</option>
</select>
<button class="btn btn-sm btn-outline-success" onclick="refreshTrends()" title="更新趨勢">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</div>
<div class="card-body py-2">
<!-- 熱門關鍵字標籤雲 -->
<div class="mb-2">
<small class="text-muted fw-bold">熱門關鍵字:</small>
<div id="trendKeywordCloud" class="d-flex flex-wrap gap-1 mt-1">
<span class="badge bg-light text-dark border">載入中...</span>
</div>
</div>
<!-- 趨勢列表 -->
<div id="trendListArea" style="max-height: 280px; overflow-y: auto;">
<div class="text-center py-2">
<div class="spinner-border spinner-border-sm"></div>
</div>
</div>
</div>
</div>
</div>
<!-- 熱銷商品面板 -->
<div class="tab-pane fade" id="bestsellers-panel" role="tabpanel">
<div class="card shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center py-2">
<div>
<h6 class="mb-0"><i class="fas fa-fire-alt text-danger me-2"></i>熱銷商品參考</h6>
<small class="text-muted">點選商品快速填入</small>
</div>
<div class="d-flex align-items-center gap-1">
<div class="btn-group btn-group-sm">
<input type="radio" class="btn-check" name="platform" id="platformPchome" value="pchome" checked>
<label class="btn btn-outline-primary btn-sm px-2" for="platformPchome">PChome</label>
<input type="radio" class="btn-check" name="platform" id="platformMomo" value="momo" disabled>
<label class="btn btn-outline-secondary btn-sm px-2" for="platformMomo" style="opacity: 0.5;">MOMO</label>
</div>
<select class="form-select form-select-sm" id="bestsellersCategory" style="width: 80px;" onchange="loadBestsellers()">
<option value="面膜">面膜</option>
<option value="精華液">精華液</option>
<option value="乳液">乳液</option>
<option value="保濕">保濕</option>
<option value="美白">美白</option>
</select>
</div>
</div>
<div class="card-body py-2" id="bestsellersCard" style="max-height: 350px; overflow-y: auto;">
<div class="text-center py-3"><div class="spinner-border spinner-border-sm"></div></div>
</div>
</div>
</div>
<!-- 排行榜面板 -->
<div class="tab-pane fade" id="rankings-panel" role="tabpanel">
<div class="row g-3">
<!-- COSME 排行榜 -->
<div class="col-12 col-md-6">
<div class="card shadow-sm h-100">
<div class="card-header bg-white d-flex justify-content-between align-items-center py-2">
<div>
<h6 class="mb-0"><i class="fas fa-crown text-warning me-2"></i>COSME</h6>
<small class="text-muted">日本美妝排行</small>
</div>
<select class="form-select form-select-sm" id="cosmeCategory" style="width: 80px;" onchange="loadCosmeRankings()">
<option value="mask">面膜</option>
<option value="serum">精華液</option>
<option value="lotion">乳液</option>
<option value="sunscreen">防曬</option>
<option value="lipstick">唇彩</option>
<option value="foundation">底妝</option>
</select>
</div>
<div class="card-body py-2" id="cosmeCard" style="max-height: 300px; overflow-y: auto;">
<div class="text-center py-3"><div class="spinner-border spinner-border-sm"></div></div>
</div>
</div>
</div>
<!-- mybest 推薦文章 -->
<div class="col-12 col-md-6">
<div class="card shadow-sm h-100">
<div class="card-header bg-white d-flex justify-content-between align-items-center py-2">
<div>
<h6 class="mb-0"><i class="fas fa-trophy text-success me-2"></i>mybest</h6>
<small class="text-muted">專業評測</small>
</div>
<select class="form-select form-select-sm" id="mybestCategory" style="width: 80px;" onchange="loadMybestArticles()">
<option value="skincare">保養</option>
<option value="makeup">彩妝</option>
<option value="health">健康</option>
<option value="baby">嬰兒</option>
<option value="maternity">孕婦</option>
<option value="kids">兒童</option>
</select>
</div>
<div class="card-body py-2" id="mybestCard" style="max-height: 300px; overflow-y: auto;">
<div class="text-center py-3"><div class="spinner-border spinner-border-sm"></div></div>
</div>
</div>
</div>
</div>
</div>
<!-- 趨勢新聞面板 -->
<div class="tab-pane fade" id="news-panel" role="tabpanel">
<div class="card shadow-sm">
<div class="card-header bg-white py-2">
<div class="d-flex justify-content-between align-items-center mb-2">
<div>
<h6 class="mb-0"><i class="fas fa-newspaper text-info me-2"></i>趨勢新聞</h6>
<small class="text-muted">點選新聞快速填入商品</small>
</div>
<button class="btn btn-sm btn-outline-secondary" onclick="loadTrends()" title="重新載入">
<i class="fas fa-sync-alt"></i>
</button>
</div>
<!-- 趨勢分類頁籤 -->
<ul class="nav nav-tabs card-header-tabs" id="trendTabs">
<li class="nav-item">
<a class="nav-link active py-1 px-2" href="#" data-trend-type="news" onclick="switchTrendTab(this, 'news')">
<i class="fas fa-newspaper me-1"></i>新聞
</a>
</li>
<li class="nav-item">
<a class="nav-link py-1 px-2" href="#" data-trend-type="social" onclick="switchTrendTab(this, 'social')">
<i class="fas fa-hashtag me-1"></i>社群
</a>
</li>
<li class="nav-item">
<a class="nav-link py-1 px-2" href="#" data-trend-type="search" onclick="switchTrendTab(this, 'search')">
<i class="fas fa-search me-1"></i>搜尋
</a>
</li>
</ul>
</div>
<div id="newsCard" style="max-height: 350px; overflow-y: auto;">
<div class="text-center py-4"><div class="spinner-border"></div><p class="mt-2 text-muted small">載入中...</p></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 使用說明 Modal -->
<div class="modal fade" id="helpModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header py-2">
<h6 class="modal-title"><i class="fas fa-question-circle text-primary me-2"></i>使用說明</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body py-3">
<div class="d-flex align-items-start mb-3">
<span class="badge bg-primary me-2">1</span>
<div>
<strong>輸入商品名稱</strong>
<p class="text-muted small mb-0">可直接輸入或從右側熱銷商品點選</p>
</div>
</div>
<div class="d-flex align-items-start mb-3">
<span class="badge bg-primary me-2">2</span>
<div>
<strong>選擇文案風格</strong>
<p class="text-muted small mb-0">吸睛活潑、專業權威、溫馨感性、限時急迫</p>
</div>
</div>
<div class="d-flex align-items-start mb-3">
<span class="badge bg-primary me-2">3</span>
<div>
<strong>點擊生成文案</strong>
<p class="text-muted small mb-0">AI 會根據市場趨勢自動生成銷售文案</p>
</div>
</div>
<hr>
<p class="small text-muted mb-0">
<i class="fas fa-lightbulb text-warning me-1"></i>
<strong>小技巧:</strong>使用「AI 智慧搜尋」可獲得更多市場洞察,
點選「商品洞察」可分析競品與市場定位。
</p>
</div>
</div>
</div>
</div>
<!-- 載入動畫 -->
<div id="loadingOverlay" class="position-fixed top-0 start-0 w-100 h-100 d-none" style="background: rgba(0,0,0,0.6); z-index: 9999;">
<div class="d-flex justify-content-center align-items-center h-100">
<div class="text-center text-white">
<div class="spinner-border spinner-border-lg mb-3"></div>
<h5 id="loadingText">AI 正在思考中...</h5>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// 台灣節日資料
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 = '<span class="text-muted">近期無重大節日</span>';
return;
}
container.innerHTML = holidays.map(h => {
const cls = h.daysUntil <= 7 ? 'bg-danger' : h.daysUntil <= 14 ? 'bg-warning text-dark' : 'bg-info';
return `<span class="badge ${cls} me-2">${h.name} (${h.daysUntil}天後)</span>`;
}).join('');
}
// 載入熱銷商品
function loadBestsellers() {
const category = document.getElementById('bestsellersCategory').value;
const platform = document.querySelector('input[name="platform"]:checked')?.value || 'momo';
const container = document.getElementById('bestsellersCard');
container.innerHTML = '<div class="text-center py-3"><div class="spinner-border spinner-border-sm"></div></div>';
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) => `
<div class="d-flex align-items-center px-3 py-2 border-bottom" style="cursor: pointer;" onclick="setProduct('${escapeHtml(p.name)}')">
<span class="badge bg-secondary me-2">${i + 1}</span>
<div class="flex-grow-1 overflow-hidden">
<small class="text-truncate d-block">${p.name}</small>
<small class="text-muted">$${p.price?.toLocaleString() || 'N/A'}</small>
</div>
<a href="${p.url}" target="_blank" class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation()" title="前往電商網站查看商品"><i class="fas fa-external-link-alt"></i></a>
</div>
`).join('') + `<div class="text-center text-muted small py-1">${data.data.source}</div>`;
} else {
container.innerHTML = '<p class="text-muted text-center py-3 mb-0">無法載入熱銷商品</p>';
}
})
.catch(e => container.innerHTML = '<p class="text-danger text-center py-3 mb-0">載入失敗</p>');
}
// 載入 COSME 排行榜
function loadCosmeRankings() {
const category = document.getElementById('cosmeCategory').value;
const container = document.getElementById('cosmeCard');
container.innerHTML = '<div class="text-center py-3"><div class="spinner-border spinner-border-sm"></div></div>';
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) => `
<div class="d-flex align-items-center px-2 py-1 border-bottom" style="cursor: pointer;" onclick="setProduct('${escapeHtml(p.name)}')">
<span class="badge ${i < 3 ? 'bg-warning text-dark' : 'bg-secondary'} me-2" style="font-size: 0.7rem;">${p.rank}</span>
<div class="flex-grow-1 overflow-hidden">
<small class="text-truncate d-block" style="font-size: 0.8rem;">${p.brand} ${p.name}</small>
<small class="text-muted" style="font-size: 0.7rem;">評分: ${p.rating || 'N/A'}</small>
</div>
</div>
`).join('') + `<div class="text-center text-muted small py-1">${data.data.source}</div>`;
} else {
container.innerHTML = '<p class="text-muted text-center py-2 mb-0 small">暫無資料</p>';
}
})
.catch(e => container.innerHTML = '<p class="text-danger text-center py-2 mb-0 small">載入失敗</p>');
}
// 載入 mybest 推薦文章
function loadMybestArticles() {
const category = document.getElementById('mybestCategory').value;
const container = document.getElementById('mybestCard');
container.innerHTML = '<div class="text-center py-3"><div class="spinner-border spinner-border-sm"></div></div>';
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 => `
<div class="d-flex align-items-center px-2 py-1 border-bottom" style="cursor: pointer;" onclick="setProduct('${escapeHtml(a.title)}')">
<i class="fas fa-star text-success me-2" style="font-size: 0.7rem;"></i>
<div class="flex-grow-1 overflow-hidden">
<small class="text-truncate d-block" style="font-size: 0.8rem;">${a.title}</small>
<small class="text-muted" style="font-size: 0.7rem;">${a.product_count ? a.product_count + '款推薦' : ''}</small>
</div>
${a.article_url ? `<a href="${a.article_url}" target="_blank" class="btn btn-sm btn-link p-0" onclick="event.stopPropagation()" title="查看完整文章"><i class="fas fa-external-link-alt" style="font-size: 0.7rem;"></i></a>` : ''}
</div>
`).join('') + `<div class="text-center text-muted small py-1">${data.data.source}</div>`;
} else {
container.innerHTML = '<p class="text-muted text-center py-2 mb-0 small">暫無資料</p>';
}
})
.catch(e => container.innerHTML = '<p class="text-danger text-center py-2 mb-0 small">載入失敗</p>');
}
// 趨勢資料快取
let trendDataCache = null;
let currentTrendType = 'news';
// 載入趨勢新聞
function loadTrends() {
const container = document.getElementById('newsCard');
container.innerHTML = '<div class="text-center py-4"><div class="spinner-border"></div></div>';
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 = '<p class="text-muted text-center py-4">無法載入趨勢資料</p>';
}
})
.catch(e => container.innerHTML = '<p class="text-danger text-center py-4">載入失敗</p>');
}
// 切換趨勢分類頁籤
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 = '<p class="text-muted text-center py-4">請先載入趨勢資料</p>';
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 = `<p class="text-muted text-center py-4">暫無${type === 'news' ? '新聞' : type === 'social' ? '社群' : '搜尋'}資料</p>`;
return;
}
container.innerHTML = items.slice(0, 20).map(n => `
<div class="d-flex align-items-center px-3 py-2 border-bottom news-item" style="cursor: pointer;" onclick="setProduct('${escapeHtml(n.title || n.query || '')}')">
<i class="fas ${icon} ${iconClass} me-2"></i>
<div class="flex-grow-1 overflow-hidden">
<small class="text-truncate d-block fw-medium">${n.title || n.query || ''}</small>
<small class="text-muted">${n.source || ''} ${formatDate(n.published)}</small>
</div>
</div>
`).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 ''; }
}
// ====== AI 引擎切換相關 ======
// AI 引擎切換處理
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';
// 顯示/隱藏 Gemini 使用量面板
document.getElementById('geminiUsagePanel').style.display = isGemini ? 'block' : 'none';
// 如果選擇 Gemini載入使用量
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('geminiMonthlyCost').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);
});
}
// 初始化 AI 引擎選擇(頁面載入時)
function initAIProvider() {
// 確保 Ollama 為預設
const provider = document.getElementById('aiProvider').value;
onProviderChange();
}
// ====== 其他功能 ======
// 設定商品名稱
function setProduct(name) {
document.getElementById('productName').value = name.substring(0, 100);
}
// 切換關鍵字
function toggleKeyword(el) {
el.classList.toggle('bg-primary');
el.classList.toggle('text-white');
el.classList.toggle('bg-light');
el.classList.toggle('text-dark');
updateKeywordCount();
}
// 更新已選關鍵字計數
function updateKeywordCount() {
const count = document.querySelectorAll('.keyword-badge.bg-primary').length;
const countEl = document.getElementById('selectedKeywordCount');
if (countEl) {
countEl.textContent = count + ' 個已選';
countEl.className = count > 0 ? 'badge bg-primary ms-1' : 'badge bg-light text-muted ms-1';
}
}
// 取得已選關鍵字
function getSelectedKeywords() {
return Array.from(document.querySelectorAll('.keyword-badge.bg-primary')).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 > div.d-flex');
return Array.from(items).slice(0, 3).map(el => {
const name = el.querySelector('small.text-truncate')?.textContent || '';
const priceText = el.querySelector('small.text-muted')?.textContent || '';
const price = parseInt(priceText.replace(/[^0-9]/g, '')) || 0;
return { name, price };
});
}
// 生成文案
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 = '<span class="spinner-border spinner-border-sm me-2" role="status"></span>AI 正在生成中...';
btn.classList.add('btn-secondary');
btn.classList.remove('btn-primary');
// 顯示全螢幕載入動畫
showLoading('AI 正在生成完整文案套組...');
// 取得 AI 設定
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 = `<i class="fas fa-robot me-1"></i>${data.data.provider === 'gemini' ? 'Gemini' : 'Ollama'}: ${data.data.model}`;
metaHtml += ` | <i class="fas fa-clock me-1"></i>耗時: ${data.data.duration}`;
// 如果是 Gemini顯示費用
if (data.data.provider === 'gemini' && data.data.cost) {
metaHtml += ` | <i class="fas fa-coins me-1"></i>費用: $${data.data.cost.total.toFixed(4)}`;
metaHtml += ` | Token: ${data.data.tokens.total}`;
// 刷新使用量面板
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, '<div class="mt-2 mb-1"><span class="badge bg-danger me-2">大標題</span></div>')
.replace(/【中標題】/g, '<div class="mt-3 mb-1"><span class="badge bg-warning text-dark me-2">中標題</span></div>')
.replace(/【小標題】/g, '<div class="mt-3 mb-1"><span class="badge bg-info me-2">小標題</span></div>')
.replace(/【詳細文案】/g, '<div class="mt-3 mb-1"><span class="badge bg-success me-2">詳細文案</span></div>')
.replace(/【推廣建議】/g, '<div class="mt-3 mb-1"><span class="badge bg-primary me-2">推廣建議</span></div>')
.replace(/• 社群推廣:/g, '<div class="ms-3 mt-2"><i class="fab fa-facebook text-primary me-1"></i><strong>社群推廣:</strong>')
.replace(/• 影音內容:/g, '</div><div class="ms-3 mt-2"><i class="fab fa-youtube text-danger me-1"></i><strong>影音內容:</strong>')
.replace(/• 其他建議:/g, '</div><div class="ms-3 mt-2"><i class="fas fa-lightbulb text-warning me-1"></i><strong>其他建議:</strong></div><div class="ms-3">')
.replace(/\n/g, '<br>');
// 確保最後一個 div 閉合
if (formatted.includes('其他建議')) {
formatted += '</div>';
}
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 = '<i class="fas fa-check me-1"></i>已複製';
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
}
// 顯示/隱藏載入
function showLoading(text) {
document.getElementById('loadingText').textContent = text;
document.getElementById('loadingOverlay').classList.remove('d-none');
}
function hideLoading() {
document.getElementById('loadingOverlay').classList.add('d-none');
}
// ===== Web Search 功能 =====
// 快速搜尋
function quickWebSearch(query) {
document.getElementById('webSearchQuery').value = query;
doWebSearch();
}
// AI 網路搜尋
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 = '<span class="spinner-border spinner-border-sm me-1"></span>搜尋中...';
// 顯示結果區
resultArea.style.display = 'block';
contentArea.innerHTML = '<div class="text-center py-4"><div class="spinner-border text-primary"></div><br><small class="text-muted mt-2 d-block">AI 正在分析市場資訊...</small><small class="text-muted">(可能需要 30-60 秒)</small></div>';
// 設定前端超時 (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 = '<i class="fas fa-brain me-1"></i>AI 搜尋';
if (data.success) {
renderWebSearchResult(data.data);
} else {
contentArea.innerHTML = `<div class="alert alert-danger py-2 mb-0"><i class="fas fa-exclamation-triangle me-1"></i>${data.error}</div>`;
}
})
.catch(e => {
clearTimeout(timeoutId);
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-brain me-1"></i>AI 搜尋';
if (e.name === 'AbortError') {
contentArea.innerHTML = `<div class="alert alert-warning py-2 mb-0"><i class="fas fa-clock me-1"></i>AI 伺服器回應較慢,請稍後再試。</div>`;
} else {
contentArea.innerHTML = `<div class="alert alert-danger py-2 mb-0">搜尋失敗: ${e.message}</div>`;
}
});
}
// 渲染 Web Search 結果 - 卡片式顯示
function renderWebSearchResult(data) {
const contentArea = document.getElementById('webSearchContent');
let html = '';
// 如果有解析後的 JSON 結果
if (data.parsed) {
const p = data.parsed;
// 摘要卡片
if (p.summary) {
html += `
<div class="card border-primary mb-2">
<div class="card-header bg-primary bg-opacity-10 py-2">
<h6 class="mb-0 small"><i class="fas fa-quote-left text-primary me-1"></i>AI 分析摘要</h6>
</div>
<div class="card-body py-2">
<p class="mb-0 small">${escapeHtml(p.summary)}</p>
</div>
</div>`;
}
// 分析結果卡片
if (p.results && p.results.length > 0) {
html += `
<div class="card border-success mb-2">
<div class="card-header bg-success bg-opacity-10 py-2">
<h6 class="mb-0 small"><i class="fas fa-list-check text-success me-1"></i>分析結果 (${p.results.length})</h6>
</div>
<div class="card-body py-2">
<div class="list-group list-group-flush">`;
p.results.forEach((r, idx) => {
html += `
<div class="list-group-item px-0 py-1 border-0 bg-transparent">
<div class="d-flex">
<span class="badge bg-success me-2">${idx + 1}</span>
<div>
<strong class="small">${escapeHtml(r.title || '')}</strong>
<p class="mb-0 text-muted small">${escapeHtml(r.description || '')}</p>
</div>
</div>
</div>`;
});
html += `
</div>
</div>
</div>`;
}
// 洞察與建議並排顯示
if ((p.insights && p.insights.length > 0) || (p.recommended_actions && p.recommended_actions.length > 0)) {
html += '<div class="row g-2">';
// 洞察卡片
if (p.insights && p.insights.length > 0) {
const colClass = (p.recommended_actions && p.recommended_actions.length > 0) ? 'col-md-6' : 'col-12';
html += `
<div class="${colClass}">
<div class="card border-warning h-100">
<div class="card-header bg-warning bg-opacity-10 py-2">
<h6 class="mb-0 small"><i class="fas fa-lightbulb text-warning me-1"></i>關鍵洞察</h6>
</div>
<div class="card-body py-2">
<div class="d-flex flex-wrap gap-1">
${p.insights.map(i => `<span class="badge bg-warning text-dark">${escapeHtml(i)}</span>`).join('')}
</div>
</div>
</div>
</div>`;
}
// 建議行動卡片
if (p.recommended_actions && p.recommended_actions.length > 0) {
const colClass = (p.insights && p.insights.length > 0) ? 'col-md-6' : 'col-12';
html += `
<div class="${colClass}">
<div class="card border-info h-100">
<div class="card-header bg-info bg-opacity-10 py-2">
<h6 class="mb-0 small"><i class="fas fa-tasks text-info me-1"></i>建議行動</h6>
</div>
<div class="card-body py-2">
<div class="d-flex flex-wrap gap-1">
${p.recommended_actions.map(a => `<span class="badge bg-info">${escapeHtml(a)}</span>`).join('')}
</div>
</div>
</div>
</div>`;
}
html += '</div>';
}
} else {
// 顯示原始內容
html = `
<div class="card border-secondary">
<div class="card-header bg-secondary bg-opacity-10 py-2">
<h6 class="mb-0 small"><i class="fas fa-file-alt text-secondary me-1"></i>搜尋結果</h6>
</div>
<div class="card-body py-2">
<pre class="mb-0 small" style="white-space: pre-wrap; word-break: break-word;">${escapeHtml(data.raw_content)}</pre>
</div>
</div>`;
}
// 底部資訊
html += `
<div class="text-end mt-2">
<small class="text-muted">
<i class="fas fa-robot me-1"></i>${data.model || 'AI'} |
<i class="fas fa-clock me-1"></i>${data.duration || '?'}
</small>
</div>`;
contentArea.innerHTML = html;
}
// 商品洞察分析 - 整合 Web Search
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 = '<span class="spinner-border spinner-border-sm me-1"></span>搜尋中...';
// 顯示結果區
placeholder.style.display = 'none';
resultArea.style.display = 'block';
resultArea.innerHTML = `
<div class="text-center py-4">
<div class="spinner-border text-warning mb-2"></div>
<div class="progress mb-2" style="height: 4px;">
<div class="progress-bar progress-bar-striped progress-bar-animated bg-warning" id="insightProgress" style="width: 20%"></div>
</div>
<p class="text-muted mb-1" id="insightStatus">步驟 1/2搜尋網路最新資訊...</p>
<small class="text-muted">(整體約需 60-90 秒)</small>
</div>`;
// 設定前端超時 (4 分鐘,因為需要兩步)
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 240000);
// 步驟 1: 先進行 Web Search 取得最新資訊
fetch('/api/ai/web_search', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
body: JSON.stringify({
query: `${productName} 市場分析 競品比較 價格 評價 2024`,
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/2AI 深度分析中...';
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>分析中...';
// 準備搜尋結果摘要
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`;
});
}
} else if (searchData.data.raw_content) {
webContext = `\n\n【網路搜尋結果】\n${searchData.data.raw_content.substring(0, 500)}`;
}
}
// 步驟 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 = '<i class="fas fa-search-dollar me-1"></i>分析商品';
if (data.success) {
renderProductInsights(data.data, productName);
} else {
resultArea.innerHTML = `<div class="alert alert-danger py-2 mb-0">${data.error}</div>`;
}
})
.catch(e => {
clearTimeout(timeoutId);
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-search-dollar me-1"></i>分析商品';
if (e.name === 'AbortError') {
resultArea.innerHTML = `<div class="alert alert-warning py-2 mb-0"><i class="fas fa-clock me-1"></i>AI 伺服器回應較慢,請稍後再試。</div>`;
} else {
resultArea.innerHTML = `<div class="alert alert-danger py-2 mb-0">分析失敗: ${e.message}</div>`;
}
});
}
// 渲染商品洞察結果 - 完整顯示版本
function renderProductInsights(data, productName) {
const resultArea = document.getElementById('productInsightsResult');
let html = '';
// 標題
html += `<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0"><i class="fas fa-chart-pie text-warning me-2"></i>${escapeHtml(productName || '商品')} 市場分析</h6>
<small class="text-muted"><i class="fas fa-globe me-1"></i>含網路即時資訊</small>
</div>`;
if (data.insights) {
const ins = data.insights;
// 市場定位 - 卡片樣式
if (ins.market_position) {
const mp = ins.market_position;
html += `<div class="card mb-3 border-primary">
<div class="card-body py-2">
<div class="d-flex align-items-center mb-2">
<span class="badge bg-primary me-2"><i class="fas fa-crosshairs me-1"></i>市場定位</span>
${mp.price_range ? `<span class="badge bg-success">${escapeHtml(mp.price_range)}</span>` : ''}
</div>
<p class="mb-0 small">${escapeHtml(mp.positioning || mp.target_audience || mp.description || '')}</p>
</div>
</div>`;
}
// 競品分析 - 表格樣式
if (ins.competitors && ins.competitors.length > 0) {
html += `<div class="card mb-3 border-warning">
<div class="card-header py-2 bg-warning bg-opacity-10">
<span class="badge bg-warning text-dark"><i class="fas fa-users me-1"></i>競品分析</span>
</div>
<div class="card-body py-2">
<div class="table-responsive">
<table class="table table-sm table-borderless mb-0 small">
<thead><tr><th>競品</th><th class="text-success">優勢</th><th class="text-danger">劣勢</th></tr></thead>
<tbody>`;
ins.competitors.slice(0, 5).forEach(c => {
html += `<tr>
<td class="fw-bold">${escapeHtml(c.name || c.brand || '')}</td>
<td class="text-success">${escapeHtml(c.strength || c.advantage || '-')}</td>
<td class="text-danger">${escapeHtml(c.weakness || c.disadvantage || '-')}</td>
</tr>`;
});
html += `</tbody></table></div></div></div>`;
}
// 市場趨勢
if (ins.trends) {
html += `<div class="card mb-3 border-info">
<div class="card-body py-2">
<span class="badge bg-info mb-2"><i class="fas fa-chart-line me-1"></i>市場趨勢</span>
<p class="mb-0 small">${escapeHtml(ins.trends.current || ins.trends.description || '')}</p>
${ins.trends.forecast ? `<p class="mb-0 small text-info"><i class="fas fa-arrow-right me-1"></i>預測:${escapeHtml(ins.trends.forecast)}</p>` : ''}
</div>
</div>`;
}
// 銷售建議
if (ins.recommendations && ins.recommendations.length > 0) {
html += `<div class="card mb-3 border-success">
<div class="card-body py-2">
<span class="badge bg-success mb-2"><i class="fas fa-lightbulb me-1"></i>銷售建議</span>
<ul class="mb-0 ps-3 small">`;
ins.recommendations.forEach(r => {
html += `<li>${escapeHtml(r)}</li>`;
});
html += `</ul></div></div>`;
}
// 行銷關鍵字 - 可點擊加入
if (ins.keywords && ins.keywords.length > 0) {
html += `<div class="mb-2">
<span class="badge bg-secondary me-2"><i class="fas fa-tags me-1"></i>行銷關鍵字</span>
<small class="text-muted">點擊可加入文案</small>
<div class="mt-2">`;
ins.keywords.forEach(k => {
html += `<span class="badge bg-light text-dark border me-1 mb-1" style="cursor:pointer;" onclick="addKeywordFromInsight('${escapeHtml(k)}')" title="點擊加入關鍵字">${escapeHtml(k)}</span>`;
});
html += `</div></div>`;
}
} else if (data.raw_content) {
// 原始內容顯示
html += `<div class="card">
<div class="card-body py-2">
<div class="small" style="white-space: pre-wrap; line-height: 1.6;">${escapeHtml(data.raw_content)}</div>
</div>
</div>`;
}
// 底部資訊
html += `<div class="d-flex justify-content-between align-items-center mt-3 pt-2 border-top">
<small class="text-muted">
<i class="fas fa-robot me-1"></i>${data.model || 'AI'} |
<i class="fas fa-clock me-1"></i>${data.duration || '?'}
</small>
<button class="btn btn-sm btn-outline-secondary" onclick="doProductInsights()">
<i class="fas fa-redo me-1"></i>重新分析
</button>
</div>`;
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('bg-primary')) {
toggleKeyword(badge);
}
return;
}
}
// 如果不存在,新增一個選中的關鍵字
const newBadge = document.createElement('span');
newBadge.className = 'badge bg-primary text-white 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 =
'<span class="badge bg-light text-muted border">暫無關鍵字資料</span>';
}
// 渲染趨勢列表
if (recordsData.success && recordsData.data.length > 0) {
renderTrendList(recordsData.data);
} else {
document.getElementById('trendListArea').innerHTML =
'<p class="text-muted text-center py-3 mb-0">暫無趨勢資料,請先觸發爬取</p>';
}
})
.catch(e => {
console.error('載入趨勢資料失敗:', e);
document.getElementById('trendKeywordCloud').innerHTML =
'<span class="badge bg-light text-muted border">載入失敗</span>';
document.getElementById('trendListArea').innerHTML =
'<p class="text-danger text-center py-3 mb-0">載入失敗</p>';
});
}
// 渲染趨勢關鍵字標籤雲
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 `<span class="badge bg-success bg-opacity-75 me-1 mb-1"
style="font-size: ${size}px; cursor: pointer;"
onclick="useTrendKeyword('${escapeHtml(kw.keyword)}')"
title="提及 ${kw.total_mentions} 次">${escapeHtml(kw.keyword)}</span>`;
}).join('');
container.innerHTML = html;
}
// 渲染趨勢列表
function renderTrendList(records) {
const container = document.getElementById('trendListArea');
const sourceIcons = {
'ptt': '<span class="badge bg-primary me-1">PTT</span>',
'dcard': '<span class="badge bg-success me-1">Dcard</span>',
'google_news': '<span class="badge bg-danger me-1">新聞</span>',
'youtube': '<span class="badge bg-warning text-dark me-1">YT</span>',
'ollama_web_search': '<span class="badge bg-info me-1">AI</span>'
};
const html = records.map(r => `
<div class="d-flex align-items-start py-1 border-bottom" style="cursor: pointer;"
onclick="useTrendForProduct('${escapeHtml(r.title)}')">
${sourceIcons[r.source] || '<span class="badge bg-secondary me-1">其他</span>'}
<div class="flex-grow-1 overflow-hidden">
<small class="text-truncate d-block">${escapeHtml(r.title.substring(0, 40))}${r.title.length > 40 ? '...' : ''}</small>
<small class="text-muted">${r.category || ''} · 熱度 ${r.popularity_score || 0}</small>
</div>
</div>
`).join('');
container.innerHTML = html;
}
// 使用趨勢關鍵字
function useTrendKeyword(keyword) {
addKeywordFromInsight(keyword);
}
// 使用趨勢作為商品名稱
function useTrendForProduct(title) {
document.getElementById('productName').value = title;
}
// 初始化
document.addEventListener('DOMContentLoaded', function() {
initAIProvider(); // 初始化 AI 引擎選擇
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();
}
}
});
});
});
</script>
<style>
.news-item:hover { background-color: #f8f9fa; }
.keyword-badge { transition: all 0.15s ease; }
.keyword-badge:hover { transform: scale(1.05); }
/* 文案結果樣式 */
.copy-result {
font-size: 0.9rem;
color: #333;
}
.copy-result .badge {
font-size: 0.8rem;
padding: 0.35em 0.7em;
}
/* 頁籤導航樣式 */
#marketInfoTabs .nav-link {
font-size: 0.85rem;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
color: #6c757d;
transition: all 0.2s ease;
}
#marketInfoTabs .nav-link:hover {
background-color: #f8f9fa;
}
#marketInfoTabs .nav-link.active {
background-color: #0d6efd;
color: white;
}
/* 卡片內滾動區域樣式 */
.card-body::-webkit-scrollbar {
width: 4px;
}
.card-body::-webkit-scrollbar-thumb {
background-color: #dee2e6;
border-radius: 2px;
}
.card-body::-webkit-scrollbar-thumb:hover {
background-color: #adb5bd;
}
/* 可點擊項目樣式 */
.d-flex[style*="cursor: pointer"]:hover,
.border-bottom[style*="cursor: pointer"]:hover {
background-color: #f8f9fa;
}
/* 緊湊型 accordion */
.accordion-button:not(.collapsed) {
background-color: transparent;
color: inherit;
box-shadow: none;
}
.accordion-button:focus {
box-shadow: none;
}
/* 響應式調整 */
@media (max-width: 991px) {
#marketInfoTabs .nav-link {
font-size: 0.75rem;
padding: 0.4rem 0.5rem;
}
}
</style>
{% endblock extra_js %}