657 lines
29 KiB
HTML
657 lines
29 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" style="height: calc(100vh - 56px); margin-top: 56px;">
|
|
<!-- 左侧面板 -->
|
|
<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>
|
|
|
|
<!-- 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 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' : ''} onchange="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) {
|
|
selectedTags[tagId] = optionId;
|
|
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')}`);
|
|
}
|
|
}
|
|
|
|
// 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));
|
|
|
|
// 自定义输入变化时更新预览
|
|
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
|
|
}));
|
|
|
|
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
|
|
});
|
|
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],
|
|
});
|
|
}, 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 || [];
|
|
}
|
|
|
|
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 = [];
|
|
suggestions = [];
|
|
document.getElementById('suggestions-card').style.display = 'none';
|
|
Draft.clear('builder');
|
|
renderTags();
|
|
renderSnippets();
|
|
updatePreview();
|
|
Toast.success('已重置');
|
|
}
|
|
|
|
// 全局回车键处理
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
closeAddTagModal();
|
|
closeSaveModal();
|
|
}
|
|
});
|
|
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|