sql-assistant 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. sql_assistant/__init__.py +3 -0
  2. sql_assistant/api/__init__.py +1 -0
  3. sql_assistant/api/backup.py +116 -0
  4. sql_assistant/api/config.py +183 -0
  5. sql_assistant/api/conversation.py +71 -0
  6. sql_assistant/api/dependencies.py +22 -0
  7. sql_assistant/api/history.py +61 -0
  8. sql_assistant/api/models.py +221 -0
  9. sql_assistant/api/query.py +275 -0
  10. sql_assistant/api/routes.py +19 -0
  11. sql_assistant/api/schema.py +21 -0
  12. sql_assistant/config.py +144 -0
  13. sql_assistant/database/__init__.py +1 -0
  14. sql_assistant/database/backup.py +568 -0
  15. sql_assistant/database/connectors/__init__.py +1 -0
  16. sql_assistant/database/connectors/base.py +185 -0
  17. sql_assistant/database/connectors/exceptions.py +88 -0
  18. sql_assistant/database/connectors/mongodb.py +194 -0
  19. sql_assistant/database/connectors/mysql.py +110 -0
  20. sql_assistant/database/connectors/postgresql.py +133 -0
  21. sql_assistant/database/connectors/redis.py +132 -0
  22. sql_assistant/database/connectors/sqlserver.py +140 -0
  23. sql_assistant/database/history.py +290 -0
  24. sql_assistant/database/manager.py +178 -0
  25. sql_assistant/database/security.py +230 -0
  26. sql_assistant/llm/__init__.py +1 -0
  27. sql_assistant/llm/base.py +28 -0
  28. sql_assistant/llm/exceptions.py +96 -0
  29. sql_assistant/llm/manager.py +82 -0
  30. sql_assistant/llm/prompts.py +29 -0
  31. sql_assistant/llm/providers/__init__.py +1 -0
  32. sql_assistant/llm/providers/claude.py +132 -0
  33. sql_assistant/llm/providers/gemini.py +127 -0
  34. sql_assistant/llm/providers/openai_compatible.py +103 -0
  35. sql_assistant/llm/retry.py +88 -0
  36. sql_assistant/main.py +94 -0
  37. sql_assistant/settings.py +219 -0
  38. sql_assistant/web/__init__.py +1 -0
  39. sql_assistant/web/static/css/base.css +25 -0
  40. sql_assistant/web/static/css/components/backup.css +146 -0
  41. sql_assistant/web/static/css/components/chat.css +465 -0
  42. sql_assistant/web/static/css/components/modal.css +143 -0
  43. sql_assistant/web/static/css/components/settings.css +358 -0
  44. sql_assistant/web/static/css/components/sidebar.css +235 -0
  45. sql_assistant/web/static/css/components/toast.css +30 -0
  46. sql_assistant/web/static/css/style.css +10 -0
  47. sql_assistant/web/static/css/theme.css +200 -0
  48. sql_assistant/web/static/js/api.js +38 -0
  49. sql_assistant/web/static/js/app.js +161 -0
  50. sql_assistant/web/static/js/backup.js +216 -0
  51. sql_assistant/web/static/js/chat.js +238 -0
  52. sql_assistant/web/static/js/color-theme-manager.js +121 -0
  53. sql_assistant/web/static/js/confirm.js +95 -0
  54. sql_assistant/web/static/js/conversations.js +182 -0
  55. sql_assistant/web/static/js/settings.js +425 -0
  56. sql_assistant/web/static/js/state.js +43 -0
  57. sql_assistant/web/static/js/theme-manager.js +64 -0
  58. sql_assistant/web/static/js/ui.js +53 -0
  59. sql_assistant/web/templates/index.html +373 -0
  60. sql_assistant-1.0.0.dist-info/METADATA +24 -0
  61. sql_assistant-1.0.0.dist-info/RECORD +64 -0
  62. sql_assistant-1.0.0.dist-info/WHEEL +4 -0
  63. sql_assistant-1.0.0.dist-info/entry_points.txt +2 -0
  64. sql_assistant-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Chat Module
