dtSpark 1.1.0a2__py3-none-any.whl → 1.1.0a6__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/_version.txt +1 -1
- dtSpark/aws/authentication.py +1 -1
- dtSpark/aws/bedrock.py +238 -239
- dtSpark/aws/costs.py +9 -5
- dtSpark/aws/pricing.py +25 -21
- dtSpark/cli_interface.py +69 -62
- dtSpark/conversation_manager.py +54 -47
- dtSpark/core/application.py +151 -111
- dtSpark/core/context_compaction.py +241 -226
- dtSpark/daemon/__init__.py +36 -22
- dtSpark/daemon/action_monitor.py +46 -17
- dtSpark/daemon/daemon_app.py +126 -104
- dtSpark/daemon/daemon_manager.py +59 -23
- dtSpark/daemon/pid_file.py +3 -2
- dtSpark/database/autonomous_actions.py +3 -0
- dtSpark/database/credential_prompt.py +52 -54
- dtSpark/files/manager.py +6 -12
- dtSpark/limits/__init__.py +1 -1
- dtSpark/limits/tokens.py +2 -2
- dtSpark/llm/anthropic_direct.py +246 -141
- dtSpark/llm/ollama.py +3 -1
- dtSpark/mcp_integration/manager.py +4 -4
- dtSpark/mcp_integration/tool_selector.py +83 -77
- dtSpark/resources/config.yaml.template +10 -0
- dtSpark/safety/patterns.py +45 -46
- dtSpark/safety/prompt_inspector.py +8 -1
- dtSpark/scheduler/creation_tools.py +273 -181
- dtSpark/scheduler/executor.py +503 -221
- dtSpark/tools/builtin.py +70 -53
- dtSpark/web/endpoints/autonomous_actions.py +12 -9
- dtSpark/web/endpoints/chat.py +18 -6
- dtSpark/web/endpoints/conversations.py +57 -17
- dtSpark/web/endpoints/main_menu.py +132 -105
- dtSpark/web/endpoints/streaming.py +2 -2
- dtSpark/web/server.py +65 -5
- dtSpark/web/ssl_utils.py +3 -3
- dtSpark/web/static/css/dark-theme.css +8 -29
- dtSpark/web/static/js/actions.js +2 -1
- dtSpark/web/static/js/chat.js +6 -8
- dtSpark/web/static/js/main.js +8 -8
- dtSpark/web/static/js/sse-client.js +130 -122
- dtSpark/web/templates/actions.html +5 -5
- dtSpark/web/templates/base.html +13 -0
- dtSpark/web/templates/chat.html +52 -50
- dtSpark/web/templates/conversations.html +50 -22
- dtSpark/web/templates/goodbye.html +2 -2
- dtSpark/web/templates/main_menu.html +17 -17
- dtSpark/web/templates/new_conversation.html +51 -20
- dtSpark/web/web_interface.py +2 -2
- {dtspark-1.1.0a2.dist-info → dtspark-1.1.0a6.dist-info}/METADATA +9 -2
- dtspark-1.1.0a6.dist-info/RECORD +96 -0
- dtspark-1.1.0a2.dist-info/RECORD +0 -96
- {dtspark-1.1.0a2.dist-info → dtspark-1.1.0a6.dist-info}/WHEEL +0 -0
- {dtspark-1.1.0a2.dist-info → dtspark-1.1.0a6.dist-info}/entry_points.txt +0 -0
- {dtspark-1.1.0a2.dist-info → dtspark-1.1.0a6.dist-info}/licenses/LICENSE +0 -0
- {dtspark-1.1.0a2.dist-info → dtspark-1.1.0a6.dist-info}/top_level.txt +0 -0
dtSpark/web/static/js/actions.js
CHANGED
|
@@ -624,7 +624,8 @@ async function loadModels() {
|
|
|
624
624
|
const response = await fetch('/api/models');
|
|
625
625
|
if (!response.ok) throw new Error('Failed to load models');
|
|
626
626
|
|
|
627
|
-
|
|
627
|
+
const data = await response.json();
|
|
628
|
+
availableModels = data.models || data;
|
|
628
629
|
|
|
629
630
|
const select = document.getElementById('actionModel');
|
|
630
631
|
select.innerHTML = '<option value="">Select a model...</option>';
|
dtSpark/web/static/js/chat.js
CHANGED
|
@@ -50,8 +50,6 @@ async function loadChatHistory(conversationId) {
|
|
|
50
50
|
* @param {string} timestamp - Message timestamp (optional)
|
|
51
51
|
*/
|
|
52
52
|
function appendMessage(role, content, timestamp = null) {
|
|
53
|
-
const messagesContainer = document.getElementById('chat-messages');
|
|
54
|
-
|
|
55
53
|
// Check if content contains tool results
|
|
56
54
|
if (content.startsWith('[TOOL_RESULTS]')) {
|
|
57
55
|
appendToolResults(content, timestamp);
|
|
@@ -107,7 +105,7 @@ function appendRegularMessage(role, content, timestamp = null) {
|
|
|
107
105
|
messageDiv.className = `chat-message ${role}`;
|
|
108
106
|
|
|
109
107
|
// Generate unique ID for the copy button
|
|
110
|
-
const copyBtnId = 'copy-btn-' + Date.now() + '-' + Math.random().toString(36).
|
|
108
|
+
const copyBtnId = 'copy-btn-' + Date.now() + '-' + Math.random().toString(36).substring(2, 11);
|
|
111
109
|
|
|
112
110
|
// Create message header with copy icon
|
|
113
111
|
const header = document.createElement('div');
|
|
@@ -308,8 +306,8 @@ function removeStatus(idOrElement) {
|
|
|
308
306
|
document.getElementById(idOrElement) :
|
|
309
307
|
idOrElement;
|
|
310
308
|
|
|
311
|
-
if (element
|
|
312
|
-
element.
|
|
309
|
+
if (element?.parentNode) {
|
|
310
|
+
element.remove();
|
|
313
311
|
}
|
|
314
312
|
}
|
|
315
313
|
|
|
@@ -360,7 +358,7 @@ function updateStreamingMessage(content, messageElement = null) {
|
|
|
360
358
|
messageElement.className = 'chat-message assistant';
|
|
361
359
|
|
|
362
360
|
// Generate unique ID for the copy button
|
|
363
|
-
const copyBtnId = 'stream-copy-btn-' + Date.now() + '-' + Math.random().toString(36).
|
|
361
|
+
const copyBtnId = 'stream-copy-btn-' + Date.now() + '-' + Math.random().toString(36).substring(2, 11);
|
|
364
362
|
|
|
365
363
|
messageElement.innerHTML = `
|
|
366
364
|
<div class="message-header d-flex justify-content-between align-items-center">
|
|
@@ -463,7 +461,7 @@ function showToast(message, type = 'info') {
|
|
|
463
461
|
const toast = document.createElement('div');
|
|
464
462
|
toast.id = toastId;
|
|
465
463
|
toast.className = 'toast';
|
|
466
|
-
toast.
|
|
464
|
+
toast.role = 'alert';
|
|
467
465
|
|
|
468
466
|
let bgClass = 'bg-primary';
|
|
469
467
|
if (type === 'success') bgClass = 'bg-success';
|
|
@@ -550,7 +548,7 @@ async function showToolPermissionDialog(requestId, toolName, toolDescription) {
|
|
|
550
548
|
const buttons = modalElement.querySelectorAll('.permission-btn');
|
|
551
549
|
buttons.forEach(button => {
|
|
552
550
|
button.addEventListener('click', async () => {
|
|
553
|
-
const response = button.
|
|
551
|
+
const response = button.dataset.response;
|
|
554
552
|
|
|
555
553
|
// Send response to server
|
|
556
554
|
try {
|
dtSpark/web/static/js/main.js
CHANGED
|
@@ -74,7 +74,7 @@ if (typeof marked !== 'undefined') {
|
|
|
74
74
|
|
|
75
75
|
// Handle mermaid diagrams
|
|
76
76
|
if (lang === 'mermaid') {
|
|
77
|
-
const id = 'mermaid-' + Math.random().toString(36).
|
|
77
|
+
const id = 'mermaid-' + Math.random().toString(36).substring(2, 11);
|
|
78
78
|
return `<div class="mermaid-container"><pre class="mermaid" id="${id}">${escapeHtmlForMermaid(code)}</pre></div>`;
|
|
79
79
|
}
|
|
80
80
|
|
|
@@ -157,7 +157,7 @@ async function copySvgToClipboard(svgElement, button) {
|
|
|
157
157
|
bgRect.setAttribute('width', '100%');
|
|
158
158
|
bgRect.setAttribute('height', '100%');
|
|
159
159
|
bgRect.setAttribute('fill', '#1a1a1a');
|
|
160
|
-
svgClone.
|
|
160
|
+
svgClone.prepend(bgRect);
|
|
161
161
|
|
|
162
162
|
// Serialise SVG to string
|
|
163
163
|
const serializer = new XMLSerializer();
|
|
@@ -255,7 +255,7 @@ async function renderMermaidDiagrams(container) {
|
|
|
255
255
|
|
|
256
256
|
for (const block of mermaidBlocks) {
|
|
257
257
|
try {
|
|
258
|
-
const id = block.id || 'mermaid-' + Math.random().toString(36).
|
|
258
|
+
const id = block.id || 'mermaid-' + Math.random().toString(36).substring(2, 11);
|
|
259
259
|
const code = block.textContent;
|
|
260
260
|
|
|
261
261
|
// Render the diagram
|
|
@@ -287,7 +287,7 @@ async function renderMermaidDiagrams(container) {
|
|
|
287
287
|
}
|
|
288
288
|
};
|
|
289
289
|
|
|
290
|
-
block.
|
|
290
|
+
block.replaceWith(wrapper);
|
|
291
291
|
} catch (e) {
|
|
292
292
|
console.error('Mermaid rendering error:', e);
|
|
293
293
|
// Show error message in the block
|
|
@@ -370,9 +370,9 @@ function showToast(message, type = 'info') {
|
|
|
370
370
|
// Create toast
|
|
371
371
|
const toast = document.createElement('div');
|
|
372
372
|
toast.className = `toast align-items-center text-white bg-${type === 'error' ? 'danger' : type} border-0`;
|
|
373
|
-
toast.
|
|
374
|
-
toast.
|
|
375
|
-
toast.
|
|
373
|
+
toast.role = 'alert';
|
|
374
|
+
toast.ariaLive = 'assertive';
|
|
375
|
+
toast.ariaAtomic = 'true';
|
|
376
376
|
|
|
377
377
|
toast.innerHTML = `
|
|
378
378
|
<div class="d-flex">
|
|
@@ -432,7 +432,7 @@ function downloadFile(content, filename, mimeType = 'text/plain') {
|
|
|
432
432
|
link.download = filename;
|
|
433
433
|
document.body.appendChild(link);
|
|
434
434
|
link.click();
|
|
435
|
-
|
|
435
|
+
link.remove();
|
|
436
436
|
URL.revokeObjectURL(url);
|
|
437
437
|
}
|
|
438
438
|
|
|
@@ -17,151 +17,159 @@ async function sendMessageWithSSE(conversationId, message) {
|
|
|
17
17
|
let streamingMessageElement = null;
|
|
18
18
|
let accumulatedContent = '';
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
try {
|
|
22
|
+
// Create EventSource for SSE
|
|
23
|
+
const encodedMessage = encodeURIComponent(message);
|
|
24
|
+
const eventSource = new EventSource(
|
|
25
|
+
`/api/stream/chat?message=${encodedMessage}&conversation_id=${conversationId}`
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Handle different event types
|
|
29
|
+
eventSource.addEventListener('status', (event) => {
|
|
30
|
+
const data = JSON.parse(event.data);
|
|
31
|
+
console.log('Status:', data);
|
|
32
|
+
|
|
33
|
+
// Update typing indicator with status
|
|
34
|
+
if (typingIndicator && typingIndicator.parentNode) {
|
|
35
|
+
const statusText = data.message || '';
|
|
36
|
+
typingIndicator.querySelector('.visually-hidden').nextSibling.textContent = statusText;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
eventSource.addEventListener('response', (event) => {
|
|
41
|
+
const data = JSON.parse(event.data);
|
|
42
|
+
console.log('Received response event:', data);
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
// Hide typing indicator on first response
|
|
45
|
+
if (typingIndicator && typingIndicator.parentNode) {
|
|
46
|
+
console.log('Hiding typing indicator');
|
|
47
|
+
hideTypingIndicator();
|
|
48
|
+
typingIndicator = null; // Clear reference after hiding
|
|
49
|
+
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
51
|
+
if (data.type === 'text') {
|
|
52
|
+
if (data.final) {
|
|
53
|
+
// Final response - this is the main assistant response after tool execution
|
|
54
|
+
console.log('Final response received, content length:', data.content ? data.content.length : 0);
|
|
55
|
+
accumulatedContent += data.content;
|
|
56
|
+
console.log('Accumulated content:', accumulatedContent);
|
|
57
|
+
|
|
58
|
+
// Ensure we have content before displaying
|
|
59
|
+
if (accumulatedContent.trim().length > 0) {
|
|
60
|
+
streamingMessageElement = updateStreamingMessage(
|
|
61
|
+
accumulatedContent,
|
|
62
|
+
streamingMessageElement
|
|
63
|
+
);
|
|
64
|
+
console.log('Updated streaming message element:', streamingMessageElement);
|
|
65
|
+
} else {
|
|
66
|
+
console.warn('No content to display');
|
|
67
|
+
}
|
|
68
|
+
eventSource.close();
|
|
69
|
+
resolve();
|
|
64
70
|
} else {
|
|
65
|
-
|
|
71
|
+
// Non-final response - text that appears with tool calls
|
|
72
|
+
// Display immediately as a separate message
|
|
73
|
+
console.log('Non-final response, appending:', data.content);
|
|
74
|
+
appendMessage('assistant', data.content);
|
|
66
75
|
}
|
|
67
|
-
eventSource.close();
|
|
68
|
-
} else {
|
|
69
|
-
// Non-final response - text that appears with tool calls
|
|
70
|
-
// Display immediately as a separate message
|
|
71
|
-
console.log('Non-final response, appending:', data.content);
|
|
72
|
-
appendMessage('assistant', data.content);
|
|
73
76
|
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
eventSource.addEventListener('tool_start', (event) => {
|
|
80
|
+
const data = JSON.parse(event.data);
|
|
81
|
+
appendToolCall(data.tool_name, data.input);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
eventSource.addEventListener('tool_complete', (event) => {
|
|
85
|
+
const data = JSON.parse(event.data);
|
|
86
|
+
// Display tool result
|
|
87
|
+
appendToolResult(data.tool_use_id, { content: data.content });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
eventSource.addEventListener('tool_error', (event) => {
|
|
91
|
+
const data = JSON.parse(event.data);
|
|
92
|
+
appendToolResult(data.tool_name, { error: data.error });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
eventSource.addEventListener('permission_request', (event) => {
|
|
96
|
+
const data = JSON.parse(event.data);
|
|
97
|
+
console.log('Permission request received:', data);
|
|
98
|
+
|
|
99
|
+
// Show permission modal/dialog
|
|
100
|
+
showToolPermissionDialog(data.request_id, data.tool_name, data.tool_description);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
eventSource.addEventListener('progress', (event) => {
|
|
104
|
+
const data = JSON.parse(event.data);
|
|
105
|
+
// Update progress (if we want to show a progress bar)
|
|
106
|
+
console.log('Progress:', data);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
eventSource.addEventListener('complete', (event) => {
|
|
110
|
+
// Stream complete
|
|
111
|
+
console.log('Stream complete');
|
|
112
|
+
eventSource.close();
|
|
87
113
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
114
|
+
// Hide typing indicator if still visible
|
|
115
|
+
if (typingIndicator && typingIndicator.parentNode) {
|
|
116
|
+
console.log('Hiding typing indicator on complete');
|
|
117
|
+
hideTypingIndicator();
|
|
118
|
+
typingIndicator = null;
|
|
119
|
+
}
|
|
92
120
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
console.log('Permission request received:', data);
|
|
121
|
+
resolve();
|
|
122
|
+
});
|
|
96
123
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
124
|
+
eventSource.addEventListener('error', (event) => {
|
|
125
|
+
console.error('SSE error:', event);
|
|
126
|
+
const data = JSON.parse(event.data);
|
|
100
127
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
128
|
+
// Hide typing indicator
|
|
129
|
+
if (typingIndicator && typingIndicator.parentNode) {
|
|
130
|
+
console.log('Hiding typing indicator on error');
|
|
131
|
+
hideTypingIndicator();
|
|
132
|
+
typingIndicator = null;
|
|
133
|
+
}
|
|
106
134
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
console.log('Stream complete');
|
|
110
|
-
eventSource.close();
|
|
135
|
+
// Show error message
|
|
136
|
+
appendMessage('system', `Error: ${data.message}`);
|
|
111
137
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
hideTypingIndicator();
|
|
116
|
-
typingIndicator = null;
|
|
117
|
-
}
|
|
118
|
-
});
|
|
138
|
+
eventSource.close();
|
|
139
|
+
resolve();
|
|
140
|
+
});
|
|
119
141
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const data = JSON.parse(event.data);
|
|
142
|
+
eventSource.onerror = (error) => {
|
|
143
|
+
console.error('EventSource failed:', error);
|
|
123
144
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
145
|
+
// Hide typing indicator
|
|
146
|
+
if (typingIndicator && typingIndicator.parentNode) {
|
|
147
|
+
console.log('Hiding typing indicator on connection error');
|
|
148
|
+
hideTypingIndicator();
|
|
149
|
+
typingIndicator = null;
|
|
150
|
+
}
|
|
130
151
|
|
|
131
|
-
|
|
132
|
-
|
|
152
|
+
// If we haven't received any content, show error
|
|
153
|
+
if (!streamingMessageElement) {
|
|
154
|
+
appendMessage('system', 'Connection error. Please try again.');
|
|
155
|
+
}
|
|
133
156
|
|
|
134
|
-
|
|
135
|
-
|
|
157
|
+
eventSource.close();
|
|
158
|
+
resolve();
|
|
159
|
+
};
|
|
136
160
|
|
|
137
|
-
|
|
138
|
-
console.error('
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('Error sending message:', error);
|
|
139
163
|
|
|
140
164
|
// Hide typing indicator
|
|
141
|
-
if (typingIndicator
|
|
142
|
-
console.log('Hiding typing indicator on connection error');
|
|
165
|
+
if (typingIndicator) {
|
|
143
166
|
hideTypingIndicator();
|
|
144
|
-
typingIndicator = null;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// If we haven't received any content, show error
|
|
148
|
-
if (!streamingMessageElement) {
|
|
149
|
-
appendMessage('system', 'Connection error. Please try again.');
|
|
150
167
|
}
|
|
151
168
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
} catch (error) {
|
|
156
|
-
console.error('Error sending message:', error);
|
|
157
|
-
|
|
158
|
-
// Hide typing indicator
|
|
159
|
-
if (typingIndicator) {
|
|
160
|
-
hideTypingIndicator();
|
|
169
|
+
showToast('Failed to send message', 'error');
|
|
170
|
+
resolve();
|
|
161
171
|
}
|
|
162
|
-
|
|
163
|
-
showToast('Failed to send message', 'error');
|
|
164
|
-
}
|
|
172
|
+
});
|
|
165
173
|
}
|
|
166
174
|
|
|
167
175
|
/**
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
<tbody id="actions-tbody">
|
|
54
54
|
<tr>
|
|
55
55
|
<td colspan="9" class="text-center text-muted py-4">
|
|
56
|
-
<
|
|
56
|
+
<output class="spinner-border spinner-border-sm"></output>
|
|
57
57
|
Loading actions...
|
|
58
58
|
</td>
|
|
59
59
|
</tr>
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
<tbody id="runs-tbody">
|
|
86
86
|
<tr>
|
|
87
87
|
<td colspan="7" class="text-center text-muted py-4">
|
|
88
|
-
<
|
|
88
|
+
<output class="spinner-border spinner-border-sm"></output>
|
|
89
89
|
Loading recent runs...
|
|
90
90
|
</td>
|
|
91
91
|
</tr>
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
|
|
173
173
|
<!-- Tool Permissions -->
|
|
174
174
|
<div class="mb-3">
|
|
175
|
-
<
|
|
175
|
+
<span class="form-label d-block">Tool Permissions</span>
|
|
176
176
|
<div class="card">
|
|
177
177
|
<div class="card-body" style="max-height: 200px; overflow-y: auto;">
|
|
178
178
|
<div id="toolPermissions">
|
|
@@ -374,14 +374,14 @@
|
|
|
374
374
|
<div class="alert alert-success mb-0">
|
|
375
375
|
<i class="bi bi-check-circle-fill"></i>
|
|
376
376
|
Action created successfully!
|
|
377
|
-
<
|
|
377
|
+
<button type="button" id="viewCreatedAction" class="btn btn-link p-0 align-baseline" onclick="viewCreatedAction();">View action details</button>
|
|
378
378
|
</div>
|
|
379
379
|
</div>
|
|
380
380
|
</div>
|
|
381
381
|
|
|
382
382
|
<!-- Loading Indicator -->
|
|
383
383
|
<div id="aiCreateLoading" class="d-none text-center py-4">
|
|
384
|
-
<
|
|
384
|
+
<output class="spinner-border text-primary"></output>
|
|
385
385
|
<p class="mt-2 text-muted">AI is thinking...</p>
|
|
386
386
|
</div>
|
|
387
387
|
</div>
|
dtSpark/web/templates/base.html
CHANGED
|
@@ -53,11 +53,13 @@
|
|
|
53
53
|
<i class="bi bi-plus-circle"></i> New
|
|
54
54
|
</a>
|
|
55
55
|
</li>
|
|
56
|
+
{% if actions_enabled %}
|
|
56
57
|
<li class="nav-item">
|
|
57
58
|
<a class="nav-link" href="/actions">
|
|
58
59
|
<i class="bi bi-robot"></i> Actions
|
|
59
60
|
</a>
|
|
60
61
|
</li>
|
|
62
|
+
{% endif %}
|
|
61
63
|
</ul>
|
|
62
64
|
<ul class="navbar-nav">
|
|
63
65
|
<li class="nav-item">
|
|
@@ -89,5 +91,16 @@
|
|
|
89
91
|
<script src="/static/js/main.js"></script>
|
|
90
92
|
|
|
91
93
|
{% block extra_scripts %}{% endblock %}
|
|
94
|
+
|
|
95
|
+
{% if heartbeat_enabled %}
|
|
96
|
+
<!-- Browser heartbeat - auto-shutdown when browser closes -->
|
|
97
|
+
<script>
|
|
98
|
+
setInterval(() => {
|
|
99
|
+
fetch('/api/heartbeat', { method: 'POST' }).catch(() => {});
|
|
100
|
+
}, {{ heartbeat_interval_ms }});
|
|
101
|
+
// Send initial heartbeat immediately
|
|
102
|
+
fetch('/api/heartbeat', { method: 'POST' }).catch(() => {});
|
|
103
|
+
</script>
|
|
104
|
+
{% endif %}
|
|
92
105
|
</body>
|
|
93
106
|
</html>
|