+
+export const Default: Story = {
+ render: (args) => ({
+ components: { TabList, Tab },
+ setup() {
+ const activeTab = ref(args.modelValue || 'tab1')
+ return { activeTab }
+ },
+ template: `
+
+ Tab 1
+ Tab 2
+ Tab 3
+
+
+ Selected tab: {{ activeTab }}
+
+ `
+ }),
+ args: {
+ modelValue: 'tab1'
+ }
+}
+
+export const ManyTabs: Story = {
+ render: () => ({
+ components: { TabList, Tab },
+ setup() {
+ const activeTab = ref('tab1')
+ return { activeTab }
+ },
+ template: `
+
+ Dashboard
+ Analytics
+ Reports
+ Settings
+ Profile
+
+
+ Selected tab: {{ activeTab }}
+
+ `
+ })
+}
+
+export const WithIcons: Story = {
+ render: () => ({
+ components: { TabList, Tab },
+ setup() {
+ const activeTab = ref('home')
+ return { activeTab }
+ },
+ template: `
+
+
+
+ Home
+
+
+
+ Users
+
+
+
+ Settings
+
+
+
+ Selected tab: {{ activeTab }}
+
+ `
+ })
+}
+
+export const LongLabels: Story = {
+ render: () => ({
+ components: { TabList, Tab },
+ setup() {
+ const activeTab = ref('overview')
+ return { activeTab }
+ },
+ template: `
+
+ Project Overview
+ Documentation & Guides
+ Deployment Settings
+ Monitoring & Analytics
+
+
+ Selected tab: {{ activeTab }}
+
+ `
+ })
+}
+
+export const Interactive: Story = {
+ render: () => ({
+ components: { TabList, Tab },
+ setup() {
+ const activeTab = ref('input')
+ const handleTabChange = (value: string) => {
+ console.log('Tab changed to:', value)
+ }
+ return { activeTab, handleTabChange }
+ },
+ template: `
+
+
+
Example: Media Assets
+
+ Imported
+ Generated
+
+
+
+
+
+
Showing imported assets...
+
+
+
Showing generated assets...
+
+
+
+
+ Current tab value: {{ activeTab }}
+
+
+ `
+ })
+}
diff --git a/src/components/tab/TabList.vue b/src/components/tab/TabList.vue
new file mode 100644
index 0000000000..5db691d96e
--- /dev/null
+++ b/src/components/tab/TabList.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index 03b322de4a..728715b972 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -597,6 +597,7 @@
"templates": "Templates",
"assets": "Assets",
"mediaAssets": "Media Assets",
+ "backToAssets": "Back to all assets",
"labels": {
"queue": "Queue",
"nodes": "Nodes",
@@ -2072,6 +2073,14 @@
"loadingAsset": "Loading asset"
}
},
+ "mediaAsset": {
+ "jobIdToast": {
+ "jobIdCopied": "Job ID copied to clipboard",
+ "jobIdCopyFailed": "Failed to copy Job ID",
+ "copied": "Copied",
+ "error": "Error"
+ }
+ },
"actionbar": {
"dockToTop": "Dock to top"
},
diff --git a/src/platform/assets/components/MediaAssetActions.vue b/src/platform/assets/components/MediaAssetActions.vue
index 73144d4d22..74e29fb237 100644
--- a/src/platform/assets/components/MediaAssetActions.vue
+++ b/src/platform/assets/components/MediaAssetActions.vue
@@ -3,7 +3,7 @@
-
+
-
+
diff --git a/src/platform/assets/components/MediaAssetCard.vue b/src/platform/assets/components/MediaAssetCard.vue
index 3b20567c9a..b2b1ea87c5 100644
--- a/src/platform/assets/components/MediaAssetCard.vue
+++ b/src/platform/assets/components/MediaAssetCard.vue
@@ -39,7 +39,7 @@
:asset="adaptedAsset"
:context="{ type: assetType }"
@view="handleZoomClick"
- @download="actions.downloadAsset(asset.id)"
+ @download="actions.downloadAsset()"
@play="actions.playAsset(asset.id)"
@video-playing-state-changed="isVideoPlaying = $event"
@video-controls-changed="showVideoControls = $event"
@@ -51,6 +51,7 @@
@@ -89,8 +90,8 @@
@@ -172,14 +173,17 @@ function getBottomComponent(kind: MediaKind) {
return mediaComponents.bottom[kind] || mediaComponents.bottom.image
}
-const { asset, loading, selected } = defineProps<{
+const { asset, loading, selected, showOutputCount, outputCount } = defineProps<{
asset?: AssetItem
loading?: boolean
selected?: boolean
+ showOutputCount?: boolean
+ outputCount?: number
}>()
const emit = defineEmits<{
zoom: [asset: AssetItem]
+ 'output-count-click': []
}>()
const cardContainerRef = ref()
@@ -277,7 +281,11 @@ const showHoverActions = computed(
() => !loading && !!asset && isCardOrOverlayHovered.value
)
-const showActionsOverlay = false
+const showActionsOverlay = computed(
+ () =>
+ showHoverActions.value &&
+ (!isVideoPlaying.value || isCardOrOverlayHovered.value)
+)
const showZoomOverlay = computed(
() =>
@@ -302,10 +310,6 @@ const showFileFormatChip = computed(
(!isVideoPlaying.value || isCardOrOverlayHovered.value)
)
-const showOutputCount = computed(
- () => false // Remove output count for simplified version
-)
-
const handleCardClick = () => {
if (adaptedAsset.value) {
actions.selectAsset(adaptedAsset.value)
@@ -329,4 +333,8 @@ const handleZoomClick = () => {
const handleImageLoaded = (width: number, height: number) => {
imageDimensions.value = { width, height }
}
+
+const handleOutputCountClick = () => {
+ emit('output-count-click')
+}
diff --git a/src/platform/assets/components/MediaAssetMoreMenu.vue b/src/platform/assets/components/MediaAssetMoreMenu.vue
index 62f793aa26..b62fc20599 100644
--- a/src/platform/assets/components/MediaAssetMoreMenu.vue
+++ b/src/platform/assets/components/MediaAssetMoreMenu.vue
@@ -63,6 +63,7 @@
-
+
void
}>()
+const emit = defineEmits<{
+ inspect: []
+}>()
+
const { asset, context } = inject(MediaAssetKey)!
const actions = useMediaAssetActions()
-const galleryStore = useMediaAssetGalleryStore()
const showWorkflowOptions = computed(() => context.value.type)
+// Only show Copy Job ID for output assets (not for imported/input assets)
+const showCopyJobId = computed(() => {
+ const assetType = asset.value?.tags?.[0] || context.value?.type
+ return assetType !== 'input'
+})
+
const handleInspect = () => {
- if (asset.value) {
- galleryStore.openSingle(asset.value)
- }
+ emit('inspect')
close()
}
@@ -124,7 +131,7 @@ const handleAddToWorkflow = () => {
const handleDownload = () => {
if (asset.value) {
- actions.downloadAsset(asset.value.id)
+ actions.downloadAsset()
}
close()
}
@@ -143,9 +150,9 @@ const handleExportWorkflow = () => {
close()
}
-const handleCopyJobId = () => {
+const handleCopyJobId = async () => {
if (asset.value) {
- actions.copyAssetUrl(asset.value.id)
+ await actions.copyJobId()
}
close()
}
diff --git a/src/platform/assets/composables/useMediaAssetActions.ts b/src/platform/assets/composables/useMediaAssetActions.ts
index 8a31663933..0972abd486 100644
--- a/src/platform/assets/composables/useMediaAssetActions.ts
+++ b/src/platform/assets/composables/useMediaAssetActions.ts
@@ -1,13 +1,50 @@
/* eslint-disable no-console */
+import { useToast } from 'primevue/usetoast'
+import { inject } from 'vue'
+
+import { downloadFile } from '@/base/common/downloadUtil'
+import { t } from '@/i18n'
+import { api } from '@/scripts/api'
+import { extractPromptIdFromAssetId } from '@/utils/uuidUtil'
+
import type { AssetMeta } from '../schemas/mediaAssetSchema'
+import { MediaAssetKey } from '../schemas/mediaAssetSchema'
export function useMediaAssetActions() {
+ const toast = useToast()
+ const mediaContext = inject(MediaAssetKey, null)
+
const selectAsset = (asset: AssetMeta) => {
console.log('Asset selected:', asset)
}
- const downloadAsset = (assetId: string) => {
- console.log('Downloading asset:', assetId)
+ const downloadAsset = () => {
+ const asset = mediaContext?.asset.value
+ if (!asset) return
+
+ try {
+ const assetType = asset.tags?.[0] || 'output'
+ const filename = asset.name
+ const downloadUrl = api.apiURL(
+ `/view?filename=${encodeURIComponent(filename)}&type=${assetType}`
+ )
+
+ downloadFile(downloadUrl, filename)
+
+ toast.add({
+ severity: 'success',
+ summary: t('g.success'),
+ detail: t('g.downloadStarted'),
+ life: 2000
+ })
+ } catch (error) {
+ toast.add({
+ severity: 'error',
+ summary: t('g.error'),
+ detail: t('g.failedToDownloadImage'),
+ life: 3000
+ })
+ }
}
const deleteAsset = (assetId: string) => {
@@ -18,12 +55,38 @@ export function useMediaAssetActions() {
console.log('Playing asset:', assetId)
}
- const copyAssetUrl = (assetId: string) => {
- console.log('Copy asset URL:', assetId)
- }
+ const copyJobId = async () => {
+ const asset = mediaContext?.asset.value
+ if (!asset) return
+
+ const promptId = extractPromptIdFromAssetId(asset.id)
+
+ if (!promptId) {
+ toast.add({
+ severity: 'warn',
+ summary: t('g.warning'),
+ detail: 'No job ID found for this asset',
+ life: 2000
+ })
+ return
+ }
- const copyJobId = (jobId: string) => {
- console.log('Copy job ID:', jobId)
+ try {
+ await navigator.clipboard.writeText(promptId)
+ toast.add({
+ severity: 'success',
+ summary: t('g.success'),
+ detail: t('mediaAsset.jobIdToast.jobIdCopied'),
+ life: 2000
+ })
+ } catch (error) {
+ toast.add({
+ severity: 'error',
+ summary: t('g.error'),
+ detail: t('mediaAsset.jobIdToast.jobIdCopyFailed'),
+ life: 3000
+ })
+ }
}
const addWorkflow = (assetId: string) => {
@@ -47,7 +110,6 @@ export function useMediaAssetActions() {
downloadAsset,
deleteAsset,
playAsset,
- copyAssetUrl,
copyJobId,
addWorkflow,
openWorkflow,
diff --git a/src/platform/assets/composables/useMediaAssets/assetMappers.ts b/src/platform/assets/composables/useMediaAssets/assetMappers.ts
index 52721bb0e5..8e319b1d01 100644
--- a/src/platform/assets/composables/useMediaAssets/assetMappers.ts
+++ b/src/platform/assets/composables/useMediaAssets/assetMappers.ts
@@ -1,4 +1,5 @@
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
+import type { OutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
import type { AssetContext } from '@/platform/assets/schemas/mediaAssetSchema'
import { api } from '@/scripts/api'
import type { ResultItemImpl, TaskItemImpl } from '@/stores/queueStore'
@@ -25,25 +26,13 @@ export function mapTaskOutputToAssetItem(
taskItem: TaskItemImpl,
output: ResultItemImpl
): AssetItem {
- const metadata: Record = {
+ const metadata: OutputAssetMetadata = {
promptId: taskItem.promptId,
nodeId: output.nodeId,
- subfolder: output.subfolder
- }
-
- // Add execution time if available
- if (taskItem.executionTimeInSeconds) {
- metadata.executionTimeInSeconds = taskItem.executionTimeInSeconds
- }
-
- // Add format if available
- if (output.format) {
- metadata.format = output.format
- }
-
- // Add workflow if available
- if (taskItem.workflow) {
- metadata.workflow = taskItem.workflow
+ subfolder: output.subfolder,
+ executionTimeInSeconds: taskItem.executionTimeInSeconds,
+ format: output.format,
+ workflow: taskItem.workflow
}
return {
diff --git a/src/platform/assets/composables/useMediaAssets/useAssetsApi.ts b/src/platform/assets/composables/useMediaAssets/useAssetsApi.ts
index 927e41d51a..56370e4db7 100644
--- a/src/platform/assets/composables/useMediaAssets/useAssetsApi.ts
+++ b/src/platform/assets/composables/useMediaAssets/useAssetsApi.ts
@@ -19,12 +19,24 @@ async function fetchInputAssets(directory: string): Promise {
*/
function fetchOutputAssets(): AssetItem[] {
const queueStore = useQueueStore()
- return queueStore.flatTasks
+
+ const assetItems: AssetItem[] = queueStore.tasks
.filter((task) => task.previewOutput && task.displayStatus === 'Completed')
.map((task) => {
const output = task.previewOutput!
- return mapTaskOutputToAssetItem(task, output)
+ const assetItem = mapTaskOutputToAssetItem(task, output)
+
+ // Add output count and all outputs for folder view
+ assetItem.user_metadata = {
+ ...assetItem.user_metadata,
+ outputCount: task.flatOutputs.filter((o) => o.supportsPreview).length,
+ allOutputs: task.flatOutputs.filter((o) => o.supportsPreview)
+ }
+
+ return assetItem
})
+
+ return assetItems
}
/**
diff --git a/src/platform/assets/composables/useMediaAssets/useInternalFilesApi.ts b/src/platform/assets/composables/useMediaAssets/useInternalFilesApi.ts
index dcc17cd7fe..3555f6101b 100644
--- a/src/platform/assets/composables/useMediaAssets/useInternalFilesApi.ts
+++ b/src/platform/assets/composables/useMediaAssets/useInternalFilesApi.ts
@@ -34,16 +34,29 @@ async function fetchInputFiles(directory: string): Promise {
*/
function fetchOutputFiles(): AssetItem[] {
const queueStore = useQueueStore()
- return queueStore.flatTasks
+
+ // Use tasks (already grouped by promptId) instead of flatTasks
+ const assetItems: AssetItem[] = queueStore.tasks
.filter((task) => task.previewOutput && task.displayStatus === 'Completed')
.map((task) => {
const output = task.previewOutput!
- return mapTaskOutputToAssetItem(task, output)
+ const assetItem = mapTaskOutputToAssetItem(task, output)
+
+ // Add output count and all outputs for folder view
+ assetItem.user_metadata = {
+ ...assetItem.user_metadata,
+ outputCount: task.flatOutputs.filter((o) => o.supportsPreview).length,
+ allOutputs: task.flatOutputs.filter((o) => o.supportsPreview)
+ }
+
+ return assetItem
})
- .sort(
- (a, b) =>
- new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
- )
+
+ // Sort by creation date (newest first)
+ return assetItems.sort(
+ (a, b) =>
+ new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
+ )
}
/**
diff --git a/src/platform/assets/schemas/assetMetadataSchema.ts b/src/platform/assets/schemas/assetMetadataSchema.ts
new file mode 100644
index 0000000000..335e899082
--- /dev/null
+++ b/src/platform/assets/schemas/assetMetadataSchema.ts
@@ -0,0 +1,41 @@
+import type { ResultItemImpl } from '@/stores/queueStore'
+
+/**
+ * Metadata for output assets from queue store
+ * Extends Record for compatibility with AssetItem schema
+ */
+export interface OutputAssetMetadata extends Record {
+ promptId: string
+ nodeId: string | number
+ subfolder: string
+ executionTimeInSeconds?: number
+ format?: string
+ workflow?: unknown
+ outputCount?: number
+ allOutputs?: ResultItemImpl[]
+}
+
+/**
+ * Type guard to check if metadata is OutputAssetMetadata
+ */
+function isOutputAssetMetadata(
+ metadata: Record | undefined
+): metadata is OutputAssetMetadata {
+ if (!metadata) return false
+ return (
+ typeof metadata.promptId === 'string' &&
+ (typeof metadata.nodeId === 'string' || typeof metadata.nodeId === 'number')
+ )
+}
+
+/**
+ * Safely extract output asset metadata
+ */
+export function getOutputAssetMetadata(
+ userMetadata: Record | undefined
+): OutputAssetMetadata | null {
+ if (isOutputAssetMetadata(userMetadata)) {
+ return userMetadata
+ }
+ return null
+}
diff --git a/src/platform/assets/schemas/mediaAssetSchema.ts b/src/platform/assets/schemas/mediaAssetSchema.ts
index d35dad6c59..818db6b4a2 100644
--- a/src/platform/assets/schemas/mediaAssetSchema.ts
+++ b/src/platform/assets/schemas/mediaAssetSchema.ts
@@ -19,9 +19,7 @@ const zMediaAssetDisplayItemSchema = assetItemSchema.extend({
// New optional fields
duration: z.number().nonnegative().optional(),
- dimensions: zDimensionsSchema.optional(),
- jobId: z.string().optional(),
- isMulti: z.boolean().optional()
+ dimensions: zDimensionsSchema.optional()
})
// Asset context schema
diff --git a/src/utils/uuidUtil.ts b/src/utils/uuidUtil.ts
new file mode 100644
index 0000000000..0f52254705
--- /dev/null
+++ b/src/utils/uuidUtil.ts
@@ -0,0 +1,45 @@
+import { validate as uuidValidate } from 'uuid'
+
+/**
+ * UUID utility functions
+ */
+
+/**
+ * Regular expression for matching UUID format at the beginning of a string
+ * Format: 8-4-4-4-12 (e.g., 98b0b007-7d78-4e3f-b7a8-0f483b9cf2d3)
+ */
+const UUID_REGEX =
+ /^([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i
+
+/**
+ * Extract UUID from the beginning of a string
+ * @param str - The string to extract UUID from
+ * @returns The extracted UUID or null if not found
+ */
+export function extractUuidFromString(str: string): string | null {
+ const match = str.match(UUID_REGEX)
+ if (!match) return null
+
+ const uuid = match[1]
+ // Validate the extracted string is a valid UUID
+ return uuidValidate(uuid) ? uuid : null
+}
+
+/**
+ * Check if a string is a valid UUID (any version)
+ * @param str - The string to check
+ * @returns true if the string is a valid UUID
+ */
+export function isValidUuid(str: string): boolean {
+ return uuidValidate(str)
+}
+
+/**
+ * Extract prompt ID from asset ID
+ * Asset ID format: "promptId-nodeId-filename"
+ * @param assetId - The asset ID string
+ * @returns The extracted prompt ID (UUID) or null
+ */
+export function extractPromptIdFromAssetId(assetId: string): string | null {
+ return extractUuidFromString(assetId)
+}
diff --git a/tests-ui/tests/utils/uuidUtil.test.ts b/tests-ui/tests/utils/uuidUtil.test.ts
new file mode 100644
index 0000000000..8a05d346ca
--- /dev/null
+++ b/tests-ui/tests/utils/uuidUtil.test.ts
@@ -0,0 +1,155 @@
+import { describe, expect, it } from 'vitest'
+
+import {
+ extractPromptIdFromAssetId,
+ extractUuidFromString,
+ isValidUuid
+} from '@/utils/uuidUtil'
+
+describe('uuidUtil', () => {
+ describe('extractUuidFromString', () => {
+ it('should extract UUID from the beginning of a string', () => {
+ const str = '98b0b007-7d78-4e3f-b7a8-0f483b9cf2d3-9-ComfyUI_00042_.png'
+ const result = extractUuidFromString(str)
+ expect(result).toBe('98b0b007-7d78-4e3f-b7a8-0f483b9cf2d3')
+ })
+
+ it('should extract UUID with uppercase letters', () => {
+ const str = 'A8B0B007-7D78-4E3F-B7A8-0F483B9CF2D3-node-file.png'
+ const result = extractUuidFromString(str)
+ expect(result).toBe('A8B0B007-7D78-4E3F-B7A8-0F483B9CF2D3')
+ })
+
+ it('should return null if no UUID at the beginning', () => {
+ const str = 'not-a-uuid-98b0b007-7d78-4e3f-b7a8-0f483b9cf2d3'
+ const result = extractUuidFromString(str)
+ expect(result).toBeNull()
+ })
+
+ it('should extract valid UUID even with extra content', () => {
+ const str = '12345678-1234-5234-a234-123456789abc-extra'
+ const result = extractUuidFromString(str)
+ expect(result).toBe('12345678-1234-5234-a234-123456789abc')
+ })
+
+ it('should return null for invalid UUID format', () => {
+ const str = '12345678-1234-1234-1234-123456789xyz-extra' // xyz is not valid hex
+ const result = extractUuidFromString(str)
+ expect(result).toBeNull()
+ })
+
+ it('should handle UUID without trailing content', () => {
+ const str = '98b0b007-7d78-4e3f-b7a8-0f483b9cf2d3'
+ const result = extractUuidFromString(str)
+ expect(result).toBe('98b0b007-7d78-4e3f-b7a8-0f483b9cf2d3')
+ })
+
+ it('should return null for empty string', () => {
+ const result = extractUuidFromString('')
+ expect(result).toBeNull()
+ })
+
+ it('should return null for malformed UUID', () => {
+ const str = '98b0b007-7d78-4e3f-b7a8'
+ const result = extractUuidFromString(str)
+ expect(result).toBeNull()
+ })
+ })
+
+ describe('isValidUuid', () => {
+ it('should return true for valid UUID v4', () => {
+ const uuid = '98b0b007-7d78-4e3f-b7a8-0f483b9cf2d3'
+ expect(isValidUuid(uuid)).toBe(true)
+ })
+
+ it('should return true for uppercase UUID', () => {
+ const uuid = 'A8B0B007-7D78-4E3F-B7A8-0F483B9CF2D3'
+ expect(isValidUuid(uuid)).toBe(true)
+ })
+
+ it('should return true for mixed case UUID', () => {
+ const uuid = 'a8B0b007-7D78-4e3F-B7a8-0F483b9CF2d3'
+ expect(isValidUuid(uuid)).toBe(true)
+ })
+
+ it('should return false for UUID with extra characters', () => {
+ const uuid = '98b0b007-7d78-4e3f-b7a8-0f483b9cf2d3-extra'
+ expect(isValidUuid(uuid)).toBe(false)
+ })
+
+ it('should return false for incomplete UUID', () => {
+ const uuid = '98b0b007-7d78-4e3f-b7a8'
+ expect(isValidUuid(uuid)).toBe(false)
+ })
+
+ it('should return false for UUID with wrong segment lengths', () => {
+ const uuid = '98b0b0007-7d78-4e3f-b7a8-0f483b9cf2d3'
+ expect(isValidUuid(uuid)).toBe(false)
+ })
+
+ it('should return false for UUID with invalid characters', () => {
+ const uuid = '98b0b007-7d78-4e3f-b7a8-0f483b9cfzd3'
+ expect(isValidUuid(uuid)).toBe(false)
+ })
+
+ it('should return false for empty string', () => {
+ expect(isValidUuid('')).toBe(false)
+ })
+
+ it('should return false for null-like values', () => {
+ expect(isValidUuid('null')).toBe(false)
+ expect(isValidUuid('undefined')).toBe(false)
+ })
+ })
+
+ describe('extractPromptIdFromAssetId', () => {
+ it('should extract promptId from typical asset ID', () => {
+ const assetId =
+ '98b0b007-7d78-4e3f-b7a8-0f483b9cf2d3-9-ComfyUI_00042_.png'
+ const result = extractPromptIdFromAssetId(assetId)
+ expect(result).toBe('98b0b007-7d78-4e3f-b7a8-0f483b9cf2d3')
+ })
+
+ it('should extract promptId with multiple dashes in filename', () => {
+ const assetId =
+ '98b0b007-7d78-4e3f-b7a8-0f483b9cf2d3-15-my-image-file.png'
+ const result = extractPromptIdFromAssetId(assetId)
+ expect(result).toBe('98b0b007-7d78-4e3f-b7a8-0f483b9cf2d3')
+ })
+
+ it('should handle asset ID with just UUID and node ID', () => {
+ const assetId = '98b0b007-7d78-4e3f-b7a8-0f483b9cf2d3-42'
+ const result = extractPromptIdFromAssetId(assetId)
+ expect(result).toBe('98b0b007-7d78-4e3f-b7a8-0f483b9cf2d3')
+ })
+
+ it('should return null for input folder asset ID', () => {
+ const assetId = 'input-0-myimage.png'
+ const result = extractPromptIdFromAssetId(assetId)
+ expect(result).toBeNull()
+ })
+
+ it('should return null for malformed asset ID', () => {
+ const assetId = 'not-a-valid-asset-id'
+ const result = extractPromptIdFromAssetId(assetId)
+ expect(result).toBeNull()
+ })
+
+ it('should handle asset ID with special characters in filename', () => {
+ const assetId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890-1-image(1)[2].png'
+ const result = extractPromptIdFromAssetId(assetId)
+ expect(result).toBe('a1b2c3d4-e5f6-7890-abcd-ef1234567890')
+ })
+
+ it('should return null for empty string', () => {
+ const result = extractPromptIdFromAssetId('')
+ expect(result).toBeNull()
+ })
+
+ it('should handle asset ID with underscores in UUID position', () => {
+ const assetId = 'output_1_image.png'
+ const result = extractPromptIdFromAssetId(assetId)
+ expect(result).toBeNull()
+ })
+ })
+})