non image generic icons working
deci committed
Jun 23, 2025 at 16:54 UTC
b4881359c8c1aa9a537d2bf8d8539af922db2478
3 files changed
+258
-71
python/api/image_get.py
+101
-6
@@ -9,6 +9,10 @@ class ImageGet(ApiHandler):
9
async def process(self, input: dict, request: Request) -> dict | Response:
10
# input data
11
path = input.get("path", request.args.get("path", ""))
12
+ metadata = input.get("metadata", request.args.get("metadata", "false")).lower() == "true"
13
+
14
+ print(f"ImageGet: Processing path={path}, metadata={metadata}") # Debug
15
+
16
if not path:
17
raise ValueError("No path provided")
18
@@ -16,26 +20,42 @@ class ImageGet(ApiHandler):
20
if not files.is_in_base_dir(path):
21
raise ValueError("Path is outside of allowed directory")
22
19
- # get file extension
23
+ # get file extension and info
24
file_ext = os.path.splitext(path)[1].lower()
25
+ filename = os.path.basename(path)
26
+
27
+ print(f"ImageGet: file_ext={file_ext}, filename={filename}") # Debug
28
29
# list of allowed image extensions
30
image_extensions = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg"]
31
32
+ # If metadata is requested, return file information
33
+ if metadata:
34
+ return self._get_file_metadata(path, filename, file_ext, image_extensions)
35
+
36
if file_ext in image_extensions:
37
# Handle image files
38
+ print(f"ImageGet: Handling as image file") # Debug
39
if not os.path.exists(path):
40
# If image doesn't exist, return default image icon
41
return self._get_fallback_icon("image")
42
31
- # send actual image file
32
- return send_file(path)
43
+ # send actual image file with proper headers for device sync
44
+ response = send_file(path)
45
+ # Add cache headers for better device sync performance
46
+ response.headers['Cache-Control'] = 'public, max-age=3600'
47
+ response.headers['X-File-Type'] = 'image'
48
+ response.headers['X-File-Name'] = filename
49
+ return response
50
else:
51
# Handle non-image files with fallback icons
35
- return self._get_file_type_icon(file_ext)
52
+ print(f"ImageGet: Handling as non-image file, getting icon") # Debug
53
+ return self._get_file_type_icon(file_ext, filename)
54
37
- def _get_file_type_icon(self, file_ext):
55
+ def _get_file_type_icon(self, file_ext, filename=None):
56
"""Return appropriate icon for file type"""
57
+ print(f"_get_file_type_icon: file_ext={file_ext}, filename={filename}") # Debug
58
+
59
# Map file extensions to icon names
60
icon_mapping = {
61
# Archive files
@@ -80,21 +100,96 @@ class ImageGet(ApiHandler):
100
101
# Get icon name, default to 'file' if not found
102
icon_name = icon_mapping.get(file_ext, 'file')
83
- return self._get_fallback_icon(icon_name)
103
+ print(f"_get_file_type_icon: icon_name={icon_name}") # Debug
104
+
105
+ response = self._get_fallback_icon(icon_name)
106
+
107
+ # Add headers for device sync
108
+ if hasattr(response, 'headers'):
109
+ response.headers['Cache-Control'] = 'public, max-age=86400' # Cache icons for 24 hours
110
+ response.headers['X-File-Type'] = 'icon'
111
+ response.headers['X-Icon-Type'] = icon_name
112
+ if filename:
113
+ response.headers['X-File-Name'] = filename
114
+
115
+ return response
116
+
117
+ def _get_file_metadata(self, path, filename, file_ext, image_extensions):
118
+ """Return file metadata for device sync and UI enhancement"""
119
+ metadata = {
120
+ 'filename': filename,
121
+ 'extension': file_ext,
122
+ 'exists': os.path.exists(path),
123
+ 'is_image': file_ext in image_extensions,
124
+ 'file_type': 'image' if file_ext in image_extensions else self._get_file_category(file_ext),
125
+ 'api_url': f'/image_get?path={path}',
126
+ 'icon_type': self._get_icon_type(file_ext) if file_ext not in image_extensions else 'image'
127
+ }
128
+
129
+ # Add file size if file exists
130
+ if metadata['exists']:
131
+ try:
132
+ metadata['size'] = os.path.getsize(path)
133
+ metadata['size_human'] = self._format_file_size(metadata['size'])
134
+ except OSError:
135
+ metadata['size'] = 0
136
+ metadata['size_human'] = 'Unknown'
137
+
138
+ return metadata
139
+
140
+ def _get_file_category(self, file_ext):
141
+ """Get file category for metadata"""
142
+ categories = {
143
+ '.zip': 'archive', '.rar': 'archive', '.7z': 'archive', '.tar': 'archive', '.gz': 'archive',
144
+ '.pdf': 'document', '.doc': 'document', '.docx': 'document', '.txt': 'document',
145
+ '.py': 'code', '.js': 'code', '.html': 'code', '.css': 'code', '.json': 'code',
146
+ '.xls': 'spreadsheet', '.xlsx': 'spreadsheet', '.csv': 'spreadsheet',
147
+ '.ppt': 'presentation', '.pptx': 'presentation'
148
+ }
149
+ return categories.get(file_ext, 'file')
150
+
151
+ def _get_icon_type(self, file_ext):
152
+ """Get icon type for metadata (matches the icon mapping)"""
153
+ icon_mapping = {
154
+ '.zip': 'archive', '.rar': 'archive', '.7z': 'archive', '.tar': 'archive', '.gz': 'archive',
155
+ '.pdf': 'document', '.doc': 'document', '.docx': 'document', '.txt': 'document', '.rtf': 'document', '.odt': 'document',
156
+ '.py': 'code', '.js': 'code', '.html': 'code', '.css': 'code', '.json': 'code', '.xml': 'code', '.md': 'code',
157
+ '.yml': 'code', '.yaml': 'code', '.sql': 'code', '.sh': 'code', '.bat': 'code',
158
+ '.xls': 'document', '.xlsx': 'document', '.csv': 'document',
159
+ '.ppt': 'document', '.pptx': 'document', '.odp': 'document'
160
+ }
161
+ return icon_mapping.get(file_ext, 'file')
162
+
163
+ def _format_file_size(self, size_bytes):
164
+ """Format file size in human readable format"""
165
+ if size_bytes == 0:
166
+ return "0 B"
167
+ size_names = ["B", "KB", "MB", "GB", "TB"]
168
+ import math
169
+ i = int(math.floor(math.log(size_bytes, 1024)))
170
+ p = math.pow(1024, i)
171
+ s = round(size_bytes / p, 2)
172
+ return f"{s} {size_names[i]}"
173
174
def _get_fallback_icon(self, icon_name):
175
"""Return fallback icon from public directory"""
176
+ print(f"_get_fallback_icon: icon_name={icon_name}") # Debug
177
+
178
# Path to public icons
179
icon_path = files.get_abs_path(f"webui/public/{icon_name}.svg")
180
+ print(f"_get_fallback_icon: icon_path={icon_path}") # Debug
181
182
# Check if specific icon exists, fallback to generic file icon
183
if not os.path.exists(icon_path):
184
+ print(f"_get_fallback_icon: Icon not found, falling back to file.svg") # Debug
185
icon_path = files.get_abs_path("webui/public/file.svg")
186
187
# Final fallback if file.svg doesn't exist
188
if not os.path.exists(icon_path):
189
+ print(f"_get_fallback_icon: ERROR - file.svg not found at {icon_path}") # Debug
190
raise ValueError(f"Fallback icon not found: {icon_path}")
191
192
+ print(f"_get_fallback_icon: Sending file {icon_path}") # Debug
193
return send_file(icon_path, mimetype='image/svg+xml')
194
195
\ No newline at end of file
webui/components/chat/attachments/attachmentsStore.js
+79
@@ -229,6 +229,85 @@ const model = {
229
});
230
},
231
232
+ // Generate server-side API URL for file (for device sync)
233
+ getServerFileUrl(filename) {
234
+ return `/image_get?path=/a0/tmp/uploads/${encodeURIComponent(filename)}`;
235
+ },
236
+
237
+ // Get file metadata from server (for device sync and enhanced UI)
238
+ async getFileMetadata(filename) {
239
+ try {
240
+ const response = await fetch(`/image_get?path=/a0/tmp/uploads/${encodeURIComponent(filename)}&metadata=true`);
241
+ if (response.ok) {
242
+ return await response.json();
243
+ }
244
+ return null;
245
+ } catch (error) {
246
+ console.error('Failed to get file metadata:', error);
247
+ return null;
248
+ }
249
+ },
250
+
251
+ // Check if file is an image based on extension
252
+ isImageFile(filename) {
253
+ const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'];
254
+ const extension = filename.split('.').pop().toLowerCase();
255
+ return imageExtensions.includes(extension);
256
+ },
257
+
258
+ // Get attachment preview URL (server URL for persistence, blob URL for current session)
259
+ getAttachmentPreviewUrl(attachment) {
260
+ // If attachment has a name and we're dealing with a server-stored file
261
+ if (typeof attachment === 'string') {
262
+ // attachment is just a filename (from loaded chat)
263
+ return this.getServerFileUrl(attachment);
264
+ } else if (attachment.name && attachment.file) {
265
+ // attachment is an object from current session
266
+ if (attachment.type === 'image') {
267
+ // For images, use blob URL for current session preview
268
+ return attachment.url || URL.createObjectURL(attachment.file);
269
+ } else {
270
+ // For non-image files, use server URL to get appropriate icon
271
+ return this.getServerFileUrl(attachment.name);
272
+ }
273
+ }
274
+ return null;
275
+ },
276
+
277
+ // Enhanced method to get attachment display info for UI
278
+ getAttachmentDisplayInfo(attachment) {
279
+ if (typeof attachment === 'string') {
280
+ // attachment is filename only (from persistent storage)
281
+ const filename = attachment;
282
+ const extension = filename.split('.').pop();
283
+ return {
284
+ filename: filename,
285
+ extension: extension.toUpperCase(),
286
+ isImage: this.isImageFile(filename),
287
+ previewUrl: this.getServerFileUrl(filename),
288
+ clickHandler: () => {
289
+ if (this.isImageFile(filename)) {
290
+ this.openImageModal(this.getServerFileUrl(filename), filename);
291
+ }
292
+ }
293
+ };
294
+ } else {
295
+ // attachment is object (from current session)
296
+ return {
297
+ filename: attachment.name,
298
+ extension: attachment.extension.toUpperCase(),
299
+ isImage: attachment.type === 'image',
300
+ previewUrl: this.getAttachmentPreviewUrl(attachment),
301
+ clickHandler: () => {
302
+ if (attachment.type === 'image') {
303
+ const imageUrl = this.getAttachmentPreviewUrl(attachment);
304
+ this.openImageModal(imageUrl, attachment.name);
305
+ }
306
+ }
307
+ };
308
+ }
309
+ },
310
+
311
// Generate GUID for unique filenames
312
generateGUID() {
313
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
webui/js/messages.js
+78
-65
@@ -318,103 +318,116 @@ export function drawMessageUser(
318
const attachmentDiv = document.createElement("div");
319
attachmentDiv.classList.add("attachment-item");
320
321
- // Helper function to generate server-side image URL
321
+ // Get attachment store for enhanced device sync support
322
+ const attachmentStore = window.Alpine && window.Alpine.store('chatAttachments');
323
+
324
+ // Helper function to generate server-side image URL (fallback if store not available)
325
const getServerImageUrl = (filename) => {
326
+ if (attachmentStore) {
327
+ return attachmentStore.getServerFileUrl(filename);
328
+ }
329
return `/image_get?path=/a0/tmp/uploads/${encodeURIComponent(filename)}`;
330
};
331
326
- // Helper function to check if file is an image
332
+ // Helper function to check if file is an image (fallback if store not available)
333
const isImageFile = (filename) => {
334
+ if (attachmentStore) {
335
+ return attachmentStore.isImageFile(filename);
336
+ }
337
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'];
338
const extension = filename.split('.').pop().toLowerCase();
339
return imageExtensions.includes(extension);
340
};
341
333
- if (typeof attachment === "string") {
334
- // attachment is filename only (from persistent storage)
335
- const filename = attachment;
336
- const extension = filename.split(".").pop().toUpperCase();
337
-
338
- if (isImageFile(filename)) {
339
- // Render as image with server URL
340
- const imgWrapper = document.createElement("div");
341
- imgWrapper.classList.add("image-wrapper");
342
-
343
- const img = document.createElement("img");
344
- img.src = getServerImageUrl(filename);
345
- img.alt = filename;
346
- img.classList.add("attachment-preview");
347
- img.style.cursor = "pointer";
348
- img.addEventListener('click', () => {
349
- if (window.Alpine && window.Alpine.store('chatAttachments')) {
350
- window.Alpine.store('chatAttachments').openImageModal(getServerImageUrl(filename), filename);
342
+ // Use enhanced attachment store methods for better device sync
343
+ let displayInfo;
344
+ if (attachmentStore) {
345
+ displayInfo = attachmentStore.getAttachmentDisplayInfo(attachment);
346
+ } else {
347
+ // Fallback for when store is not available
348
+ if (typeof attachment === "string") {
349
+ const filename = attachment;
350
+ const extension = filename.split(".").pop();
351
+ displayInfo = {
352
+ filename: filename,
353
+ extension: extension.toUpperCase(),
354
+ isImage: isImageFile(filename),
355
+ previewUrl: getServerImageUrl(filename),
356
+ clickHandler: () => {
357
+ if (isImageFile(filename) && window.Alpine && window.Alpine.store('chatAttachments')) {
358
+ window.Alpine.store('chatAttachments').openImageModal(getServerImageUrl(filename), filename);
359
+ }
360
}
352
- });
353
-
354
- const fileInfo = document.createElement("div");
355
- fileInfo.classList.add("file-info");
356
- fileInfo.innerHTML = `
357
- <span class="filename">${filename}</span>
358
- <span class="extension">${extension}</span>
359
- `;
360
-
361
- imgWrapper.appendChild(img);
362
- attachmentDiv.appendChild(imgWrapper);
363
- attachmentDiv.appendChild(fileInfo);
361
+ };
362
} else {
365
- // Render as file icon
366
- attachmentDiv.classList.add("file-type");
367
- attachmentDiv.innerHTML = `
368
- <div class="file-preview">
369
- <span class="filename">${filename}</span>
370
- <span class="extension">${extension}</span>
371
- </div>
372
- `;
363
+ displayInfo = {
364
+ filename: attachment.name,
365
+ extension: attachment.extension.toUpperCase(),
366
+ isImage: attachment.type === 'image',
367
+ previewUrl: attachment.url,
368
+ clickHandler: () => {
369
+ if (attachment.type === 'image' && window.Alpine && window.Alpine.store('chatAttachments')) {
370
+ window.Alpine.store('chatAttachments').openImageModal(attachment.url, attachment.name);
371
+ }
372
+ }
373
+ };
374
}
374
- } else if (attachment.type === "image") {
375
- // attachment is object (from current session)
375
+ }
376
+
377
+ if (displayInfo.isImage) {
378
+ // Render as image with enhanced device sync support
379
const imgWrapper = document.createElement("div");
380
imgWrapper.classList.add("image-wrapper");
381
382
const img = document.createElement("img");
380
- // Use server URL if we have filename, otherwise fall back to blob URL for current session
381
- let imageUrl;
382
- if (attachment.name && !attachment.url.startsWith('blob:')) {
383
- imageUrl = getServerImageUrl(attachment.name);
384
- } else {
385
- imageUrl = attachment.url;
386
- }
387
- img.src = imageUrl;
388
- img.alt = attachment.name;
383
+ img.src = displayInfo.previewUrl;
384
+ img.alt = displayInfo.filename;
385
img.classList.add("attachment-preview");
386
img.style.cursor = "pointer";
391
- img.addEventListener('click', () => {
392
- if (window.Alpine && window.Alpine.store('chatAttachments')) {
393
- window.Alpine.store('chatAttachments').openImageModal(imageUrl, attachment.name);
394
- }
395
- });
387
+ img.addEventListener('click', displayInfo.clickHandler);
388
389
const fileInfo = document.createElement("div");
390
fileInfo.classList.add("file-info");
391
fileInfo.innerHTML = `
400
- <span class="filename">${attachment.name}</span>
401
- <span class="extension">${attachment.extension.toUpperCase()}</span>
392
+ <span class="filename">${displayInfo.filename}</span>
393
+ <span class="extension">${displayInfo.extension}</span>
394
`;
395
396
imgWrapper.appendChild(img);
397
attachmentDiv.appendChild(imgWrapper);
398
attachmentDiv.appendChild(fileInfo);
399
} else {
408
- // attachment is object but not image (from current session)
400
+ // Render as file with icon support for device sync
401
attachmentDiv.classList.add("file-type");
410
- attachmentDiv.innerHTML = `
411
- <div class="file-preview">
412
- <span class="filename">${attachment.name}</span>
413
- <span class="extension">${attachment.extension.toUpperCase()}</span>
414
- </div>
415
- `;
402
+
403
+ // Create file preview with potential server-side icon
404
+ const filePreview = document.createElement("div");
405
+ filePreview.classList.add("file-preview");
406
+
407
+ // If we have a preview URL (server icon), show it
408
+ if (displayInfo.previewUrl && displayInfo.previewUrl !== displayInfo.filename) {
409
+ const iconImg = document.createElement("img");
410
+ iconImg.src = displayInfo.previewUrl;
411
+ iconImg.alt = `${displayInfo.extension} file`;
412
+ iconImg.classList.add("file-icon");
413
+ iconImg.style.width = "24px";
414
+ iconImg.style.height = "24px";
415
+ iconImg.style.marginRight = "8px";
416
+ filePreview.appendChild(iconImg);
417
+ }
418
+
419
+ const textInfo = document.createElement("div");
420
+ textInfo.innerHTML = `
421
+ <span class="filename">${displayInfo.filename}</span>
422
+ <span class="extension">${displayInfo.extension}</span>
423
+ `;
424
+ filePreview.appendChild(textInfo);
425
+
426
+ attachmentDiv.appendChild(filePreview);
427
}
428
429
+
430
+
431
attachmentsContainer.appendChild(attachmentDiv);
432
});
433