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