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.
- dtSpark/__init__.py +0 -0
- dtSpark/_description.txt +1 -0
- dtSpark/_full_name.txt +1 -0
- dtSpark/_licence.txt +21 -0
- dtSpark/_metadata.yaml +6 -0
- dtSpark/_name.txt +1 -0
- dtSpark/_version.txt +1 -0
- dtSpark/aws/__init__.py +7 -0
- dtSpark/aws/authentication.py +296 -0
- dtSpark/aws/bedrock.py +578 -0
- dtSpark/aws/costs.py +318 -0
- dtSpark/aws/pricing.py +580 -0
- dtSpark/cli_interface.py +2645 -0
- dtSpark/conversation_manager.py +3050 -0
- dtSpark/core/__init__.py +12 -0
- dtSpark/core/application.py +3355 -0
- dtSpark/core/context_compaction.py +735 -0
- dtSpark/daemon/__init__.py +104 -0
- dtSpark/daemon/__main__.py +10 -0
- dtSpark/daemon/action_monitor.py +213 -0
- dtSpark/daemon/daemon_app.py +730 -0
- dtSpark/daemon/daemon_manager.py +289 -0
- dtSpark/daemon/execution_coordinator.py +194 -0
- dtSpark/daemon/pid_file.py +169 -0
- dtSpark/database/__init__.py +482 -0
- dtSpark/database/autonomous_actions.py +1191 -0
- dtSpark/database/backends.py +329 -0
- dtSpark/database/connection.py +122 -0
- dtSpark/database/conversations.py +520 -0
- dtSpark/database/credential_prompt.py +218 -0
- dtSpark/database/files.py +205 -0
- dtSpark/database/mcp_ops.py +355 -0
- dtSpark/database/messages.py +161 -0
- dtSpark/database/schema.py +673 -0
- dtSpark/database/tool_permissions.py +186 -0
- dtSpark/database/usage.py +167 -0
- dtSpark/files/__init__.py +4 -0
- dtSpark/files/manager.py +322 -0
- dtSpark/launch.py +39 -0
- dtSpark/limits/__init__.py +10 -0
- dtSpark/limits/costs.py +296 -0
- dtSpark/limits/tokens.py +342 -0
- dtSpark/llm/__init__.py +17 -0
- dtSpark/llm/anthropic_direct.py +446 -0
- dtSpark/llm/base.py +146 -0
- dtSpark/llm/context_limits.py +438 -0
- dtSpark/llm/manager.py +177 -0
- dtSpark/llm/ollama.py +578 -0
- dtSpark/mcp_integration/__init__.py +5 -0
- dtSpark/mcp_integration/manager.py +653 -0
- dtSpark/mcp_integration/tool_selector.py +225 -0
- dtSpark/resources/config.yaml.template +631 -0
- dtSpark/safety/__init__.py +22 -0
- dtSpark/safety/llm_service.py +111 -0
- dtSpark/safety/patterns.py +229 -0
- dtSpark/safety/prompt_inspector.py +442 -0
- dtSpark/safety/violation_logger.py +346 -0
- dtSpark/scheduler/__init__.py +20 -0
- dtSpark/scheduler/creation_tools.py +599 -0
- dtSpark/scheduler/execution_queue.py +159 -0
- dtSpark/scheduler/executor.py +1152 -0
- dtSpark/scheduler/manager.py +395 -0
- dtSpark/tools/__init__.py +4 -0
- dtSpark/tools/builtin.py +833 -0
- dtSpark/web/__init__.py +20 -0
- dtSpark/web/auth.py +152 -0
- dtSpark/web/dependencies.py +37 -0
- dtSpark/web/endpoints/__init__.py +17 -0
- dtSpark/web/endpoints/autonomous_actions.py +1125 -0
- dtSpark/web/endpoints/chat.py +621 -0
- dtSpark/web/endpoints/conversations.py +353 -0
- dtSpark/web/endpoints/main_menu.py +547 -0
- dtSpark/web/endpoints/streaming.py +421 -0
- dtSpark/web/server.py +578 -0
- dtSpark/web/session.py +167 -0
- dtSpark/web/ssl_utils.py +195 -0
- dtSpark/web/static/css/dark-theme.css +427 -0
- dtSpark/web/static/js/actions.js +1101 -0
- dtSpark/web/static/js/chat.js +614 -0
- dtSpark/web/static/js/main.js +496 -0
- dtSpark/web/static/js/sse-client.js +242 -0
- dtSpark/web/templates/actions.html +408 -0
- dtSpark/web/templates/base.html +93 -0
- dtSpark/web/templates/chat.html +814 -0
- dtSpark/web/templates/conversations.html +350 -0
- dtSpark/web/templates/goodbye.html +81 -0
- dtSpark/web/templates/login.html +90 -0
- dtSpark/web/templates/main_menu.html +983 -0
- dtSpark/web/templates/new_conversation.html +191 -0
- dtSpark/web/web_interface.py +137 -0
- dtspark-1.0.4.dist-info/METADATA +187 -0
- dtspark-1.0.4.dist-info/RECORD +96 -0
- dtspark-1.0.4.dist-info/WHEEL +5 -0
- dtspark-1.0.4.dist-info/entry_points.txt +3 -0
- dtspark-1.0.4.dist-info/licenses/LICENSE +21 -0
- 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, "&")
|
|
441
|
+
.replace(/</g, "<")
|
|
442
|
+
.replace(/>/g, ">")
|
|
443
|
+
.replace(/"/g, """)
|
|
444
|
+
.replace(/'/g, "'");
|
|
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
|
+
}
|