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,496 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main JavaScript utilities for Spark web interface
|
|
3
|
+
*
|
|
4
|
+
* Provides common functions used across all pages
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// MARKDOWN, CODE HIGHLIGHTING, AND MERMAID CONFIGURATION
|
|
9
|
+
// =============================================================================
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Initialise Mermaid.js with dark theme
|
|
13
|
+
*/
|
|
14
|
+
if (typeof mermaid !== 'undefined') {
|
|
15
|
+
mermaid.initialize({
|
|
16
|
+
startOnLoad: false, // We'll manually trigger rendering
|
|
17
|
+
theme: 'dark',
|
|
18
|
+
themeVariables: {
|
|
19
|
+
primaryColor: '#3b82f6',
|
|
20
|
+
primaryTextColor: '#e0e0e0',
|
|
21
|
+
primaryBorderColor: '#404040',
|
|
22
|
+
lineColor: '#606060',
|
|
23
|
+
secondaryColor: '#1e3a5f',
|
|
24
|
+
tertiaryColor: '#2a2a2a',
|
|
25
|
+
background: '#1a1a1a',
|
|
26
|
+
mainBkg: '#2a2a2a',
|
|
27
|
+
nodeBorder: '#404040',
|
|
28
|
+
clusterBkg: '#2a2a2a',
|
|
29
|
+
clusterBorder: '#404040',
|
|
30
|
+
titleColor: '#e0e0e0',
|
|
31
|
+
edgeLabelBackground: '#2a2a2a',
|
|
32
|
+
},
|
|
33
|
+
flowchart: {
|
|
34
|
+
useMaxWidth: true,
|
|
35
|
+
htmlLabels: true,
|
|
36
|
+
curve: 'basis'
|
|
37
|
+
},
|
|
38
|
+
sequence: {
|
|
39
|
+
useMaxWidth: true,
|
|
40
|
+
diagramMarginX: 50,
|
|
41
|
+
diagramMarginY: 10,
|
|
42
|
+
actorMargin: 50,
|
|
43
|
+
width: 150,
|
|
44
|
+
height: 65,
|
|
45
|
+
boxMargin: 10,
|
|
46
|
+
boxTextMargin: 5,
|
|
47
|
+
noteMargin: 10,
|
|
48
|
+
messageMargin: 35
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Configure Marked.js with highlight.js for code blocks
|
|
55
|
+
*/
|
|
56
|
+
if (typeof marked !== 'undefined') {
|
|
57
|
+
// Create custom renderer for code blocks
|
|
58
|
+
const renderer = new marked.Renderer();
|
|
59
|
+
|
|
60
|
+
// Override code block rendering
|
|
61
|
+
// Note: marked.js v5+ passes a token object instead of separate parameters
|
|
62
|
+
renderer.code = function(tokenOrCode, language) {
|
|
63
|
+
// Handle both old API (code, language) and new API (token object)
|
|
64
|
+
let code, lang;
|
|
65
|
+
if (typeof tokenOrCode === 'object' && tokenOrCode !== null) {
|
|
66
|
+
// New marked.js API (v5+): receives token object
|
|
67
|
+
code = tokenOrCode.text || '';
|
|
68
|
+
lang = tokenOrCode.lang || '';
|
|
69
|
+
} else {
|
|
70
|
+
// Old marked.js API: receives separate parameters
|
|
71
|
+
code = tokenOrCode || '';
|
|
72
|
+
lang = language || '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Handle mermaid diagrams
|
|
76
|
+
if (lang === 'mermaid') {
|
|
77
|
+
const id = 'mermaid-' + Math.random().toString(36).substr(2, 9);
|
|
78
|
+
return `<div class="mermaid-container"><pre class="mermaid" id="${id}">${escapeHtmlForMermaid(code)}</pre></div>`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Use highlight.js for other code blocks
|
|
82
|
+
if (typeof hljs !== 'undefined' && lang && hljs.getLanguage(lang)) {
|
|
83
|
+
try {
|
|
84
|
+
const highlighted = hljs.highlight(code, { language: lang, ignoreIllegals: true }).value;
|
|
85
|
+
return `<pre><code class="hljs language-${lang}">${highlighted}</code></pre>`;
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.warn('Highlight.js error:', e);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Fallback: auto-detect language or plain text
|
|
92
|
+
if (typeof hljs !== 'undefined' && code) {
|
|
93
|
+
try {
|
|
94
|
+
const highlighted = hljs.highlightAuto(code).value;
|
|
95
|
+
return `<pre><code class="hljs">${highlighted}</code></pre>`;
|
|
96
|
+
} catch (e) {
|
|
97
|
+
console.warn('Highlight.js auto-detect error:', e);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Final fallback: plain code block
|
|
102
|
+
return `<pre><code>${escapeHtml(code)}</code></pre>`;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Configure marked options
|
|
106
|
+
marked.setOptions({
|
|
107
|
+
renderer: renderer,
|
|
108
|
+
gfm: true, // GitHub Flavoured Markdown
|
|
109
|
+
breaks: true, // Convert \n to <br>
|
|
110
|
+
pedantic: false,
|
|
111
|
+
smartLists: true,
|
|
112
|
+
smartypants: false,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Escape HTML for mermaid (preserve diagram syntax)
|
|
118
|
+
* @param {string} text - Text to escape
|
|
119
|
+
* @returns {string} Escaped text safe for mermaid
|
|
120
|
+
*/
|
|
121
|
+
function escapeHtmlForMermaid(text) {
|
|
122
|
+
return text
|
|
123
|
+
.replace(/&/g, '&')
|
|
124
|
+
.replace(/</g, '<')
|
|
125
|
+
.replace(/>/g, '>');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Copy SVG element to clipboard as PNG image
|
|
130
|
+
* @param {SVGElement} svgElement - The SVG element to copy
|
|
131
|
+
* @param {HTMLElement} button - The button element (for feedback)
|
|
132
|
+
*/
|
|
133
|
+
async function copySvgToClipboard(svgElement, button) {
|
|
134
|
+
try {
|
|
135
|
+
// Get SVG dimensions
|
|
136
|
+
const svgRect = svgElement.getBoundingClientRect();
|
|
137
|
+
const width = Math.ceil(svgRect.width) || 800;
|
|
138
|
+
const height = Math.ceil(svgRect.height) || 600;
|
|
139
|
+
|
|
140
|
+
// Clone SVG and prepare for rendering
|
|
141
|
+
const svgClone = svgElement.cloneNode(true);
|
|
142
|
+
|
|
143
|
+
// Set explicit dimensions and viewBox
|
|
144
|
+
svgClone.setAttribute('width', width);
|
|
145
|
+
svgClone.setAttribute('height', height);
|
|
146
|
+
if (!svgClone.getAttribute('viewBox')) {
|
|
147
|
+
svgClone.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Add xmlns if missing (required for data URL)
|
|
151
|
+
if (!svgClone.getAttribute('xmlns')) {
|
|
152
|
+
svgClone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Add dark background for better visibility
|
|
156
|
+
const bgRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
157
|
+
bgRect.setAttribute('width', '100%');
|
|
158
|
+
bgRect.setAttribute('height', '100%');
|
|
159
|
+
bgRect.setAttribute('fill', '#1a1a1a');
|
|
160
|
+
svgClone.insertBefore(bgRect, svgClone.firstChild);
|
|
161
|
+
|
|
162
|
+
// Serialise SVG to string
|
|
163
|
+
const serializer = new XMLSerializer();
|
|
164
|
+
let svgString = serializer.serializeToString(svgClone);
|
|
165
|
+
|
|
166
|
+
// Encode SVG as base64 data URL (avoids tainted canvas issues)
|
|
167
|
+
const base64Svg = btoa(unescape(encodeURIComponent(svgString)));
|
|
168
|
+
const dataUrl = 'data:image/svg+xml;base64,' + base64Svg;
|
|
169
|
+
|
|
170
|
+
// Create canvas and draw image
|
|
171
|
+
const canvas = document.createElement('canvas');
|
|
172
|
+
const ctx = canvas.getContext('2d');
|
|
173
|
+
const img = new Image();
|
|
174
|
+
|
|
175
|
+
// Set up promise-based loading
|
|
176
|
+
await new Promise((resolve, reject) => {
|
|
177
|
+
img.onload = () => {
|
|
178
|
+
// Set canvas size with scale for better quality
|
|
179
|
+
const scale = 2;
|
|
180
|
+
canvas.width = width * scale;
|
|
181
|
+
canvas.height = height * scale;
|
|
182
|
+
ctx.scale(scale, scale);
|
|
183
|
+
|
|
184
|
+
// Draw image on canvas
|
|
185
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
186
|
+
resolve();
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
img.onerror = (e) => {
|
|
190
|
+
reject(new Error('Failed to load SVG image'));
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
img.src = dataUrl;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Convert to blob and copy to clipboard
|
|
197
|
+
const blob = await new Promise((resolve) => {
|
|
198
|
+
canvas.toBlob(resolve, 'image/png');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (!blob) {
|
|
202
|
+
throw new Error('Failed to create image blob');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
await navigator.clipboard.write([
|
|
207
|
+
new ClipboardItem({ 'image/png': blob })
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
// Show success feedback
|
|
211
|
+
if (button) {
|
|
212
|
+
const icon = button.querySelector('i');
|
|
213
|
+
if (icon) {
|
|
214
|
+
icon.className = 'bi bi-check-circle-fill text-success';
|
|
215
|
+
setTimeout(() => {
|
|
216
|
+
icon.className = 'bi bi-clipboard';
|
|
217
|
+
}, 2000);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
showToast('Diagram copied to clipboard', 'success');
|
|
221
|
+
} catch (clipboardError) {
|
|
222
|
+
console.error('Clipboard write failed:', clipboardError);
|
|
223
|
+
// Fallback: offer download
|
|
224
|
+
downloadDiagramAsPng(canvas, 'diagram.png');
|
|
225
|
+
showToast('Clipboard not available - downloading image instead', 'info');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
} catch (e) {
|
|
229
|
+
console.error('Error copying diagram:', e);
|
|
230
|
+
showToast('Failed to copy diagram: ' + e.message, 'error');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Download canvas as PNG file (fallback when clipboard not available)
|
|
236
|
+
* @param {HTMLCanvasElement} canvas - Canvas element
|
|
237
|
+
* @param {string} filename - Download filename
|
|
238
|
+
*/
|
|
239
|
+
function downloadDiagramAsPng(canvas, filename) {
|
|
240
|
+
const link = document.createElement('a');
|
|
241
|
+
link.download = filename;
|
|
242
|
+
link.href = canvas.toDataURL('image/png');
|
|
243
|
+
link.click();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Render all mermaid diagrams in a container
|
|
248
|
+
* @param {HTMLElement} container - Container element to search for mermaid blocks
|
|
249
|
+
*/
|
|
250
|
+
async function renderMermaidDiagrams(container) {
|
|
251
|
+
if (typeof mermaid === 'undefined') return;
|
|
252
|
+
|
|
253
|
+
const mermaidBlocks = container.querySelectorAll('pre.mermaid');
|
|
254
|
+
if (mermaidBlocks.length === 0) return;
|
|
255
|
+
|
|
256
|
+
for (const block of mermaidBlocks) {
|
|
257
|
+
try {
|
|
258
|
+
const id = block.id || 'mermaid-' + Math.random().toString(36).substr(2, 9);
|
|
259
|
+
const code = block.textContent;
|
|
260
|
+
|
|
261
|
+
// Render the diagram
|
|
262
|
+
const { svg } = await mermaid.render(id + '-svg', code);
|
|
263
|
+
|
|
264
|
+
// Create wrapper with copy button
|
|
265
|
+
const wrapper = document.createElement('div');
|
|
266
|
+
wrapper.className = 'mermaid-diagram';
|
|
267
|
+
|
|
268
|
+
// Add copy button
|
|
269
|
+
const copyBtn = document.createElement('button');
|
|
270
|
+
copyBtn.className = 'diagram-copy-btn btn btn-sm';
|
|
271
|
+
copyBtn.title = 'Copy diagram to clipboard';
|
|
272
|
+
copyBtn.innerHTML = '<i class="bi bi-clipboard"></i>';
|
|
273
|
+
|
|
274
|
+
// Add SVG content
|
|
275
|
+
const svgContainer = document.createElement('div');
|
|
276
|
+
svgContainer.className = 'mermaid-svg-container';
|
|
277
|
+
svgContainer.innerHTML = svg;
|
|
278
|
+
|
|
279
|
+
wrapper.appendChild(copyBtn);
|
|
280
|
+
wrapper.appendChild(svgContainer);
|
|
281
|
+
|
|
282
|
+
// Add click handler for copy button
|
|
283
|
+
copyBtn.onclick = () => {
|
|
284
|
+
const svgElement = svgContainer.querySelector('svg');
|
|
285
|
+
if (svgElement) {
|
|
286
|
+
copySvgToClipboard(svgElement, copyBtn);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
block.parentNode.replaceChild(wrapper, block);
|
|
291
|
+
} catch (e) {
|
|
292
|
+
console.error('Mermaid rendering error:', e);
|
|
293
|
+
// Show error message in the block
|
|
294
|
+
block.classList.add('mermaid-error');
|
|
295
|
+
block.innerHTML = `<span class="text-danger">Diagram rendering error: ${escapeHtml(e.message || 'Unknown error')}</span>\n\n${block.textContent}`;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Parse markdown and render with syntax highlighting and mermaid support
|
|
302
|
+
* @param {string} content - Markdown content to parse
|
|
303
|
+
* @param {HTMLElement} targetElement - Element to render into
|
|
304
|
+
*/
|
|
305
|
+
async function renderMarkdown(content, targetElement) {
|
|
306
|
+
if (typeof marked === 'undefined') {
|
|
307
|
+
targetElement.textContent = content;
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Parse markdown
|
|
312
|
+
targetElement.innerHTML = marked.parse(content);
|
|
313
|
+
|
|
314
|
+
// Render mermaid diagrams
|
|
315
|
+
await renderMermaidDiagrams(targetElement);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// =============================================================================
|
|
319
|
+
// GENERAL UTILITIES
|
|
320
|
+
// =============================================================================
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Format a timestamp to local date/time string
|
|
324
|
+
* @param {string|Date} timestamp - The timestamp to format
|
|
325
|
+
* @returns {string} Formatted date/time string
|
|
326
|
+
*/
|
|
327
|
+
function formatTimestamp(timestamp) {
|
|
328
|
+
const date = new Date(timestamp);
|
|
329
|
+
return date.toLocaleString();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Format a number with thousands separators
|
|
334
|
+
* @param {number} num - The number to format
|
|
335
|
+
* @returns {string} Formatted number string
|
|
336
|
+
*/
|
|
337
|
+
function formatNumber(num) {
|
|
338
|
+
return num.toLocaleString();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Truncate text to a maximum length
|
|
343
|
+
* @param {string} text - The text to truncate
|
|
344
|
+
* @param {number} maxLength - Maximum length
|
|
345
|
+
* @returns {string} Truncated text with ellipsis if needed
|
|
346
|
+
*/
|
|
347
|
+
function truncateText(text, maxLength = 100) {
|
|
348
|
+
if (text.length <= maxLength) {
|
|
349
|
+
return text;
|
|
350
|
+
}
|
|
351
|
+
return text.substring(0, maxLength) + '...';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Show a toast notification
|
|
356
|
+
* @param {string} message - The message to display
|
|
357
|
+
* @param {string} type - Toast type (success, error, warning, info)
|
|
358
|
+
*/
|
|
359
|
+
function showToast(message, type = 'info') {
|
|
360
|
+
// Create toast container if it doesn't exist
|
|
361
|
+
let container = document.getElementById('toast-container');
|
|
362
|
+
if (!container) {
|
|
363
|
+
container = document.createElement('div');
|
|
364
|
+
container.id = 'toast-container';
|
|
365
|
+
container.className = 'position-fixed bottom-0 end-0 p-3';
|
|
366
|
+
container.style.zIndex = '11';
|
|
367
|
+
document.body.appendChild(container);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Create toast
|
|
371
|
+
const toast = document.createElement('div');
|
|
372
|
+
toast.className = `toast align-items-center text-white bg-${type === 'error' ? 'danger' : type} border-0`;
|
|
373
|
+
toast.setAttribute('role', 'alert');
|
|
374
|
+
toast.setAttribute('aria-live', 'assertive');
|
|
375
|
+
toast.setAttribute('aria-atomic', 'true');
|
|
376
|
+
|
|
377
|
+
toast.innerHTML = `
|
|
378
|
+
<div class="d-flex">
|
|
379
|
+
<div class="toast-body">
|
|
380
|
+
${message}
|
|
381
|
+
</div>
|
|
382
|
+
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
|
383
|
+
</div>
|
|
384
|
+
`;
|
|
385
|
+
|
|
386
|
+
container.appendChild(toast);
|
|
387
|
+
|
|
388
|
+
const bsToast = new bootstrap.Toast(toast);
|
|
389
|
+
bsToast.show();
|
|
390
|
+
|
|
391
|
+
// Remove toast element after it's hidden
|
|
392
|
+
toast.addEventListener('hidden.bs.toast', () => {
|
|
393
|
+
toast.remove();
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Escape HTML to prevent XSS
|
|
399
|
+
* @param {string} text - Text to escape
|
|
400
|
+
* @returns {string} Escaped text
|
|
401
|
+
*/
|
|
402
|
+
function escapeHtml(text) {
|
|
403
|
+
const div = document.createElement('div');
|
|
404
|
+
div.textContent = text;
|
|
405
|
+
return div.innerHTML;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Copy text to clipboard
|
|
410
|
+
* @param {string} text - Text to copy
|
|
411
|
+
*/
|
|
412
|
+
async function copyToClipboard(text) {
|
|
413
|
+
try {
|
|
414
|
+
await navigator.clipboard.writeText(text);
|
|
415
|
+
showToast('Copied to clipboard', 'success');
|
|
416
|
+
} catch (err) {
|
|
417
|
+
showToast('Failed to copy to clipboard', 'error');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Download text as a file
|
|
423
|
+
* @param {string} content - File content
|
|
424
|
+
* @param {string} filename - File name
|
|
425
|
+
* @param {string} mimeType - MIME type
|
|
426
|
+
*/
|
|
427
|
+
function downloadFile(content, filename, mimeType = 'text/plain') {
|
|
428
|
+
const blob = new Blob([content], { type: mimeType });
|
|
429
|
+
const url = URL.createObjectURL(blob);
|
|
430
|
+
const link = document.createElement('a');
|
|
431
|
+
link.href = url;
|
|
432
|
+
link.download = filename;
|
|
433
|
+
document.body.appendChild(link);
|
|
434
|
+
link.click();
|
|
435
|
+
document.body.removeChild(link);
|
|
436
|
+
URL.revokeObjectURL(url);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Make an API request with error handling
|
|
441
|
+
* @param {string} url - API endpoint URL
|
|
442
|
+
* @param {object} options - Fetch options
|
|
443
|
+
* @returns {Promise<any>} Response data
|
|
444
|
+
*/
|
|
445
|
+
async function apiRequest(url, options = {}) {
|
|
446
|
+
try {
|
|
447
|
+
const response = await fetch(url, {
|
|
448
|
+
...options,
|
|
449
|
+
headers: {
|
|
450
|
+
'Content-Type': 'application/json',
|
|
451
|
+
...options.headers,
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
if (!response.ok) {
|
|
456
|
+
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
|
457
|
+
throw new Error(error.detail || `HTTP ${response.status}`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return await response.json();
|
|
461
|
+
} catch (error) {
|
|
462
|
+
console.error('API request failed:', error);
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Debounce function to limit function call frequency
|
|
469
|
+
* @param {Function} func - Function to debounce
|
|
470
|
+
* @param {number} wait - Wait time in milliseconds
|
|
471
|
+
* @returns {Function} Debounced function
|
|
472
|
+
*/
|
|
473
|
+
function debounce(func, wait) {
|
|
474
|
+
let timeout;
|
|
475
|
+
return function executedFunction(...args) {
|
|
476
|
+
const later = () => {
|
|
477
|
+
clearTimeout(timeout);
|
|
478
|
+
func(...args);
|
|
479
|
+
};
|
|
480
|
+
clearTimeout(timeout);
|
|
481
|
+
timeout = setTimeout(later, wait);
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Format file size to human-readable string
|
|
487
|
+
* @param {number} bytes - File size in bytes
|
|
488
|
+
* @returns {string} Formatted file size
|
|
489
|
+
*/
|
|
490
|
+
function formatFileSize(bytes) {
|
|
491
|
+
if (bytes === 0) return '0 Bytes';
|
|
492
|
+
const k = 1024;
|
|
493
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
494
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
495
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
496
|
+
}
|