Files
article-writer/templates/topic.html

614 lines
34 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.
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{topic.name}} - 文章编写系统</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
:root { --bg-dark: #0d1117; --bg-card: #161b22; --bg-hover: #1c2333; --border: #30363d; --text: #c9d1d9; --text-muted: #9eaab8; --accent: #58a6ff; --accent-hover: #79c0ff; --danger: #f85149; --success: #3fb950; --warning: #d29922; }
* { box-sizing: border-box; }
body { background: var(--bg-dark); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; }
.text-muted { color: var(--text-muted) !important; }
.navbar { background: var(--bg-card); border-bottom: 1px solid var(--border); }
.navbar-brand, .navbar-brand:hover { color: var(--accent) !important; font-weight: 700; }
.section-title { color: var(--text); font-size: 1.1rem; font-weight: 600; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
.section-title i { font-size: 1.2rem; }
.card-dark { background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px; }
.card-dark .card-header { background: transparent; border-bottom: 1px solid var(--border); padding: 12px 16px; }
.card-dark .card-body { padding: 16px; }
.material-item { background: var(--bg-dark); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 8px; transition: all 0.15s; }
.material-item:hover { border-color: var(--accent); }
.material-item .mat-content { white-space: pre-wrap; word-break: break-word; font-size: 0.92rem; line-height: 1.5; max-height: 120px; overflow: hidden; }
.material-item img { max-width: 100%; max-height: 200px; border-radius: 6px; margin-top: 6px; }
.material-item .mat-meta { color: var(--text-muted); font-size: 0.8rem; margin-top: 6px; }
.btn-accent { background: var(--accent); color: #fff; border: none; }
.btn-accent:hover { background: var(--accent-hover); color: #fff; }
.btn-outline-accent { border-color: var(--accent); color: var(--accent); }
.btn-outline-accent:hover { background: var(--accent); color: #fff; }
.btn-secondary { background: var(--border); color: var(--text); border: none; }
.btn-secondary:hover { background: #444c56; color: var(--text); }
.modal-content { background: var(--bg-card); border: 1px solid var(--border); color: var(--text); }
.modal-header { border-bottom: 1px solid var(--border); }
.modal-footer { border-top: 1px solid var(--border); }
.form-control, .form-select { background: var(--bg-dark); border-color: var(--border); color: var(--text); }
.form-control::placeholder, .form-select::placeholder { color: #6e7a88; opacity: 1; }
.form-control:focus, .form-select:focus { background: var(--bg-dark); border-color: var(--accent); color: var(--text); box-shadow: 0 0 0 2px rgba(88,166,255,0.2); }
.form-label { color: var(--text); font-size: 0.9rem; font-weight: 500; }
.form-text { color: var(--text-muted) !important; }
.btn-close { filter: invert(1); }
.article-content { white-space: pre-wrap; word-break: break-word; line-height: 1.8; font-size: 0.95rem; }
.prompt-area { background: var(--bg-dark); border: 1px solid var(--border); border-radius: 8px; padding: 12px; min-height: 80px; }
.generating { opacity: 0.7; pointer-events: none; }
.spinner-generate { display: inline-block; width: 18px; height: 18px; border: 2px solid rgba(255,255,255,0.3); border-top-color: #fff; border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.article-card { border: 1px solid var(--border); border-radius: 8px; margin-bottom: 12px; overflow: hidden; }
.article-card .article-header { background: var(--bg-dark); padding: 10px 14px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); }
.article-card .article-body { padding: 14px; background: var(--bg-card); max-height: 400px; overflow-y: auto; }
.copy-btn { cursor: pointer; color: var(--text-muted); }
.copy-btn:hover { color: var(--accent); }
.prompt-history-item { background: var(--bg-dark); border: 1px solid var(--border); border-radius: 6px; padding: 8px 12px; margin-bottom: 6px; cursor: pointer; transition: all 0.15s; }
.prompt-history-item:hover { border-color: var(--accent); }
.prompt-history-item .ph-text { font-size: 0.85rem; white-space: pre-wrap; max-height: 60px; overflow: hidden; }
.prompt-history-item .ph-meta { font-size: 0.75rem; color: var(--text-muted); margin-top: 4px; }
.paste-zone { border: 2px dashed var(--border); border-radius: 10px; padding: 24px; text-align: center; color: var(--text-muted); cursor: pointer; transition: all 0.2s; }
.paste-zone:hover, .paste-zone.active { border-color: var(--accent); background: rgba(88,166,255,0.05); color: var(--accent); }
.paste-zone i { font-size: 2rem; display: block; margin-bottom: 8px; }
.collapsible-prompt { max-height: 0; overflow: hidden; transition: max-height 0.3s ease; }
.collapsible-prompt.show { max-height: 200px; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--bg-dark); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
/* 分类和标签样式 */
.badge-cat { background: rgba(210,153,34,0.15); color: var(--warning); font-size: 0.78rem; }
.badge-tag { background: rgba(63,185,80,0.15); color: var(--success); font-size: 0.78rem; }
.info-bar { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; padding: 8px 16px; background: var(--bg-card); border-bottom: 1px solid var(--border); }
.info-bar .edit-btn { cursor: pointer; color: var(--text-muted); font-size: 0.8rem; }
.info-bar .edit-btn:hover { color: var(--accent); }
.select-pills { display: flex; flex-wrap: wrap; gap: 6px; }
.pill-option { padding: 4px 12px; border-radius: 16px; border: 1px solid var(--border); cursor: pointer; font-size: 0.85rem; transition: all 0.15s; color: var(--text-muted); }
.pill-option:hover { border-color: var(--accent); color: var(--accent); }
.pill-option.selected { background: rgba(88,166,255,0.15); border-color: var(--accent); color: var(--accent); }
.pill-option.selected-tag { background: rgba(63,185,80,0.15); border-color: var(--success); color: var(--success); }
.tag-input-group { display: flex; gap: 6px; }
.tag-input-group input { flex: 1; }
</style>
</head>
<body>
<nav class="navbar navbar-expand">
<div class="container-fluid px-4">
<a class="navbar-brand" href="/"><i class="bi bi-arrow-left me-2"></i>文章编写系统</a>
<span class="navbar-text text-warning fw-bold">{{topic.name}}</span>
</div>
</nav>
<!-- 分类/标签信息条 -->
<div class="info-bar">
<span class="text-muted small"><i class="bi bi-folder2 me-1"></i>分类:</span>
{% if topic.categories %}
{% for c in topic.categories %}
<span class="badge badge-cat">{{c.name}}</span>
{% endfor %}
{% else %}
<span class="text-muted small">未分类</span>
{% endif %}
<span class="text-muted small ms-2"><i class="bi bi-tags me-1"></i>标签:</span>
{% if topic.tags %}
{% for tg in topic.tags %}
<span class="badge badge-tag">{{tg.name}}</span>
{% endfor %}
{% else %}
<span class="text-muted small">无标签</span>
{% endif %}
<span class="edit-btn ms-2" onclick="showEditCatTagModal()"><i class="bi bi-pencil"></i> 编辑</span>
</div>
<div class="container-fluid px-4 py-3">
<div class="row g-3">
<!-- 左栏:素材 + Prompt -->
<div class="col-lg-5">
<!-- Prompt区 -->
<div class="card-dark mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<span class="section-title mb-0"><i class="bi bi-chat-square-text text-warning"></i>生成Prompt</span>
<div class="d-flex gap-1">
<button class="btn btn-sm btn-outline-accent" onclick="togglePromptHistory()" title="历史记录">
<i class="bi bi-clock-history"></i>
</button>
<button class="btn btn-sm btn-outline-accent" onclick="savePrompt()"><i class="bi bi-check-lg me-1"></i>保存</button>
</div>
</div>
<div class="card-body">
<!-- Prompt历史折叠区 -->
<div id="promptHistoryArea" class="collapsible-prompt mb-2">
<div class="mb-2 small text-muted"><i class="bi bi-clock-history me-1"></i>历史Prompt点击恢复</div>
<div id="promptHistoryList"></div>
</div>
<textarea class="form-control" id="promptText" rows="4" placeholder="请根据以下素材撰写一篇结构清晰、内容丰富的文章:">{{topic.prompt or ''}}</textarea>
{% if topic.description %}<div class="mt-2 text-muted small"><i class="bi bi-info-circle me-1"></i>主题说明:{{topic.description}}</div>{% endif %}
</div>
</div>
<!-- 素材区 -->
<div class="card-dark">
<div class="card-header d-flex justify-content-between align-items-center">
<span class="section-title mb-0"><i class="bi bi-collection text-info"></i>素材库 <span class="badge bg-secondary ms-1">{{materials|length}}</span></span>
<div>
<button class="btn btn-sm btn-outline-accent me-1" onclick="showAddText()"><i class="bi bi-type me-1"></i>文本</button>
<button class="btn btn-sm btn-outline-accent" onclick="showAddImage()"><i class="bi bi-image me-1"></i>图片</button>
</div>
</div>
<div class="card-body" id="materialsList" style="max-height: 60vh; overflow-y: auto;">
<!-- 粘贴上传区 -->
<div class="paste-zone mb-3" id="pasteZone" tabindex="0" onclick="document.getElementById('imageFile').click()">
<i class="bi bi-clipboard"></i>
<small>Ctrl+V 粘贴截图 或 点击选择图片</small>
</div>
{% if materials %}
{% for m in materials %}
<div class="material-item" id="mat-{{m.id}}">
<div class="d-flex justify-content-between align-items-start">
<span class="badge {{'bg-info' if m.type=='text' else 'bg-warning'}}">{{'文本' if m.type=='text' else '图片'}}</span>
<button class="btn btn-sm btn-link text-danger p-0" onclick="deleteMaterial('{{m.id}}')" title="删除"><i class="bi bi-x-lg"></i></button>
</div>
{% if m.type == 'text' %}
<div class="mat-content mt-1">{{m.content}}</div>
{% else %}
{% if m.content %}<div class="small text-muted mt-1">{{m.content}}</div>{% endif %}
<img src="/static/{{m.file_path}}" alt="素材图片">
{% endif %}
<div class="mat-meta">{{m.created_at}}</div>
</div>
{% endfor %}
{% else %}
<div class="text-center text-muted py-3" id="materialsEmpty">
<small>添加素材开始创作</small>
</div>
{% endif %}
</div>
</div>
</div>
<!-- 右栏:文章生成 -->
<div class="col-lg-7">
<div class="card-dark">
<div class="card-header d-flex justify-content-between align-items-center">
<span class="section-title mb-0"><i class="bi bi-file-earmark-richtext text-success"></i>文章生成</span>
<button class="btn btn-accent" id="generateBtn" onclick="generateArticle()">
<i class="bi bi-stars me-1"></i>生成文章
</button>
</div>
<div class="card-body" id="articlesArea" style="max-height: 80vh; overflow-y: auto;">
{% if articles %}
{% for a in articles %}
<div class="article-card" id="article-{{a.id}}">
<div class="article-header">
<span class="small text-muted">
<i class="bi bi-clock me-1"></i>{{a.created_at}} · {{a.model}}
{% if a.prompt_used %}<span class="copy-btn ms-2" onclick="toggleArticlePrompt('{{a.id}}')" title="查看Prompt"><i class="bi bi-chat-left-text"></i></span>{% endif %}
</span>
<div>
<span class="copy-btn me-2" onclick="copyArticle('{{a.id}}')" title="复制"><i class="bi bi-clipboard"></i></span>
<span class="copy-btn me-2" onclick="downloadArticle('{{a.id}}')" title="下载"><i class="bi bi-download"></i></span>
<span class="copy-btn text-danger" onclick="deleteArticle('{{a.id}}')" title="删除"><i class="bi bi-trash3"></i></span>
</div>
</div>
{% if a.prompt_used %}
<div class="collapsible-prompt" id="article-prompt-{{a.id}}">
<div style="padding: 8px 14px; background: rgba(88,166,255,0.05); border-bottom: 1px solid var(--border); font-size: 0.82rem; white-space: pre-wrap; color: var(--text-muted);">{{a.prompt_used}}</div>
</div>
{% endif %}
<div class="article-body article-content" id="article-content-{{a.id}}">{{a.content}}</div>
</div>
{% endfor %}
{% else %}
<div class="text-center text-muted py-5">
<i class="bi bi-pencil-square fs-1 d-block mb-3"></i>
<p>添加素材并编辑Prompt后点击「生成文章」</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- 编辑分类/标签模态框 -->
<div class="modal fade" id="catTagModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-tags me-2"></i>编辑分类与标签</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">分类(可多选)</label>
<div class="select-pills" id="editCategoryPills"></div>
<div class="form-text">点击选择所属分类</div>
</div>
<div class="mb-3">
<label class="form-label">标签</label>
<div class="select-pills mb-2" id="editTagPills"></div>
<div class="tag-input-group">
<input type="text" class="form-control form-control-sm" id="editNewTagInput" placeholder="输入新标签,回车添加">
<button class="btn btn-sm btn-outline-accent" onclick="addEditNewTag()">添加</button>
</div>
<div class="select-pills mt-1" id="editNewTagPills"></div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-accent" onclick="saveCatTag()">保存</button>
</div>
</div>
</div>
</div>
<!-- 添加文本模态框 -->
<div class="modal fade" id="textModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-type me-2"></i>添加文本素材</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<textarea class="form-control" id="textContent" rows="8" placeholder="在此粘贴或输入文本素材..."></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-accent" onclick="addText()">添加</button>
</div>
</div>
</div>
</div>
<!-- 添加图片模态框 -->
<div class="modal fade" id="imageModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-image me-2"></i>添加图片素材</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">选择图片</label>
<input type="file" class="form-control" id="imageFile" accept="image/*">
</div>
<div class="mb-3">
<label class="form-label">图片说明(可选)</label>
<input type="text" class="form-control" id="imageDesc" placeholder="描述图片内容">
</div>
<div id="imagePreview" class="text-center"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-accent" onclick="addImage()">上传</button>
</div>
</div>
</div>
</div>
<!-- 粘贴截图快速上传模态框 -->
<div class="modal fade" id="pasteModal" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-clipboard-check me-2"></i>粘贴上传</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="pastePreview" class="text-center mb-2"></div>
<div class="mb-2">
<label class="form-label">图片说明(可选)</label>
<input type="text" class="form-control form-control-sm" id="pasteDesc" placeholder="描述图片内容">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-accent btn-sm" onclick="uploadPastedImage()">上传</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const topicId = '{{topic.id}}';
const textModal = new bootstrap.Modal(document.getElementById('textModal'));
const imageModal = new bootstrap.Modal(document.getElementById('imageModal'));
const pasteModal = new bootstrap.Modal(document.getElementById('pasteModal'));
const catTagModal = new bootstrap.Modal(document.getElementById('catTagModal'));
let pastedBlob = null;
// 当前主题的分类和标签
const currentCategoryIds = [{% for c in topic.categories %}'{{c.id}}',{% endfor %}];
const currentTagIds = [{% for tg in topic.tags %}'{{tg.id}}',{% endfor %}];
// ===== 分类/标签编辑 =====
let editCategoryIds = [...currentCategoryIds];
let editTagIds = [...currentTagIds];
let editNewTagNames = [];
async function showEditCatTagModal() {
editCategoryIds = [...currentCategoryIds];
editTagIds = [...currentTagIds];
editNewTagNames = [];
document.getElementById('editNewTagInput').value = '';
document.getElementById('editNewTagPills').innerHTML = '';
// 加载分类选项
try {
const res = await fetch('/api/categories');
const cats = await res.json();
document.getElementById('editCategoryPills').innerHTML = cats.map(c =>
`<span class="pill-option ${editCategoryIds.includes(c.id) ? 'selected' : ''}" data-id="${c.id}" onclick="toggleEditCategory(this,'${c.id}')">${c.name}</span>`
).join('');
} catch(e) {}
// 加载标签选项
try {
const res = await fetch('/api/tags');
const tags = await res.json();
document.getElementById('editTagPills').innerHTML = tags.map(t =>
`<span class="pill-option ${editTagIds.includes(t.id) ? 'selected-tag' : ''}" data-id="${t.id}" onclick="toggleEditTag(this,'${t.id}')">${t.name}</span>`
).join('');
} catch(e) {}
catTagModal.show();
}
function toggleEditCategory(el, id) {
const idx = editCategoryIds.indexOf(id);
if (idx >= 0) { editCategoryIds.splice(idx, 1); el.classList.remove('selected'); }
else { editCategoryIds.push(id); el.classList.add('selected'); }
}
function toggleEditTag(el, id) {
const idx = editTagIds.indexOf(id);
if (idx >= 0) { editTagIds.splice(idx, 1); el.classList.remove('selected-tag'); }
else { editTagIds.push(id); el.classList.add('selected-tag'); }
}
function addEditNewTag() {
const input = document.getElementById('editNewTagInput');
const name = input.value.trim();
if (!name || editNewTagNames.includes(name)) return;
editNewTagNames.push(name);
input.value = '';
renderEditNewTagPills();
}
document.getElementById('editNewTagInput').addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); addEditNewTag(); }
});
function renderEditNewTagPills() {
document.getElementById('editNewTagPills').innerHTML = editNewTagNames.map((n, i) =>
`<span class="pill-option selected-tag" onclick="removeEditNewTag(${i})">${n} <i class="bi bi-x"></i></span>`
).join('');
}
function removeEditNewTag(idx) {
editNewTagNames.splice(idx, 1);
renderEditNewTagPills();
}
async function saveCatTag() {
const res = await fetch(`/api/topics/${topicId}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
category_ids: editCategoryIds,
tag_ids: editTagIds,
new_tags: editNewTagNames
})
});
if (res.ok) {
catTagModal.hide();
location.reload();
} else {
const d = await res.json(); alert(d.error || '保存失败');
}
}
// ===== 粘贴截图上传 =====
document.addEventListener('paste', function(e) {
const items = e.clipboardData && e.clipboardData.items;
if (!items) return;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
e.preventDefault();
const blob = items[i].getAsFile();
pastedBlob = blob;
const reader = new FileReader();
reader.onload = (ev) => {
document.getElementById('pastePreview').innerHTML = `<img src="${ev.target.result}" style="max-width:100%;max-height:200px;border-radius:6px;">`;
document.getElementById('pasteDesc').value = '';
pasteModal.show();
};
reader.readAsDataURL(blob);
break;
}
}
});
const pasteZone = document.getElementById('pasteZone');
document.addEventListener('paste', () => { pasteZone.classList.add('active'); setTimeout(() => pasteZone.classList.remove('active'), 1000); });
async function uploadPastedImage() {
if (!pastedBlob) return alert('没有粘贴的图片');
const fd = new FormData();
fd.append('type', 'image');
fd.append('file', pastedBlob, `paste_${Date.now()}.png`);
fd.append('content', document.getElementById('pasteDesc').value);
const res = await fetch(`/api/topics/${topicId}/materials`, {method: 'POST', body: fd});
if (res.ok) { pastedBlob = null; pasteModal.hide(); location.reload(); }
else { const d = await res.json(); alert(d.error); }
}
document.getElementById('imageFile').addEventListener('change', function(e) {
const preview = document.getElementById('imagePreview');
if (e.target.files[0]) {
const reader = new FileReader();
reader.onload = (ev) => { preview.innerHTML = `<img src="${ev.target.result}" style="max-width:100%;max-height:200px;border-radius:6px;">`; };
reader.readAsDataURL(e.target.files[0]);
} else { preview.innerHTML = ''; }
});
function showAddText() { document.getElementById('textContent').value = ''; textModal.show(); }
function showAddImage() { document.getElementById('imageFile').value = ''; document.getElementById('imageDesc').value = ''; document.getElementById('imagePreview').innerHTML = ''; imageModal.show(); }
async function addText() {
const content = document.getElementById('textContent').value.trim();
if (!content) return alert('请输入文本内容');
const fd = new FormData();
fd.append('type', 'text');
fd.append('content', content);
const res = await fetch(`/api/topics/${topicId}/materials`, {method: 'POST', body: fd});
if (res.ok) location.reload();
else { const d = await res.json(); alert(d.error); }
}
async function addImage() {
const file = document.getElementById('imageFile').files[0];
if (!file) return alert('请选择图片');
const fd = new FormData();
fd.append('type', 'image');
fd.append('file', file);
fd.append('content', document.getElementById('imageDesc').value);
const res = await fetch(`/api/topics/${topicId}/materials`, {method: 'POST', body: fd});
if (res.ok) location.reload();
else { const d = await res.json(); alert(d.error); }
}
async function deleteMaterial(id) {
if (!confirm('确定删除此素材?')) return;
await fetch(`/api/materials/${id}`, {method: 'DELETE'});
document.getElementById(`mat-${id}`).remove();
}
async function savePrompt() {
const prompt = document.getElementById('promptText').value;
const res = await fetch(`/api/topics/${topicId}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({prompt})
});
if (res.ok) {
const btn = event.target.closest('button');
btn.innerHTML = '<i class="bi bi-check-lg me-1"></i>已保存';
btn.classList.replace('btn-outline-accent', 'btn-success');
setTimeout(() => { btn.innerHTML = '<i class="bi bi-check-lg me-1"></i>保存'; btn.classList.replace('btn-success', 'btn-outline-accent'); }, 1500);
}
}
// ===== Prompt历史 =====
let promptHistoryLoaded = false;
async function togglePromptHistory() {
const area = document.getElementById('promptHistoryArea');
if (area.classList.contains('show')) { area.classList.remove('show'); return; }
if (!promptHistoryLoaded) {
const res = await fetch(`/api/topics/${topicId}/prompt-history`);
const history = await res.json();
const list = document.getElementById('promptHistoryList');
if (history.length === 0) {
list.innerHTML = '<div class="text-muted small">暂无历史记录</div>';
} else {
list.innerHTML = history.map(h => `
<div class="prompt-history-item" onclick="restorePrompt('${escapeAttr(h.prompt)}')">
<div class="ph-text">${escapeHtml(h.prompt)}</div>
<div class="ph-meta">${h.created_at} · ${h.model}</div>
</div>
`).join('');
}
promptHistoryLoaded = true;
}
area.classList.add('show');
}
function restorePrompt(text) { document.getElementById('promptText').value = text; }
// ===== 文章生成 =====
async function generateArticle() {
const btn = document.getElementById('generateBtn');
const orig = btn.innerHTML;
btn.innerHTML = '<span class="spinner-generate me-2"></span>生成中...';
btn.classList.add('generating');
try {
const res = await fetch(`/api/topics/${topicId}/generate`, {method: 'POST'});
if (!res.ok) { const d = await res.json(); throw new Error(d.error || '生成失败'); }
const article = await res.json();
const area = document.getElementById('articlesArea');
const empty = area.querySelector('.text-center');
if (empty) empty.remove();
const html = `<div class="article-card" id="article-${article.id}">
<div class="article-header">
<span class="small text-muted">
<i class="bi bi-clock me-1"></i>${article.created_at} · ${article.model}
${article.prompt_used ? `<span class="copy-btn ms-2" onclick="toggleArticlePrompt('${article.id}')" title="查看Prompt"><i class="bi bi-chat-left-text"></i></span>` : ''}
</span>
<div>
<span class="copy-btn me-2" onclick="copyArticle('${article.id}')" title="复制"><i class="bi bi-clipboard"></i></span>
<span class="copy-btn me-2" onclick="downloadArticle('${article.id}')" title="下载"><i class="bi bi-download"></i></span>
<span class="copy-btn text-danger" onclick="deleteArticle('${article.id}')" title="删除"><i class="bi bi-trash3"></i></span>
</div>
</div>
${article.prompt_used ? `<div class="collapsible-prompt" id="article-prompt-${article.id}"><div style="padding: 8px 14px; background: rgba(88,166,255,0.05); border-bottom: 1px solid var(--border); font-size: 0.82rem; white-space: pre-wrap; color: var(--text-muted);">${escapeHtml(article.prompt_used)}</div></div>` : ''}
<div class="article-body article-content" id="article-content-${article.id}">${escapeHtml(article.content)}</div>
</div>`;
area.insertAdjacentHTML('afterbegin', html);
} catch(e) {
alert('生成失败: ' + e.message);
} finally {
btn.innerHTML = orig;
btn.classList.remove('generating');
}
}
async function deleteArticle(id) {
if (!confirm('确定删除此文章?')) return;
await fetch(`/api/articles/${id}`, {method: 'DELETE'});
document.getElementById(`article-${id}`).remove();
}
function copyArticle(id) {
const el = document.getElementById(`article-content-${id}`);
navigator.clipboard.writeText(el.innerText).then(() => {});
}
function downloadArticle(id) {
const el = document.getElementById(`article-content-${id}`);
const blob = new Blob([el.innerText], {type: 'text/plain;charset=utf-8'});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `article_${id}.txt`;
a.click();
}
function toggleArticlePrompt(id) {
const el = document.getElementById(`article-prompt-${id}`);
if (el) el.classList.toggle('show');
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function escapeAttr(text) {
return text.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n');
}
</script>
</body>
</html>