dtSpark 1.0.4__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 (96) hide show
  1. dtSpark/__init__.py +0 -0
  2. dtSpark/_description.txt +1 -0
  3. dtSpark/_full_name.txt +1 -0
  4. dtSpark/_licence.txt +21 -0
  5. dtSpark/_metadata.yaml +6 -0
  6. dtSpark/_name.txt +1 -0
  7. dtSpark/_version.txt +1 -0
  8. dtSpark/aws/__init__.py +7 -0
  9. dtSpark/aws/authentication.py +296 -0
  10. dtSpark/aws/bedrock.py +578 -0
  11. dtSpark/aws/costs.py +318 -0
  12. dtSpark/aws/pricing.py +580 -0
  13. dtSpark/cli_interface.py +2645 -0
  14. dtSpark/conversation_manager.py +3050 -0
  15. dtSpark/core/__init__.py +12 -0
  16. dtSpark/core/application.py +3355 -0
  17. dtSpark/core/context_compaction.py +735 -0
  18. dtSpark/daemon/__init__.py +104 -0
  19. dtSpark/daemon/__main__.py +10 -0
  20. dtSpark/daemon/action_monitor.py +213 -0
  21. dtSpark/daemon/daemon_app.py +730 -0
  22. dtSpark/daemon/daemon_manager.py +289 -0
  23. dtSpark/daemon/execution_coordinator.py +194 -0
  24. dtSpark/daemon/pid_file.py +169 -0
  25. dtSpark/database/__init__.py +482 -0
  26. dtSpark/database/autonomous_actions.py +1191 -0
  27. dtSpark/database/backends.py +329 -0
  28. dtSpark/database/connection.py +122 -0
  29. dtSpark/database/conversations.py +520 -0
  30. dtSpark/database/credential_prompt.py +218 -0
  31. dtSpark/database/files.py +205 -0
  32. dtSpark/database/mcp_ops.py +355 -0
  33. dtSpark/database/messages.py +161 -0
  34. dtSpark/database/schema.py +673 -0
  35. dtSpark/database/tool_permissions.py +186 -0
  36. dtSpark/database/usage.py +167 -0
  37. dtSpark/files/__init__.py +4 -0
  38. dtSpark/files/manager.py +322 -0
  39. dtSpark/launch.py +39 -0
  40. dtSpark/limits/__init__.py +10 -0
  41. dtSpark/limits/costs.py +296 -0
  42. dtSpark/limits/tokens.py +342 -0
  43. dtSpark/llm/__init__.py +17 -0
  44. dtSpark/llm/anthropic_direct.py +446 -0
  45. dtSpark/llm/base.py +146 -0
  46. dtSpark/llm/context_limits.py +438 -0
  47. dtSpark/llm/manager.py +177 -0
  48. dtSpark/llm/ollama.py +578 -0
  49. dtSpark/mcp_integration/__init__.py +5 -0
  50. dtSpark/mcp_integration/manager.py +653 -0
  51. dtSpark/mcp_integration/tool_selector.py +225 -0
  52. dtSpark/resources/config.yaml.template +631 -0
  53. dtSpark/safety/__init__.py +22 -0
  54. dtSpark/safety/llm_service.py +111 -0
  55. dtSpark/safety/patterns.py +229 -0
  56. dtSpark/safety/prompt_inspector.py +442 -0
  57. dtSpark/safety/violation_logger.py +346 -0
  58. dtSpark/scheduler/__init__.py +20 -0
  59. dtSpark/scheduler/creation_tools.py +599 -0
  60. dtSpark/scheduler/execution_queue.py +159 -0
  61. dtSpark/scheduler/executor.py +1152 -0
  62. dtSpark/scheduler/manager.py +395 -0
  63. dtSpark/tools/__init__.py +4 -0
  64. dtSpark/tools/builtin.py +833 -0
  65. dtSpark/web/__init__.py +20 -0
  66. dtSpark/web/auth.py +152 -0
  67. dtSpark/web/dependencies.py +37 -0
  68. dtSpark/web/endpoints/__init__.py +17 -0
  69. dtSpark/web/endpoints/autonomous_actions.py +1125 -0
  70. dtSpark/web/endpoints/chat.py +621 -0
  71. dtSpark/web/endpoints/conversations.py +353 -0
  72. dtSpark/web/endpoints/main_menu.py +547 -0
  73. dtSpark/web/endpoints/streaming.py +421 -0
  74. dtSpark/web/server.py +578 -0
  75. dtSpark/web/session.py +167 -0
  76. dtSpark/web/ssl_utils.py +195 -0
  77. dtSpark/web/static/css/dark-theme.css +427 -0
  78. dtSpark/web/static/js/actions.js +1101 -0
  79. dtSpark/web/static/js/chat.js +614 -0
  80. dtSpark/web/static/js/main.js +496 -0
  81. dtSpark/web/static/js/sse-client.js +242 -0
  82. dtSpark/web/templates/actions.html +408 -0
  83. dtSpark/web/templates/base.html +93 -0
  84. dtSpark/web/templates/chat.html +814 -0
  85. dtSpark/web/templates/conversations.html +350 -0
  86. dtSpark/web/templates/goodbye.html +81 -0
  87. dtSpark/web/templates/login.html +90 -0
  88. dtSpark/web/templates/main_menu.html +983 -0
  89. dtSpark/web/templates/new_conversation.html +191 -0
  90. dtSpark/web/web_interface.py +137 -0
  91. dtspark-1.0.4.dist-info/METADATA +187 -0
  92. dtspark-1.0.4.dist-info/RECORD +96 -0
  93. dtspark-1.0.4.dist-info/WHEEL +5 -0
  94. dtspark-1.0.4.dist-info/entry_points.txt +3 -0
  95. dtspark-1.0.4.dist-info/licenses/LICENSE +21 -0
  96. dtspark-1.0.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,614 @@
