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,814 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Chat - {{ conversation_name }} - {{ app_name }}{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="row">
7
+ <div class="col-12">
8
+ <h2 class="mb-3">
9
+ <i class="bi bi-chat-dots-fill"></i> {{ conversation_name }}
10
+ <button class="btn btn-sm btn-outline-secondary float-end" data-bs-toggle="modal" data-bs-target="#infoModal">
11
+ <i class="bi bi-info-circle"></i> Info
12
+ </button>
13
+ </h2>
14
+ </div>
15
+ </div>
16
+
17
+ <!-- Chat Messages Area -->
18
+ <div class="row">
19
+ <div class="col-12">
20
+ <div class="card mb-3" style="height: 60vh;">
21
+ <div class="card-body overflow-auto" id="chat-messages">
22
+ <!-- Messages will be appended here -->
23
+ <div class="text-center text-muted">
24
+ <div class="spinner-border spinner-border-sm" role="status">
25
+ <span class="visually-hidden">Loading...</span>
26
+ </div>
27
+ <span class="ms-2">Loading chat history...</span>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ </div>
33
+
34
+ <!-- Chat Input Area -->
35
+ <div class="row">
36
+ <div class="col-12">
37
+ <div class="card">
38
+ <div class="card-body">
39
+ <form id="chat-form">
40
+ <div class="mb-3">
41
+ <label for="message-input" class="form-label">Your Message</label>
42
+ <textarea
43
+ class="form-control"
44
+ id="message-input"
45
+ rows="3"
46
+ placeholder="Type your message here..."
47
+ required
48
+ ></textarea>
49
+ </div>
50
+ <div class="d-flex justify-content-between align-items-center">
51
+ <div class="btn-group" role="group">
52
+ <button type="button" class="btn btn-outline-secondary btn-sm" onclick="showCommandMenu()">
53
+ <i class="bi bi-terminal"></i> Commands
54
+ </button>
55
+ <button type="button" class="btn btn-outline-secondary btn-sm" onclick="attachFiles()">
56
+ <i class="bi bi-paperclip"></i> Attach
57
+ </button>
58
+ </div>
59
+ <button type="submit" class="btn btn-primary" id="send-btn">
60
+ <i class="bi bi-send-fill"></i> Send
61
+ </button>
62
+ </div>
63
+ </form>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ </div>
68
+
69
+ <!-- Info Modal -->
70
+ <div class="modal fade" id="infoModal" tabindex="-1">
71
+ <div class="modal-dialog modal-lg">
72
+ <div class="modal-content">
73
+ <div class="modal-header">
74
+ <h5 class="modal-title"><i class="bi bi-info-circle"></i> Conversation Information</h5>
75
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
76
+ </div>
77
+ <div class="modal-body" id="info-content">
78
+ <div class="text-center">
79
+ <div class="spinner-border spinner-border-sm" role="status">
80
+ <span class="visually-hidden">Loading...</span>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ </div>
87
+
88
+ <script>
89
+ // Load conversation info when modal is shown
90
+ document.getElementById('infoModal').addEventListener('show.bs.modal', function() {
91
+ loadConversationInfo();
92
+ });
93
+ </script>
94
+
95
+ <!-- Command Menu Modal -->
96
+ <div class="modal fade" id="commandModal" tabindex="-1">
97
+ <div class="modal-dialog">
98
+ <div class="modal-content">
99
+ <div class="modal-header">
100
+ <h5 class="modal-title"><i class="bi bi-terminal"></i> Commands</h5>
101
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
102
+ </div>
103
+ <div class="modal-body">
104
+ <div class="list-group">
105
+ <button class="list-group-item list-group-item-action" onclick="executeCommand('history')">
106
+ <i class="bi bi-clock-history"></i> View History
107
+ </button>
108
+ <button class="list-group-item list-group-item-action" onclick="executeCommand('info')">
109
+ <i class="bi bi-info-circle"></i> Show Info
110
+ </button>
111
+ <button class="list-group-item list-group-item-action" onclick="executeCommand('changemodel')">
112
+ <i class="bi bi-arrow-repeat"></i> Change Model
113
+ </button>
114
+ <button class="list-group-item list-group-item-action" onclick="executeCommand('export')">
115
+ <i class="bi bi-download"></i> Export Conversation
116
+ </button>
117
+ <button class="list-group-item list-group-item-action" onclick="executeCommand('mcpaudit')">
118
+ <i class="bi bi-shield-check"></i> MCP Audit Log
119
+ </button>
120
+ <button class="list-group-item list-group-item-action" onclick="executeCommand('mcpservers')">
121
+ <i class="bi bi-hdd-network"></i> MCP Server Management
122
+ </button>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ {% endblock %}
129
+
130
+ {% block extra_scripts %}
131
+ <script src="/static/js/chat.js"></script>
132
+ <script src="/static/js/sse-client.js"></script>
133
+ <script>
134
+ // Initialise chat with conversation ID
135
+ const conversationId = {{ conversation_id }};
136
+
137
+ // Load chat history on page load
138
+ window.addEventListener('load', () => {
139
+ loadChatHistory(conversationId);
140
+ });
141
+
142
+ // Handle chat form submission
143
+ document.getElementById('chat-form').addEventListener('submit', async (e) => {
144
+ e.preventDefault();
145
+
146
+ const messageInput = document.getElementById('message-input');
147
+ const message = messageInput.value.trim();
148
+
149
+ if (!message) return;
150
+
151
+ // Clear input
152
+ messageInput.value = '';
153
+
154
+ // Disable send button
155
+ const sendBtn = document.getElementById('send-btn');
156
+ sendBtn.disabled = true;
157
+ sendBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> Sending...';
158
+
159
+ // Add user message to chat
160
+ appendMessage('user', message);
161
+
162
+ // Send message via SSE
163
+ await sendMessageWithSSE(conversationId, message);
164
+
165
+ // Re-enable send button
166
+ sendBtn.disabled = false;
167
+ sendBtn.innerHTML = '<i class="bi bi-send-fill"></i> Send';
168
+
169
+ // Focus on input
170
+ messageInput.focus();
171
+ });
172
+
173
+ // Show command menu
174
+ function showCommandMenu() {
175
+ const modal = new bootstrap.Modal(document.getElementById('commandModal'));
176
+ modal.show();
177
+ }
178
+
179
+ // Execute command
180
+ async function executeCommand(command) {
181
+ // Close modal
182
+ bootstrap.Modal.getInstance(document.getElementById('commandModal')).hide();
183
+
184
+ // Execute command based on type
185
+ switch (command) {
186
+ case 'info':
187
+ await loadConversationInfo();
188
+ const infoModal = new bootstrap.Modal(document.getElementById('infoModal'));
189
+ infoModal.show();
190
+ break;
191
+ case 'history':
192
+ await loadChatHistory(conversationId);
193
+ break;
194
+ case 'export':
195
+ await showExportModal();
196
+ break;
197
+ case 'changemodel':
198
+ await showChangeModelModal();
199
+ break;
200
+ case 'mcpaudit':
201
+ await showMCPAuditModal();
202
+ break;
203
+ case 'mcpservers':
204
+ await showMCPServersModal();
205
+ break;
206
+ default:
207
+ alert(`Command '${command}' not yet implemented in web UI`);
208
+ }
209
+ }
210
+
211
+ // Export conversation
212
+ async function showExportModal() {
213
+ const modalHTML = `
214
+ <div class="modal fade" id="exportModal" tabindex="-1">
215
+ <div class="modal-dialog">
216
+ <div class="modal-content">
217
+ <div class="modal-header">
218
+ <h5 class="modal-title"><i class="bi bi-download"></i> Export Conversation</h5>
219
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
220
+ </div>
221
+ <div class="modal-body">
222
+ <div class="mb-3">
223
+ <label class="form-label">Export Format</label>
224
+ <select class="form-select" id="export-format">
225
+ <option value="markdown">Markdown (.md)</option>
226
+ <option value="html">HTML (.html)</option>
227
+ <option value="csv">CSV (.csv)</option>
228
+ </select>
229
+ </div>
230
+ <div class="form-check">
231
+ <input class="form-check-input" type="checkbox" id="export-include-tools" checked>
232
+ <label class="form-check-label" for="export-include-tools">
233
+ Include tool call details
234
+ </label>
235
+ </div>
236
+ </div>
237
+ <div class="modal-footer">
238
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
239
+ <button type="button" class="btn btn-primary" onclick="performExport()">
240
+ <i class="bi bi-download"></i> Export
241
+ </button>
242
+ </div>
243
+ </div>
244
+ </div>
245
+ </div>
246
+ `;
247
+
248
+ // Remove old modal if exists
249
+ const oldModal = document.getElementById('exportModal');
250
+ if (oldModal) oldModal.remove();
251
+
252
+ document.body.insertAdjacentHTML('beforeend', modalHTML);
253
+ const modal = new bootstrap.Modal(document.getElementById('exportModal'));
254
+ modal.show();
255
+ }
256
+
257
+ async function performExport() {
258
+ const format = document.getElementById('export-format').value;
259
+ const includeTools = document.getElementById('export-include-tools').checked;
260
+
261
+ try {
262
+ const formData = new FormData();
263
+ formData.append('format', format);
264
+ formData.append('include_tools', includeTools);
265
+
266
+ const response = await fetch(`/api/chat/${conversationId}/command/export`, {
267
+ method: 'POST',
268
+ body: formData
269
+ });
270
+
271
+ const data = await response.json();
272
+
273
+ if (data.status === 'success') {
274
+ // Download file
275
+ const blob = new Blob([data.content], { type: 'text/plain' });
276
+ const url = window.URL.createObjectURL(blob);
277
+ const a = document.createElement('a');
278
+ a.href = url;
279
+ a.download = `conversation_${conversationId}.${data.format === 'markdown' ? 'md' : data.format}`;
280
+ document.body.appendChild(a);
281
+ a.click();
282
+ window.URL.revokeObjectURL(url);
283
+ a.remove();
284
+
285
+ // Close modal
286
+ bootstrap.Modal.getInstance(document.getElementById('exportModal')).hide();
287
+ showToast('Conversation exported successfully', 'success');
288
+ }
289
+ } catch (error) {
290
+ showToast('Failed to export conversation', 'error');
291
+ }
292
+ }
293
+
294
+ // Change model
295
+ async function showChangeModelModal() {
296
+ const modalHTML = `
297
+ <div class="modal fade" id="changeModelModal" tabindex="-1">
298
+ <div class="modal-dialog">
299
+ <div class="modal-content">
300
+ <div class="modal-header">
301
+ <h5 class="modal-title"><i class="bi bi-arrow-repeat"></i> Change Model</h5>
302
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
303
+ </div>
304
+ <div class="modal-body">
305
+ <div class="mb-3">
306
+ <label class="form-label">Select New Model</label>
307
+ <select class="form-select" id="new-model-select">
308
+ <option value="">Loading models...</option>
309
+ </select>
310
+ </div>
311
+ </div>
312
+ <div class="modal-footer">
313
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
314
+ <button type="button" class="btn btn-primary" onclick="performChangeModel()">
315
+ <i class="bi bi-check-circle-fill"></i> Change Model
316
+ </button>
317
+ </div>
318
+ </div>
319
+ </div>
320
+ </div>
321
+ `;
322
+
323
+ // Remove old modal if exists
324
+ const oldModal = document.getElementById('changeModelModal');
325
+ if (oldModal) oldModal.remove();
326
+
327
+ document.body.insertAdjacentHTML('beforeend', modalHTML);
328
+
329
+ // Load models
330
+ try {
331
+ const response = await fetch('/api/models');
332
+ const models = await response.json();
333
+
334
+ const select = document.getElementById('new-model-select');
335
+ select.innerHTML = '<option value="">Select a model...</option>';
336
+
337
+ models.forEach(model => {
338
+ const option = document.createElement('option');
339
+ option.value = model.id;
340
+
341
+ // Show service provider and optionally model maker
342
+ let displayText = model.name;
343
+ if (model.model_maker && model.provider !== model.model_maker) {
344
+ // Show both: "Model Name [Model Maker via Service Provider]"
345
+ displayText += ` [${model.model_maker} via ${model.provider}]`;
346
+ } else {
347
+ // Show service only: "Model Name [Service Provider]"
348
+ displayText += ` [${model.provider}]`;
349
+ }
350
+
351
+ option.textContent = displayText;
352
+ select.appendChild(option);
353
+ });
354
+ } catch (error) {
355
+ document.getElementById('new-model-select').innerHTML = '<option value="">Failed to load models</option>';
356
+ }
357
+
358
+ const modal = new bootstrap.Modal(document.getElementById('changeModelModal'));
359
+ modal.show();
360
+ }
361
+
362
+ async function performChangeModel() {
363
+ const modelId = document.getElementById('new-model-select').value;
364
+
365
+ if (!modelId) {
366
+ showToast('Please select a model', 'error');
367
+ return;
368
+ }
369
+
370
+ try {
371
+ const formData = new FormData();
372
+ formData.append('model_id', modelId);
373
+
374
+ const response = await fetch(`/api/chat/${conversationId}/command/changemodel`, {
375
+ method: 'POST',
376
+ body: formData
377
+ });
378
+
379
+ const data = await response.json();
380
+
381
+ if (data.status === 'success') {
382
+ bootstrap.Modal.getInstance(document.getElementById('changeModelModal')).hide();
383
+ showToast('Model changed successfully', 'success');
384
+ }
385
+ } catch (error) {
386
+ showToast('Failed to change model', 'error');
387
+ }
388
+ }
389
+
390
+ // MCP Audit Log
391
+ async function showMCPAuditModal() {
392
+ const modalHTML = `
393
+ <div class="modal fade" id="mcpAuditModal" tabindex="-1">
394
+ <div class="modal-dialog modal-xl">
395
+ <div class="modal-content">
396
+ <div class="modal-header">
397
+ <h5 class="modal-title"><i class="bi bi-shield-check"></i> MCP Audit Log</h5>
398
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
399
+ </div>
400
+ <div class="modal-body" id="mcp-audit-content">
401
+ <div class="text-center">
402
+ <div class="spinner-border spinner-border-sm" role="status"></div>
403
+ </div>
404
+ </div>
405
+ </div>
406
+ </div>
407
+ </div>
408
+ `;
409
+
410
+ // Remove old modal if exists
411
+ const oldModal = document.getElementById('mcpAuditModal');
412
+ if (oldModal) oldModal.remove();
413
+
414
+ document.body.insertAdjacentHTML('beforeend', modalHTML);
415
+
416
+ // Load audit log
417
+ try {
418
+ const response = await fetch(`/api/chat/${conversationId}/command/mcpaudit`);
419
+ const data = await response.json();
420
+
421
+ const transactions = data.data.transactions;
422
+
423
+ let html = '';
424
+ if (transactions.length === 0) {
425
+ html = '<p class="text-muted">No MCP transactions recorded</p>';
426
+ } else {
427
+ html = `
428
+ <div class="table-responsive">
429
+ <table class="table table-sm table-striped">
430
+ <thead>
431
+ <tr>
432
+ <th>Time</th>
433
+ <th>Tool</th>
434
+ <th>Server</th>
435
+ <th>Status</th>
436
+ <th>Exec Time</th>
437
+ </tr>
438
+ </thead>
439
+ <tbody>
440
+ `;
441
+
442
+ transactions.forEach(tx => {
443
+ const timestamp = new Date(tx.transaction_timestamp).toLocaleString();
444
+ const status = tx.is_error ? '<span class="badge bg-danger">Error</span>' : '<span class="badge bg-success">Success</span>';
445
+
446
+ html += `
447
+ <tr>
448
+ <td class="small">${timestamp}</td>
449
+ <td><code class="small">${escapeHtml(tx.tool_name)}</code></td>
450
+ <td class="small">${escapeHtml(tx.tool_server)}</td>
451
+ <td>${status}</td>
452
+ <td class="small">${tx.execution_time_ms}ms</td>
453
+ </tr>
454
+ `;
455
+ });
456
+
457
+ html += `
458
+ </tbody>
459
+ </table>
460
+ </div>
461
+ `;
462
+ }
463
+
464
+ document.getElementById('mcp-audit-content').innerHTML = html;
465
+ } catch (error) {
466
+ document.getElementById('mcp-audit-content').innerHTML = `
467
+ <div class="alert alert-danger">Failed to load MCP audit log</div>
468
+ `;
469
+ }
470
+
471
+ const modal = new bootstrap.Modal(document.getElementById('mcpAuditModal'));
472
+ modal.show();
473
+ }
474
+
475
+ // MCP Server Management
476
+ async function showMCPServersModal() {
477
+ const modalHTML = `
478
+ <div class="modal fade" id="mcpServersModal" tabindex="-1">
479
+ <div class="modal-dialog modal-lg">
480
+ <div class="modal-content">
481
+ <div class="modal-header">
482
+ <h5 class="modal-title"><i class="bi bi-hdd-network"></i> MCP Server Management</h5>
483
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
484
+ </div>
485
+ <div class="modal-body" id="mcp-servers-content">
486
+ <div class="text-center">
487
+ <div class="spinner-border spinner-border-sm" role="status"></div>
488
+ </div>
489
+ </div>
490
+ </div>
491
+ </div>
492
+ </div>
493
+ `;
494
+
495
+ // Remove old modal if exists
496
+ const oldModal = document.getElementById('mcpServersModal');
497
+ if (oldModal) oldModal.remove();
498
+
499
+ document.body.insertAdjacentHTML('beforeend', modalHTML);
500
+
501
+ // Load server states
502
+ try {
503
+ const response = await fetch(`/api/chat/${conversationId}/command/mcpservers`);
504
+ const data = await response.json();
505
+
506
+ const servers = data.data.servers;
507
+
508
+ let html = '';
509
+ if (servers.length === 0) {
510
+ html = '<p class="text-muted">No MCP servers configured</p>';
511
+ } else {
512
+ html = '<div class="list-group">';
513
+
514
+ servers.forEach(server => {
515
+ const enabled = server.enabled;
516
+ const badge = enabled ?
517
+ '<span class="badge bg-success">Enabled</span>' :
518
+ '<span class="badge bg-secondary">Disabled</span>';
519
+
520
+ html += `
521
+ <div class="list-group-item">
522
+ <div class="d-flex justify-content-between align-items-center">
523
+ <div>
524
+ <h6 class="mb-1">${escapeHtml(server.server_name)}</h6>
525
+ ${badge}
526
+ </div>
527
+ <div class="form-check form-switch">
528
+ <input
529
+ class="form-check-input"
530
+ type="checkbox"
531
+ id="server-${escapeHtml(server.server_name)}"
532
+ ${enabled ? 'checked' : ''}
533
+ onchange="toggleMCPServer('${escapeHtml(server.server_name)}', this.checked)"
534
+ >
535
+ </div>
536
+ </div>
537
+ </div>
538
+ `;
539
+ });
540
+
541
+ html += '</div>';
542
+ }
543
+
544
+ document.getElementById('mcp-servers-content').innerHTML = html;
545
+ } catch (error) {
546
+ document.getElementById('mcp-servers-content').innerHTML = `
547
+ <div class="alert alert-danger">Failed to load MCP servers</div>
548
+ `;
549
+ }
550
+
551
+ const modal = new bootstrap.Modal(document.getElementById('mcpServersModal'));
552
+ modal.show();
553
+ }
554
+
555
+ async function toggleMCPServer(serverName, enabled) {
556
+ try {
557
+ const formData = new FormData();
558
+ formData.append('server_name', serverName);
559
+ formData.append('enabled', enabled);
560
+
561
+ const response = await fetch(`/api/chat/${conversationId}/command/mcpservers/toggle`, {
562
+ method: 'POST',
563
+ body: formData
564
+ });
565
+
566
+ const data = await response.json();
567
+
568
+ if (data.status === 'success') {
569
+ showToast(`Server ${serverName} ${enabled ? 'enabled' : 'disabled'}`, 'success');
570
+ }
571
+ } catch (error) {
572
+ showToast('Failed to toggle server', 'error');
573
+ }
574
+ }
575
+
576
+ // Load conversation info
577
+ async function loadConversationInfo() {
578
+ try {
579
+ document.getElementById('info-content').innerHTML = `
580
+ <div class="text-center">
581
+ <div class="spinner-border spinner-border-sm" role="status">
582
+ <span class="visually-hidden">Loading...</span>
583
+ </div>
584
+ </div>
585
+ `;
586
+
587
+ const response = await fetch(`/api/chat/${conversationId}/command/info`, {
588
+ method: 'POST'
589
+ });
590
+ const data = await response.json();
591
+
592
+ const conv = data.data.conversation;
593
+ const modelUsage = data.data.model_usage;
594
+ const rollupSettings = data.data.rollup_settings;
595
+
596
+ let infoHTML = `
597
+ <div class="mb-3">
598
+ <h6>Conversation Details</h6>
599
+ <ul class="list-unstyled small">
600
+ <li><strong>Name:</strong> ${conv.name}</li>
601
+ <li><strong>Model:</strong> <code>${conv.model_id}</code></li>
602
+ <li><strong>Created:</strong> ${new Date(conv.created_at).toLocaleString()}</li>
603
+ <li><strong>Total Tokens:</strong> ${conv.total_tokens.toLocaleString()}</li>
604
+ </ul>
605
+ </div>
606
+ `;
607
+
608
+ if (modelUsage && modelUsage.length > 0) {
609
+ infoHTML += `
610
+ <div class="mb-3">
611
+ <h6>Model Usage</h6>
612
+ <div class="table-responsive">
613
+ <table class="table table-sm">
614
+ <thead>
615
+ <tr>
616
+ <th>Model</th>
617
+ <th>Input</th>
618
+ <th>Output</th>
619
+ <th>Total</th>
620
+ </tr>
621
+ </thead>
622
+ <tbody>
623
+ `;
624
+
625
+ modelUsage.forEach(usage => {
626
+ infoHTML += `
627
+ <tr>
628
+ <td><code class="small">${usage.model_id}</code></td>
629
+ <td>${usage.input_tokens.toLocaleString()}</td>
630
+ <td>${usage.output_tokens.toLocaleString()}</td>
631
+ <td>${usage.total_tokens.toLocaleString()}</td>
632
+ </tr>
633
+ `;
634
+ });
635
+
636
+ infoHTML += `
637
+ </tbody>
638
+ </table>
639
+ </div>
640
+ </div>
641
+ `;
642
+ }
643
+
644
+ // Add rollup/compaction settings
645
+ if (rollupSettings && !rollupSettings.error) {
646
+ const usagePercent = rollupSettings.context_usage_percent || 0;
647
+ const compactionThresholdPercent = (rollupSettings.compaction_threshold * 100).toFixed(0);
648
+ const emergencyThresholdPercent = (rollupSettings.emergency_threshold * 100).toFixed(0);
649
+
650
+ // Determine status colour based on usage
651
+ let statusClass = 'text-success';
652
+ let statusText = 'Normal';
653
+ if (usagePercent >= rollupSettings.emergency_threshold * 100) {
654
+ statusClass = 'text-danger';
655
+ statusText = 'Emergency';
656
+ } else if (usagePercent >= rollupSettings.compaction_threshold * 100) {
657
+ statusClass = 'text-warning';
658
+ statusText = 'Compaction Due';
659
+ }
660
+
661
+ infoHTML += `
662
+ <div class="mb-3">
663
+ <h6>Context &amp; Rollup Settings</h6>
664
+ <div class="small">
665
+ <div class="mb-2">
666
+ <strong>Context Usage:</strong>
667
+ <span class="${statusClass}">${usagePercent}%</span>
668
+ <span class="text-muted">(${rollupSettings.current_tokens?.toLocaleString() || 0} / ${rollupSettings.context_window?.toLocaleString() || 0} tokens)</span>
669
+ <span class="badge ${statusClass === 'text-success' ? 'bg-success' : statusClass === 'text-warning' ? 'bg-warning text-dark' : 'bg-danger'}">${statusText}</span>
670
+ </div>
671
+ <div class="progress mb-2" style="height: 8px;">
672
+ <div class="progress-bar ${usagePercent >= rollupSettings.emergency_threshold * 100 ? 'bg-danger' : usagePercent >= rollupSettings.compaction_threshold * 100 ? 'bg-warning' : 'bg-success'}"
673
+ role="progressbar"
674
+ style="width: ${Math.min(usagePercent, 100)}%">
675
+ </div>
676
+ <div class="position-absolute" style="left: ${compactionThresholdPercent}%; width: 2px; height: 8px; background-color: orange; top: 0;"></div>
677
+ <div class="position-absolute" style="left: ${emergencyThresholdPercent}%; width: 2px; height: 8px; background-color: red; top: 0;"></div>
678
+ </div>
679
+ <ul class="list-unstyled mb-0">
680
+ <li><strong>Context Window:</strong> ${rollupSettings.context_window?.toLocaleString() || 'Unknown'} tokens</li>
681
+ <li><strong>Max Output:</strong> ${rollupSettings.max_output?.toLocaleString() || 'Unknown'} tokens</li>
682
+ <li><strong>Compaction Threshold:</strong> ${compactionThresholdPercent}% (${rollupSettings.compaction_trigger_tokens?.toLocaleString() || 0} tokens)</li>
683
+ <li><strong>Emergency Threshold:</strong> ${emergencyThresholdPercent}% (${rollupSettings.emergency_trigger_tokens?.toLocaleString() || 0} tokens)</li>
684
+ <li><strong>Provider:</strong> ${rollupSettings.provider || 'Unknown'}</li>
685
+ </ul>
686
+ </div>
687
+ </div>
688
+ `;
689
+ } else if (rollupSettings && rollupSettings.error) {
690
+ infoHTML += `
691
+ <div class="mb-3">
692
+ <h6>Context &amp; Rollup Settings</h6>
693
+ <p class="text-muted small">Unable to load rollup settings: ${rollupSettings.error}</p>
694
+ </div>
695
+ `;
696
+ }
697
+
698
+ document.getElementById('info-content').innerHTML = infoHTML;
699
+
700
+ } catch (error) {
701
+ document.getElementById('info-content').innerHTML = `
702
+ <div class="alert alert-danger">
703
+ <i class="bi bi-exclamation-triangle-fill"></i> Failed to load conversation info
704
+ </div>
705
+ `;
706
+ }
707
+ }
708
+
709
+ // Attach files
710
+ function showAttachFilesModal() {
711
+ const modalHTML = `
712
+ <div class="modal fade" id="attachFilesModal" tabindex="-1">
713
+ <div class="modal-dialog">
714
+ <div class="modal-content">
715
+ <div class="modal-header">
716
+ <h5 class="modal-title"><i class="bi bi-paperclip"></i> Attach Files</h5>
717
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
718
+ </div>
719
+ <div class="modal-body">
720
+ <div class="mb-3">
721
+ <label class="form-label">Select files to attach</label>
722
+ <input type="file" class="form-control" id="attach-files-input" multiple>
723
+ <div class="form-text">
724
+ Supported formats: txt, md, json, yaml, yml, xml, csv, log, py, js, ts, java, cpp, c, h
725
+ </div>
726
+ </div>
727
+ <div id="attach-files-list" class="mb-3"></div>
728
+ </div>
729
+ <div class="modal-footer">
730
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
731
+ <button type="button" class="btn btn-primary" onclick="performAttachFiles()">
732
+ <i class="bi bi-paperclip"></i> Attach Files
733
+ </button>
734
+ </div>
735
+ </div>
736
+ </div>
737
+ </div>
738
+ `;
739
+
740
+ // Remove old modal if exists
741
+ const oldModal = document.getElementById('attachFilesModal');
742
+ if (oldModal) oldModal.remove();
743
+
744
+ document.body.insertAdjacentHTML('beforeend', modalHTML);
745
+
746
+ // Add file selection change listener
747
+ const fileInput = document.getElementById('attach-files-input');
748
+ fileInput.addEventListener('change', function() {
749
+ const filesList = document.getElementById('attach-files-list');
750
+ if (this.files.length > 0) {
751
+ let html = '<div class="alert alert-info"><strong>Selected files:</strong><ul class="mb-0 mt-2">';
752
+ for (let i = 0; i < this.files.length; i++) {
753
+ html += `<li>${this.files[i].name} (${(this.files[i].size / 1024).toFixed(2)} KB)</li>`;
754
+ }
755
+ html += '</ul></div>';
756
+ filesList.innerHTML = html;
757
+ } else {
758
+ filesList.innerHTML = '';
759
+ }
760
+ });
761
+
762
+ const modal = new bootstrap.Modal(document.getElementById('attachFilesModal'));
763
+ modal.show();
764
+ }
765
+
766
+ async function performAttachFiles() {
767
+ const fileInput = document.getElementById('attach-files-input');
768
+
769
+ if (!fileInput.files || fileInput.files.length === 0) {
770
+ showToast('Please select at least one file', 'error');
771
+ return;
772
+ }
773
+
774
+ try {
775
+ const formData = new FormData();
776
+
777
+ // Add all selected files
778
+ for (let i = 0; i < fileInput.files.length; i++) {
779
+ formData.append('files', fileInput.files[i]);
780
+ }
781
+
782
+ const response = await fetch(`/api/chat/${conversationId}/command/attach`, {
783
+ method: 'POST',
784
+ body: formData
785
+ });
786
+
787
+ const data = await response.json();
788
+
789
+ if (data.status === 'success' || data.status === 'partial') {
790
+ // Close modal
791
+ bootstrap.Modal.getInstance(document.getElementById('attachFilesModal')).hide();
792
+
793
+ // Show success message
794
+ const message = data.status === 'success'
795
+ ? `Successfully attached ${data.data.files.length} file(s)`
796
+ : `${data.message} - Some files may have failed`;
797
+ showToast(message, data.status === 'success' ? 'success' : 'warning');
798
+
799
+ // Reload conversation info to show updated file list
800
+ await loadConversationInfo();
801
+ } else {
802
+ showToast(data.message || 'Failed to attach files', 'error');
803
+ }
804
+ } catch (error) {
805
+ console.error('Error attaching files:', error);
806
+ showToast('Failed to attach files', 'error');
807
+ }
808
+ }
809
+
810
+ function attachFiles() {
811
+ showAttachFilesModal();
812
+ }
813
+ </script>
814
+ {% endblock %}