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 <noreply@anthropic.com>
This commit is contained in:
+54
-3
@@ -162,7 +162,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderPromptCard(prompt, index) {
|
function renderPromptCard(prompt, index) {
|
||||||
const preview = truncateText(prompt.prompt, 150);
|
|
||||||
return `
|
return `
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@@ -176,7 +175,7 @@
|
|||||||
${renderPromptContent(prompt.prompt)}
|
${renderPromptContent(prompt.prompt)}
|
||||||
</div>
|
</div>
|
||||||
<div id="prompt-struct-${index}" class="prompt-struct-view hidden">
|
<div id="prompt-struct-${index}" class="prompt-struct-view hidden">
|
||||||
<div class="text-sm text-muted">结构视图暂不可用(需要关联构建会话数据)</div>
|
<div class="text-sm text-muted" id="struct-content-${index}">加载中...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -227,7 +226,7 @@
|
|||||||
return parts.length > 0 ? parts.join('') : `<div style="white-space:pre-wrap">${escapeHtml(text)}</div>`;
|
return parts.length > 0 ? parts.join('') : `<div style="white-space:pre-wrap">${escapeHtml(text)}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function togglePromptView(index) {
|
async function togglePromptView(index) {
|
||||||
const textView = document.getElementById(`prompt-text-${index}`);
|
const textView = document.getElementById(`prompt-text-${index}`);
|
||||||
const structView = document.getElementById(`prompt-struct-${index}`);
|
const structView = document.getElementById(`prompt-struct-${index}`);
|
||||||
const btn = document.getElementById(`view-btn-${index}`);
|
const btn = document.getElementById(`view-btn-${index}`);
|
||||||
@@ -240,6 +239,58 @@
|
|||||||
textView.classList.add('hidden');
|
textView.classList.add('hidden');
|
||||||
structView.classList.remove('hidden');
|
structView.classList.remove('hidden');
|
||||||
btn.textContent = '切换到文本视图';
|
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 = '<div class="text-sm text-muted">未找到关联的构建会话</div>';
|
||||||
|
container.dataset.loaded = 'true';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
if (data.tags && data.tags.length > 0) {
|
||||||
|
html += '<div class="mb-3"><div class="text-sm font-medium mb-2">🏷️ 标签选择</div>';
|
||||||
|
html += data.tags.map(t => `
|
||||||
|
<div class="mb-2 p-2" style="background:var(--bg-tertiary);border-radius:6px">
|
||||||
|
<div class="text-sm font-medium">${escapeHtml(t.tag_name)}</div>
|
||||||
|
<div class="text-xs text-muted mt-1">选项: ${escapeHtml(t.option_label)}</div>
|
||||||
|
<div class="text-xs mt-1" style="color:var(--text-secondary)">${escapeHtml(t.constraint_text)}</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snippets
|
||||||
|
if (data.snippets && data.snippets.length > 0) {
|
||||||
|
html += '<div class="mb-3"><div class="text-sm font-medium mb-2">📄 片段</div>';
|
||||||
|
html += data.snippets.map(s => `
|
||||||
|
<div class="mb-2 p-2" style="background:var(--bg-tertiary);border-radius:6px">
|
||||||
|
<div class="text-sm font-medium">${escapeHtml(s.name)} <span class="text-xs text-muted">(${escapeHtml(s.category)})</span></div>
|
||||||
|
<div class="text-xs mt-1" style="color:var(--text-secondary);white-space:pre-wrap">${escapeHtml(s.content.substring(0, 200))}${s.content.length > 200 ? '...' : ''}</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!html) {
|
||||||
|
html = '<div class="text-sm text-muted">无标签和片段数据</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
container.dataset.loaded = 'true';
|
||||||
|
} catch (e) {
|
||||||
|
container.innerHTML = '<div class="text-sm text-muted">加载结构视图失败</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,36 @@ func AutoMigrate() error {
|
|||||||
return nil
|
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)
|
// EnsurePromptsTable creates the prompts table if it doesn't exist (for dev environments)
|
||||||
func EnsurePromptsTable() {
|
func EnsurePromptsTable() {
|
||||||
_, err := DB.Exec(`CREATE TABLE IF NOT EXISTS prompts (
|
_, err := DB.Exec(`CREATE TABLE IF NOT EXISTS prompts (
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"prompt-generator/internal/db"
|
"prompt-generator/internal/db"
|
||||||
|
"prompt-generator/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetClaudeSessions(w http.ResponseWriter, r *http.Request) {
|
func GetClaudeSessions(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -47,6 +48,81 @@ func GetClaudeSessions(w http.ResponseWriter, r *http.Request) {
|
|||||||
success(w, sessions)
|
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) {
|
func GetClaudePrompts(w http.ResponseWriter, r *http.Request) {
|
||||||
sessionID := r.PathValue("session_id")
|
sessionID := r.PathValue("session_id")
|
||||||
if sessionID == "" {
|
if sessionID == "" {
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ func main() {
|
|||||||
// Ensure prompts table exists for dev environments
|
// Ensure prompts table exists for dev environments
|
||||||
db.EnsurePromptsTable()
|
db.EnsurePromptsTable()
|
||||||
|
|
||||||
|
// Load persisted settings from database
|
||||||
|
db.LoadSettings(cfg)
|
||||||
|
|
||||||
// Setup routes
|
// Setup routes
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
@@ -89,6 +92,7 @@ func main() {
|
|||||||
|
|
||||||
apiMux.HandleFunc("GET /api/claude/sessions", handlers.GetClaudeSessions)
|
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}/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", handlers.GetDashboardProjects)
|
||||||
apiMux.HandleFunc("GET /api/dashboard/projects/{name}/sessions", handlers.GetDashboardSessions)
|
apiMux.HandleFunc("GET /api/dashboard/projects/{name}/sessions", handlers.GetDashboardSessions)
|
||||||
|
|||||||
Reference in New Issue
Block a user