Files
prompt-generator/frontend/index.html
T
wonder 8c5d540565 fix: 修复所有页面 Tab 导航下方冗余空白
- 将 body 布局从普通流改为 flex 纵向布局,navbar 和内容区自然排列
- navbar 移除 position:sticky,改为 flex-shrink:0 作为固定头部
- 新增 .main-content 工具类(flex:1; overflow:hidden),替代 calc(100vh-56px)+margin-top 方案
- builder/dashboard 页面使用 main-content 类占满剩余空间
- settings 页面 margin-top 从 80px 调整为 24px
2026-06-26 17:44:01 +08:00

749 lines
33 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prompt Generator — 构建器</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/static/common.css">
</head>
<body>
<div class="flex main-content">
<!-- 左侧面板 -->
<div class="w-1/3 border-r overflow-y-auto p-4 flex flex-col gap-4" style="border-color: var(--border-color); background: var(--bg-primary);">
<!-- 项目/会话 -->
<div class="card">
<div class="card-header">
📁 项目 / 会话
</div>
<div class="card-body flex flex-col gap-3">
<div>
<label class="text-sm font-medium mb-2" style="display:block">项目名称</label>
<input class="input" id="project-name" placeholder="输入项目名称(可选)">
</div>
<div>
<label class="text-sm font-medium mb-2" style="display:block">关联 Claude Session</label>
<select class="select" id="claude-session">
<option value="">不关联</option>
</select>
</div>
<div id="session-prompts" class="hidden">
<div class="collapse-header" onclick="toggleCollapse(this)">
<span class="collapse-arrow">▶</span>
<span class="text-sm font-medium">历史 Prompt 参考</span>
</div>
<div class="collapse-content" id="session-prompts-list"></div>
</div>
</div>
</div>
<!-- 标签选择 -->
<div class="card">
<div class="card-header">
🏷️ 标签选择
<div class="search-box" style="width:160px">
<span class="search-icon">🔍</span>
<input class="input" id="tag-search" placeholder="搜索标签..." style="font-size:12px;padding:4px 8px 4px 30px">
</div>
</div>
<div class="card-body">
<div id="system-tags" class="mb-3">
<div class="text-xs font-medium text-muted mb-2">系统标签</div>
<div id="system-tags-list"></div>
</div>
<div id="personal-tags">
<div class="flex items-center justify-between mb-2">
<span class="text-xs font-medium text-muted">个人标签</span>
<button class="btn btn-sm" onclick="showAddTagModal()">+ 新增</button>
</div>
<div id="personal-tags-list"></div>
</div>
</div>
</div>
<!-- 片段选择 -->
<div class="card">
<div class="card-header">
📄 片段选择
<div class="search-box" style="width:160px">
<span class="search-icon">🔍</span>
<input class="input" id="snippet-search" placeholder="搜索片段..." style="font-size:12px;padding:4px 8px 4px 30px">
</div>
</div>
<div class="card-body" id="snippets-container"></div>
</div>
<!-- 参考材料 -->
<div class="card">
<div class="card-header">
📦 参考材料
<button class="btn btn-sm" onclick="addMaterial()">+ 添加</button>
</div>
<div class="card-body" id="materials-container">
<div class="text-sm text-muted" id="materials-empty">暂无参考材料</div>
</div>
</div>
<!-- CLAUDE.md -->
<div class="card">
<div class="collapse-header" onclick="toggleCollapse(this)">
<span class="collapse-arrow">▶</span>
<span class="text-sm font-medium">📋 CLAUDE.md 内容(可选)</span>
</div>
<div class="collapse-content">
<textarea class="textarea" id="claude-md" placeholder="粘贴 CLAUDE.md 内容,用于智能建议分析(不会注入到最终 Prompt)" rows="4"></textarea>
</div>
</div>
<!-- 自定义输入 -->
<div class="card">
<div class="card-header">✏️ 自定义输入</div>
<div class="card-body">
<textarea class="textarea" id="custom-content" placeholder="输入自定义的 Prompt 内容..." rows="6"></textarea>
</div>
</div>
</div>
<!-- 右侧面板 -->
<div class="w-2/3 overflow-y-auto p-4 flex flex-col gap-4" style="background: var(--bg-secondary);">
<!-- 预览区 -->
<div class="card flex-1">
<div class="card-header">
📝 最终 Prompt 预览
<div class="flex gap-2">
<button class="btn btn-sm" onclick="saveSession()">💾 保存</button>
<button class="btn btn-sm btn-primary" onclick="copyPrompt()">📋 复制到剪贴板</button>
</div>
</div>
<div class="card-body">
<pre id="prompt-preview" style="white-space: pre-wrap; word-break: break-word; font-size: 13px; line-height: 1.6; min-height: 200px; color: var(--text-secondary);">选择标签和片段后,这里将实时预览最终的 Prompt...</pre>
</div>
</div>
<!-- 智能建议区 -->
<div class="card" id="suggestions-card" style="display:none">
<div class="card-header">
💡 智能建议
<div class="flex gap-2">
<button class="btn btn-sm" onclick="toggleAllSuggestions(true)">全选</button>
<button class="btn btn-sm" onclick="toggleAllSuggestions(false)">全不选</button>
</div>
</div>
<div class="card-body" id="suggestions-list"></div>
</div>
<div class="flex gap-2">
<button class="btn btn-primary" id="suggest-btn" onclick="getSuggestions()">🤖 智能建议</button>
<button class="btn" onclick="resetBuilder()">🔄 清空重置</button>
</div>
</div>
</div>
<!-- 新增标签模态框 -->
<div id="add-tag-modal" class="modal-overlay hidden">
<div class="modal">
<div class="modal-title">新增个人标签</div>
<div class="flex flex-col gap-3">
<div>
<label class="text-sm font-medium mb-2" style="display:block">标签名称</label>
<input class="input" id="new-tag-name" placeholder="如:部署环境">
</div>
<div>
<label class="text-sm font-medium mb-2" style="display:block">标签说明</label>
<input class="input" id="new-tag-desc" placeholder="简要说明标签用途">
</div>
</div>
<div class="flex gap-2 mt-4" style="justify-content:flex-end">
<button class="btn" onclick="closeAddTagModal()">取消</button>
<button class="btn btn-primary" onclick="createTag()">创建</button>
</div>
</div>
</div>
<!-- 保存会话模态框 -->
<div id="save-session-modal" class="modal-overlay hidden">
<div class="modal">
<div class="modal-title">保存构建会话</div>
<div class="flex flex-col gap-3">
<div>
<label class="text-sm font-medium mb-2" style="display:block">会话标题</label>
<input class="input" id="session-title" placeholder="为这次构建起个名字">
</div>
</div>
<div class="flex gap-2 mt-4" style="justify-content:flex-end">
<button class="btn" onclick="closeSaveModal()">取消</button>
<button class="btn btn-primary" onclick="doSaveSession()">保存</button>
</div>
</div>
</div>
<script src="/static/common.js"></script>
<script>
// 状态
let allTags = [];
let allSnippets = [];
let selectedTags = {}; // tagId -> optionId
let selectedSnippets = []; // [{snippetId, sortOrder}]
let materials = []; // [{title, content}]
let suggestions = [];
// 初始化
async function init() {
const ok = await Auth.check();
if (!ok) {
Auth.showLoginModal();
return;
}
renderNavbar('builder');
loadDraft();
await Promise.all([loadTags(), loadSnippets()]);
setupProjectNameListener();
setupAutoSave();
}
// 加载标签
async function loadTags() {
try {
allTags = await API.get('/api/tags');
renderTags();
} catch (e) {
Toast.error('加载标签失败: ' + e.message);
}
}
function renderTags() {
const keyword = document.getElementById('tag-search').value.toLowerCase();
const systemTags = allTags.filter(t => t.scope === 'system' && t.name.toLowerCase().includes(keyword));
const personalTags = allTags.filter(t => t.scope === 'personal' && t.name.toLowerCase().includes(keyword));
document.getElementById('system-tags-list').innerHTML = systemTags.map(t => renderTagItem(t)).join('');
document.getElementById('personal-tags-list').innerHTML = personalTags.length
? personalTags.map(t => renderTagItem(t)).join('')
: '<div class="text-sm text-muted">暂无个人标签</div>';
}
function renderTagItem(tag) {
const checked = selectedTags[tag.id] !== undefined;
const optionsHtml = tag.options.map(o => `
<label class="radio-label">
<input type="radio" name="tag_${tag.id}" value="${o.id}" ${selectedTags[tag.id] === o.id ? 'checked' : ''} onclick="selectTagOption(${tag.id}, ${o.id})">
<span class="text-sm">${escapeHtml(o.label)}</span>
</label>
`).join('');
return `
<div class="mb-2">
<div class="collapse-header ${checked ? 'open' : ''}" onclick="toggleTagCollapse(this, ${tag.id})">
<span class="collapse-arrow">▶</span>
<input type="checkbox" ${checked ? 'checked' : ''} onclick="event.stopPropagation(); toggleTag(${tag.id}, this.checked)" style="accent-color:var(--accent-color)">
<span class="text-sm font-medium">${escapeHtml(tag.name)}</span>
<span class="tag ${tag.scope === 'system' ? 'tag-system' : 'tag-personal'}">${tag.scope === 'system' ? '系统' : '个人'}</span>
</div>
<div class="collapse-content ${checked ? 'open' : ''}" id="tag-options-${tag.id}">
<div class="radio-group">${optionsHtml}</div>
</div>
</div>
`;
}
function toggleTagCollapse(header, tagId) {
header.classList.toggle('open');
const content = document.getElementById(`tag-options-${tagId}`);
content.classList.toggle('open');
}
function toggleTag(tagId, checked) {
if (checked) {
// Select first option by default
const tag = allTags.find(t => t.id === tagId);
if (tag && tag.options.length > 0) {
selectedTags[tagId] = tag.options[0].id;
}
} else {
delete selectedTags[tagId];
}
renderTags();
updatePreview();
}
function selectTagOption(tagId, optionId) {
if (selectedTags[tagId] === optionId) {
// 再次点击已选中的选项 → 取消选择(需要 renderTags 重建 DOM 以取消 radio 的 checked 状态)
delete selectedTags[tagId];
renderTags();
} else {
selectedTags[tagId] = optionId;
// 同步勾选父标签复选框
const checkbox = document.querySelector(`#tag-options-${tagId}`)
?.closest('.mb-2')
?.querySelector('input[type="checkbox"]');
if (checkbox) checkbox.checked = true;
}
updatePreview();
}
// 加载片段
async function loadSnippets() {
try {
allSnippets = await API.get('/api/snippets');
renderSnippets();
} catch (e) {
Toast.error('加载片段失败: ' + e.message);
}
}
function renderSnippets() {
const keyword = document.getElementById('snippet-search').value.toLowerCase();
const filtered = allSnippets.filter(s =>
s.name.toLowerCase().includes(keyword) || s.category.toLowerCase().includes(keyword)
);
// Group by category
const groups = {};
filtered.forEach(s => {
const cat = s.category || '未分类';
if (!groups[cat]) groups[cat] = [];
groups[cat].push(s);
});
const container = document.getElementById('snippets-container');
if (Object.keys(groups).length === 0) {
container.innerHTML = '<div class="text-sm text-muted">暂无片段</div>';
return;
}
container.innerHTML = Object.entries(groups).map(([cat, snippets]) => `
<div class="mb-3">
<div class="collapse-header open" onclick="toggleCollapse(this)">
<span class="collapse-arrow">▶</span>
<span class="text-sm font-medium">${escapeHtml(cat)}</span>
</div>
<div class="collapse-content open">
${snippets.map(s => renderSnippetItem(s)).join('')}
</div>
</div>
`).join('');
}
function renderSnippetItem(snippet) {
const isSelected = selectedSnippets.some(s => s.snippetId === snippet.id);
const preview = snippet.content.substring(0, 50) + (snippet.content.length > 50 ? '...' : '');
return `
<label class="checkbox-label mb-1" title="${escapeHtml(snippet.content)}">
<input type="checkbox" ${isSelected ? 'checked' : ''} onchange="toggleSnippet(${snippet.id}, this.checked)">
<span class="text-sm">${escapeHtml(snippet.name)}</span>
<span class="text-xs text-muted truncate" style="max-width:120px">${escapeHtml(preview)}</span>
<span class="tag ${snippet.scope === 'system' ? 'tag-system' : 'tag-personal'}">${snippet.scope === 'system' ? '系统' : '个人'}</span>
</label>
`;
}
function toggleSnippet(snippetId, checked) {
if (checked) {
selectedSnippets.push({ snippetId, sortOrder: selectedSnippets.length });
} else {
selectedSnippets = selectedSnippets.filter(s => s.snippetId !== snippetId);
// Re-order
selectedSnippets.forEach((s, i) => s.sortOrder = i);
}
updatePreview();
}
// 更新预览
function updatePreview() {
const parts = [];
// Project context
const projectName = document.getElementById('project-name').value.trim();
if (projectName) {
parts.push(`## 项目上下文\n\n项目名称: ${projectName}`);
}
// Custom content
const customContent = document.getElementById('custom-content').value.trim();
if (customContent) {
parts.push(`## 用户输入\n\n${customContent}`);
}
// Tag constraints
const tagConstraints = [];
for (const [tagId, optionId] of Object.entries(selectedTags)) {
const tag = allTags.find(t => t.id == tagId);
if (!tag) continue;
const option = tag.options.find(o => o.id == optionId);
if (!option) continue;
tagConstraints.push(`- ${option.constraint_text}`);
}
if (tagConstraints.length > 0) {
parts.push(`## 约束\n\n${tagConstraints.join('\n')}`);
}
// Snippets
if (selectedSnippets.length > 0) {
const snippetParts = selectedSnippets
.sort((a, b) => a.sortOrder - b.sortOrder)
.map(s => {
const snippet = allSnippets.find(sn => sn.id === s.snippetId);
return snippet ? `--- ${snippet.name} ---\n${snippet.content}` : '';
})
.filter(Boolean);
if (snippetParts.length > 0) {
parts.push(`## 参考片段\n\n${snippetParts.join('\n\n')}`);
}
}
// Materials
const validMaterials = materials.filter(m => m.content.trim());
if (validMaterials.length > 0) {
const materialParts = validMaterials.map((m, i) => {
const title = m.title.trim() || `参考材料${i + 1}`;
return `--- ${title} ---\n${m.content}`;
});
parts.push(`## 参考材料\n\n${materialParts.join('\n\n')}`);
}
// Suggestions
const adoptedSuggestions = suggestions.filter(s => s.adopted);
if (adoptedSuggestions.length > 0) {
parts.push(`## 补充建议\n\n${adoptedSuggestions.map(s => s.constraint_text).join('\n')}`);
}
const preview = parts.length > 0 ? parts.join('\n\n') : '选择标签和片段后,这里将实时预览最终的 Prompt...';
document.getElementById('prompt-preview').textContent = preview;
}
// 项目名输入监听 — 加载 Claude Session 列表
function setupProjectNameListener() {
const input = document.getElementById('project-name');
const debouncedLoad = debounce(async () => {
const name = input.value.trim();
const select = document.getElementById('claude-session');
if (!name) {
select.innerHTML = '<option value="">不关联</option>';
return;
}
try {
const sessions = await API.get(`/api/claude/sessions?project_name=${encodeURIComponent(name)}`);
select.innerHTML = '<option value="">不关联</option>';
sessions.forEach(s => {
const shortId = s.session_id.substring(0, 8);
const opt = document.createElement('option');
opt.value = s.session_id;
opt.textContent = `${shortId}... (${s.prompt_count}条, ${formatDate(s.last_time)})`;
select.appendChild(opt);
});
} catch {}
}, 800);
input.addEventListener('input', debouncedLoad);
// Session 选择变化时加载历史 Prompt
document.getElementById('claude-session').addEventListener('change', async (e) => {
const sessionId = e.target.value;
const container = document.getElementById('session-prompts');
const list = document.getElementById('session-prompts-list');
if (!sessionId) {
container.classList.add('hidden');
return;
}
try {
const prompts = await API.get(`/api/claude/sessions/${sessionId}/prompts`);
container.classList.remove('hidden');
list.innerHTML = prompts.map(p => `
<div class="mb-2 p-2" style="background:var(--bg-tertiary);border-radius:6px;cursor:pointer" onclick="this.querySelector('.full-content').classList.toggle('hidden')">
<div class="text-xs text-muted">${formatDate(p.created_at)}</div>
<div class="text-sm mt-1">${escapeHtml(p.prompt.substring(0, 100))}${p.prompt.length > 100 ? '...' : ''}</div>
<div class="full-content hidden mt-2 text-sm" style="white-space:pre-wrap">${escapeHtml(p.prompt)}</div>
</div>
`).join('') || '<div class="text-sm text-muted">暂无历史 Prompt</div>';
} catch {}
});
}
// 标签搜索
document.getElementById('tag-search')?.addEventListener('input', debounce(() => renderTags(), 300));
document.getElementById('snippet-search')?.addEventListener('input', debounce(() => renderSnippets(), 300));
// 参考材料管理
function addMaterial() {
materials.push({ title: '', content: '' });
renderMaterials();
updatePreview();
}
function removeMaterial(index) {
materials.splice(index, 1);
renderMaterials();
updatePreview();
}
function renderMaterials() {
const container = document.getElementById('materials-container');
const emptyHint = document.getElementById('materials-empty');
if (materials.length === 0) {
container.innerHTML = '<div class="text-sm text-muted" id="materials-empty">暂无参考材料</div>';
return;
}
container.innerHTML = materials.map((m, i) => `
<div class="mb-3 p-3" style="background:var(--bg-tertiary);border-radius:8px">
<div class="flex items-center justify-between mb-2">
<input class="input" placeholder="参考材料${i + 1}" value="${escapeHtml(m.title)}"
oninput="updateMaterialTitle(${i}, this.value)" style="flex:1;margin-right:8px">
<button class="btn btn-sm" onclick="removeMaterial(${i})" style="color:var(--error-color)">✕</button>
</div>
<textarea class="textarea" placeholder="输入参考材料内容..." rows="3"
oninput="updateMaterialContent(${i}, this.value)">${escapeHtml(m.content)}</textarea>
</div>
`).join('');
}
function updateMaterialTitle(index, title) {
materials[index].title = title;
updatePreview();
}
function updateMaterialContent(index, content) {
materials[index].content = content;
updatePreview();
}
// 自定义输入变化时更新预览
document.getElementById('custom-content')?.addEventListener('input', debounce(() => updatePreview(), 200));
document.getElementById('project-name')?.addEventListener('input', debounce(() => updatePreview(), 200));
// 折叠切换
function toggleCollapse(header) {
header.classList.toggle('open');
const content = header.nextElementSibling;
content.classList.toggle('open');
}
// 智能建议
async function getSuggestions() {
const prompt = document.getElementById('prompt-preview').textContent;
if (!prompt || prompt.startsWith('选择标签')) {
Toast.error('请先组装 Prompt 内容');
return;
}
const btn = document.getElementById('suggest-btn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> 分析中...';
try {
const claudeMD = document.getElementById('claude-md').value;
const result = await API.post('/api/suggestions', {
current_prompt: prompt,
claude_md: claudeMD
});
suggestions = (result || []).map(s => ({ ...s, adopted: false }));
renderSuggestions();
document.getElementById('suggestions-card').style.display = 'block';
} catch (e) {
Toast.error('获取建议失败: ' + e.message);
} finally {
btn.disabled = false;
btn.innerHTML = '🤖 智能建议';
}
}
function renderSuggestions() {
const list = document.getElementById('suggestions-list');
if (suggestions.length === 0) {
list.innerHTML = '<div class="text-sm text-muted">暂无建议</div>';
return;
}
list.innerHTML = suggestions.map((s, i) => `
<label class="checkbox-label mb-2 p-2" style="background:var(--bg-tertiary);border-radius:6px;align-items:flex-start">
<input type="checkbox" ${s.adopted ? 'checked' : ''} onchange="toggleSuggestion(${i}, this.checked)" style="margin-top:3px">
<div>
<div class="text-sm font-medium">${escapeHtml(s.title)}</div>
<div class="text-xs text-muted mt-1">${escapeHtml(s.description)}</div>
</div>
</label>
`).join('');
}
function toggleSuggestion(index, checked) {
suggestions[index].adopted = checked;
updatePreview();
}
function toggleAllSuggestions(checked) {
suggestions.forEach(s => s.adopted = checked);
renderSuggestions();
updatePreview();
}
// 复制到剪贴板
async function copyPrompt() {
const text = document.getElementById('prompt-preview').textContent;
if (!text || text.startsWith('选择标签')) {
Toast.error('没有可复制的内容');
return;
}
try {
await navigator.clipboard.writeText(text);
Toast.success('已复制到剪贴板');
} catch {
// Fallback
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
textarea.remove();
Toast.success('已复制到剪贴板');
}
}
// 新增标签
function showAddTagModal() {
document.getElementById('add-tag-modal').classList.remove('hidden');
document.getElementById('new-tag-name').focus();
}
function closeAddTagModal() {
document.getElementById('add-tag-modal').classList.add('hidden');
document.getElementById('new-tag-name').value = '';
document.getElementById('new-tag-desc').value = '';
}
async function createTag() {
const name = document.getElementById('new-tag-name').value.trim();
const desc = document.getElementById('new-tag-desc').value.trim();
if (!name) {
Toast.error('标签名称不能为空');
return;
}
try {
await API.post('/api/tags', { name, description: desc });
closeAddTagModal();
Toast.success('标签创建成功');
await loadTags();
} catch (e) {
Toast.error(e.message);
}
}
// 保存会话
function saveSession() {
document.getElementById('save-session-modal').classList.remove('hidden');
document.getElementById('session-title').focus();
}
function closeSaveModal() {
document.getElementById('save-session-modal').classList.add('hidden');
}
async function doSaveSession() {
const title = document.getElementById('session-title').value.trim();
if (!title) {
Toast.error('请输入会话标题');
return;
}
const projectName = document.getElementById('project-name').value.trim();
const claudeSessionId = document.getElementById('claude-session').value;
const finalPrompt = document.getElementById('prompt-preview').textContent;
const tagOptionIds = Object.entries(selectedTags).map(([tagId, optionId]) => ({
tag_id: parseInt(tagId),
tag_option_id: optionId
}));
const snippetIds = selectedSnippets.map(s => ({
snippet_id: s.snippetId,
sort_order: s.sortOrder
}));
const materialsData = materials
.filter(m => m.content.trim())
.map((m, i) => ({
title: m.title.trim(),
content: m.content,
sort_order: i
}));
try {
await API.post('/api/builder/sessions', {
title,
project_name: projectName,
final_prompt: finalPrompt,
claude_session_id: claudeSessionId,
tag_option_ids: tagOptionIds,
snippet_ids: snippetIds,
materials: materialsData
});
closeSaveModal();
Toast.success('会话已保存');
} catch (e) {
Toast.error(e.message);
}
}
// 草稿自动保存
function setupAutoSave() {
const saveDraftDebounced = debounce(() => {
Draft.save('builder', {
projectName: document.getElementById('project-name').value,
customContent: document.getElementById('custom-content').value,
claudeMD: document.getElementById('claude-md').value,
selectedTags: { ...selectedTags },
selectedSnippets: [...selectedSnippets],
materials: materials.map(m => ({ title: m.title, content: m.content })),
});
}, 1000);
document.getElementById('project-name').addEventListener('input', saveDraftDebounced);
document.getElementById('custom-content').addEventListener('input', saveDraftDebounced);
document.getElementById('claude-md').addEventListener('input', saveDraftDebounced);
}
function loadDraft() {
const draft = Draft.load('builder');
if (!draft) return;
document.getElementById('project-name').value = draft.projectName || '';
document.getElementById('custom-content').value = draft.customContent || '';
document.getElementById('claude-md').value = draft.claudeMD || '';
selectedTags = draft.selectedTags || {};
selectedSnippets = draft.selectedSnippets || [];
materials = draft.materials || [];
renderMaterials();
}
function resetBuilder() {
if (!confirm('确定要清空所有内容吗?')) return;
document.getElementById('project-name').value = '';
document.getElementById('custom-content').value = '';
document.getElementById('claude-md').value = '';
document.getElementById('claude-session').innerHTML = '<option value="">不关联</option>';
document.getElementById('session-prompts').classList.add('hidden');
selectedTags = {};
selectedSnippets = [];
materials = [];
suggestions = [];
document.getElementById('suggestions-card').style.display = 'none';
Draft.clear('builder');
renderTags();
renderSnippets();
renderMaterials();
updatePreview();
Toast.success('已重置');
}
// 全局回车键处理
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeAddTagModal();
closeSaveModal();
}
});
init();
</script>
</body>
</html>