3
+ */
4
+
5
+ function updateConnectionStatus() {
6
+ const hasLLM = state.activeLLM;
7
+ const hasDB = state.activeDB;
8
+ const dot = dom.connectionStatus.querySelector('.status-dot');
9
+ const text = dom.connectionStatus.querySelector('.status-text');
10
+
11
+ const refreshBtn = $('btn-refresh-schema');
12
+
13
+ if (hasLLM && hasDB) {
14
+ if (state.schemaError) {
15
+ dot.className = 'status-dot error';
16
+ text.textContent = `DB: ${state.activeDB} | Schema 加载失败`;
17
+ } else if (state.schemaLoaded) {
18
+ dot.className = 'status-dot online';
19
+ text.textContent = `LLM: ${state.activeLLM} | DB: ${state.activeDB} (${state.tableCount} 表)`;
20
+ } else {
21
+ dot.className = 'status-dot online';
22
+ text.textContent = `LLM: ${state.activeLLM} | DB: ${state.activeDB}`;
23
+ }
24
+ dom.btnSend.disabled = false;
25
+ if (refreshBtn) refreshBtn.style.display = 'flex';
26
+ } else if (hasLLM || hasDB) {
27
+ dot.className = 'status-dot error';
28
+ text.textContent = hasLLM ? '请配置数据库连接' : '请配置 LLM';
29
+ dom.btnSend.disabled = true;
30
+ if (refreshBtn) refreshBtn.style.display = 'none';
31
+ } else {
32
+ dot.className = 'status-dot offline';
33
+ text.textContent = '未配置连接';
34
+ dom.btnSend.disabled = true;
35
+ if (refreshBtn) refreshBtn.style.display = 'none';
36
+ }
37
+ }
38
+
39
+ async function refreshSchema() {
40
+ try {
41
+ state.schemaLoaded = false;
42
+ state.schemaError = '';
43
+ const schema = await API.post('/api/schema/refresh');
44
+ if (schema.error) {
45
+ state.schemaError = schema.error;
46
+ showToast('Schema 加载失败: ' + schema.error, 'error');
47
+ } else {
48
+ const tables = schema.tables || schema.keys || [];
49
+ state.tableCount = tables.length;
50
+ state.schemaLoaded = true;
51
+ showToast(`Schema 已刷新: ${tables.length} 个对象`, 'success');
52
+ }
53
+ } catch (err) {
54
+ state.schemaError = err.message;
55
+ showToast('Schema 刷新失败: ' + err.message, 'error');
56
+ }
57
+ updateConnectionStatus();
58
+ }
59
+
60
+ function addMessage(type, content) {
61
+ const welcome = dom.chatMessages.querySelector('.welcome-message');
62
+ if (welcome) welcome.remove();
63
+
64
+ const msg = document.createElement('div');
65
+ msg.className = `message ${type}`;
66
+ msg.innerHTML = `<div class="message-content">${content}</div>`;
67
+ dom.chatMessages.appendChild(msg);
68
+ dom.chatMessages.scrollTop = dom.chatMessages.scrollHeight;
69
+ return msg;
70
+ }
71
+
72
+ function addSQLBlock(sql) {
73
+ const block = document.createElement('div');
74
+ block.className = 'sql-block';
75
+ block.innerHTML = `
76
+ <div class="sql-block-header">
77
+ <span>📋 SQL</span>
78
+ <button class="btn-copy" onclick="copySQL(this)">复制</button>
79
+ </div>
80
+ <pre><code class="language-sql">${escapeHtml(sql)}</code></pre>
81
+ `;
82
+ const codeEl = block.querySelector('code');
83
+ if (typeof hljs !== 'undefined') {
84
+ hljs.highlightElement(codeEl);
85
+ }
86
+ return block;
87
+ }
88
+
89
+ function addResultTable(result, pagination) {
90
+ const wrapper = document.createElement('div');
91
+ if (!result || result.error) {
92
+ wrapper.innerHTML = `<div class="error-message">${escapeHtml(result?.error || '执行失败')}</div>`;
93
+ return wrapper;
94
+ }
95
+
96
+ const { columns, rows, row_count, affected_rows, sql_type } = result;
97
+
98
+ if (sql_type !== 'SELECT' && affected_rows !== undefined && affected_rows > 0) {
99
+ wrapper.innerHTML = `
100
+ <div class="result-meta" style="color: var(--success); font-size: 14px;">
101
+ ✅ 执行成功,影响 ${affected_rows} 行
102
+ </div>
103
+ `;
104
+ return wrapper;
105
+ }
106
+
107
+ if (!columns || !rows) {
108
+ wrapper.innerHTML = `<div class="result-meta">执行完成</div>`;
109
+ return wrapper;
110
+ }
111
+
112
+ let html = '<div class="result-table-wrapper"><table class="result-table">';
113
+ html += '<thead><tr>';
114
+ for (const col of columns) {
115
+ html += `<th>${escapeHtml(String(col))}</th>`;
116
+ }
117
+ html += '</tr></thead><tbody>';
118
+ for (const row of rows) {
119
+ html += '<tr>';
120
+ for (const cell of row) {
121
+ html += `<td>${escapeHtml(String(cell ?? 'NULL'))}</td>`;
122
+ }
123
+ html += '</tr>';
124
+ }
125
+ html += '</tbody></table></div>';
126
+
127
+ let meta = '';
128
+ if (pagination && pagination.total_rows > 0) {
129
+ const start = (pagination.page - 1) * pagination.page_size + 1;
130
+ const end = Math.min(pagination.page * pagination.page_size, pagination.total_rows);
131
+ meta = `显示 ${start}-${end} 条,共 ${pagination.total_rows} 条`;
132
+ } else {
133
+ meta = `显示 ${row_count} 条`;
134
+ }
135
+ meta += ` (${sql_type})`;
136
+ html += `<div class="result-meta">${meta}</div>`;
137
+
138
+ if (pagination && pagination.total_pages > 1) {
139
+ html += `<div class="result-pagination">`;
140
+ html += `<button class="btn-page" onclick="changePage(-1)" ${pagination.page <= 1 ? 'disabled' : ''}>上一页</button>`;
141
+ html += `<span class="page-info">第 ${pagination.page} / ${pagination.total_pages} 页</span>`;
142
+ html += `<button class="btn-page" onclick="changePage(1)" ${pagination.page >= pagination.total_pages ? 'disabled' : ''}>下一页</button>`;
143
+ html += `</div>`;
144
+ }
145
+
146
+ wrapper.innerHTML = html;
147
+ wrapper.dataset.pagination = JSON.stringify(pagination || {});
148
+ return wrapper;
149
+ }
150
+
151
+ function changePage(delta) {
152
+ const resultDiv = document.querySelector('.result-pagination')?.closest('.result-wrapper');
153
+ if (!resultDiv) return;
154
+
155
+ let pagination = {};
156
+ try {
157
+ pagination = JSON.parse(resultDiv.dataset.pagination || '{}');
158
+ } catch (e) {}
159
+
160
+ if (!pagination.page) return;
161
+
162
+ const newPage = pagination.page + delta;
163
+ if (newPage < 1 || newPage > pagination.total_pages) return;
164
+
165
+ state.pendingPageChange = { page: newPage, page_size: pagination.page_size };
166
+ sendQuery(state.pendingQuestion);
167
+ }
168
+
169
+ async function sendQuery(question) {
170
+ if (!question.trim()) return;
171
+
172
+ addMessage('user', escapeHtml(question));
173
+ dom.queryInput.value = '';
174
+ dom.queryInput.style.height = 'auto';
175
+
176
+ const msgDiv = addMessage('assistant', '<div class="loading-dots"><span></span><span></span><span></span></div>');
177
+ const contentDiv = msgDiv.querySelector('.message-content');
178
+
179
+ try {
180
+ if (!state.currentConversationId) {
181
+ const convData = await API.post('/api/conversations', { title: question.slice(0, 50) });
182
+ state.currentConversationId = convData.id;
183
+ await loadConversations();
184
+ }
185
+
186
+ const queryParams = {
187
+ question,
188
+ conversation_id: state.currentConversationId
189
+ };
190
+
191
+ if (state.pendingPageChange) {
192
+ queryParams.page = state.pendingPageChange.page;
193
+ queryParams.page_size = state.pendingPageChange.page_size;
194
+ state.pendingPageChange = null;
195
+ }
196
+ state.pendingQuestion = question;
197
+
198
+ contentDiv.innerHTML = '<div class="streaming-indicator">正在生成 SQL...</div>';
199
+
200
+ const previewData = await API.post('/api/query/preview', queryParams);
201
+
202
+ if (!previewData.success) {
203
+ contentDiv.innerHTML = `<div class="error-message">${escapeHtml(previewData.error)}</div>`;
204
+ return;
205
+ }
206
+
207
+ contentDiv.innerHTML = '';
208
+
209
+ contentDiv.appendChild(addSQLBlock(previewData.sql));
210
+
211
+ if (previewData.requires_confirmation) {
212
+ window.pendingSQLData = {
213
+ question: question,
214
+ sql: previewData.sql,
215
+ sqlHash: previewData.sql_hash,
216
+ conversationId: state.currentConversationId,
217
+ contentDiv: contentDiv,
218
+ msgDiv: msgDiv
219
+ };
220
+ showSQLConfirmDialog(previewData);
221
+ } else {
222
+ await executeConfirmedQuery(queryParams, contentDiv, msgDiv);
223
+ }
224
+ } catch (err) {
225
+ contentDiv.innerHTML = `<div class="error-message">${escapeHtml(err.message)}</div>`;
226
+ }
227
+
228
+ dom.chatMessages.scrollTop = dom.chatMessages.scrollHeight;
229
+ loadConversations();
230
+ }
231
+
232
+ window.updateConnectionStatus = updateConnectionStatus;
233
+ window.refreshSchema = refreshSchema;
234
+ window.addMessage = addMessage;
235
+ window.addSQLBlock = addSQLBlock;
236
+ window.addResultTable = addResultTable;
237
+ window.changePage = changePage;
238
+ window.sendQuery = sendQuery;
@@ -0,0 +1,121 @@
1
+ /**
2
+ * 颜色主题管理器 - 处理主题色切换
3
+ */
4
+
5
+ const ColorThemeManager = {
6
+ STORAGE_KEY: 'sql_assistant_color_theme',
7
+ currentColor: 'blue',
8
+
9
+ COLOR_PRESETS: {
10
+ blue: {
11
+ name: '蓝色',
12
+ accent: '#58a6ff',
13
+ 'accent-hover': '#79c0ff',
14
+ },
15
+ purple: {
16
+ name: '紫色',
17
+ accent: '#a371f7',
18
+ 'accent-hover': '#b87ff7',
19
+ },
20
+ pink: {
21
+ name: '粉色',
22
+ accent: '#f778ba',
23
+ 'accent-hover': '#ff9bce',
24
+ },
25
+ red: {
26
+ name: '红色',
27
+ accent: '#f85149',
28
+ 'accent-hover': '#ff7b72',
29
+ },
30
+ orange: {
31
+ name: '橙色',
32
+ accent: '#d29922',
33
+ 'accent-hover': '#e3b341',
34
+ },
35
+ yellow: {
36
+ name: '黄色',
37
+ accent: '#d4a72c',
38
+ 'accent-hover': '#e6c454',
39
+ },
40
+ green: {
41
+ name: '绿色',
42
+ accent: '#3fb950',
43
+ 'accent-hover': '#56d364',
44
+ },
45
+ cyan: {
46
+ name: '青色',
47
+ accent: '#39c5cf',
48
+ 'accent-hover': '#56d4dd',
49
+ },
50
+ },
51
+
52
+ init() {
53
+ this.loadColor();
54
+ this.bindEvents();
55
+ },
56
+
57
+ loadColor() {
58
+ const saved = localStorage.getItem(this.STORAGE_KEY);
59
+ if (saved && this.COLOR_PRESETS[saved]) {
60
+ this.currentColor = saved;
61
+ }
62
+ this.applyColor();
63
+ },
64
+
65
+ bindEvents() {
66
+ document.querySelectorAll('.color-btn').forEach(btn => {
67
+ btn.addEventListener('click', (e) => {
68
+ const color = e.target.dataset.color;
69
+ if (color) {
70
+ this.setColor(color);
71
+ }
72
+ });
73
+ });
74
+ },
75
+
76
+ setColor(colorName) {
77
+ if (!this.COLOR_PRESETS[colorName]) {
78
+ console.warn(`Color ${colorName} not found`);
79
+ return;
80
+ }
81
+ this.currentColor = colorName;
82
+ this.saveColor();
83
+ this.applyColor();
84
+ this.updateColorButtons();
85
+ },
86
+
87
+ saveColor() {
88
+ localStorage.setItem(this.STORAGE_KEY, this.currentColor);
89
+ },
90
+
91
+ applyColor() {
92
+ const preset = this.COLOR_PRESETS[this.currentColor];
93
+ if (!preset) return;
94
+
95
+ const root = document.documentElement;
96
+ root.style.setProperty('--accent', preset.accent);
97
+ root.style.setProperty('--accent-hover', preset['accent-hover']);
98
+
99
+ this.updateColorButtons();
100
+ },
101
+
102
+ updateColorButtons() {
103
+ document.querySelectorAll('.color-btn').forEach(btn => {
104
+ if (btn.dataset.color === this.currentColor) {
105
+ btn.classList.add('active');
106
+ } else {
107
+ btn.classList.remove('active');
108
+ }
109
+ });
110
+ },
111
+
112
+ getCurrentColor() {
113
+ return this.currentColor;
114
+ },
115
+
116
+ getColorName(colorName) {
117
+ return this.COLOR_PRESETS[colorName]?.name || colorName;
118
+ }
119
+ };
120
+
121
+ window.ColorThemeManager = ColorThemeManager;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * SQL Confirmation Dialog Module
3
+ */
4
+
5
+ window.pendingSQLData = null;
6
+
7
+ function showSQLConfirmDialog(previewData) {
8
+ const modal = document.getElementById('sql-confirm-modal');
9
+ const titleEl = document.getElementById('sql-confirm-title');
10
+ const warningEl = document.getElementById('sql-confirm-warning');
11
+ const reasonEl = document.getElementById('sql-confirm-reason');
12
+ const codeEl = document.getElementById('sql-confirm-code');
13
+
14
+ titleEl.textContent = '⚠️ 确认执行 SQL';
15
+
16
+ if (previewData.warning) {
17
+ warningEl.textContent = previewData.warning;
18
+ warningEl.style.display = 'block';
19
+ warningEl.className = 'sql-warning-box ' + previewData.risk_level;
20
+ } else {
21
+ warningEl.style.display = 'none';
22
+ }
23
+
24
+ reasonEl.textContent = previewData.confirmation_reason || '此操作可能修改数据,请确认后再执行';
25
+
26
+ codeEl.textContent = previewData.sql;
27
+ if (typeof hljs !== 'undefined') {
28
+ hljs.highlightElement(codeEl);
29
+ }
30
+
31
+ modal.classList.add('active');
32
+ }
33
+
34
+ function hideSQLConfirmDialog() {
35
+ const modal = document.getElementById('sql-confirm-modal');
36
+ modal.classList.remove('active');
37
+ window.pendingSQLData = null;
38
+ }
39
+
40
+ async function confirmAndExecuteSQL() {
41
+ if (!window.pendingSQLData) return;
42
+
43
+ const { question, sql, sqlHash, conversationId, contentDiv, msgDiv } = window.pendingSQLData;
44
+
45
+ hideSQLConfirmDialog();
46
+
47
+ const existingLoading = contentDiv.querySelector('.loading-dots');
48
+ if (existingLoading) existingLoading.remove();
49
+
50
+ const loadingDiv = document.createElement('div');
51
+ loadingDiv.className = 'loading-dots';
52
+ loadingDiv.innerHTML = '<span></span><span></span><span></span>';
53
+ loadingDiv.style.marginTop = '12px';
54
+ contentDiv.appendChild(loadingDiv);
55
+
56
+ const queryParams = {
57
+ question,
58
+ conversation_id: conversationId,
59
+ confirmed: true,
60
+ sql_hash: sqlHash,
61
+ sql: sql // 直接传递 SQL 语句,避免 LLM 重新生成导致 hash 不匹配
62
+ };
63
+
64
+ await executeConfirmedQuery(queryParams, contentDiv, msgDiv);
65
+ }
66
+
67
+ async function executeConfirmedQuery(queryParams, contentDiv, msgDiv) {
68
+ try {
69
+ const data = await API.post('/api/query', queryParams);
70
+
71
+ // 移除 loading 动画
72
+ const loadingDots = contentDiv.querySelector('.loading-dots');
73
+ if (loadingDots) loadingDots.remove();
74
+
75
+ if (data.result) {
76
+ contentDiv.appendChild(addResultTable(data.result, data.pagination));
77
+ }
78
+
79
+ if (data.error && !data.result) {
80
+ contentDiv.innerHTML += `<div class="error-message">${escapeHtml(data.error)}</div>`;
81
+ }
82
+ } catch (err) {
83
+ const loadingDots = contentDiv.querySelector('.loading-dots');
84
+ if (loadingDots) loadingDots.remove();
85
+ contentDiv.innerHTML += `<div class="error-message">${escapeHtml(err.message)}</div>`;
86
+ }
87
+
88
+ dom.chatMessages.scrollTop = dom.chatMessages.scrollTop;
89
+ loadConversations();
90
+ }
91
+
92
+ window.showSQLConfirmDialog = showSQLConfirmDialog;
93
+ window.hideSQLConfirmDialog = hideSQLConfirmDialog;
94
+ window.confirmAndExecuteSQL = confirmAndExecuteSQL;
95
+ window.executeConfirmedQuery = executeConfirmedQuery;
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Conversation Management Module
3
+ */
4
+
5
+ // ============================================================
6
+ // Conversation Management
7
+ // ============================================================
8
+ async function loadConversations() {
9
+ try {
10
+ const data = await API.get('/api/conversations');
11
+ state.conversations = data.conversations || [];
12
+ renderConversations();
13
+
14
+ if (state.conversations.length > 0 && !state.currentConversationId) {
15
+ state.currentConversationId = state.conversations[0].id;
16
+ renderConversations();
17
+ }
18
+ } catch (err) {
19
+ dom.historyList.innerHTML = '<div class="history-empty">加载失败</div>';
20
+ }
21
+ }
22
+
23
+ function renderConversations() {
24
+ if (!state.conversations || state.conversations.length === 0) {
25
+ dom.historyList.innerHTML = '<div class="history-empty">暂无对话</div>';
26
+ return;
27
+ }
28
+
29
+ dom.historyList.innerHTML = state.conversations.map(c => `
30
+ <div class="history-item ${state.currentConversationId === c.id ? 'active' : ''}" onclick="selectConversation(${c.id})">
31
+ <div class="history-item-main">
32
+ <div class="history-item-title">${escapeHtml(c.title || '未命名对话')}</div>
33
+ <div class="history-item-actions">
34
+ <button class="btn-icon-chat" onclick="event.stopPropagation(); startRenameConversation(${c.id})" title="重命名">
35
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
36
+ </button>
37
+ <button class="btn-delete-chat" onclick="event.stopPropagation(); deleteConversation(${c.id})" title="删除对话">
38
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
39
+ </button>
40
+ </div>
41
+ </div>
42
+ <div class="meta">
43
+ <span>${c.message_count || 0} 条消息</span>
44
+ <span>${c.updated_at?.split('T')[0] || ''}</span>
45
+ </div>
46
+ </div>
47
+ `).join('');
48
+ }
49
+
50
+ function startRenameConversation(conversationId) {
51
+ const item = document.querySelector(`.history-item[onclick*="selectConversation(${conversationId})"]`);
52
+ if (!item) return;
53
+
54
+ const titleDiv = item.querySelector('.history-item-title');
55
+ const currentTitle = titleDiv.textContent;
56
+
57
+ const input = document.createElement('input');
58
+ input.type = 'text';
59
+ input.className = 'rename-input';
60
+ input.value = currentTitle;
61
+ input.maxLength = 50;
62
+
63
+ titleDiv.innerHTML = '';
64
+ titleDiv.appendChild(input);
65
+ input.focus();
66
+ input.select();
67
+
68
+ const finishRename = async () => {
69
+ const newTitle = input.value.trim() || currentTitle;
70
+ try {
71
+ await API.put(`/api/conversations/${conversationId}`, { title: newTitle });
72
+ await loadConversations();
73
+ showToast('重命名成功', 'success');
74
+ } catch (err) {
75
+ showToast(err.message, 'error');
76
+ }
77
+ };
78
+
79
+ input.addEventListener('blur', finishRename);
80
+ input.addEventListener('keydown', (e) => {
81
+ if (e.key === 'Enter') {
82
+ e.preventDefault();
83
+ input.blur();
84
+ } else if (e.key === 'Escape') {
85
+ input.value = currentTitle;
86
+ input.blur();
87
+ }
88
+ });
89
+ }
90
+
91
+ async function createNewConversation() {
92
+ try {
93
+ const data = await API.post('/api/conversations', { title: '新对话' });
94
+ state.currentConversationId = data.id;
95
+ clearChatMessages();
96
+ loadConversations();
97
+ renderConversations();
98
+ showToast('已创建新对话', 'success');
99
+ } catch (err) {
100
+ showToast(err.message, 'error');
101
+ }
102
+ }
103
+
104
+ async function selectConversation(conversationId) {
105
+ try {
106
+ state.currentConversationId = conversationId;
107
+ const data = await API.get(`/api/conversations/${conversationId}`);
108
+
109
+ clearChatMessages();
110
+
111
+ if (data.messages && data.messages.length > 0) {
112
+ for (const msg of data.messages) {
113
+ addMessage('user', escapeHtml(msg.question));
114
+
115
+ const msgDiv = addMessage('assistant', '');
116
+ const contentDiv = msgDiv.querySelector('.message-content');
117
+
118
+ if (msg.sql) {
119
+ contentDiv.appendChild(addSQLBlock(msg.sql));
120
+ }
121
+
122
+ if (msg.result_json) {
123
+ try {
124
+ const result = JSON.parse(msg.result_json);
125
+ contentDiv.appendChild(addResultTable(result));
126
+ } catch (e) {
127
+ // ignore
128
+ }
129
+ }
130
+
131
+ if (!msg.success && msg.error_message) {
132
+ contentDiv.innerHTML += `<div class="error-message">${escapeHtml(msg.error_message)}</div>`;
133
+ }
134
+ }
135
+ }
136
+
137
+ renderConversations();
138
+ } catch (err) {
139
+ showToast('加载对话失败', 'error');
140
+ }
141
+ }
142
+
143
+ async function deleteConversation(conversationId) {
144
+ if (!confirm('确定删除这个对话吗?')) return;
145
+ try {
146
+ await API.del(`/api/conversations/${conversationId}`);
147
+ if (state.currentConversationId === conversationId) {
148
+ state.currentConversationId = null;
149
+ clearChatMessages();
150
+ }
151
+ loadConversations();
152
+ showToast('对话已删除', 'success');
153
+ } catch (err) {
154
+ showToast(err.message, 'error');
155
+ }
156
+ }
157
+
158
+ function clearChatMessages() {
159
+ dom.chatMessages.innerHTML = `
160
+ <div class="welcome-message">
161
+ <div class="welcome-icon">💬</div>
162
+ <h2>SQL 智能助手</h2>
163
+ <p>用自然语言描述你的查询需求,AI 会帮你生成 SQL 并执行</p>
164
+ <p class="welcome-hint">请先在设置中配置 LLM API Key 和数据库连接</p>
165
+ <div class="welcome-examples">
166
+ <span class="example-tag" data-question="查询所有用户信息">查询所有用户信息</span>
167
+ <span class="example-tag" data-question="统计每个部门的员工数量">统计每个部门的员工数量</span>
168
+ <span class="example-tag" data-question="添加一条商品记录,名称 iPhone 15,价格 6999">添加商品记录</span>
169
+ <span class="example-tag" data-question="查询最近7天的订单">查询最近7天订单</span>
170
+ </div>
171
+ </div>
172
+ `;
173
+ }
174
+
175
+ // Expose functions
176
+ window.loadConversations = loadConversations;
177
+ window.renderConversations = renderConversations;
178
+ window.startRenameConversation = startRenameConversation;
179
+ window.createNewConversation = createNewConversation;
180
+ window.selectConversation = selectConversation;
181
+ window.deleteConversation = deleteConversation;
182
+ window.clearChatMessages = clearChatMessages;