agentgui 1.0.536 → 1.0.538
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.
package/package.json
CHANGED
package/static/index.html
CHANGED
|
@@ -3247,6 +3247,7 @@
|
|
|
3247
3247
|
</script>
|
|
3248
3248
|
<script defer src="/gm/js/event-processor.js"></script>
|
|
3249
3249
|
<script defer src="/gm/js/streaming-renderer.js"></script>
|
|
3250
|
+
<script defer src="/gm/js/image-loader.js"></script>
|
|
3250
3251
|
<script defer src="/gm/js/kalman-filter.js"></script>
|
|
3251
3252
|
<script defer src="/gm/js/event-consolidator.js"></script>
|
|
3252
3253
|
<script defer src="/gm/js/websocket-manager.js"></script>
|
package/static/js/client.js
CHANGED
|
@@ -102,6 +102,12 @@ class AgentGUIClient {
|
|
|
102
102
|
// Initialize renderer
|
|
103
103
|
this.renderer.init(this.config.outputContainerId, this.config.scrollContainerId);
|
|
104
104
|
|
|
105
|
+
// Initialize image loader
|
|
106
|
+
if (typeof ImageLoader !== 'undefined') {
|
|
107
|
+
window.imageLoader = new ImageLoader();
|
|
108
|
+
console.log('Image loader initialized');
|
|
109
|
+
}
|
|
110
|
+
|
|
105
111
|
// Setup event listeners
|
|
106
112
|
this.setupWebSocketListeners();
|
|
107
113
|
this.setupRendererListeners();
|
|
@@ -1649,10 +1655,6 @@ class AgentGUIClient {
|
|
|
1649
1655
|
}
|
|
1650
1656
|
|
|
1651
1657
|
let finalPrompt = prompt;
|
|
1652
|
-
if (subAgent && agentId === 'claude-code') {
|
|
1653
|
-
const displaySubAgent = subAgent.split('-·-')[0];
|
|
1654
|
-
finalPrompt = `use ${displaySubAgent} subagent to ${prompt}`;
|
|
1655
|
-
}
|
|
1656
1658
|
const streamBody = { id: conversationId, content: finalPrompt, agentId };
|
|
1657
1659
|
if (model) streamBody.model = model;
|
|
1658
1660
|
if (subAgent) streamBody.subAgent = subAgent;
|
|
@@ -119,6 +119,20 @@ class EventProcessor {
|
|
|
119
119
|
this.stats.transformedEvents++;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
if (event.type === 'file_read' && event.path && this.isImagePath(event.path)) {
|
|
123
|
+
processed.isImage = true;
|
|
124
|
+
processed.imagePath = event.path;
|
|
125
|
+
this.stats.transformedEvents++;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if ((event.type === 'text_block' || event.type === 'command_execute' || event.type === 'streaming_progress') && event.content || event.output) {
|
|
129
|
+
const imagePaths = this.extractImagePaths(event.content || event.output || '');
|
|
130
|
+
if (imagePaths.length > 0) {
|
|
131
|
+
processed.detectedImages = imagePaths;
|
|
132
|
+
this.stats.transformedEvents++;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
122
136
|
processed.processTime = performance.now() - startTime;
|
|
123
137
|
this.stats.processedEvents++;
|
|
124
138
|
|
|
@@ -444,6 +458,32 @@ class EventProcessor {
|
|
|
444
458
|
avgProcessTime: 0
|
|
445
459
|
};
|
|
446
460
|
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Check if a path is an image file
|
|
464
|
+
*/
|
|
465
|
+
isImagePath(filePath) {
|
|
466
|
+
if (!filePath || typeof filePath !== 'string') return false;
|
|
467
|
+
const imageExts = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'];
|
|
468
|
+
const ext = this.getFileExtension(filePath);
|
|
469
|
+
return imageExts.includes(ext);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Extract image file paths from text content
|
|
474
|
+
*/
|
|
475
|
+
extractImagePaths(content) {
|
|
476
|
+
if (typeof content !== 'string') return [];
|
|
477
|
+
const paths = [];
|
|
478
|
+
const pathPattern = /(?:\/[a-zA-Z0-9_.\-]+)+\/[a-zA-Z0-9_.\-]+\.(?:png|jpg|jpeg|gif|webp|svg)/gi;
|
|
479
|
+
let match;
|
|
480
|
+
while ((match = pathPattern.exec(content)) !== null) {
|
|
481
|
+
if (this.isImagePath(match[0])) {
|
|
482
|
+
paths.push(match[0]);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return [...new Set(paths)];
|
|
486
|
+
}
|
|
447
487
|
}
|
|
448
488
|
|
|
449
489
|
// Export for use in browser
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Loader Module
|
|
3
|
+
* Detects image file reads from agent events and manages lazy loading
|
|
4
|
+
* Supports PNG, JPG, JPEG, GIF, WebP, SVG formats
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class ImageLoader {
|
|
8
|
+
constructor(config = {}) {
|
|
9
|
+
this.config = {
|
|
10
|
+
supportedExts: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'],
|
|
11
|
+
lazyLoadThreshold: config.lazyLoadThreshold || 0.5,
|
|
12
|
+
maxImageDisplaySize: config.maxImageDisplaySize || '600px',
|
|
13
|
+
...config
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
this.imageCache = new Map();
|
|
17
|
+
this.pendingImages = new Map();
|
|
18
|
+
this.intersectionObserver = null;
|
|
19
|
+
this.drawerObserver = null;
|
|
20
|
+
this.initIntersectionObserver();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if a path is an image file
|
|
25
|
+
*/
|
|
26
|
+
isImagePath(filePath) {
|
|
27
|
+
if (!filePath || typeof filePath !== 'string') return false;
|
|
28
|
+
const ext = this.getExtension(filePath).toLowerCase();
|
|
29
|
+
return this.config.supportedExts.includes(ext);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Extract file extension from path
|
|
34
|
+
*/
|
|
35
|
+
getExtension(filePath) {
|
|
36
|
+
const match = filePath.match(/\.([^.]+)$/);
|
|
37
|
+
return match ? match[1] : '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Extract image paths from text content
|
|
42
|
+
*/
|
|
43
|
+
extractImagePaths(content) {
|
|
44
|
+
if (typeof content !== 'string') return [];
|
|
45
|
+
|
|
46
|
+
const paths = [];
|
|
47
|
+
const pathPattern = /(?:\/[a-zA-Z0-9_.\-]+)+\/[a-zA-Z0-9_.\-]+\.(?:png|jpg|jpeg|gif|webp|svg)/gi;
|
|
48
|
+
|
|
49
|
+
let match;
|
|
50
|
+
while ((match = pathPattern.exec(content)) !== null) {
|
|
51
|
+
if (this.isImagePath(match[0])) {
|
|
52
|
+
paths.push(match[0]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return [...new Set(paths)];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Register images from event
|
|
61
|
+
*/
|
|
62
|
+
registerImagesFromEvent(event) {
|
|
63
|
+
const images = [];
|
|
64
|
+
|
|
65
|
+
if (event.type === 'file_read' && event.path && this.isImagePath(event.path)) {
|
|
66
|
+
images.push({
|
|
67
|
+
path: event.path,
|
|
68
|
+
type: 'file_read',
|
|
69
|
+
eventId: event.id || event.sessionId,
|
|
70
|
+
timestamp: event.timestamp || Date.now()
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (event.content && typeof event.content === 'string') {
|
|
75
|
+
const paths = this.extractImagePaths(event.content);
|
|
76
|
+
paths.forEach(path => {
|
|
77
|
+
images.push({
|
|
78
|
+
path,
|
|
79
|
+
type: 'extracted',
|
|
80
|
+
eventId: event.id || event.sessionId,
|
|
81
|
+
timestamp: event.timestamp || Date.now()
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (event.output && typeof event.output === 'string') {
|
|
87
|
+
const paths = this.extractImagePaths(event.output);
|
|
88
|
+
paths.forEach(path => {
|
|
89
|
+
images.push({
|
|
90
|
+
path,
|
|
91
|
+
type: 'extracted',
|
|
92
|
+
eventId: event.id || event.sessionId,
|
|
93
|
+
timestamp: event.timestamp || Date.now()
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
images.forEach(img => {
|
|
99
|
+
const key = img.path;
|
|
100
|
+
if (!this.imageCache.has(key)) {
|
|
101
|
+
this.imageCache.set(key, img);
|
|
102
|
+
this.pendingImages.set(key, img);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return images;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Create image element with lazy loading
|
|
111
|
+
*/
|
|
112
|
+
createImageElement(imagePath, options = {}) {
|
|
113
|
+
const container = document.createElement('div');
|
|
114
|
+
container.className = 'image-container';
|
|
115
|
+
container.dataset.imagePath = imagePath;
|
|
116
|
+
container.style.cssText = `
|
|
117
|
+
display: flex;
|
|
118
|
+
flex-direction: column;
|
|
119
|
+
gap: 0.5rem;
|
|
120
|
+
padding: 0.75rem;
|
|
121
|
+
border-radius: 0.375rem;
|
|
122
|
+
background: var(--color-bg-secondary);
|
|
123
|
+
border: 1px solid var(--color-border);
|
|
124
|
+
`;
|
|
125
|
+
|
|
126
|
+
const placeholder = document.createElement('div');
|
|
127
|
+
placeholder.className = 'image-placeholder';
|
|
128
|
+
placeholder.style.cssText = `
|
|
129
|
+
background: linear-gradient(90deg, var(--color-bg-tertiary) 25%, var(--color-bg-secondary) 50%, var(--color-bg-tertiary) 75%);
|
|
130
|
+
background-size: 200% 100%;
|
|
131
|
+
animation: loading 1.5s infinite;
|
|
132
|
+
border-radius: 0.375rem;
|
|
133
|
+
aspect-ratio: 16/9;
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
justify-content: center;
|
|
137
|
+
color: var(--color-text-secondary);
|
|
138
|
+
font-size: 0.875rem;
|
|
139
|
+
`;
|
|
140
|
+
placeholder.innerHTML = 'Loading image...';
|
|
141
|
+
placeholder.dataset.path = imagePath;
|
|
142
|
+
|
|
143
|
+
const img = document.createElement('img');
|
|
144
|
+
img.className = 'lazy-image';
|
|
145
|
+
img.alt = imagePath;
|
|
146
|
+
img.style.cssText = `
|
|
147
|
+
max-width: 100%;
|
|
148
|
+
max-height: ${this.config.maxImageDisplaySize};
|
|
149
|
+
border-radius: 0.375rem;
|
|
150
|
+
display: none;
|
|
151
|
+
`;
|
|
152
|
+
img.dataset.src = imagePath;
|
|
153
|
+
|
|
154
|
+
const caption = document.createElement('div');
|
|
155
|
+
caption.className = 'image-caption';
|
|
156
|
+
caption.style.cssText = `
|
|
157
|
+
font-size: 0.75rem;
|
|
158
|
+
color: var(--color-text-secondary);
|
|
159
|
+
word-break: break-all;
|
|
160
|
+
font-family: 'Monaco', 'Menlo', monospace;
|
|
161
|
+
`;
|
|
162
|
+
caption.textContent = imagePath;
|
|
163
|
+
|
|
164
|
+
container.appendChild(placeholder);
|
|
165
|
+
container.appendChild(img);
|
|
166
|
+
container.appendChild(caption);
|
|
167
|
+
|
|
168
|
+
img.addEventListener('load', () => {
|
|
169
|
+
placeholder.style.display = 'none';
|
|
170
|
+
img.style.display = 'block';
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
img.addEventListener('error', () => {
|
|
174
|
+
placeholder.textContent = 'Failed to load image';
|
|
175
|
+
placeholder.style.background = 'var(--color-bg-error)';
|
|
176
|
+
placeholder.style.color = 'var(--color-text-error)';
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (this.intersectionObserver) {
|
|
180
|
+
this.intersectionObserver.observe(container);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return container;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Initialize Intersection Observer for lazy loading
|
|
188
|
+
*/
|
|
189
|
+
initIntersectionObserver() {
|
|
190
|
+
this.intersectionObserver = new IntersectionObserver(
|
|
191
|
+
(entries) => {
|
|
192
|
+
entries.forEach(entry => {
|
|
193
|
+
if (entry.isIntersecting) {
|
|
194
|
+
const img = entry.target.querySelector('img.lazy-image');
|
|
195
|
+
if (img && img.dataset.src && !img.src) {
|
|
196
|
+
img.src = img.dataset.src;
|
|
197
|
+
this.intersectionObserver.unobserve(entry.target);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
{ threshold: this.config.lazyLoadThreshold }
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Setup drawer observer to load images when drawer opens
|
|
208
|
+
*/
|
|
209
|
+
setupDrawerObserver(drawerSelector = '.drawer-panel, [role="dialog"]') {
|
|
210
|
+
const drawers = document.querySelectorAll(drawerSelector);
|
|
211
|
+
drawers.forEach(drawer => {
|
|
212
|
+
const observer = new MutationObserver(() => {
|
|
213
|
+
if (drawer.offsetHeight > 0 && drawer.offsetWidth > 0) {
|
|
214
|
+
this.loadVisibleImages(drawer);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
observer.observe(drawer, { attributes: true, attributeFilter: ['style', 'class'] });
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Load all visible images in a container
|
|
224
|
+
*/
|
|
225
|
+
loadVisibleImages(container = document) {
|
|
226
|
+
const images = container.querySelectorAll('img.lazy-image[data-src]');
|
|
227
|
+
images.forEach(img => {
|
|
228
|
+
if (!img.src && img.dataset.src) {
|
|
229
|
+
img.src = img.dataset.src;
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get cached images for a session/conversation
|
|
236
|
+
*/
|
|
237
|
+
getImages(eventId = null) {
|
|
238
|
+
if (!eventId) {
|
|
239
|
+
return Array.from(this.imageCache.values());
|
|
240
|
+
}
|
|
241
|
+
return Array.from(this.imageCache.values()).filter(img => img.eventId === eventId);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Clear cache
|
|
246
|
+
*/
|
|
247
|
+
clear() {
|
|
248
|
+
this.imageCache.clear();
|
|
249
|
+
this.pendingImages.clear();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
254
|
+
module.exports = ImageLoader;
|
|
255
|
+
}
|
|
@@ -1626,6 +1626,26 @@ class StreamingRenderer {
|
|
|
1626
1626
|
return div;
|
|
1627
1627
|
}
|
|
1628
1628
|
|
|
1629
|
+
/**
|
|
1630
|
+
* Detect if content is a base64-encoded image
|
|
1631
|
+
*/
|
|
1632
|
+
detectBase64Image(content) {
|
|
1633
|
+
if (!content || typeof content !== 'string') return null;
|
|
1634
|
+
const trimmed = content.trim();
|
|
1635
|
+
const signatures = {
|
|
1636
|
+
'png': /^iVBORw0KGgo/,
|
|
1637
|
+
'jpeg': /^\/9j\/4AAQ/,
|
|
1638
|
+
'webp': /^UklGRi/,
|
|
1639
|
+
'gif': /^R0lGODlh/
|
|
1640
|
+
};
|
|
1641
|
+
for (const [type, pattern] of Object.entries(signatures)) {
|
|
1642
|
+
if (pattern.test(trimmed)) {
|
|
1643
|
+
return { type, isBase64: true, data: trimmed };
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
return null;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1629
1649
|
/**
|
|
1630
1650
|
* Render file read event
|
|
1631
1651
|
*/
|
|
@@ -1651,7 +1671,13 @@ class StreamingRenderer {
|
|
|
1651
1671
|
let html = '';
|
|
1652
1672
|
if (event.path) html += this.renderFilePath(event.path);
|
|
1653
1673
|
if (event.content) {
|
|
1654
|
-
|
|
1674
|
+
const imageInfo = this.detectBase64Image(event.content);
|
|
1675
|
+
if (imageInfo) {
|
|
1676
|
+
const mimeType = imageInfo.type === 'jpeg' ? 'image/jpeg' : `image/${imageInfo.type}`;
|
|
1677
|
+
html += `<div style="padding:0.5rem;display:flex;flex-direction:column;gap:0.5rem"><img src="data:${mimeType};base64,${this.escapeHtml(imageInfo.data)}" style="max-width:100%;max-height:600px;border-radius:0.375rem;border:1px solid #334155" loading="lazy"><div style="font-size:0.7rem;color:#64748b;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;word-break:break-all">${this.escapeHtml(event.path)}</div></div>`;
|
|
1678
|
+
} else {
|
|
1679
|
+
html += `<pre style="background:#1e293b;padding:0.75rem;border-radius:0.375rem;overflow-x:auto;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem;line-height:1.5;color:#e2e8f0;margin:0.5rem 0 0 0"><code class="lazy-hl">${this.escapeHtml(this.truncateContent(event.content, 2000))}</code></pre>`;
|
|
1680
|
+
}
|
|
1655
1681
|
}
|
|
1656
1682
|
body.innerHTML = html;
|
|
1657
1683
|
details.appendChild(body);
|
|
@@ -1950,6 +1976,85 @@ class StreamingRenderer {
|
|
|
1950
1976
|
return div;
|
|
1951
1977
|
}
|
|
1952
1978
|
|
|
1979
|
+
/**
|
|
1980
|
+
* Render file read event with image detection
|
|
1981
|
+
*/
|
|
1982
|
+
renderFileRead(event) {
|
|
1983
|
+
const div = document.createElement('div');
|
|
1984
|
+
div.className = 'event-file-read mb-3 p-3 rounded border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900';
|
|
1985
|
+
div.dataset.eventId = event.id || '';
|
|
1986
|
+
div.dataset.eventType = 'file_read';
|
|
1987
|
+
|
|
1988
|
+
const filePath = event.path || '';
|
|
1989
|
+
const fileName = pathBasename(filePath);
|
|
1990
|
+
const isImage = this.isImagePath(filePath);
|
|
1991
|
+
|
|
1992
|
+
let html = `
|
|
1993
|
+
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:${isImage ? '0.75rem' : '0'}">
|
|
1994
|
+
<svg viewBox="0 0 20 20" fill="currentColor" style="width:1rem;height:1rem;color:var(--color-info)">
|
|
1995
|
+
<path d="M8 16a2 2 0 100-4 2 2 0 000 4zM9 2a1 1 0 011 1v2h2V3a1 1 0 112 0v2h2V3a1 1 0 112 0v2h2a2 2 0 012 2v2h2a1 1 0 110 2h-2v2h2a1 1 0 110 2h-2v2a2 2 0 01-2 2h-2v2a1 1 0 11-2 0v-2h-2v2a1 1 0 11-2 0v-2H9a2 2 0 01-2-2v-2H5a1 1 0 110-2h2V9H5a1 1 0 010-2h2V5H5a1 1 0 010-2h2V3a1 1 0 011-1z"/>
|
|
1996
|
+
</svg>
|
|
1997
|
+
<code style="font-size:0.75rem;color:var(--color-text-primary);font-family:'Monaco','Menlo','Ubuntu Mono',monospace">${this.escapeHtml(filePath)}</code>
|
|
1998
|
+
</div>
|
|
1999
|
+
`;
|
|
2000
|
+
|
|
2001
|
+
if (isImage) {
|
|
2002
|
+
html += `<div style="margin-top:0.75rem" class="lazy-image-container" data-image-path="${this.escapeHtml(filePath)}"></div>`;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
div.innerHTML = html;
|
|
2006
|
+
|
|
2007
|
+
if (isImage) {
|
|
2008
|
+
const container = div.querySelector('.lazy-image-container');
|
|
2009
|
+
if (container && window.imageLoader) {
|
|
2010
|
+
const imgElement = window.imageLoader.createImageElement(filePath);
|
|
2011
|
+
container.appendChild(imgElement);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
return div;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
/**
|
|
2019
|
+
* Render file write event
|
|
2020
|
+
*/
|
|
2021
|
+
renderFileWrite(event) {
|
|
2022
|
+
const div = document.createElement('div');
|
|
2023
|
+
div.className = 'event-file-write mb-3 p-3 rounded border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900';
|
|
2024
|
+
div.dataset.eventId = event.id || '';
|
|
2025
|
+
div.dataset.eventType = 'file_write';
|
|
2026
|
+
|
|
2027
|
+
const filePath = event.path || '';
|
|
2028
|
+
const icon = '<svg viewBox="0 0 20 20" fill="currentColor" style="width:1rem;height:1rem;color:var(--color-success)"><path d="M11 3a1 1 0 10-2 0v1a1 1 0 102 0V3zM15.657 5.757a1 1 0 00-1.414-1.414l-.707.707a1 1 0 001.414 1.414l.707-.707zM18 10a1 1 0 01-1 1h-1a1 1 0 110-2h1a1 1 0 011 1zM16.243 15.657a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414l.707.707zM10 15a1 1 0 01-1-1v-1a1 1 0 112 0v1a1 1 0 01-1 1zM5.757 16.243a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414l-.707.707zM5 10a1 1 0 01-1-1V8a1 1 0 012 0v1a1 1 0 01-1 1zM5.757 5.757a1 1 0 000-1.414l-.707-.707a1 1 0 00-1.414 1.414l.707.707zM10 5a1 1 0 011-1h1a1 1 0 110 2h-1a1 1 0 01-1-1z"/></svg>';
|
|
2029
|
+
|
|
2030
|
+
div.innerHTML = `
|
|
2031
|
+
<div style="display:flex;align-items:center;gap:0.5rem">
|
|
2032
|
+
${icon}
|
|
2033
|
+
<code style="font-size:0.75rem;color:var(--color-text-primary);font-family:'Monaco','Menlo','Ubuntu Mono',monospace">${this.escapeHtml(filePath)}</code>
|
|
2034
|
+
</div>
|
|
2035
|
+
`;
|
|
2036
|
+
|
|
2037
|
+
return div;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
/**
|
|
2041
|
+
* Check if a path is an image file
|
|
2042
|
+
*/
|
|
2043
|
+
isImagePath(filePath) {
|
|
2044
|
+
if (!filePath || typeof filePath !== 'string') return false;
|
|
2045
|
+
const imageExts = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'];
|
|
2046
|
+
const ext = this.getFileExtension(filePath);
|
|
2047
|
+
return imageExts.includes(ext);
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
/**
|
|
2051
|
+
* Extract file extension
|
|
2052
|
+
*/
|
|
2053
|
+
getFileExtension(filePath) {
|
|
2054
|
+
const match = filePath.match(/\.([^.]+)$/);
|
|
2055
|
+
return match ? match[1].toLowerCase() : '';
|
|
2056
|
+
}
|
|
2057
|
+
|
|
1953
2058
|
/**
|
|
1954
2059
|
* Render generic event with formatted key-value pairs
|
|
1955
2060
|
*/
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<!-- Image Display Container with Lazy Loading -->
|
|
2
|
+
<div class="image-container" data-image-path="{{ imagePath }}" role="img" aria-label="{{ imagePath }}">
|
|
3
|
+
<!-- Placeholder shown while loading -->
|
|
4
|
+
<div class="image-placeholder" data-path="{{ imagePath }}" style="
|
|
5
|
+
background: linear-gradient(90deg, var(--color-bg-tertiary) 25%, var(--color-bg-secondary) 50%, var(--color-bg-tertiary) 75%);
|
|
6
|
+
background-size: 200% 100%;
|
|
7
|
+
animation: loading 1.5s infinite;
|
|
8
|
+
border-radius: 0.375rem;
|
|
9
|
+
aspect-ratio: 16/9;
|
|
10
|
+
display: flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
justify-content: center;
|
|
13
|
+
color: var(--color-text-secondary);
|
|
14
|
+
font-size: 0.875rem;
|
|
15
|
+
">
|
|
16
|
+
Loading image...
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<!-- Actual image element with lazy loading -->
|
|
20
|
+
<img
|
|
21
|
+
class="lazy-image"
|
|
22
|
+
data-src="{{ imagePath }}"
|
|
23
|
+
alt="{{ imagePath }}"
|
|
24
|
+
loading="lazy"
|
|
25
|
+
style="
|
|
26
|
+
max-width: 100%;
|
|
27
|
+
max-height: 600px;
|
|
28
|
+
border-radius: 0.375rem;
|
|
29
|
+
display: none;
|
|
30
|
+
"
|
|
31
|
+
/>
|
|
32
|
+
|
|
33
|
+
<!-- Image path caption -->
|
|
34
|
+
<div class="image-caption" style="
|
|
35
|
+
font-size: 0.75rem;
|
|
36
|
+
color: var(--color-text-secondary);
|
|
37
|
+
word-break: break-all;
|
|
38
|
+
font-family: 'Monaco', 'Menlo', monospace;
|
|
39
|
+
">
|
|
40
|
+
{{ imagePath }}
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<style>
|
|
45
|
+
@keyframes loading {
|
|
46
|
+
0% {
|
|
47
|
+
background-position: 200% 0;
|
|
48
|
+
}
|
|
49
|
+
100% {
|
|
50
|
+
background-position: -200% 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.image-container {
|
|
55
|
+
display: flex;
|
|
56
|
+
flex-direction: column;
|
|
57
|
+
gap: 0.5rem;
|
|
58
|
+
padding: 0.75rem;
|
|
59
|
+
border-radius: 0.375rem;
|
|
60
|
+
background: var(--color-bg-secondary);
|
|
61
|
+
border: 1px solid var(--color-border);
|
|
62
|
+
margin: 0.5rem 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.image-placeholder {
|
|
66
|
+
animation: loading 1.5s infinite;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.lazy-image {
|
|
70
|
+
transition: opacity 0.3s ease-in-out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.image-caption {
|
|
74
|
+
white-space: pre-wrap;
|
|
75
|
+
}
|
|
76
|
+
</style>
|