feat: add frontend — builder, dashboard, and settings pages
- common.css: 全局样式,支持亮色/暗色主题,完整组件库 - common.js: API 封装、认证模块、Toast 提示、主题切换、草稿管理 - index.html: Prompt 构建器完整 UI - 左侧面板:项目/会话选择、标签系统(搜索/折叠/单选)、片段选择(分组/搜索)、CLAUDE.md 输入、自定义内容 - 右侧面板:实时 Prompt 预览、智能建议列表、复制到剪贴板 - 草稿自动保存(localStorage,防抖 1s) - 新增个人标签模态框 - 保存构建会话功能 - dashboard.html: 复盘看板 - 左侧项目树(项目→会话层级) - 右侧 Prompt 列表(文本视图,XML 标签折叠) - 导出功能(Markdown/JSON/纯文本) - 关键词搜索 - settings.html: 设置页 - LLM 配置(API URL/Key/模型) - 主题切换 - 退出登录 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
<!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-64 border-r overflow-y-auto p-4" style="border-color: var(--border-color); background: var(--bg-primary);">
|
||||
<div class="search-box mb-4">
|
||||
<span class="search-icon">🔍</span>
|
||||
<input class="input" id="tree-search" placeholder="搜索项目..." style="font-size:12px;padding:6px 8px 6px 30px">
|
||||
</div>
|
||||
<div id="project-tree"></div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<div class="flex-1 overflow-y-auto p-4" style="background: var(--bg-secondary);">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="flex gap-3 mb-4">
|
||||
<div class="search-box flex-1">
|
||||
<span class="search-icon">🔍</span>
|
||||
<input class="input" id="prompt-search" placeholder="搜索 Prompt 内容..." style="padding-left:36px">
|
||||
</div>
|
||||
<button class="btn" onclick="exportPrompts('markdown')">📥 导出 Markdown</button>
|
||||
<button class="btn" onclick="exportPrompts('json')">📥 导出 JSON</button>
|
||||
<button class="btn" onclick="exportPrompts('text')">📥 导出文本</button>
|
||||
</div>
|
||||
|
||||
<!-- 内容展示区 -->
|
||||
<div id="content-area">
|
||||
<div class="text-center text-muted p-6" style="padding-top:120px">
|
||||
<div style="font-size:48px;margin-bottom:16px">📋</div>
|
||||
<div class="text-lg font-medium mb-2">选择项目和会话查看 Prompt</div>
|
||||
<div class="text-sm">在左侧项目树中选择一个会话开始浏览</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/common.js"></script>
|
||||
<script>
|
||||
let currentProject = '';
|
||||
let currentSession = '';
|
||||
let currentPrompts = [];
|
||||
let projects = [];
|
||||
|
||||
async function init() {
|
||||
const ok = await Auth.check();
|
||||
if (!ok) {
|
||||
Auth.showLoginModal();
|
||||
return;
|
||||
}
|
||||
renderNavbar('dashboard');
|
||||
await loadProjects();
|
||||
setupSearch();
|
||||
}
|
||||
|
||||
async function loadProjects() {
|
||||
try {
|
||||
projects = await API.get('/api/dashboard/projects');
|
||||
renderProjectTree();
|
||||
} catch (e) {
|
||||
Toast.error('加载项目列表失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function renderProjectTree() {
|
||||
const keyword = document.getElementById('tree-search').value.toLowerCase();
|
||||
const filtered = projects.filter(p => p.project_name.toLowerCase().includes(keyword));
|
||||
const tree = document.getElementById('project-tree');
|
||||
|
||||
if (filtered.length === 0) {
|
||||
tree.innerHTML = '<div class="text-sm text-muted">暂无项目</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
tree.innerHTML = filtered.map(p => `
|
||||
<div class="mb-2">
|
||||
<div class="collapse-header ${currentProject === p.project_name ? 'open' : ''}"
|
||||
onclick="toggleProject(this, '${escapeHtml(p.project_name)}')">
|
||||
<span class="collapse-arrow">▶</span>
|
||||
<span class="text-sm font-medium">${escapeHtml(p.project_name)}</span>
|
||||
<span class="text-xs text-muted ml-auto">${p.session_count}</span>
|
||||
</div>
|
||||
<div class="collapse-content ${currentProject === p.project_name ? 'open' : ''}"
|
||||
id="project-${escapeHtml(p.project_name).replace(/[^a-zA-Z0-9]/g, '_')}">
|
||||
<div class="sessions-container text-sm text-muted">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function toggleProject(header, projectName) {
|
||||
header.classList.toggle('open');
|
||||
const content = header.nextElementSibling;
|
||||
content.classList.toggle('open');
|
||||
|
||||
if (content.classList.contains('open')) {
|
||||
currentProject = projectName;
|
||||
await loadSessions(projectName, content.querySelector('.sessions-container'));
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSessions(projectName, container) {
|
||||
try {
|
||||
const sessions = await API.get(`/api/dashboard/projects/${encodeURIComponent(projectName)}/sessions`);
|
||||
if (sessions.length === 0) {
|
||||
container.innerHTML = '<div class="text-sm text-muted p-2">暂无会话</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = sessions.map(s => `
|
||||
<div class="p-2 rounded cursor-pointer mb-1 ${currentSession === s.session_id ? 'bg-blue-50' : ''}"
|
||||
style="${currentSession === s.session_id ? 'background:var(--bg-tertiary)' : ''}"
|
||||
onclick="selectSession('${escapeHtml(s.session_id)}', '${escapeHtml(projectName)}')">
|
||||
<div class="text-xs font-medium truncate">${s.session_id.substring(0, 12)}...</div>
|
||||
<div class="text-xs text-muted">${s.prompt_count} 条 · ${formatDate(s.last_time)}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (e) {
|
||||
container.innerHTML = '<div class="text-sm text-muted p-2">加载失败</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function selectSession(sessionId, projectName) {
|
||||
currentSession = sessionId;
|
||||
currentProject = projectName;
|
||||
renderProjectTree(); // Re-render to highlight
|
||||
|
||||
try {
|
||||
const data = await API.get(`/api/dashboard/prompts?session_id=${encodeURIComponent(sessionId)}&page_size=50`);
|
||||
currentPrompts = data.prompts || [];
|
||||
renderPrompts();
|
||||
} catch (e) {
|
||||
Toast.error('加载 Prompt 失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPrompts() {
|
||||
const area = document.getElementById('content-area');
|
||||
if (currentPrompts.length === 0) {
|
||||
area.innerHTML = '<div class="text-center text-muted p-6">暂无 Prompt 记录</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
area.innerHTML = `
|
||||
<div class="mb-3 text-sm text-muted">
|
||||
会话: ${currentSession.substring(0, 12)}... · 共 ${currentPrompts.length} 条 Prompt
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
${currentPrompts.map((p, i) => renderPromptCard(p, i)).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderPromptCard(prompt, index) {
|
||||
const preview = truncateText(prompt.prompt, 150);
|
||||
return `
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="text-sm">#${index + 1} · ${formatDate(prompt.created_at)}</span>
|
||||
<button class="btn btn-sm" onclick="togglePromptView(${index})">
|
||||
<span id="view-btn-${index}">切换到结构视图</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="prompt-text-${index}" class="prompt-text-view">
|
||||
${renderPromptContent(prompt.prompt)}
|
||||
</div>
|
||||
<div id="prompt-struct-${index}" class="prompt-struct-view hidden">
|
||||
<div class="text-sm text-muted">结构视图暂不可用(需要关联构建会话数据)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderPromptContent(text) {
|
||||
if (!text) return '<div class="text-sm text-muted">空内容</div>';
|
||||
|
||||
// 折叠 XML 标签
|
||||
const parts = [];
|
||||
let lastIndex = 0;
|
||||
const xmlRegex = /<([a-zA-Z_-][a-zA-Z0-9_-]*)[^>]*>[\s\S]*?<\/\1>/g;
|
||||
let match;
|
||||
|
||||
while ((match = xmlRegex.exec(text)) !== null) {
|
||||
// Add text before this tag
|
||||
if (match.index > lastIndex) {
|
||||
const before = text.substring(lastIndex, match.index);
|
||||
if (before.trim()) {
|
||||
parts.push(`<div class="mb-2" style="white-space:pre-wrap">${escapeHtml(before)}</div>`);
|
||||
}
|
||||
}
|
||||
|
||||
// Add collapsible tag
|
||||
const tagName = match[1];
|
||||
parts.push(`
|
||||
<div class="mb-2" style="border:1px solid var(--border-color);border-radius:6px;overflow:hidden">
|
||||
<div class="collapse-header" onclick="toggleCollapse(this)" style="background:var(--bg-tertiary);padding:6px 12px">
|
||||
<span class="collapse-arrow">▶</span>
|
||||
<span class="text-xs font-medium text-muted"><${escapeHtml(tagName)}></span>
|
||||
</div>
|
||||
<div class="collapse-content" style="padding:8px 12px;font-size:12px;white-space:pre-wrap;max-height:300px;overflow-y:auto">${escapeHtml(match[0])}</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
|
||||
// Add remaining text
|
||||
if (lastIndex < text.length) {
|
||||
const remaining = text.substring(lastIndex);
|
||||
if (remaining.trim()) {
|
||||
parts.push(`<div style="white-space:pre-wrap">${escapeHtml(remaining)}</div>`);
|
||||
}
|
||||
}
|
||||
|
||||
return parts.length > 0 ? parts.join('') : `<div style="white-space:pre-wrap">${escapeHtml(text)}</div>`;
|
||||
}
|
||||
|
||||
function togglePromptView(index) {
|
||||
const textView = document.getElementById(`prompt-text-${index}`);
|
||||
const structView = document.getElementById(`prompt-struct-${index}`);
|
||||
const btn = document.getElementById(`view-btn-${index}`);
|
||||
|
||||
if (textView.classList.contains('hidden')) {
|
||||
textView.classList.remove('hidden');
|
||||
structView.classList.add('hidden');
|
||||
btn.textContent = '切换到结构视图';
|
||||
} else {
|
||||
textView.classList.add('hidden');
|
||||
structView.classList.remove('hidden');
|
||||
btn.textContent = '切换到文本视图';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCollapse(header) {
|
||||
header.classList.toggle('open');
|
||||
const content = header.nextElementSibling;
|
||||
if (content) content.classList.toggle('open');
|
||||
}
|
||||
|
||||
// 导出
|
||||
function exportPrompts(format) {
|
||||
if (currentPrompts.length === 0) {
|
||||
Toast.error('没有可导出的内容');
|
||||
return;
|
||||
}
|
||||
|
||||
let content, filename, mimeType;
|
||||
|
||||
switch (format) {
|
||||
case 'markdown':
|
||||
content = `# Prompt 导出\n\n项目: ${currentProject}\n会话: ${currentSession}\n\n---\n\n`;
|
||||
content += currentPrompts.map((p, i) =>
|
||||
`## Prompt #${i + 1} (${formatDate(p.created_at)})\n\n${p.prompt}\n\n---\n`
|
||||
).join('\n');
|
||||
filename = `prompts-${currentSession.substring(0, 8)}.md`;
|
||||
mimeType = 'text/markdown';
|
||||
break;
|
||||
|
||||
case 'json':
|
||||
content = JSON.stringify(currentPrompts, null, 2);
|
||||
filename = `prompts-${currentSession.substring(0, 8)}.json`;
|
||||
mimeType = 'application/json';
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
content = currentPrompts.map((p, i) =>
|
||||
`[${i + 1}] ${formatDate(p.created_at)}\n${p.prompt}\n`
|
||||
).join('\n---\n\n');
|
||||
filename = `prompts-${currentSession.substring(0, 8)}.txt`;
|
||||
mimeType = 'text/plain';
|
||||
break;
|
||||
}
|
||||
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
Toast.success('导出成功');
|
||||
}
|
||||
|
||||
// 搜索
|
||||
function setupSearch() {
|
||||
const searchInput = document.getElementById('prompt-search');
|
||||
const debouncedSearch = debounce(async () => {
|
||||
if (!currentSession) return;
|
||||
const keyword = searchInput.value.trim();
|
||||
try {
|
||||
const data = await API.get(`/api/dashboard/prompts?session_id=${encodeURIComponent(currentSession)}&keyword=${encodeURIComponent(keyword)}&page_size=50`);
|
||||
currentPrompts = data.prompts || [];
|
||||
renderPrompts();
|
||||
} catch (e) {
|
||||
Toast.error('搜索失败: ' + e.message);
|
||||
}
|
||||
}, 500);
|
||||
searchInput.addEventListener('input', debouncedSearch);
|
||||
|
||||
document.getElementById('tree-search').addEventListener('input', debounce(() => renderProjectTree(), 300));
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user