diff --git a/src/composables/widgets/useComboWidget.ts b/src/composables/widgets/useComboWidget.ts index 4a6643af4..da3048c7b 100644 --- a/src/composables/widgets/useComboWidget.ts +++ b/src/composables/widgets/useComboWidget.ts @@ -64,18 +64,27 @@ const FILE_EXTENSIONS = [ ] /** - * Check if options contain filename-like values + * Check if options contain filename-like values (UUIDs or legacy hashes) */ function hasFilenameOptions(options: any[]): boolean { return options.some((opt: any) => { if (typeof opt !== 'string') return false - // Check for common file extensions + + // Check for UUID format (new system) + const isUUID = + /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test( + opt + ) + + // Check for common file extensions (legacy) const hasExtension = FILE_EXTENSIONS.some((ext) => opt.toLowerCase().endsWith(ext) ) - // Check for hash-like filenames (ComfyUI hashed files) + + // Check for hash-like filenames (legacy ComfyUI hashed files) const isHashLike = /^[a-f0-9]{8,}\./i.test(opt) - return hasExtension || isHashLike + + return isUUID || hasExtension || isHashLike }) } @@ -98,9 +107,9 @@ function applyFilenameMappingToWidget( // Cast to extended interface for type safety const mappingWidget = widget as IFilenameMappingWidget - // Override serializeValue to ensure hash is used for API + // Override serializeValue to ensure asset ID is used for API mappingWidget.serializeValue = function () { - // Always return the actual widget value (hash) for serialization + // Always return the actual widget value (asset ID) for serialization return mappingWidget.value } @@ -110,16 +119,16 @@ function applyFilenameMappingToWidget( get() { if (mappingWidget.computedDisabled) return '' - // Get current hash value - const hashValue = mappingWidget.value - if (typeof hashValue !== 'string') return String(hashValue) + // Get current asset ID value + const assetId = mappingWidget.value + if (typeof assetId !== 'string') return String(assetId) // Try to get human-readable name from cache (deduplicated for display) const mapping = fileNameMappingService.getCachedMapping('input', true) - const humanName = mapping[hashValue] + const humanName = mapping[assetId] - // Return human name for display, fallback to hash - return humanName || hashValue + // Return human name for display, fallback to asset ID + return humanName || assetId }, configurable: true }) @@ -237,19 +246,19 @@ function applyFilenameMappingToWidget( 'input', true ) - const hashValue = reverseMapping[selectedValue] || selectedValue + const assetId = reverseMapping[selectedValue] || selectedValue - // Set the hash value - mappingWidget.value = hashValue + // Set the asset ID + mappingWidget.value = assetId - // Call original setValue with hash value if it exists + // Call original setValue with asset ID if it exists if (originalSetValue) { - originalSetValue.call(mappingWidget, hashValue, options) + originalSetValue.call(mappingWidget, assetId, options) } - // Trigger callback with hash value + // Trigger callback with asset ID if (mappingWidget.callback) { - mappingWidget.callback.call(mappingWidget, hashValue) + mappingWidget.callback.call(mappingWidget, assetId) } } else { mappingWidget.value = selectedValue @@ -273,14 +282,14 @@ function applyFilenameMappingToWidget( 'input', true ) - const hashValue = reverseMapping[selectedValue] || selectedValue + const assetId = reverseMapping[selectedValue] || selectedValue - // Set the hash value - mappingWidget.value = hashValue + // Set the asset ID + mappingWidget.value = assetId - // Call original callback with hash value + // Call original callback with asset ID if (originalCallback) { - originalCallback.call(mappingWidget, hashValue) + originalCallback.call(mappingWidget, assetId) } } else { mappingWidget.value = selectedValue diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 3e815e87d..6a5860780 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -951,6 +951,16 @@ export class ComfyApp { async registerNodes() { // Load node definitions from the backend const defs = await this.getNodeDefs() + + // Load filename mappings alongside node definitions for better UX + import('@/services/fileNameMappingService').then( + ({ fileNameMappingService }) => { + fileNameMappingService.ensureMappingsLoaded('input').catch(() => { + // Silently fail - lazy loading will still work + }) + } + ) + await this.registerNodesFromDefs(defs) await useExtensionService().invokeExtensionsAsync('registerCustomNodes') if (this.vueAppReady) { diff --git a/src/services/fileNameMappingService.ts b/src/services/fileNameMappingService.ts index 196c080f8..01a40525f 100644 --- a/src/services/fileNameMappingService.ts +++ b/src/services/fileNameMappingService.ts @@ -3,7 +3,7 @@ import { api } from '@/scripts/api' export type FileType = 'input' | 'output' | 'temp' export interface FileNameMapping { - [hashFilename: string]: string // hash -> human readable name + [assetId: string]: string // asset-id -> asset name } export interface CacheEntry { @@ -16,8 +16,8 @@ export interface CacheEntry { } /** - * Service for fetching and caching filename mappings from the backend. - * Maps SHA256 hash filenames to their original human-readable names. + * Service for fetching and caching asset mappings from the backend. + * Maps asset IDs (UUIDs) to their human-readable asset names. */ export class FileNameMappingService { private cache = new Map() @@ -49,46 +49,46 @@ export class FileNameMappingService { } /** - * Get human-readable filename from hash filename. - * @param hashFilename - The SHA256 hash filename + * Get human-readable asset name from asset ID. + * @param assetId - The asset ID (UUID) * @param fileType - The type of file * @returns Promise resolving to human-readable name or original if not found */ async getHumanReadableName( - hashFilename: string, + assetId: string, fileType: FileType = 'input' ): Promise { try { const mapping = await this.getMapping(fileType) - return mapping[hashFilename] ?? hashFilename + return mapping[assetId] ?? assetId } catch (error) { // Log error without exposing file paths if (process.env.NODE_ENV === 'development') { console.warn('Failed to get human readable name:', error) } - return hashFilename + return assetId } } /** - * Apply filename mapping to an array of hash filenames. - * @param hashFilenames - Array of SHA256 hash filenames + * Apply asset mapping to an array of asset IDs. + * @param assetIds - Array of asset IDs (UUIDs) * @param fileType - The type of files * @returns Promise resolving to array of human-readable names */ async applyMappingToArray( - hashFilenames: string[], + assetIds: string[], fileType: FileType = 'input' ): Promise { try { const mapping = await this.getMapping(fileType) - return hashFilenames.map((filename) => mapping[filename] ?? filename) + return assetIds.map((assetId) => mapping[assetId] ?? assetId) } catch (error) { // Log error without exposing sensitive data if (process.env.NODE_ENV === 'development') { - console.warn('Failed to apply filename mapping') + console.warn('Failed to apply asset mapping') } - return hashFilenames + return assetIds } } @@ -137,12 +137,12 @@ export class FileNameMappingService { } /** - * Convert a human-readable name back to its hash filename. - * @param humanName - The human-readable filename + * Convert a human-readable name back to its asset ID. + * @param humanName - The human-readable asset name * @param fileType - The file type - * @returns The hash filename or the original if no mapping exists + * @returns The asset ID or the original if no mapping exists */ - getHashFromHumanName( + getAssetIdFromHumanName( humanName: string, fileType: FileType = 'input' ): string { @@ -351,7 +351,7 @@ export class FileNameMappingService { const currentIndex = (nameIndex.get(humanName) || 0) + 1 nameIndex.set(humanName, currentIndex) - // Extract file extension if present + // Extract file extension from human name if present const lastDotIndex = humanName.lastIndexOf('.') let baseName = humanName let extension = '' @@ -361,13 +361,25 @@ export class FileNameMappingService { extension = humanName.substring(lastDotIndex) } - // Add suffix: use first 8 chars of hash (without extension) - // Remove extension from hash if present - const hashWithoutExt = hash.includes('.') - ? hash.substring(0, hash.lastIndexOf('.')) - : hash - const hashSuffix = hashWithoutExt.substring(0, 8) - dedupMapping[hash] = `${baseName}_${hashSuffix}${extension}` + // Create suffix from hash/UUID + let hashSuffix: string + if ( + /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test( + hash + ) + ) { + // UUID format - use first 8 characters + hashSuffix = hash.substring(0, 8) + } else { + // Legacy hash format - remove extension if present and take first 8 chars + const hashWithoutExt = hash.includes('.') + ? hash.substring(0, hash.lastIndexOf('.')) + : hash + hashSuffix = hashWithoutExt.substring(0, 8) + } + + const dedupName = `${baseName}_${hashSuffix}${extension}` + dedupMapping[hash] = dedupName } } diff --git a/tests-ui/tests/services/fileNameMappingService.test.ts b/tests-ui/tests/services/fileNameMappingService.test.ts index d50a8808a..8032d564d 100644 --- a/tests-ui/tests/services/fileNameMappingService.test.ts +++ b/tests-ui/tests/services/fileNameMappingService.test.ts @@ -324,8 +324,11 @@ describe('FileNameMappingService', () => { await service.getMapping('input') - const hash = service.getHashFromHumanName('vacation_photo.png', 'input') - expect(hash).toBe('abc123.png') + const assetId = service.getAssetIdFromHumanName( + 'vacation_photo.png', + 'input' + ) + expect(assetId).toBe('abc123.png') }) it('should return original name if no mapping exists', async () => { @@ -337,7 +340,7 @@ describe('FileNameMappingService', () => { await service.getMapping('input') - const result = service.getHashFromHumanName('unknown.png', 'input') + const result = service.getAssetIdFromHumanName('unknown.png', 'input') expect(result).toBe('unknown.png') }) })