Files
ewoooc/templates/ai_history.html

830 lines
30 KiB
HTML

{% extends 'ewoooc_base.html' %}
{% block title %}AI 生成歷史 · EwoooC{% endblock %}
{% block ewooo_content %}
<div class="ai-history-page">
<!-- 頁面標題 -->
<section class="ai-history-hero">
<div>
<h1 class="ai-history-title">
<i class="fas fa-history"></i>
AI 生成歷史
</h1>
<p class="ai-history-subtitle">回收有效文案,追蹤哪些銷售動作可再用。</p>
</div>
<div class="ai-history-actions">
<a href="/ai_recommend" class="btn btn-primary ai-history-action-btn">
<i class="fas fa-magic me-1"></i>生成新文案
</a>
</div>
</section>
<!-- 統計卡片 -->
<div class="row mb-4" id="statsRow">
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<p class="text-muted mb-1 small">總生成次數</p>
<h3 class="mb-0" id="statTotal">-</h3>
</div>
<div class="bg-primary bg-opacity-10 rounded-circle p-3">
<i class="fas fa-robot text-primary fa-lg"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<p class="text-muted mb-1 small">收藏文案</p>
<h3 class="mb-0" id="statFavorite">-</h3>
</div>
<div class="bg-warning bg-opacity-10 rounded-circle p-3">
<i class="fas fa-star text-warning fa-lg"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<p class="text-muted mb-1 small">已使用文案</p>
<h3 class="mb-0" id="statUsed">-</h3>
</div>
<div class="bg-success bg-opacity-10 rounded-circle p-3">
<i class="fas fa-check-circle text-success fa-lg"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<p class="text-muted mb-1 small">平均生成時間</p>
<h3 class="mb-0"><span id="statDuration">-</span><small class="text-muted fs-6"></small></h3>
</div>
<div class="bg-info bg-opacity-10 rounded-circle p-3">
<i class="fas fa-clock text-info fa-lg"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 篩選列 -->
<div class="card shadow-sm mb-4 ai-history-panel">
<div class="card-body py-3">
<div class="row g-3 align-items-end">
<div class="col-md-3">
<label class="form-label small text-muted">搜尋</label>
<div class="input-group">
<span class="input-group-text bg-white"><i class="fas fa-search text-muted"></i></span>
<input type="text" class="form-control" id="searchInput" placeholder="商品名稱或內容...">
</div>
</div>
<div class="col-md-2">
<label class="form-label small text-muted">類型</label>
<select class="form-select" id="filterType">
<option value="">全部類型</option>
<option value="copy">銷售文案</option>
<option value="recommend">商品推薦</option>
<option value="weather_analysis">天氣分析</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label small text-muted">收藏</label>
<select class="form-select" id="filterFavorite">
<option value="">全部</option>
<option value="true">僅收藏</option>
<option value="false">未收藏</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label small text-muted">開始日期</label>
<input type="date" class="form-control" id="filterStartDate">
</div>
<div class="col-md-2">
<label class="form-label small text-muted">結束日期</label>
<input type="date" class="form-control" id="filterEndDate">
</div>
<div class="col-md-1">
<button class="btn btn-outline-secondary w-100" onclick="resetFilters()">
<i class="fas fa-undo"></i>
</button>
</div>
</div>
</div>
</div>
<!-- 批次操作列 -->
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center gap-2">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="selectAll">
<label class="form-check-label" for="selectAll">全選</label>
</div>
<button class="btn btn-outline-danger btn-sm" id="batchDeleteBtn" disabled onclick="batchDelete()">
<i class="fas fa-trash me-1"></i>批次刪除 (<span id="selectedCount">0</span>)
</button>
</div>
<div class="text-muted small">
<strong id="totalCount">0</strong> 筆記錄
</div>
</div>
<!-- 歷史記錄列表 -->
<div id="historyList">
<!-- 動態載入 -->
</div>
<!-- 分頁 -->
<nav aria-label="Page navigation" class="mt-4">
<ul class="pagination justify-content-center" id="pagination">
<!-- 動態生成 -->
</ul>
</nav>
</div>
<!-- 編輯 Modal -->
<div class="modal fade" id="editModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-edit me-2"></i>編輯文案
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="editId">
<div class="mb-3">
<label class="form-label fw-bold">商品名稱</label>
<input type="text" class="form-control" id="editProductName" readonly>
</div>
<div class="mb-3">
<label class="form-label fw-bold">生成內容</label>
<textarea class="form-control" id="editContent" rows="6"></textarea>
</div>
<div class="row">
<div class="col-md-4">
<label class="form-label fw-bold">評分</label>
<div class="rating-stars" id="editRating">
<i class="fas fa-star" data-rating="1"></i>
<i class="fas fa-star" data-rating="2"></i>
<i class="fas fa-star" data-rating="3"></i>
<i class="fas fa-star" data-rating="4"></i>
<i class="fas fa-star" data-rating="5"></i>
</div>
</div>
<div class="col-md-4">
<label class="form-label fw-bold">收藏</label>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="editFavorite">
<label class="form-check-label" for="editFavorite">標記為收藏</label>
</div>
</div>
<div class="col-md-4">
<label class="form-label fw-bold">已使用</label>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="editUsed">
<label class="form-check-label" for="editUsed">標記為已使用</label>
</div>
</div>
</div>
<div class="mt-3">
<label class="form-label fw-bold">備註</label>
<textarea class="form-control" id="editNotes" rows="2" placeholder="添加備註..."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveEdit()">
<i class="fas fa-save me-1"></i>儲存變更
</button>
</div>
</div>
</div>
</div>
<!-- 複製成功 Toast -->
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div id="copyToast" class="toast" role="alert">
<div class="toast-header bg-success text-white">
<i class="fas fa-check-circle me-2"></i>
<strong class="me-auto">複製成功</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body">
文案已複製到剪貼簿
</div>
</div>
</div>
{% endblock %}
{% block extra_css %}
<style>
.ai-history-page {
display: flex;
flex-direction: column;
gap: 18px;
}
.ai-history-hero {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 16px;
align-items: center;
padding: 22px;
border: 1px solid var(--momo-border-strong);
border-radius: 8px;
background:
radial-gradient(circle at 18px 18px, rgba(42, 37, 32, 0.12) 1px, transparent 1px),
linear-gradient(135deg, rgba(242, 178, 90, 0.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-history-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.45rem, 2vw, 2.05rem);
font-weight: 800;
letter-spacing: 0;
}
.ai-history-title i {
color: var(--momo-warm-caramel);
}
.ai-history-subtitle {
margin: 8px 0 0;
color: var(--momo-text-muted);
}
.ai-history-actions {
display: flex;
justify-content: flex-end;
}
.ai-history-action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 40px;
border-radius: 8px;
background: var(--momo-text-strong);
border-color: var(--momo-text-strong);
font-weight: 800;
}
.ai-history-page #statsRow .card,
.ai-history-panel,
.history-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-history-page #statsRow .card-body {
min-height: 112px;
}
.ai-history-page #statsRow h3 {
color: var(--momo-text-strong);
font-family: var(--momo-font-mono);
font-weight: 800;
}
.ai-history-page #statsRow .small,
.ai-history-page .form-label,
.ai-history-page .text-muted {
color: var(--momo-text-muted) !important;
}
.ai-history-panel .input-group-text,
.ai-history-panel .form-control,
.ai-history-panel .form-select {
border-color: var(--momo-border-subtle);
border-radius: 8px;
}
.ai-history-panel .input-group-text {
background: rgba(250, 247, 240, 0.74) !important;
}
.history-card {
transition: all 0.2s ease;
}
.history-card:hover {
box-shadow: 0 10px 26px rgba(42, 37, 32, 0.1);
transform: translateY(-2px);
}
.history-card.selected {
border-color: rgba(172, 92, 58, 0.42) !important;
background: rgba(255, 247, 235, 0.9);
}
.copy-content {
max-height: 120px;
overflow: hidden;
position: relative;
border: 1px solid rgba(42, 37, 32, 0.08);
background: rgba(250, 247, 240, 0.74) !important;
}
.copy-content::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 30px;
background: linear-gradient(transparent, rgba(250, 247, 240, 0.96));
}
.rating-stars i {
color: #dee2e6;
cursor: pointer;
font-size: 1.2rem;
margin-right: 2px;
transition: color 0.2s;
}
.rating-stars i.active,
.rating-stars i:hover {
color: #ffc107;
}
.badge-type {
font-size: 0.7rem;
font-weight: 500;
}
.keyword-badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
@media (max-width: 768px) {
.ai-history-hero {
grid-template-columns: 1fr;
}
.ai-history-actions,
.ai-history-action-btn {
width: 100%;
}
}
</style>
{% endblock %}
{% block extra_js %}
<script>
let currentPage = 1;
let perPage = 10;
let selectedIds = new Set();
let currentRating = 0;
// 頁面載入
document.addEventListener('DOMContentLoaded', function() {
loadStatistics();
loadHistory();
// 搜尋防抖
let searchTimeout;
document.getElementById('searchInput').addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
currentPage = 1;
loadHistory();
}, 300);
});
// 篩選變更
['filterType', 'filterFavorite', 'filterStartDate', 'filterEndDate'].forEach(id => {
document.getElementById(id).addEventListener('change', () => {
currentPage = 1;
loadHistory();
});
});
// 全選
document.getElementById('selectAll').addEventListener('change', function() {
document.querySelectorAll('.history-checkbox').forEach(cb => {
cb.checked = this.checked;
updateSelection(parseInt(cb.dataset.id), this.checked);
});
});
// 評分星星點擊
document.querySelectorAll('#editRating i').forEach(star => {
star.addEventListener('click', function() {
currentRating = parseInt(this.dataset.rating);
updateStars(currentRating);
});
star.addEventListener('mouseenter', function() {
updateStars(parseInt(this.dataset.rating));
});
});
document.getElementById('editRating').addEventListener('mouseleave', function() {
updateStars(currentRating);
});
});
// 載入統計資料
async function loadStatistics() {
try {
const response = await fetch('/api/ai/statistics?days=30');
const result = await response.json();
if (result.success) {
const stats = result.data;
document.getElementById('statTotal').textContent = stats.total_count;
document.getElementById('statFavorite').textContent = stats.favorite_count;
document.getElementById('statUsed').textContent = stats.used_count;
document.getElementById('statDuration').textContent = stats.avg_duration;
}
} catch (error) {
console.error('載入統計失敗:', error);
}
}
// 載入歷史記錄
async function loadHistory() {
const params = new URLSearchParams({
page: currentPage,
per_page: perPage
});
const search = document.getElementById('searchInput').value;
const type = document.getElementById('filterType').value;
const favorite = document.getElementById('filterFavorite').value;
const startDate = document.getElementById('filterStartDate').value;
const endDate = document.getElementById('filterEndDate').value;
if (search) params.append('search', search);
if (type) params.append('type', type);
if (favorite) params.append('favorite', favorite);
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
try {
const response = await fetch(`/api/ai/history?${params}`);
const result = await response.json();
if (result.success) {
renderHistory(result.data.items);
renderPagination(result.data);
document.getElementById('totalCount').textContent = result.data.total;
}
} catch (error) {
console.error('載入歷史失敗:', error);
document.getElementById('historyList').innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-circle me-2"></i>載入失敗,請稍後再試
</div>
`;
}
}
// 渲染歷史列表
function renderHistory(items) {
const container = document.getElementById('historyList');
if (items.length === 0) {
container.innerHTML = `
<div class="text-center py-5">
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
<p class="text-muted">尚無可回收文案</p>
<a href="/ai_recommend" class="btn btn-primary">
<i class="fas fa-magic me-1"></i>產生銷售動作
</a>
</div>
`;
return;
}
container.innerHTML = items.map(item => {
const typeLabels = {
'copy': { text: '銷售文案', class: 'bg-primary' },
'recommend': { text: '商品推薦', class: 'bg-success' },
'weather_analysis': { text: '天氣分析', class: 'bg-info' }
};
const typeInfo = typeLabels[item.generation_type] || { text: item.generation_type, class: 'bg-secondary' };
const keywords = item.input_keywords || [];
const date = new Date(item.created_at).toLocaleString('zh-TW');
return `
<div class="card history-card mb-3 ${selectedIds.has(item.id) ? 'selected' : ''}" data-id="${item.id}">
<div class="card-body">
<div class="row">
<div class="col-auto d-flex align-items-start">
<input type="checkbox" class="form-check-input history-checkbox mt-1"
data-id="${item.id}" ${selectedIds.has(item.id) ? 'checked' : ''}
onchange="updateSelection(${item.id}, this.checked)">
</div>
<div class="col">
<div class="d-flex justify-content-between align-items-start mb-2">
<div>
<span class="badge ${typeInfo.class} badge-type me-2">${typeInfo.text}</span>
<strong class="fs-5">${escapeHtml(item.product_name || '未命名')}</strong>
${item.is_favorite ? '<i class="fas fa-star text-warning ms-2"></i>' : ''}
${item.is_used ? '<span class="badge bg-success-subtle text-success ms-2">已使用</span>' : ''}
</div>
<div class="text-muted small">
<i class="fas fa-clock me-1"></i>${date}
</div>
</div>
${keywords.length > 0 ? `
<div class="mb-2">
${keywords.map(k => `<span class="badge bg-light text-dark keyword-badge me-1">#${escapeHtml(k)}</span>`).join('')}
</div>
` : ''}
<div class="copy-content bg-light rounded p-3 mb-3">
<p class="mb-0 text-break">${escapeHtml(item.output_content)}</p>
</div>
<div class="d-flex justify-content-between align-items-center">
<div class="small text-muted">
<span class="me-3"><i class="fas fa-wand-magic-sparkles me-1"></i>建議引擎</span>
${item.generation_duration ? `<span><i class="fas fa-stopwatch me-1"></i>${item.generation_duration}秒</span>` : ''}
${item.input_style ? `<span class="ms-3"><i class="fas fa-palette me-1"></i>${item.input_style}</span>` : ''}
</div>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-secondary" onclick="copyContent('${escapeAttr(item.output_content)}')" title="複製">
<i class="fas fa-copy"></i>
</button>
<button class="btn btn-outline-warning" onclick="toggleFavorite(${item.id})" title="${item.is_favorite ? '取消收藏' : '收藏'}">
<i class="fas fa-star"></i>
</button>
<button class="btn btn-outline-primary" onclick="openEdit(${item.id})" title="編輯">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-outline-danger" onclick="deleteItem(${item.id})" title="刪除">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;
}).join('');
}
// 渲染分頁
function renderPagination(data) {
const pagination = document.getElementById('pagination');
const totalPages = data.total_pages;
if (totalPages <= 1) {
pagination.innerHTML = '';
return;
}
let html = '';
// 上一頁
html += `<li class="page-item ${currentPage === 1 ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="goToPage(${currentPage - 1})">
<i class="fas fa-chevron-left"></i>
</a>
</li>`;
// 頁碼
const startPage = Math.max(1, currentPage - 2);
const endPage = Math.min(totalPages, currentPage + 2);
if (startPage > 1) {
html += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(1)">1</a></li>`;
if (startPage > 2) {
html += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
}
}
for (let i = startPage; i <= endPage; i++) {
html += `<li class="page-item ${i === currentPage ? 'active' : ''}">
<a class="page-link" href="#" onclick="goToPage(${i})">${i}</a>
</li>`;
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
html += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
}
html += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(${totalPages})">${totalPages}</a></li>`;
}
// 下一頁
html += `<li class="page-item ${currentPage === totalPages ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="goToPage(${currentPage + 1})">
<i class="fas fa-chevron-right"></i>
</a>
</li>`;
pagination.innerHTML = html;
}
function goToPage(page) {
currentPage = page;
loadHistory();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
// 選擇管理
function updateSelection(id, checked) {
if (checked) {
selectedIds.add(id);
} else {
selectedIds.delete(id);
}
const count = selectedIds.size;
document.getElementById('selectedCount').textContent = count;
document.getElementById('batchDeleteBtn').disabled = count === 0;
// 更新卡片樣式
const card = document.querySelector(`.history-card[data-id="${id}"]`);
if (card) {
card.classList.toggle('selected', checked);
}
}
// 複製內容
function copyContent(content) {
navigator.clipboard.writeText(content).then(() => {
const toast = new bootstrap.Toast(document.getElementById('copyToast'));
toast.show();
});
}
// 切換收藏
async function toggleFavorite(id) {
try {
const response = await fetch(`/api/ai/history/${id}/favorite`, { method: 'POST' });
const result = await response.json();
if (result.success) {
loadHistory();
loadStatistics();
}
} catch (error) {
console.error('切換收藏失敗:', error);
}
}
// 開啟編輯
async function openEdit(id) {
try {
const response = await fetch(`/api/ai/history/${id}`);
const result = await response.json();
if (result.success) {
const item = result.data;
document.getElementById('editId').value = item.id;
document.getElementById('editProductName').value = item.product_name || '';
document.getElementById('editContent').value = item.output_content;
document.getElementById('editFavorite').checked = item.is_favorite;
document.getElementById('editUsed').checked = item.is_used;
document.getElementById('editNotes').value = item.notes || '';
currentRating = item.rating || 0;
updateStars(currentRating);
new bootstrap.Modal(document.getElementById('editModal')).show();
}
} catch (error) {
console.error('載入詳情失敗:', error);
}
}
function updateStars(rating) {
document.querySelectorAll('#editRating i').forEach(star => {
star.classList.toggle('active', parseInt(star.dataset.rating) <= rating);
});
}
// 儲存編輯
async function saveEdit() {
const id = document.getElementById('editId').value;
try {
const response = await fetch(`/api/ai/history/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
output_content: document.getElementById('editContent').value,
rating: currentRating || null,
is_favorite: document.getElementById('editFavorite').checked,
is_used: document.getElementById('editUsed').checked,
notes: document.getElementById('editNotes').value
})
});
const result = await response.json();
if (result.success) {
bootstrap.Modal.getInstance(document.getElementById('editModal')).hide();
loadHistory();
loadStatistics();
} else {
alert('儲存失敗: ' + result.error);
}
} catch (error) {
console.error('儲存失敗:', error);
alert('儲存失敗');
}
}
// 刪除單筆
async function deleteItem(id) {
if (!confirm('確定要刪除這筆記錄嗎?')) return;
try {
const response = await fetch(`/api/ai/history/${id}`, { method: 'DELETE' });
const result = await response.json();
if (result.success) {
loadHistory();
loadStatistics();
}
} catch (error) {
console.error('刪除失敗:', error);
}
}
// 批次刪除
async function batchDelete() {
if (!confirm(`確定要刪除選取的 ${selectedIds.size} 筆記錄嗎?`)) return;
try {
const response = await fetch('/api/ai/history/batch', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: Array.from(selectedIds) })
});
const result = await response.json();
if (result.success) {
selectedIds.clear();
document.getElementById('selectedCount').textContent = '0';
document.getElementById('batchDeleteBtn').disabled = true;
document.getElementById('selectAll').checked = false;
loadHistory();
loadStatistics();
}
} catch (error) {
console.error('批次刪除失敗:', error);
}
}
// 重置篩選
function resetFilters() {
document.getElementById('searchInput').value = '';
document.getElementById('filterType').value = '';
document.getElementById('filterFavorite').value = '';
document.getElementById('filterStartDate').value = '';
document.getElementById('filterEndDate').value = '';
currentPage = 1;
loadHistory();
}
// 工具函數
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 escapeAttr(text) {
if (!text) return '';
return String(text)
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r');
}
</script>
{% endblock extra_js %}