1
+ /**
2
+ * Chat interface JavaScript for Spark web interface
3
+ *
4
+ * Handles chat message display, history loading, and UI interactions
5
+ */
6
+
7
+ /**
8
+ * Load chat history for a conversation
9
+ * @param {number} conversationId - The conversation ID
10
+ */
11
+ async function loadChatHistory(conversationId) {
12
+ try {
13
+ const response = await fetch(`/api/chat/${conversationId}/history`);
14
+ const data = await response.json();
15
+
16
+ const messagesContainer = document.getElementById('chat-messages');
17
+ messagesContainer.innerHTML = '';
18
+
19
+ if (data.messages.length === 0) {
20
+ messagesContainer.innerHTML = `
21
+ <div class="text-center text-muted">
22
+ <i class="bi bi-chat"></i>
23
+ <p class="mt-2">No messages yet. Start the conversation!</p>
24
+ </div>
25
+ `;
26
+ return;
27
+ }
28
+
29
+ data.messages.forEach(message => {
30
+ appendMessage(message.role, message.content, message.timestamp);
31
+ });
32
+
33
+ // Scroll to bottom
34
+ scrollToBottom();
35
+
36
+ } catch (error) {
37
+ console.error('Error loading chat history:', error);
38
+ document.getElementById('chat-messages').innerHTML = `
39
+ <div class="alert alert-danger">
40
+ <i class="bi bi-exclamation-triangle-fill"></i> Failed to load chat history
41
+ </div>
42
+ `;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Append a message to the chat
48
+ * @param {string} role - Message role ('user' or 'assistant')
49
+ * @param {string} content - Message content
50
+ * @param {string} timestamp - Message timestamp (optional)
51
+ */
52
+ function appendMessage(role, content, timestamp = null) {
53
+ const messagesContainer = document.getElementById('chat-messages');
54
+
55
+ // Check if content contains tool results
56
+ if (content.startsWith('[TOOL_RESULTS]')) {
57
+ appendToolResults(content, timestamp);
58
+ return;
59
+ }
60
+
61
+ // Check if content contains tool calls (JSON with tool_use blocks)
62
+ if (role === 'assistant' && content.trim().startsWith('[')) {
63
+ try {
64
+ const blocks = JSON.parse(content);
65
+ if (Array.isArray(blocks)) {
66
+ // Check if this contains tool_use blocks
67
+ const hasToolUse = blocks.some(block => block.type === 'tool_use');
68
+ if (hasToolUse) {
69
+ // Display tool calls and text separately
70
+ blocks.forEach(block => {
71
+ if (block.type === 'text' && block.text) {
72
+ // Display text content normally
73
+ appendRegularMessage('assistant', block.text, timestamp);
74
+ } else if (block.type === 'tool_use') {
75
+ // Display tool call
76
+ appendToolCall(block.name, block.input);
77
+ }
78
+ });
79
+ return;
80
+ }
81
+ }
82
+ } catch (e) {
83
+ // Not JSON or not the expected format, fall through to regular display
84
+ }
85
+ }
86
+
87
+ // Check if this is a rollup summary
88
+ if (content.startsWith('[Summary of previous conversation]')) {
89
+ appendRollupSummary(content, timestamp);
90
+ return;
91
+ }
92
+
93
+ // Regular message display
94
+ appendRegularMessage(role, content, timestamp);
95
+ }
96
+
97
+ /**
98
+ * Append a regular message (no special formatting)
99
+ * @param {string} role - Message role
100
+ * @param {string} content - Message content
101
+ * @param {string} timestamp - Message timestamp (optional)
102
+ */
103
+ function appendRegularMessage(role, content, timestamp = null) {
104
+ const messagesContainer = document.getElementById('chat-messages');
105
+
106
+ const messageDiv = document.createElement('div');
107
+ messageDiv.className = `chat-message ${role}`;
108
+
109
+ // Generate unique ID for the copy button
110
+ const copyBtnId = 'copy-btn-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
111
+
112
+ // Create message header with copy icon
113
+ const header = document.createElement('div');
114
+ header.className = 'message-header d-flex justify-content-between align-items-center';
115
+ header.innerHTML = `
116
+ <div>
117
+ <i class="bi bi-${role === 'user' ? 'person-fill' : 'robot'}"></i>
118
+ <strong>${role === 'user' ? 'You' : 'Assistant'}</strong>
119
+ ${timestamp ? `<span class="ms-2 text-muted small">${formatTimestamp(timestamp)}</span>` : ''}
120
+ </div>
121
+ <button id="${copyBtnId}" class="btn btn-link btn-sm p-0 copy-icon-btn" title="Copy to clipboard">
122
+ <i class="bi bi-clipboard"></i>
123
+ </button>
124
+ `;
125
+
126
+ // Create message content
127
+ const contentDiv = document.createElement('div');
128
+ contentDiv.className = 'message-content markdown-content';
129
+
130
+ if (role === 'assistant') {
131
+ // Render markdown for assistant messages
132
+ contentDiv.innerHTML = marked.parse(content);
133
+ } else {
134
+ // Plain text for user messages
135
+ contentDiv.textContent = content;
136
+ }
137
+
138
+ messageDiv.appendChild(header);
139
+ messageDiv.appendChild(contentDiv);
140
+ messagesContainer.appendChild(messageDiv);
141
+
142
+ // Add click handler for copy button after element is in DOM
143
+ const copyBtn = document.getElementById(copyBtnId);
144
+ if (copyBtn) {
145
+ copyBtn.onclick = () => copyMessageToClipboard(content, copyBtn);
146
+ }
147
+
148
+ // Render mermaid diagrams if present (async, non-blocking)
149
+ if (role === 'assistant' && typeof renderMermaidDiagrams === 'function') {
150
+ renderMermaidDiagrams(contentDiv);
151
+ }
152
+
153
+ // Scroll to bottom
154
+ scrollToBottom();
155
+ }
156
+
157
+ /**
158
+ * Append tool results from [TOOL_RESULTS] content
159
+ * @param {string} content - Content starting with [TOOL_RESULTS]
160
+ * @param {string} timestamp - Message timestamp (optional)
161
+ */
162
+ function appendToolResults(content, timestamp = null) {
163
+ const messagesContainer = document.getElementById('chat-messages');
164
+
165
+ try {
166
+ // Parse the JSON after the [TOOL_RESULTS] marker
167
+ const jsonContent = content.replace('[TOOL_RESULTS]', '').trim();
168
+ const results = JSON.parse(jsonContent);
169
+
170
+ if (Array.isArray(results)) {
171
+ results.forEach((result, index) => {
172
+ const resultDiv = document.createElement('div');
173
+ resultDiv.className = 'tool-result';
174
+
175
+ const toolId = result.tool_use_id || 'unknown';
176
+ const resultContent = result.content || JSON.stringify(result);
177
+
178
+ resultDiv.innerHTML = `
179
+ <div class="small">
180
+ <strong><i class="bi bi-check-circle-fill"></i> Tool Result ${index + 1}:</strong>
181
+ <code>${escapeHtml(toolId)}</code>
182
+ ${timestamp ? `<span class="ms-2">${formatTimestamp(timestamp)}</span>` : ''}
183
+ </div>
184
+ <pre class="small mb-0 mt-1">${escapeHtml(typeof resultContent === 'string' ? resultContent : JSON.stringify(resultContent, null, 2))}</pre>
185
+ `;
186
+
187
+ messagesContainer.appendChild(resultDiv);
188
+ });
189
+ }
190
+ } catch (e) {
191
+ console.error('Error parsing tool results:', e);
192
+ // Fall back to displaying the raw content
193
+ const resultDiv = document.createElement('div');
194
+ resultDiv.className = 'tool-result';
195
+ resultDiv.innerHTML = `
196
+ <div class="small"><strong><i class="bi bi-check-circle-fill"></i> Tool Results</strong></div>
197
+ <pre class="small mb-0 mt-1">${escapeHtml(content)}</pre>
198
+ `;
199
+ messagesContainer.appendChild(resultDiv);
200
+ }
201
+
202
+ scrollToBottom();
203
+ }
204
+
205
+ /**
206
+ * Append a rollup summary message
207
+ * @param {string} content - Rollup summary content
208
+ * @param {string} timestamp - Message timestamp (optional)
209
+ */
210
+ function appendRollupSummary(content, timestamp = null) {
211
+ const messagesContainer = document.getElementById('chat-messages');
212
+
213
+ const summaryDiv = document.createElement('div');
214
+ summaryDiv.className = 'chat-message rollup-summary';
215
+ summaryDiv.innerHTML = `
216
+ <div class="message-header">
217
+ <i class="bi bi-archive-fill"></i>
218
+ <strong>Rollup Summary</strong>
219
+ ${timestamp ? `<span class="ms-2">${formatTimestamp(timestamp)}</span>` : ''}
220
+ </div>
221
+ <div class="message-content markdown-content">
222
+ ${marked.parse(content.replace('[Summary of previous conversation]', '').trim())}
223
+ </div>
224
+ `;
225
+
226
+ messagesContainer.appendChild(summaryDiv);
227
+ scrollToBottom();
228
+ }
229
+
230
+ /**
231
+ * Append a tool call message
232
+ * @param {string} toolName - Tool name
233
+ * @param {object} toolInput - Tool input parameters
234
+ */
235
+ function appendToolCall(toolName, toolInput) {
236
+ const messagesContainer = document.getElementById('chat-messages');
237
+
238
+ const toolDiv = document.createElement('div');
239
+ toolDiv.className = 'tool-call';
240
+ toolDiv.innerHTML = `
241
+ <div class="small">
242
+ <strong><i class="bi bi-tools"></i> Tool Call:</strong> <code>${escapeHtml(toolName)}</code>
243
+ </div>
244
+ <pre class="small mb-0 mt-1">${escapeHtml(JSON.stringify(toolInput, null, 2))}</pre>
245
+ `;
246
+
247
+ messagesContainer.appendChild(toolDiv);
248
+ scrollToBottom();
249
+ }
250
+
251
+ /**
252
+ * Append a tool result message
253
+ * @param {string} toolName - Tool name
254
+ * @param {object} toolResult - Tool result data
255
+ */
256
+ function appendToolResult(toolName, toolResult) {
257
+ const messagesContainer = document.getElementById('chat-messages');
258
+
259
+ const resultDiv = document.createElement('div');
260
+ resultDiv.className = 'tool-result';
261
+ resultDiv.innerHTML = `
262
+ <div class="small">
263
+ <strong><i class="bi bi-check-circle-fill"></i> Tool Result:</strong> <code>${escapeHtml(toolName)}</code>
264
+ </div>
265
+ <pre class="small mb-0 mt-1">${escapeHtml(JSON.stringify(toolResult, null, 2))}</pre>
266
+ `;
267
+
268
+ messagesContainer.appendChild(resultDiv);
269
+ scrollToBottom();
270
+ }
271
+
272
+ /**
273
+ * Append a status message (e.g., "Processing...", "Generating response...")
274
+ * @param {string} message - Status message
275
+ * @param {string} id - Optional ID for the status element (for later removal)
276
+ * @returns {HTMLElement} The created status element
277
+ */
278
+ function appendStatus(message, id = null) {
279
+ const messagesContainer = document.getElementById('chat-messages');
280
+
281
+ const statusDiv = document.createElement('div');
282
+ statusDiv.className = 'chat-message system';
283
+ statusDiv.style.display = 'flex';
284
+ statusDiv.style.alignItems = 'center';
285
+ statusDiv.style.justifyContent = 'center';
286
+ if (id) {
287
+ statusDiv.id = id;
288
+ }
289
+ statusDiv.innerHTML = `
290
+ <div class="spinner-border spinner-border-sm me-2" role="status">
291
+ <span class="visually-hidden">Loading...</span>
292
+ </div>
293
+ <span>${escapeHtml(message)}</span>
294
+ `;
295
+
296
+ messagesContainer.appendChild(statusDiv);
297
+ scrollToBottom();
298
+
299
+ return statusDiv;
300
+ }
301
+
302
+ /**
303
+ * Remove a status message
304
+ * @param {string|HTMLElement} idOrElement - Status ID or element
305
+ */
306
+ function removeStatus(idOrElement) {
307
+ const element = typeof idOrElement === 'string' ?
308
+ document.getElementById(idOrElement) :
309
+ idOrElement;
310
+
311
+ if (element && element.parentNode) {
312
+ element.parentNode.removeChild(element);
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Scroll chat messages to bottom
318
+ */
319
+ function scrollToBottom() {
320
+ const messagesContainer = document.getElementById('chat-messages');
321
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
322
+ }
323
+
324
+ /**
325
+ * Clear all messages
326
+ */
327
+ function clearMessages() {
328
+ document.getElementById('chat-messages').innerHTML = '';
329
+ }
330
+
331
+ /**
332
+ * Show typing indicator
333
+ * @returns {HTMLElement} The typing indicator element
334
+ */
335
+ function showTypingIndicator() {
336
+ return appendStatus('', 'typing-indicator');
337
+ }
338
+
339
+ /**
340
+ * Hide typing indicator
341
+ */
342
+ function hideTypingIndicator() {
343
+ removeStatus('typing-indicator');
344
+ }
345
+
346
+ /**
347
+ * Update an assistant message with streaming content
348
+ * @param {string} content - The content to append
349
+ * @param {HTMLElement} messageElement - The message element to update (optional)
350
+ * @returns {HTMLElement} The message element
351
+ */
352
+ function updateStreamingMessage(content, messageElement = null) {
353
+ console.log('updateStreamingMessage called with content:', content, 'messageElement:', messageElement);
354
+
355
+ if (!messageElement) {
356
+ // Create new message element with copy button
357
+ console.log('Creating new message element');
358
+ const messagesContainer = document.getElementById('chat-messages');
359
+ messageElement = document.createElement('div');
360
+ messageElement.className = 'chat-message assistant';
361
+
362
+ // Generate unique ID for the copy button
363
+ const copyBtnId = 'stream-copy-btn-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
364
+
365
+ messageElement.innerHTML = `
366
+ <div class="message-header d-flex justify-content-between align-items-center">
367
+ <div>
368
+ <i class="bi bi-robot"></i>
369
+ <strong>Assistant</strong>
370
+ </div>
371
+ <button id="${copyBtnId}" class="btn btn-link btn-sm p-0 copy-icon-btn" title="Copy to clipboard">
372
+ <i class="bi bi-clipboard"></i>
373
+ </button>
374
+ </div>
375
+ <div class="message-content markdown-content"></div>
376
+ `;
377
+ messagesContainer.appendChild(messageElement);
378
+
379
+ // Store content reference for copy functionality
380
+ messageElement.dataset.rawContent = content;
381
+
382
+ console.log('Message element created and appended');
383
+ } else {
384
+ // Update stored content for copy functionality
385
+ messageElement.dataset.rawContent = content;
386
+ }
387
+
388
+ // Update content
389
+ const contentDiv = messageElement.querySelector('.message-content');
390
+ console.log('Parsing content with marked');
391
+ try {
392
+ contentDiv.innerHTML = marked.parse(content);
393
+ console.log('Content updated, innerHTML:', contentDiv.innerHTML);
394
+ } catch (error) {
395
+ console.error('Error parsing markdown:', error);
396
+ // Fallback to plain text
397
+ contentDiv.textContent = content;
398
+ }
399
+
400
+ // Update copy button handler with latest content
401
+ const copyBtn = messageElement.querySelector('.copy-icon-btn');
402
+ if (copyBtn) {
403
+ copyBtn.onclick = () => copyMessageToClipboard(messageElement.dataset.rawContent, copyBtn);
404
+ }
405
+
406
+ // Render mermaid diagrams if present (async, non-blocking)
407
+ if (typeof renderMermaidDiagrams === 'function') {
408
+ renderMermaidDiagrams(contentDiv);
409
+ }
410
+
411
+ scrollToBottom();
412
+
413
+ return messageElement;
414
+ }
415
+
416
+ /**
417
+ * Format timestamp for display
418
+ * @param {string} timestamp - ISO timestamp string
419
+ * @returns {string} Formatted timestamp
420
+ */
421
+ function formatTimestamp(timestamp) {
422
+ try {
423
+ const date = new Date(timestamp);
424
+ return date.toLocaleString();
425
+ } catch (e) {
426
+ return timestamp;
427
+ }
428
+ }
429
+
430
+ /**
431
+ * Escape HTML to prevent XSS
432
+ * @param {string} unsafe - Unsafe string
433
+ * @returns {string} Escaped string
434
+ */
435
+ function escapeHtml(unsafe) {
436
+ if (typeof unsafe !== 'string') {
437
+ return unsafe;
438
+ }
439
+ return unsafe
440
+ .replace(/&/g, "&amp;")
441
+ .replace(/</g, "&lt;")
442
+ .replace(/>/g, "&gt;")
443
+ .replace(/"/g, "&quot;")
444
+ .replace(/'/g, "&#039;");
445
+ }
446
+
447
+ /**
448
+ * Show toast notification
449
+ * @param {string} message - Toast message
450
+ * @param {string} type - Toast type ('info', 'success', 'error', 'warning')
451
+ */
452
+ function showToast(message, type = 'info') {
453
+ // Check if toast container exists, create if not
454
+ let toastContainer = document.querySelector('.toast-container');
455
+ if (!toastContainer) {
456
+ toastContainer = document.createElement('div');
457
+ toastContainer.className = 'toast-container position-fixed bottom-0 end-0 p-3';
458
+ document.body.appendChild(toastContainer);
459
+ }
460
+
461
+ // Create toast element
462
+ const toastId = 'toast-' + Date.now();
463
+ const toast = document.createElement('div');
464
+ toast.id = toastId;
465
+ toast.className = 'toast';
466
+ toast.setAttribute('role', 'alert');
467
+
468
+ let bgClass = 'bg-primary';
469
+ if (type === 'success') bgClass = 'bg-success';
470
+ else if (type === 'error') bgClass = 'bg-danger';
471
+ else if (type === 'warning') bgClass = 'bg-warning';
472
+
473
+ toast.innerHTML = `
474
+ <div class="toast-header ${bgClass} text-white">
475
+ <strong class="me-auto">Notification</strong>
476
+ <button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
477
+ </div>
478
+ <div class="toast-body">${escapeHtml(message)}</div>
479
+ `;
480
+
481
+ toastContainer.appendChild(toast);
482
+
483
+ // Show toast
484
+ const bsToast = new bootstrap.Toast(toast);
485
+ bsToast.show();
486
+
487
+ // Remove from DOM after hidden
488
+ toast.addEventListener('hidden.bs.toast', () => {
489
+ toast.remove();
490
+ });
491
+ }
492
+
493
+ /**
494
+ * Show tool permission dialog and handle user response
495
+ * @param {string} requestId - The permission request ID
496
+ * @param {string} toolName - The tool name
497
+ * @param {string} toolDescription - Optional tool description
498
+ */
499
+ async function showToolPermissionDialog(requestId, toolName, toolDescription) {
500
+ // Create modal HTML
501
+ const modalHtml = `
502
+ <div class="modal fade" id="toolPermissionModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false">
503
+ <div class="modal-dialog modal-dialog-centered">
504
+ <div class="modal-content">
505
+ <div class="modal-header bg-warning text-dark">
506
+ <h5 class="modal-title">
507
+ <i class="bi bi-shield-lock-fill"></i> Tool Permission Request
508
+ </h5>
509
+ </div>
510
+ <div class="modal-body">
511
+ <p class="mb-3">
512
+ The assistant wants to use the tool: <strong>${escapeHtml(toolName)}</strong>
513
+ </p>
514
+ ${toolDescription ? `<p class="text-muted small mb-3">${escapeHtml(toolDescription)}</p>` : ''}
515
+ <p class="mb-2"><strong>Please choose an option:</strong></p>
516
+ <div class="d-grid gap-2">
517
+ <button type="button" class="btn btn-success permission-btn" data-response="once">
518
+ <i class="bi bi-play-circle"></i> Allow once - Run this time only
519
+ </button>
520
+ <button type="button" class="btn btn-primary permission-btn" data-response="allowed">
521
+ <i class="bi bi-check-circle"></i> Allow always - Run this time and all future times
522
+ </button>
523
+ <button type="button" class="btn btn-danger permission-btn" data-response="denied">
524
+ <i class="bi bi-x-circle"></i> Deny - Don't run this time or in the future
525
+ </button>
526
+ <button type="button" class="btn btn-secondary permission-btn" data-response="cancel">
527
+ <i class="bi bi-ban"></i> Cancel
528
+ </button>
529
+ </div>
530
+ </div>
531
+ </div>
532
+ </div>
533
+ </div>
534
+ `;
535
+
536
+ // Remove existing modal if present
537
+ const existingModal = document.getElementById('toolPermissionModal');
538
+ if (existingModal) {
539
+ existingModal.remove();
540
+ }
541
+
542
+ // Add modal to DOM
543
+ document.body.insertAdjacentHTML('beforeend', modalHtml);
544
+
545
+ // Get modal element
546
+ const modalElement = document.getElementById('toolPermissionModal');
547
+ const modal = new bootstrap.Modal(modalElement);
548
+
549
+ // Add click handlers to buttons
550
+ const buttons = modalElement.querySelectorAll('.permission-btn');
551
+ buttons.forEach(button => {
552
+ button.addEventListener('click', async () => {
553
+ const response = button.getAttribute('data-response');
554
+
555
+ // Send response to server
556
+ try {
557
+ const formData = new FormData();
558
+ formData.append('request_id', requestId);
559
+ formData.append('response', response);
560
+
561
+ const result = await fetch('/api/chat/permission/respond', {
562
+ method: 'POST',
563
+ body: formData
564
+ });
565
+
566
+ if (!result.ok) {
567
+ console.error('Failed to submit permission response:', result.statusText);
568
+ showToast('Failed to submit permission response', 'error');
569
+ }
570
+ } catch (error) {
571
+ console.error('Error submitting permission response:', error);
572
+ showToast('Error submitting permission response', 'error');
573
+ }
574
+
575
+ // Close modal
576
+ modal.hide();
577
+ });
578
+ });
579
+
580
+ // Clean up modal after it's hidden
581
+ modalElement.addEventListener('hidden.bs.modal', () => {
582
+ modalElement.remove();
583
+ });
584
+
585
+ // Show modal
586
+ modal.show();
587
+ }
588
+
589
+ /**
590
+ * Copy message content to clipboard
591
+ * @param {string} content - The message content to copy
592
+ * @param {HTMLElement} button - The copy button element
593
+ */
594
+ function copyMessageToClipboard(content, button) {
595
+ // Use the Clipboard API
596
+ navigator.clipboard.writeText(content).then(() => {
597
+ // Success - update icon temporarily
598
+ const icon = button.querySelector('i');
599
+ if (icon) {
600
+ icon.className = 'bi bi-check-circle-fill text-success';
601
+
602
+ // Reset icon after 2 seconds
603
+ setTimeout(() => {
604
+ icon.className = 'bi bi-clipboard';
605
+ }, 2000);
606
+ }
607
+
608
+ // Show toast notification
609
+ showToast('Message copied to clipboard', 'success');
610
+ }).catch(err => {
611
+ console.error('Failed to copy message:', err);
612
+ showToast('Failed to copy message', 'error');
613
+ });
614
+ }