From 173d857dbb0f7ad8acd6a3a83a2ea66e1f6cd16f Mon Sep 17 00:00:00 2001 From: wonder Date: Fri, 26 Jun 2026 15:14:10 +0800 Subject: [PATCH] feat: structure view in dashboard and settings persistence - Add GET /api/claude/sessions/{session_id}/builder API for structure view - Load builder session tags and snippets by Claude session ID - Implement structure view in dashboard: shows tag selections and snippet details - Add LoadSettings to persist LLM config changes across restarts - Settings are now loaded from database on startup Co-Authored-By: Claude --- frontend/dashboard.html | 57 ++++++++++++++++++++++++++-- internal/db/db.go | 30 +++++++++++++++ internal/handlers/claude.go | 76 +++++++++++++++++++++++++++++++++++++ main.go | 4 ++ 4 files changed, 164 insertions(+), 3 deletions(-) diff --git a/frontend/dashboard.html b/frontend/dashboard.html index 2a00e07..13f600b 100644 --- a/frontend/dashboard.html +++ b/frontend/dashboard.html @@ -162,7 +162,6 @@ } function renderPromptCard(prompt, index) { - const preview = truncateText(prompt.prompt, 150); return `
@@ -176,7 +175,7 @@ ${renderPromptContent(prompt.prompt)}
@@ -227,7 +226,7 @@ return parts.length > 0 ? parts.join('') : `
${escapeHtml(text)}
`; } - function togglePromptView(index) { + async function togglePromptView(index) { const textView = document.getElementById(`prompt-text-${index}`); const structView = document.getElementById(`prompt-struct-${index}`); const btn = document.getElementById(`view-btn-${index}`); @@ -240,6 +239,58 @@ textView.classList.add('hidden'); structView.classList.remove('hidden'); btn.textContent = '切换到文本视图'; + // Load structure view data + await loadStructureView(index); + } + } + + async function loadStructureView(index) { + const container = document.getElementById(`struct-content-${index}`); + if (container.dataset.loaded) return; + + try { + const data = await API.get(`/api/claude/sessions/${currentSession}/builder`); + if (!data || !data.session) { + container.innerHTML = '
未找到关联的构建会话
'; + container.dataset.loaded = 'true'; + return; + } + + let html = ''; + + // Tags + if (data.tags && data.tags.length > 0) { + html += '
🏷️ 标签选择
'; + html += data.tags.map(t => ` +
+
${escapeHtml(t.tag_name)}
+
选项: ${escapeHtml(t.option_label)}
+
${escapeHtml(t.constraint_text)}
+
+ `).join(''); + html += '
'; + } + + // Snippets + if (data.snippets && data.snippets.length > 0) { + html += '
📄 片段
'; + html += data.snippets.map(s => ` +
+
${escapeHtml(s.name)} (${escapeHtml(s.category)})
+
${escapeHtml(s.content.substring(0, 200))}${s.content.length > 200 ? '...' : ''}
+
+ `).join(''); + html += '
'; + } + + if (!html) { + html = '
无标签和片段数据
'; + } + + container.innerHTML = html; + container.dataset.loaded = 'true'; + } catch (e) { + container.innerHTML = '
加载结构视图失败
'; } } diff --git a/internal/db/db.go b/internal/db/db.go index 5fafec4..9f1f298 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -109,6 +109,36 @@ func AutoMigrate() error { return nil } +// LoadSettings loads persisted settings from the database into config +func LoadSettings(cfg *config.Config) { + // Create settings table if not exists + DB.Exec(`CREATE TABLE IF NOT EXISTS settings ( + key_name varchar(64) NOT NULL PRIMARY KEY, + value text NOT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`) + + rows, err := DB.Query("SELECT key_name, value FROM settings") + if err != nil { + return + } + defer rows.Close() + + for rows.Next() { + var key, value string + if err := rows.Scan(&key, &value); err != nil { + continue + } + switch key { + case "llm_api_base_url": + cfg.LLMAPIBaseURL = value + case "llm_api_key": + cfg.LLMAPIKey = value + case "llm_model_name": + cfg.LLMModelName = value + } + } +} + // EnsurePromptsTable creates the prompts table if it doesn't exist (for dev environments) func EnsurePromptsTable() { _, err := DB.Exec(`CREATE TABLE IF NOT EXISTS prompts ( diff --git a/internal/handlers/claude.go b/internal/handlers/claude.go index 8e3ee0c..4273e71 100644 --- a/internal/handlers/claude.go +++ b/internal/handlers/claude.go @@ -4,6 +4,7 @@ import ( "net/http" "prompt-generator/internal/db" + "prompt-generator/internal/models" ) func GetClaudeSessions(w http.ResponseWriter, r *http.Request) { @@ -47,6 +48,81 @@ func GetClaudeSessions(w http.ResponseWriter, r *http.Request) { success(w, sessions) } +// GetBuilderSessionByClaudeSession looks up a builder session by claude_session_id +func GetBuilderSessionByClaudeSession(w http.ResponseWriter, r *http.Request) { + sessionID := r.PathValue("session_id") + if sessionID == "" { + fail(w, 400, "session_id 必填") + return + } + + var s models.BuilderSession + err := db.DB.QueryRow("SELECT id, title, project_name, final_prompt, claude_session_id, created_at, updated_at FROM builder_sessions WHERE claude_session_id=?", sessionID). + Scan(&s.ID, &s.Title, &s.ProjectName, &s.FinalPrompt, &s.ClaudeSessionID, &s.CreatedAt, &s.UpdatedAt) + if err != nil { + // Not found is not an error + success(w, nil) + return + } + + // Load tags with details + tags := []map[string]interface{}{} + tagRows, err := db.DB.Query(` + SELECT bst.tag_id, bst.tag_option_id, t.name, to2.label, to2.constraint_text + FROM builder_session_tags bst + JOIN tags t ON t.id = bst.tag_id + JOIN tag_options to2 ON to2.id = bst.tag_option_id + WHERE bst.builder_session_id=?`, s.ID) + if err == nil { + for tagRows.Next() { + var tagID, optionID int64 + var tagName, optionLabel, constraintText string + if err := tagRows.Scan(&tagID, &optionID, &tagName, &optionLabel, &constraintText); err == nil { + tags = append(tags, map[string]interface{}{ + "tag_id": tagID, + "tag_option_id": optionID, + "tag_name": tagName, + "option_label": optionLabel, + "constraint_text": constraintText, + }) + } + } + tagRows.Close() + } + + // Load snippets with details + snippets := []map[string]interface{}{} + snippetRows, err := db.DB.Query(` + SELECT bss.snippet_id, s.name, s.content, s.category, bss.sort_order + FROM builder_session_snippets bss + JOIN snippets s ON s.id = bss.snippet_id + WHERE bss.builder_session_id=? + ORDER BY bss.sort_order`, s.ID) + if err == nil { + for snippetRows.Next() { + var snippetID int64 + var name, content, category string + var sortOrder int + if err := snippetRows.Scan(&snippetID, &name, &content, &category, &sortOrder); err == nil { + snippets = append(snippets, map[string]interface{}{ + "snippet_id": snippetID, + "name": name, + "content": content, + "category": category, + "sort_order": sortOrder, + }) + } + } + snippetRows.Close() + } + + success(w, map[string]interface{}{ + "session": s, + "tags": tags, + "snippets": snippets, + }) +} + func GetClaudePrompts(w http.ResponseWriter, r *http.Request) { sessionID := r.PathValue("session_id") if sessionID == "" { diff --git a/main.go b/main.go index 367f3e7..c86548b 100644 --- a/main.go +++ b/main.go @@ -41,6 +41,9 @@ func main() { // Ensure prompts table exists for dev environments db.EnsurePromptsTable() + // Load persisted settings from database + db.LoadSettings(cfg) + // Setup routes mux := http.NewServeMux() @@ -89,6 +92,7 @@ func main() { apiMux.HandleFunc("GET /api/claude/sessions", handlers.GetClaudeSessions) apiMux.HandleFunc("GET /api/claude/sessions/{session_id}/prompts", handlers.GetClaudePrompts) + apiMux.HandleFunc("GET /api/claude/sessions/{session_id}/builder", handlers.GetBuilderSessionByClaudeSession) apiMux.HandleFunc("GET /api/dashboard/projects", handlers.GetDashboardProjects) apiMux.HandleFunc("GET /api/dashboard/projects/{name}/sessions", handlers.GetDashboardSessions)