From f456cdb5658fad8f06c24ef2fe4a694bd6cedf51 Mon Sep 17 00:00:00 2001 From: wonder Date: Fri, 26 Jun 2026 15:05:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20add=20frontend=20=E2=80=94=20builder,?= =?UTF-8?q?=20dashboard,=20and=20settings=20pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- frontend/dashboard.html | 315 ++++++++++++++++++ frontend/index.html | 656 +++++++++++++++++++++++++++++++++++++ frontend/settings.html | 112 +++++++ frontend/static/common.css | 408 +++++++++++++++++++++++ frontend/static/common.js | 196 +++++++++++ 5 files changed, 1687 insertions(+) create mode 100644 frontend/dashboard.html create mode 100644 frontend/index.html create mode 100644 frontend/settings.html create mode 100644 frontend/static/common.css create mode 100644 frontend/static/common.js diff --git a/frontend/dashboard.html b/frontend/dashboard.html new file mode 100644 index 0000000..0131aad --- /dev/null +++ b/frontend/dashboard.html @@ -0,0 +1,315 @@ + + + + + + Prompt Generator — 复盘看板 + + + + +
+ +
+ +
+
+ + +
+ +
+ + + + +
+ + +
+
+
📋
+
选择项目和会话查看 Prompt
+
在左侧项目树中选择一个会话开始浏览
+
+
+
+
+ + + + + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..eb73e89 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,656 @@ + + + + + + Prompt Generator — 构建器 + + + + +
+ +
+ +
+
+ 📁 项目 / 会话 +
+
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ 🏷️ 标签选择 + +
+
+
+
系统标签
+
+
+
+
+ 个人标签 + +
+
+
+
+
+ + +
+
+ 📄 片段选择 + +
+
+
+ + +
+
+ ▶ + 📋 CLAUDE.md 内容(可选) +
+
+ +
+
+ + +
+
✏️ 自定义输入
+
+ +
+
+
+ + +
+ +
+
+ 📝 最终 Prompt 预览 +
+ + +
+
+
+
选择标签和片段后,这里将实时预览最终的 Prompt...
+
+
+ + + + +
+ + +
+
+
+ + + + + + + + + + + diff --git a/frontend/settings.html b/frontend/settings.html new file mode 100644 index 0000000..192e5e6 --- /dev/null +++ b/frontend/settings.html @@ -0,0 +1,112 @@ + + + + + + Prompt Generator — 设置 + + + + +
+

⚙️ 设置

