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,425 @@
1
+ /**
2
+ * Settings Management Module
3
+ */
4
+
5
+ // ============================================================
6
+ // Settings Modal
7
+ // ============================================================
8
+ function openSettings() {
9
+ dom.settingsModal.classList.add('active');
10
+ loadSettings();
11
+ }
12
+
13
+ function closeSettings() {
14
+ dom.settingsModal.classList.remove('active');
15
+ }
16
+
17
+ async function loadSettings() {
18
+ try {
19
+ const [settings, models] = await Promise.all([
20
+ API.get('/api/config/settings'),
21
+ API.get('/api/config/llm/models')
22
+ ]);
23
+
24
+ state.llmConfigs = settings.llm_providers;
25
+ state.dbConfigs = settings.databases;
26
+ state.activeLLM = settings.active_llm;
27
+ state.activeDB = settings.active_database;
28
+ state.llmModels = models.models || {};
29
+
30
+ renderLLMConfigs();
31
+ renderDBConfigs();
32
+ updateConnectionStatus();
33
+ } catch (err) {
34
+ showToast('加载设置失败', 'error');
35
+ }
36
+ }
37
+
38
+ async function loadLLMModels(provider) {
39
+ try {
40
+ const result = await API.get(`/api/config/llm/models?provider=${encodeURIComponent(provider)}`);
41
+ state.llmModels[provider] = result.models || [];
42
+ return result.models || [];
43
+ } catch (err) {
44
+ console.error('加载模型列表失败:', err);
45
+ return [];
46
+ }
47
+ }
48
+
49
+ function updateModelSelect(provider) {
50
+ const select = $('llm-model');
51
+ select.innerHTML = '<option value="">选择模型...</option>';
52
+
53
+ const models = state.llmModels[provider] || [];
54
+ if (models.length === 0) {
55
+ return;
56
+ }
57
+
58
+ for (const model of models) {
59
+ const option = document.createElement('option');
60
+ option.value = model;
61
+ option.textContent = model;
62
+ select.appendChild(option);
63
+ }
64
+
65
+ const defaultModel = getDefaultModel(provider);
66
+ if (defaultModel) {
67
+ select.value = defaultModel;
68
+ }
69
+ }
70
+
71
+ function getDefaultModel(provider) {
72
+ const defaults = {
73
+ deepseek: 'deepseek-v4-pro',
74
+ doubao: 'doubao-seed-2-0-pro-260215',
75
+ kimi: 'kimi-k2.6',
76
+ qwen: 'qwen3.6-max-preview',
77
+ openai: 'gpt-5.5',
78
+ gemini: 'gemini-3.1-pro-preview',
79
+ claude: 'claude-opus-4-7',
80
+ glm: 'glm-5.1',
81
+ minimax: 'MiniMax-M2.7',
82
+ siliconflow: 'deepseek-ai/DeepSeek-V3.2',
83
+ openrouter: 'deepseek/deepseek-v4-pro',
84
+ grok: 'grok-4.20-reasoning',
85
+ tencent: 'hy3-preview',
86
+ mimo: 'mimo-v2.5-pro',
87
+ ollama: 'llama3.3',
88
+ };
89
+ return defaults[provider] || '';
90
+ }
91
+
92
+ function updateLLMFormByProvider(provider) {
93
+ const apiKeyField = document.querySelector('#llm-api-key').closest('.form-group');
94
+ const baseUrlField = document.querySelector('#llm-base-url').closest('.form-group');
95
+
96
+ const noApiKeyProviders = ['ollama'];
97
+ const defaultBaseUrlProviders = ['openai', 'deepseek', 'kimi', 'qwen', 'doubao', 'glm', 'grok', 'tencent', 'mimo', 'ollama'];
98
+
99
+ if (noApiKeyProviders.includes(provider)) {
100
+ apiKeyField.style.display = 'none';
101
+ $('llm-api-key').value = '';
102
+ } else {
103
+ apiKeyField.style.display = 'block';
104
+ }
105
+
106
+ if (defaultBaseUrlProviders.includes(provider)) {
107
+ baseUrlField.style.display = 'none';
108
+ $('llm-base-url').value = '';
109
+ } else {
110
+ baseUrlField.style.display = 'block';
111
+ }
112
+ }
113
+
114
+ function requiresApiKey(provider) {
115
+ return !['ollama'].includes(provider);
116
+ }
117
+
118
+ // ============================================================
119
+ // LLM Configs
120
+ // ============================================================
121
+ function renderLLMConfigs() {
122
+ if (state.llmConfigs.length === 0) {
123
+ dom.llmConfigList.innerHTML = '<p style="color: var(--text-muted); font-size: 13px; text-align: center; padding: 16px;">暂无 LLM 配置</p>';
124
+ return;
125
+ }
126
+
127
+ const providerIcons = {
128
+ deepseek: '🐋', doubao: '🫘', kimi: '🌙', qwen: '☁️',
129
+ openai: '🤖', gemini: '💎', claude: '🧠',
130
+ };
131
+
132
+ dom.llmConfigList.innerHTML = state.llmConfigs.map(c => `
133
+ <div class="config-card ${c.name === state.activeLLM ? 'active' : ''}">
134
+ <div class="provider-icon">${providerIcons[c.provider] || '🤖'}</div>
135
+ <div class="info">
136
+ <div class="name">${escapeHtml(c.name)}</div>
137
+ </div>
138
+ <div class="actions">
139
+ <button class="btn-sm active-btn" onclick="activateLLM('${escapeHtml(c.name)}')">${c.name === state.activeLLM ? '✓ 当前' : '激活'}</button>
140
+ <button class="btn-sm" onclick="editLLM('${escapeHtml(c.name)}')">编辑</button>
141
+ <button class="btn-sm btn-test" onclick="testLLM('${escapeHtml(c.name)}')">测试</button>
142
+ <button class="btn-sm danger" onclick="deleteLLM('${escapeHtml(c.name)}')">删除</button>
143
+ </div>
144
+ </div>
145
+ `).join('');
146
+ }
147
+
148
+ async function activateLLM(name) {
149
+ try {
150
+ await API.put(`/api/config/llm/active/${encodeURIComponent(name)}`);
151
+ state.activeLLM = name;
152
+ renderLLMConfigs();
153
+ updateConnectionStatus();
154
+ showToast(`已激活 LLM: ${name}`, 'success');
155
+ } catch (err) {
156
+ showToast(err.message, 'error');
157
+ }
158
+ }
159
+
160
+ function editLLM(name) {
161
+ const config = state.llmConfigs.find(c => c.name === name);
162
+ if (!config) return;
163
+
164
+ state.editingLLM = name;
165
+ $('llm-form-title').textContent = '编辑 LLM 提供商';
166
+ $('llm-name').value = config.name;
167
+ $('llm-provider').value = config.provider;
168
+ $('llm-api-key').value = '';
169
+ $('llm-api-key').placeholder = '留空保留原 Key';
170
+ $('llm-base-url').value = '';
171
+
172
+ updateModelSelect(config.provider);
173
+ $('llm-model').value = config.model || '';
174
+ updateLLMFormByProvider(config.provider);
175
+
176
+ dom.llmForm.style.display = 'block';
177
+ dom.llmForm.scrollIntoView({ behavior: 'smooth' });
178
+ }
179
+
180
+ async function deleteLLM(name) {
181
+ if (!confirm(`确定删除 LLM 配置 "${name}"?`)) return;
182
+ try {
183
+ await API.del(`/api/config/llm/${encodeURIComponent(name)}`);
184
+ if (state.activeLLM === name) state.activeLLM = '';
185
+ await loadSettings();
186
+ showToast('已删除', 'success');
187
+ } catch (err) {
188
+ showToast(err.message, 'error');
189
+ }
190
+ }
191
+
192
+ async function testLLM(name) {
193
+ try {
194
+ showToast('正在测试 LLM 连接...', 'info');
195
+ const result = await API.post('/api/config/llm/test', { name });
196
+ showToast(result.message, result.success ? 'success' : 'error');
197
+ } catch (err) {
198
+ showToast(err.message, 'error');
199
+ }
200
+ }
201
+
202
+ async function testNewLLM() {
203
+ const provider = $('llm-provider').value;
204
+ const data = {
205
+ name: $('llm-name').value.trim() || 'temp_test',
206
+ provider: provider,
207
+ api_key: $('llm-api-key').value.trim(),
208
+ base_url: $('llm-base-url').value.trim(),
209
+ model: $('llm-model').value,
210
+ };
211
+
212
+ if (requiresApiKey(provider) && !data.api_key) return showToast('请输入 API Key', 'error');
213
+ if (!data.model) return showToast('请选择模型', 'error');
214
+
215
+ try {
216
+ showToast('正在测试 LLM 连接...', 'info');
217
+ const result = await API.post('/api/config/llm/test', { config: data });
218
+ showToast(result.message, result.success ? 'success' : 'error');
219
+ } catch (err) {
220
+ showToast(err.message, 'error');
221
+ }
222
+ }
223
+
224
+ async function saveLLM() {
225
+ const name = $('llm-name').value.trim();
226
+ const provider = $('llm-provider').value;
227
+ const apiKey = $('llm-api-key').value.trim();
228
+ const baseUrl = $('llm-base-url').value.trim();
229
+ const model = $('llm-model').value;
230
+
231
+ if (!name) return showToast('请输入配置名称', 'error');
232
+ if (!state.editingLLM && requiresApiKey(provider) && !apiKey) return showToast('请输入 API Key', 'error');
233
+ if (!model) return showToast('请选择模型', 'error');
234
+
235
+ try {
236
+ await API.post('/api/config/llm', { name, provider, api_key: apiKey, base_url: baseUrl, model });
237
+ state.editingLLM = null;
238
+ cancelLLMForm();
239
+ await loadSettings();
240
+ showToast('保存成功', 'success');
241
+ } catch (err) {
242
+ showToast(err.message, 'error');
243
+ }
244
+ }
245
+
246
+ function cancelLLMForm() {
247
+ state.editingLLM = null;
248
+ dom.llmForm.style.display = 'none';
249
+ $('llm-name').value = '';
250
+ $('llm-api-key').value = '';
251
+ $('llm-base-url').value = '';
252
+ $('llm-model').innerHTML = '<option value="">选择模型...</option>';
253
+
254
+ const apiKeyField = document.querySelector('#llm-api-key').closest('.form-group');
255
+ const baseUrlField = document.querySelector('#llm-base-url').closest('.form-group');
256
+ apiKeyField.style.display = 'block';
257
+ baseUrlField.style.display = 'block';
258
+ }
259
+
260
+ // ============================================================
261
+ // DB Configs
262
+ // ============================================================
263
+ function renderDBConfigs() {
264
+ if (state.dbConfigs.length === 0) {
265
+ dom.dbConfigList.innerHTML = '<p style="color: var(--text-muted); font-size: 13px; text-align: center; padding: 16px;">暂无数据库配置</p>';
266
+ return;
267
+ }
268
+
269
+ const dbIcons = {
270
+ mysql: '🐬', postgresql: '🐘', sqlserver: '🪟', redis: '🔴', mongodb: '🍃',
271
+ };
272
+
273
+ dom.dbConfigList.innerHTML = state.dbConfigs.map(c => `
274
+ <div class="config-card ${c.name === state.activeDB ? 'active' : ''}">
275
+ <div class="provider-icon">${dbIcons[c.db_type] || '🗄️'}</div>
276
+ <div class="info">
277
+ <div class="name">${escapeHtml(c.name)}</div>
278
+ </div>
279
+ <div class="actions">
280
+ <button class="btn-sm active-btn" onclick="activateDB('${escapeHtml(c.name)}')">${c.name === state.activeDB ? '✓ 当前' : '激活'}</button>
281
+ <button class="btn-sm" onclick="editDB('${escapeHtml(c.name)}')">编辑</button>
282
+ <button class="btn-sm" onclick="testDB('${escapeHtml(c.name)}')">测试</button>
283
+ <button class="btn-sm danger" onclick="deleteDB('${escapeHtml(c.name)}')">删除</button>
284
+ </div>
285
+ </div>
286
+ `).join('');
287
+ }
288
+
289
+ async function activateDB(name) {
290
+ try {
291
+ await API.put(`/api/config/database/active/${encodeURIComponent(name)}`);
292
+ state.activeDB = name;
293
+ renderDBConfigs();
294
+ updateConnectionStatus();
295
+ showToast(`已激活数据库: ${name}`, 'success');
296
+ refreshSchema();
297
+ } catch (err) {
298
+ showToast(err.message, 'error');
299
+ }
300
+ }
301
+
302
+ function editDB(name) {
303
+ const config = state.dbConfigs.find(c => c.name === name);
304
+ if (!config) return;
305
+
306
+ state.editingDB = name;
307
+ $('db-form-title').textContent = '编辑数据库连接';
308
+ $('db-name').value = config.name;
309
+ $('db-type').value = config.db_type;
310
+ $('db-host').value = config.host;
311
+ $('db-port').value = config.port || '';
312
+ $('db-user').value = config.user;
313
+ $('db-password').value = '';
314
+ $('db-password').placeholder = '留空保留原密码';
315
+ $('db-database').value = config.database;
316
+ dom.dbForm.style.display = 'block';
317
+ dom.dbForm.scrollIntoView({ behavior: 'smooth' });
318
+ }
319
+
320
+ async function testDB(name) {
321
+ try {
322
+ showToast('正在测试连接...', 'info');
323
+ const result = await API.post('/api/config/database/test', { name });
324
+ showToast(result.message, result.success ? 'success' : 'error');
325
+ } catch (err) {
326
+ showToast(err.message, 'error');
327
+ }
328
+ }
329
+
330
+ async function deleteDB(name) {
331
+ if (!confirm(`确定删除数据库配置 "${name}"?`)) return;
332
+ try {
333
+ await API.del(`/api/config/database/${encodeURIComponent(name)}`);
334
+ if (state.activeDB === name) state.activeDB = '';
335
+ await loadSettings();
336
+ showToast('已删除', 'success');
337
+ } catch (err) {
338
+ showToast(err.message, 'error');
339
+ }
340
+ }
341
+
342
+ async function saveDB() {
343
+ const data = {
344
+ name: $('db-name').value.trim(),
345
+ db_type: $('db-type').value,
346
+ host: $('db-host').value.trim() || 'localhost',
347
+ port: parseInt($('db-port').value) || 0,
348
+ user: $('db-user').value.trim(),
349
+ password: $('db-password').value.trim(),
350
+ database: $('db-database').value.trim(),
351
+ };
352
+
353
+ if (!data.name) return showToast('请输入连接名称', 'error');
354
+ if (!data.database) return showToast('请输入数据库名', 'error');
355
+
356
+ try {
357
+ await API.post('/api/config/database', data);
358
+ state.editingDB = null;
359
+ cancelDBForm();
360
+ await loadSettings();
361
+ showToast('保存成功', 'success');
362
+ } catch (err) {
363
+ showToast(err.message, 'error');
364
+ }
365
+ }
366
+
367
+ async function testNewDB() {
368
+ const data = {
369
+ name: $('db-name').value.trim() || 'temp_test',
370
+ db_type: $('db-type').value,
371
+ host: $('db-host').value.trim() || 'localhost',
372
+ port: parseInt($('db-port').value) || 0,
373
+ user: $('db-user').value.trim(),
374
+ password: $('db-password').value.trim(),
375
+ database: $('db-database').value.trim(),
376
+ };
377
+
378
+ if (!data.database) return showToast('请输入数据库名', 'error');
379
+
380
+ try {
381
+ showToast('正在测试连接...', 'info');
382
+ const result = await API.post('/api/config/database/test', { config: data });
383
+ showToast(result.message, result.success ? 'success' : 'error');
384
+ } catch (err) {
385
+ showToast(err.message, 'error');
386
+ }
387
+ }
388
+
389
+ function cancelDBForm() {
390
+ state.editingDB = null;
391
+ dom.dbForm.style.display = 'none';
392
+ ['db-name', 'db-host', 'db-port', 'db-user', 'db-password', 'db-database'].forEach(id => $(id).value = '');
393
+ }
394
+
395
+ // ============================================================
396
+ // Settings Tabs
397
+ // ============================================================
398
+ function switchTab(tabName) {
399
+ document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
400
+ document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
401
+ document.querySelector(`[data-tab="${tabName}"]`)?.classList.add('active');
402
+ $(`tab-${tabName}`)?.classList.add('active');
403
+ }
404
+
405
+ // Expose functions
406
+ window.openSettings = openSettings;
407
+ window.closeSettings = closeSettings;
408
+ window.loadSettings = loadSettings;
409
+ window.updateModelSelect = updateModelSelect;
410
+ window.updateLLMFormByProvider = updateLLMFormByProvider;
411
+ window.activateLLM = activateLLM;
412
+ window.editLLM = editLLM;
413
+ window.testLLM = testLLM;
414
+ window.deleteLLM = deleteLLM;
415
+ window.testNewLLM = testNewLLM;
416
+ window.saveLLM = saveLLM;
417
+ window.cancelLLMForm = cancelLLMForm;
418
+ window.activateDB = activateDB;
419
+ window.editDB = editDB;
420
+ window.testDB = testDB;
421
+ window.deleteDB = deleteDB;
422
+ window.saveDB = saveDB;
423
+ window.testNewDB = testNewDB;
424
+ window.cancelDBForm = cancelDBForm;
425
+ window.switchTab = switchTab;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Application State Management
3
+ */
4
+ const state = {
5
+ llmConfigs: [],
6
+ dbConfigs: [],
7
+ activeLLM: '',
8
+ activeDB: '',
9
+ editingLLM: null,
10
+ editingDB: null,
11
+ schemaLoaded: false,
12
+ schemaError: '',
13
+ tableCount: 0,
14
+ currentConversationId: null,
15
+ conversations: [],
16
+ llmModels: {},
17
+ streamingEnabled: false,
18
+ pendingPageChange: null,
19
+ pendingQuestion: '',
20
+ };
21
+
22
+ // DOM Elements Helper
23
+ const $ = (id) => document.getElementById(id);
24
+
25
+ const dom = {
26
+ chatMessages: $('chat-messages'),
27
+ queryInput: $('query-input'),
28
+ queryForm: $('query-form'),
29
+ btnSend: $('btn-send'),
30
+ historyList: $('history-list'),
31
+ connectionStatus: $('connection-status'),
32
+ settingsModal: $('settings-modal'),
33
+
34
+ llmConfigList: $('llm-config-list'),
35
+ llmForm: $('llm-form'),
36
+ dbConfigList: $('db-config-list'),
37
+ dbForm: $('db-form'),
38
+ };
39
+
40
+ // Expose state and helpers
41
+ window.state = state;
42
+ window.$ = $;
43
+ window.dom = dom;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * 主题管理器 - 处理亮色/暗色模式切换
3
+ */
4
+
5
+ const ThemeManager = {
6
+ STORAGE_KEY: 'sql_assistant_theme',
7
+ currentTheme: 'dark',
8
+
9
+ init() {
10
+ this.loadTheme();
11
+ this.bindEvents();
12
+ },
13
+
14
+ loadTheme() {
15
+ const saved = localStorage.getItem(this.STORAGE_KEY);
16
+ if (saved) {
17
+ this.currentTheme = saved;
18
+ }
19
+ this.applyTheme();
20
+ },
21
+
22
+ bindEvents() {
23
+ const toggleBtn = document.getElementById('btn-toggle-theme');
24
+ const checkbox = document.getElementById('theme-toggle-checkbox');
25
+
26
+ if (toggleBtn) {
27
+ toggleBtn.addEventListener('click', () => this.toggle());
28
+ }
29
+
30
+ if (checkbox) {
31
+ checkbox.addEventListener('change', (e) => {
32
+ this.currentTheme = e.target.checked ? 'light' : 'dark';
33
+ this.saveTheme();
34
+ this.applyTheme();
35
+ });
36
+ checkbox.checked = this.currentTheme === 'light';
37
+ }
38
+ },
39
+
40
+ toggle() {
41
+ this.currentTheme = this.currentTheme === 'dark' ? 'light' : 'dark';
42
+ this.saveTheme();
43
+ this.applyTheme();
44
+
45
+ const checkbox = document.getElementById('theme-toggle-checkbox');
46
+ if (checkbox) {
47
+ checkbox.checked = this.currentTheme === 'light';
48
+ }
49
+ },
50
+
51
+ saveTheme() {
52
+ localStorage.setItem(this.STORAGE_KEY, this.currentTheme);
53
+ },
54
+
55
+ applyTheme() {
56
+ document.documentElement.setAttribute('data-theme', this.currentTheme);
57
+ },
58
+
59
+ getCurrentTheme() {
60
+ return this.currentTheme;
61
+ }
62
+ };
63
+
64
+ window.ThemeManager = ThemeManager;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * UI Utility Functions
3
+ */
4
+
5
+ // ============================================================
6
+ // Toast
7
+ // ============================================================
8
+ function showToast(message, type = 'info') {
9
+ const container = document.getElementById('toast-container') || createToastContainer();
10
+ const toast = document.createElement('div');
11
+ toast.className = `toast ${type}`;
12
+ toast.textContent = message;
13
+ container.appendChild(toast);
14
+ setTimeout(() => {
15
+ toast.style.opacity = '0';
16
+ toast.style.transition = 'opacity 200ms';
17
+ setTimeout(() => toast.remove(), 200);
18
+ }, 3000);
19
+ }
20
+
21
+ function createToastContainer() {
22
+ const el = document.createElement('div');
23
+ el.id = 'toast-container';
24
+ el.className = 'toast-container';
25
+ document.body.appendChild(el);
26
+ return el;
27
+ }
28
+
29
+ // ============================================================
30
+ // Utilities
31
+ // ============================================================
32
+ function escapeHtml(text) {
33
+ const div = document.createElement('div');
34
+ div.textContent = String(text ?? '');
35
+ return div.innerHTML;
36
+ }
37
+
38
+ function copySQL(btn) {
39
+ const code = btn.closest('.sql-block')?.querySelector('code')?.textContent;
40
+ if (code) {
41
+ navigator.clipboard.writeText(code).then(() => {
42
+ btn.textContent = '已复制!';
43
+ setTimeout(() => btn.textContent = '复制', 2000);
44
+ }).catch(() => {
45
+ showToast('复制失败', 'error');
46
+ });
47
+ }
48
+ }
49
+
50
+ // Expose functions
51
+ window.showToast = showToast;
52
+ window.escapeHtml = escapeHtml;
53
+ window.copySQL = copySQL;