+ + +
+
🤖 LLM 配置
+
+
+ + +
兼容 OpenAI 接口的 API 地址
+
+
+ + +
留空则不更新。API Key 仅存储在服务端
+
+
+ + +
+ +
+
+ + +
+
🎨 主题设置
+
+
+
+
暗色模式
+
切换亮色/暗色主题
+
+ +
+
+
+ + +
+
🔐 账号
+
+ +
+
+
+ + + + + diff --git a/frontend/static/common.css b/frontend/static/common.css new file mode 100644 index 0000000..f7e7ea5 --- /dev/null +++ b/frontend/static/common.css @@ -0,0 +1,408 @@ +/* Prompt Generator — 全局样式 */ + +/* 暗色主题变量 */ +:root { + --bg-primary: #ffffff; + --bg-secondary: #f8fafc; + --bg-tertiary: #f1f5f9; + --text-primary: #0f172a; + --text-secondary: #475569; + --text-muted: #94a3b8; + --border-color: #e2e8f0; + --accent-color: #3b82f6; + --accent-hover: #2563eb; + --success-color: #22c55e; + --danger-color: #ef4444; + --warning-color: #f59e0b; + --shadow-sm: 0 1px 2px rgba(0,0,0,0.05); + --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1); + --radius: 8px; +} + +[data-theme="dark"] { + --bg-primary: #0f172a; + --bg-secondary: #1e293b; + --bg-tertiary: #334155; + --text-primary: #f1f5f9; + --text-secondary: #94a3b8; + --text-muted: #64748b; + --border-color: #334155; + --accent-color: #60a5fa; + --accent-hover: #3b82f6; + --shadow-sm: 0 1px 2px rgba(0,0,0,0.2); + --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.3); +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background: var(--bg-secondary); + color: var(--text-primary); + min-height: 100vh; +} + +/* 导航栏 */ +.navbar { + background: var(--bg-primary); + border-bottom: 1px solid var(--border-color); + padding: 0 24px; + height: 56px; + display: flex; + align-items: center; + justify-content: space-between; + position: sticky; + top: 0; + z-index: 100; + box-shadow: var(--shadow-sm); +} + +.nav-brand { + font-size: 18px; + font-weight: 700; + color: var(--accent-color); + display: flex; + align-items: center; + gap: 8px; +} + +.nav-tabs { + display: flex; + gap: 4px; +} + +.nav-tab { + padding: 8px 16px; + border-radius: 6px; + color: var(--text-secondary); + text-decoration: none; + font-size: 14px; + font-weight: 500; + transition: all 0.15s; + cursor: pointer; +} + +.nav-tab:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.nav-tab.active { + background: var(--accent-color); + color: white; +} + +.nav-actions { + display: flex; + align-items: center; + gap: 12px; +} + +/* 按钮 */ +.btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + border: 1px solid var(--border-color); + background: var(--bg-primary); + color: var(--text-primary); + transition: all 0.15s; + white-space: nowrap; +} + +.btn:hover { + background: var(--bg-tertiary); +} + +.btn-primary { + background: var(--accent-color); + color: white; + border-color: var(--accent-color); +} + +.btn-primary:hover { + background: var(--accent-hover); +} + +.btn-danger { + background: var(--danger-color); + color: white; + border-color: var(--danger-color); +} + +.btn-danger:hover { + opacity: 0.9; +} + +.btn-sm { + padding: 4px 10px; + font-size: 12px; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* 输入框 */ +.input, .textarea, .select { + padding: 8px 12px; + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 14px; + background: var(--bg-primary); + color: var(--text-primary); + width: 100%; + transition: border-color 0.15s; +} + +.input:focus, .textarea:focus, .select:focus { + outline: none; + border-color: var(--accent-color); + box-shadow: 0 0 0 3px rgba(59,130,246,0.1); +} + +.textarea { + resize: vertical; + min-height: 80px; + font-family: inherit; +} + +/* 卡片 */ +.card { + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: var(--radius); + box-shadow: var(--shadow-sm); +} + +.card-header { + padding: 12px 16px; + border-bottom: 1px solid var(--border-color); + font-weight: 600; + font-size: 14px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.card-body { + padding: 16px; +} + +/* 复选框和单选框 */ +.checkbox-group, .radio-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.checkbox-label, .radio-label { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + font-size: 14px; + color: var(--text-primary); +} + +.checkbox-label input, .radio-label input { + accent-color: var(--accent-color); + width: 16px; + height: 16px; +} + +/* 可折叠区域 */ +.collapse-header { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + cursor: pointer; + user-select: none; + border-radius: 6px; + transition: background 0.15s; +} + +.collapse-header:hover { + background: var(--bg-tertiary); +} + +.collapse-arrow { + transition: transform 0.2s; + font-size: 12px; + color: var(--text-muted); +} + +.collapse-header.open .collapse-arrow { + transform: rotate(90deg); +} + +.collapse-content { + display: none; + padding: 8px 12px 8px 32px; +} + +.collapse-content.open { + display: block; +} + +/* 标签 */ +.tag { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; +} + +.tag-system { + background: #dbeafe; + color: #1d4ed8; +} + +.tag-personal { + background: #dcfce7; + color: #15803d; +} + +[data-theme="dark"] .tag-system { + background: #1e3a5f; + color: #93c5fd; +} + +[data-theme="dark"] .tag-personal { + background: #14532d; + color: #86efac; +} + +/* 加载状态 */ +.spinner { + display: inline-block; + width: 20px; + height: 20px; + border: 2px solid var(--border-color); + border-top-color: var(--accent-color); + border-radius: 50%; + animation: spin 0.6s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* 消息提示 */ +.toast { + position: fixed; + top: 72px; + right: 24px; + padding: 12px 20px; + border-radius: 8px; + font-size: 14px; + z-index: 1000; + animation: slideIn 0.3s ease; + box-shadow: var(--shadow-md); +} + +.toast-success { + background: var(--success-color); + color: white; +} + +.toast-error { + background: var(--danger-color); + color: white; +} + +@keyframes slideIn { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +/* 模态框 */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 200; +} + +.modal { + background: var(--bg-primary); + border-radius: 12px; + box-shadow: var(--shadow-md); + width: 100%; + max-width: 480px; + padding: 24px; +} + +.modal-title { + font-size: 18px; + font-weight: 600; + margin-bottom: 16px; +} + +/* 分页 */ +.pagination { + display: flex; + align-items: center; + gap: 8px; + justify-content: center; + padding: 16px 0; +} + +/* 搜索框 */ +.search-box { + position: relative; +} + +.search-box .input { + padding-left: 36px; +} + +.search-icon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: var(--text-muted); + font-size: 14px; +} + +/* 工具类 */ +.flex { display: flex; } +.flex-col { flex-direction: column; } +.items-center { align-items: center; } +.justify-between { justify-content: space-between; } +.gap-2 { gap: 8px; } +.gap-3 { gap: 12px; } +.gap-4 { gap: 16px; } +.p-4 { padding: 16px; } +.p-6 { padding: 24px; } +.mb-2 { margin-bottom: 8px; } +.mb-3 { margin-bottom: 12px; } +.mb-4 { margin-bottom: 16px; } +.mt-2 { margin-top: 8px; } +.mt-4 { margin-top: 16px; } +.text-sm { font-size: 13px; } +.text-xs { font-size: 12px; } +.text-muted { color: var(--text-muted); } +.font-medium { font-weight: 500; } +.font-bold { font-weight: 700; } +.w-full { width: 100%; } +.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.hidden { display: none !important; } diff --git a/frontend/static/common.js b/frontend/static/common.js new file mode 100644 index 0000000..2554042 --- /dev/null +++ b/frontend/static/common.js @@ -0,0 +1,196 @@ +// Prompt Generator — 全局 JS 模块 + +const API = { + async request(method, path, body) { + const opts = { + method, + headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + }; + if (body !== undefined) { + opts.body = JSON.stringify(body); + } + const resp = await fetch(path, opts); + const data = await resp.json(); + if (resp.status === 401) { + Auth.showLoginModal(); + throw new Error('未登录'); + } + if (data.code !== 0) { + throw new Error(data.message || '请求失败'); + } + return data.data; + }, + get(path) { return this.request('GET', path); }, + post(path, body) { return this.request('POST', path, body); }, + put(path, body) { return this.request('PUT', path, body); }, + del(path) { return this.request('DELETE', path); }, +}; + +// 认证模块 +const Auth = { + modal: null, + + showLoginModal() { + if (this.modal) return; + const overlay = document.createElement('div'); + overlay.className = 'modal-overlay'; + overlay.innerHTML = ` + + `; + document.body.appendChild(overlay); + this.modal = overlay; + + const input = overlay.querySelector('#login-password'); + const btn = overlay.querySelector('#login-btn'); + const err = overlay.querySelector('#login-error'); + + const doLogin = async () => { + try { + btn.disabled = true; + btn.textContent = '登录中...'; + await API.post('/api/auth/login', { password: input.value }); + overlay.remove(); + this.modal = null; + Toast.success('登录成功'); + // Reload page data + if (typeof onLogin === 'function') onLogin(); + } catch (e) { + err.textContent = e.message; + err.style.display = 'block'; + btn.disabled = false; + btn.textContent = '登录'; + } + }; + + btn.onclick = doLogin; + input.onkeydown = (e) => { if (e.key === 'Enter') doLogin(); }; + }, + + async check() { + try { + await API.get('/api/auth/check'); + return true; + } catch { + return false; + } + }, + + async logout() { + try { + await API.post('/api/auth/logout'); + } catch {} + location.reload(); + } +}; + +// Toast 提示 +const Toast = { + show(message, type = 'success', duration = 3000) { + const el = document.createElement('div'); + el.className = `toast toast-${type}`; + el.textContent = message; + document.body.appendChild(el); + setTimeout(() => el.remove(), duration); + }, + success(msg) { this.show(msg, 'success'); }, + error(msg) { this.show(msg, 'error', 5000); }, +}; + +// 主题切换 +const Theme = { + init() { + const saved = localStorage.getItem('theme'); + if (saved) { + document.documentElement.setAttribute('data-theme', saved); + } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) { + document.documentElement.setAttribute('data-theme', 'dark'); + } + }, + toggle() { + const current = document.documentElement.getAttribute('data-theme'); + const next = current === 'dark' ? 'light' : 'dark'; + document.documentElement.setAttribute('data-theme', next); + localStorage.setItem('theme', next); + }, + isDark() { + return document.documentElement.getAttribute('data-theme') === 'dark'; + } +}; + +// 草稿管理(localStorage) +const Draft = { + save(key, data) { + localStorage.setItem(`draft_${key}`, JSON.stringify(data)); + }, + load(key) { + const raw = localStorage.getItem(`draft_${key}`); + if (!raw) return null; + try { return JSON.parse(raw); } catch { return null; } + }, + clear(key) { + localStorage.removeItem(`draft_${key}`); + } +}; + +// 防抖 +function debounce(fn, delay = 1000) { + let timer; + return function (...args) { + clearTimeout(timer); + timer = setTimeout(() => fn.apply(this, args), delay); + }; +} + +// 工具函数 +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +function truncateText(text, maxLen = 150) { + if (!text) return ''; + // 跳过 XML 标签内容,提取用户实际输入 + const cleaned = text.replace(/<[a-zA-Z_-][^>]*>[\s\S]*?<\/[a-zA-Z_-][^>]*>/g, '').trim(); + if (cleaned.length <= maxLen) return cleaned; + return cleaned.substring(0, maxLen) + '...'; +} + +function formatDate(dateStr) { + if (!dateStr) return ''; + const d = new Date(dateStr); + const pad = n => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; +} + +// 初始化 +Theme.init(); + +// 导航栏渲染 +function renderNavbar(activePage) { + const pages = [ + { id: 'builder', label: '构建器', href: '/' }, + { id: 'dashboard', label: '复盘看板', href: '/dashboard.html' }, + { id: 'settings', label: '设置', href: '/settings.html' }, + ]; + + const nav = document.createElement('nav'); + nav.className = 'navbar'; + nav.innerHTML = ` + + + + `; + document.body.insertBefore(nav, document.body.firstChild); +}