-
+
@@ -12,18 +12,23 @@
@menu-closed="emit('menuStateChanged', false)"
>
-
+
diff --git a/src/platform/assets/components/MediaAssetCard.stories.ts b/src/platform/assets/components/MediaAssetCard.stories.ts
index 231b86a9b2..d7ccc8ac0f 100644
--- a/src/platform/assets/components/MediaAssetCard.stories.ts
+++ b/src/platform/assets/components/MediaAssetCard.stories.ts
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
import ResultGallery from '@/components/sidebar/tabs/queue/ResultGallery.vue'
import { useMediaAssetGalleryStore } from '../composables/useMediaAssetGalleryStore'
-import type { AssetMeta } from '../schemas/mediaAssetSchema'
+import type { AssetItem } from '../schemas/assetSchema'
import MediaAssetCard from './MediaAssetCard.vue'
const meta: Meta = {
@@ -28,10 +28,6 @@ const meta: Meta = {
})
],
argTypes: {
- context: {
- control: 'select',
- options: ['input', 'output']
- },
loading: {
control: 'boolean'
}
@@ -53,19 +49,20 @@ const SAMPLE_MEDIA = {
audio: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'
}
-const sampleAsset: AssetMeta = {
+const sampleAsset: AssetItem = {
id: 'asset-1',
name: 'sample-image.png',
- kind: 'image',
- duration: 3345,
size: 2048576,
- created_at: Date.now().toString(),
- src: SAMPLE_MEDIA.image1,
- dimensions: {
- width: 1920,
- height: 1080
- },
- tags: []
+ created_at: new Date().toISOString(),
+ preview_url: SAMPLE_MEDIA.image1,
+ tags: ['input'],
+ user_metadata: {
+ duration: 3345,
+ dimensions: {
+ width: 1920,
+ height: 1080
+ }
+ }
}
export const ImageAsset: Story = {
@@ -75,7 +72,6 @@ export const ImageAsset: Story = {
})
],
args: {
- context: { type: 'output', outputCount: 3 },
asset: sampleAsset,
loading: false
}
@@ -88,19 +84,18 @@ export const VideoAsset: Story = {
})
],
args: {
- context: { type: 'input' },
asset: {
...sampleAsset,
id: 'asset-2',
name: 'Big_Buck_Bunny.mp4',
- kind: 'video',
size: 10485760,
- duration: 13425,
- preview_url: SAMPLE_MEDIA.videoThumbnail, // Poster image
- src: SAMPLE_MEDIA.video, // Actual video file
- dimensions: {
- width: 1280,
- height: 720
+ preview_url: SAMPLE_MEDIA.videoThumbnail,
+ user_metadata: {
+ duration: 13425,
+ dimensions: {
+ width: 1280,
+ height: 720
+ }
}
}
}
@@ -113,16 +108,15 @@ export const Model3DAsset: Story = {
})
],
args: {
- context: { type: 'input' },
asset: {
...sampleAsset,
id: 'asset-3',
name: 'Asset-3d-model.glb',
- kind: '3D',
size: 7340032,
- src: '',
- dimensions: undefined,
- duration: 18023
+ preview_url: '',
+ user_metadata: {
+ duration: 18023
+ }
}
}
}
@@ -134,16 +128,15 @@ export const AudioAsset: Story = {
})
],
args: {
- context: { type: 'input' },
asset: {
...sampleAsset,
- id: 'asset-3',
+ id: 'asset-4',
name: 'SoundHelix-Song.mp3',
- kind: 'audio',
size: 5242880,
- src: SAMPLE_MEDIA.audio,
- dimensions: undefined,
- duration: 23180
+ preview_url: SAMPLE_MEDIA.audio,
+ user_metadata: {
+ duration: 23180
+ }
}
}
}
@@ -155,7 +148,6 @@ export const LoadingState: Story = {
})
],
args: {
- context: { type: 'input' },
asset: sampleAsset,
loading: true
}
@@ -168,7 +160,6 @@ export const LongFileName: Story = {
})
],
args: {
- context: { type: 'input' },
asset: {
...sampleAsset,
name: 'very-long-file-name-that-should-be-truncated-in-the-ui-to-prevent-overflow.png'
@@ -183,7 +174,6 @@ export const SelectedState: Story = {
})
],
args: {
- context: { type: 'output', outputCount: 2 },
asset: sampleAsset,
selected: true
}
@@ -196,21 +186,20 @@ export const WebMVideo: Story = {
})
],
args: {
- context: { type: 'input' },
asset: {
id: 'asset-webm',
name: 'animated-clip.webm',
- kind: 'video',
size: 3145728,
- created_at: Date.now().toString(),
- preview_url: SAMPLE_MEDIA.image1, // Poster image
- src: 'https://www.w3schools.com/html/movie.mp4', // Actual video
- duration: 620,
- dimensions: {
- width: 640,
- height: 360
- },
- tags: []
+ created_at: new Date().toISOString(),
+ preview_url: SAMPLE_MEDIA.image1,
+ tags: ['input'],
+ user_metadata: {
+ duration: 620,
+ dimensions: {
+ width: 640,
+ height: 360
+ }
+ }
}
}
}
@@ -222,20 +211,20 @@ export const GifAnimation: Story = {
})
],
args: {
- context: { type: 'input' },
asset: {
id: 'asset-gif',
name: 'animation.gif',
- kind: 'image',
size: 1572864,
- duration: 1345,
- created_at: Date.now().toString(),
- src: 'https://media.giphy.com/media/3o7aCTPPm4OHfRLSH6/giphy.gif',
- dimensions: {
- width: 480,
- height: 270
- },
- tags: []
+ created_at: new Date().toISOString(),
+ preview_url: 'https://media.giphy.com/media/3o7aCTPPm4OHfRLSH6/giphy.gif',
+ tags: ['input'],
+ user_metadata: {
+ duration: 1345,
+ dimensions: {
+ width: 480,
+ height: 270
+ }
+ }
}
}
}
@@ -244,83 +233,89 @@ export const GridLayout: Story = {
render: () => ({
components: { MediaAssetCard },
setup() {
- const assets: AssetMeta[] = [
+ const assets: AssetItem[] = [
{
id: 'grid-1',
name: 'image-file.jpg',
- kind: 'image',
size: 2097152,
- duration: 4500,
- created_at: Date.now().toString(),
- src: SAMPLE_MEDIA.image1,
- dimensions: { width: 1920, height: 1080 },
- tags: []
+ created_at: new Date().toISOString(),
+ preview_url: SAMPLE_MEDIA.image1,
+ tags: ['input'],
+ user_metadata: {
+ duration: 4500,
+ dimensions: { width: 1920, height: 1080 }
+ }
},
{
id: 'grid-2',
name: 'image-file.jpg',
- kind: 'image',
size: 2097152,
- duration: 4500,
- created_at: Date.now().toString(),
- src: SAMPLE_MEDIA.image2,
- dimensions: { width: 1920, height: 1080 },
- tags: []
+ created_at: new Date().toISOString(),
+ preview_url: SAMPLE_MEDIA.image2,
+ tags: ['input'],
+ user_metadata: {
+ duration: 4500,
+ dimensions: { width: 1920, height: 1080 }
+ }
},
{
id: 'grid-3',
name: 'video-file.mp4',
- kind: 'video',
size: 10485760,
- duration: 13425,
- created_at: Date.now().toString(),
- preview_url: SAMPLE_MEDIA.videoThumbnail, // Poster image
- src: SAMPLE_MEDIA.video, // Actual video
- dimensions: { width: 1280, height: 720 },
- tags: []
+ created_at: new Date().toISOString(),
+ preview_url: SAMPLE_MEDIA.videoThumbnail,
+ tags: ['input'],
+ user_metadata: {
+ duration: 13425,
+ dimensions: { width: 1280, height: 720 }
+ }
},
{
id: 'grid-4',
name: 'audio-file.mp3',
- kind: 'audio',
size: 5242880,
- duration: 180,
- created_at: Date.now().toString(),
- src: SAMPLE_MEDIA.audio,
- tags: []
+ created_at: new Date().toISOString(),
+ preview_url: SAMPLE_MEDIA.audio,
+ tags: ['input'],
+ user_metadata: {
+ duration: 180
+ }
},
{
id: 'grid-5',
name: 'animation.gif',
- kind: 'image',
size: 3145728,
- duration: 1345,
- created_at: Date.now().toString(),
- src: 'https://media.giphy.com/media/l0HlNaQ6gWfllcjDO/giphy.gif',
- dimensions: { width: 480, height: 360 },
- tags: []
+ created_at: new Date().toISOString(),
+ preview_url:
+ 'https://media.giphy.com/media/l0HlNaQ6gWfllcjDO/giphy.gif',
+ tags: ['input'],
+ user_metadata: {
+ duration: 1345,
+ dimensions: { width: 480, height: 360 }
+ }
},
{
id: 'grid-6',
name: 'Asset-3d-model.glb',
- kind: '3D',
size: 7340032,
- src: '',
- dimensions: undefined,
- duration: 18023,
- created_at: Date.now().toString(),
- tags: []
+ preview_url: '',
+ created_at: new Date().toISOString(),
+ tags: ['input'],
+ user_metadata: {
+ duration: 18023
+ }
},
{
id: 'grid-7',
name: 'image-file.jpg',
- kind: 'image',
size: 2097152,
- duration: 4500,
- created_at: Date.now().toString(),
- src: SAMPLE_MEDIA.image3,
- dimensions: { width: 1920, height: 1080 },
- tags: []
+ created_at: new Date().toISOString(),
+ preview_url: SAMPLE_MEDIA.image3,
+ tags: ['input'],
+ user_metadata: {
+ duration: 4500,
+ dimensions: { width: 1920, height: 1080 }
+ }
}
]
return { assets }
@@ -330,7 +325,6 @@ export const GridLayout: Story = {
diff --git a/src/platform/assets/components/MediaAssetCard.vue b/src/platform/assets/components/MediaAssetCard.vue
index 1681dcf3ef..d3f7fe4a4b 100644
--- a/src/platform/assets/components/MediaAssetCard.vue
+++ b/src/platform/assets/components/MediaAssetCard.vue
@@ -3,7 +3,12 @@
ref="cardContainerRef"
role="button"
:aria-label="
- asset ? `${asset.name} - ${asset.kind} asset` : 'Loading asset'
+ asset
+ ? $t('assetBrowser.ariaLabel.assetCard', {
+ name: asset.name,
+ type: fileKind
+ })
+ : $t('assetBrowser.ariaLabel.loadingAsset')
"
:tabindex="loading ? -1 : 0"
size="mini"
@@ -28,16 +33,17 @@
-
+
@@ -45,6 +51,8 @@
@@ -63,13 +71,17 @@
-
+
-
+
@@ -79,8 +91,8 @@
@@ -107,11 +119,11 @@
-
+
@@ -129,16 +141,13 @@ import CardBottom from '@/components/card/CardBottom.vue'
import CardContainer from '@/components/card/CardContainer.vue'
import CardTop from '@/components/card/CardTop.vue'
import SquareChip from '@/components/chip/SquareChip.vue'
-import { formatDuration } from '@/utils/formatUtil'
+import { formatDuration, getMediaTypeFromFilename } from '@/utils/formatUtil'
import { cn } from '@/utils/tailwindUtil'
+import { getAssetType } from '../composables/media/assetMappers'
import { useMediaAssetActions } from '../composables/useMediaAssetActions'
-import { useMediaAssetGalleryStore } from '../composables/useMediaAssetGalleryStore'
-import type {
- AssetContext,
- AssetMeta,
- MediaKind
-} from '../schemas/mediaAssetSchema'
+import type { AssetItem } from '../schemas/assetSchema'
+import type { MediaKind } from '../schemas/mediaAssetSchema'
import { MediaAssetKey } from '../schemas/mediaAssetSchema'
import MediaAssetActions from './MediaAssetActions.vue'
@@ -165,11 +174,18 @@ function getBottomComponent(kind: MediaKind) {
return mediaComponents.bottom[kind] || mediaComponents.bottom.image
}
-const { context, asset, loading, selected } = defineProps<{
- context: AssetContext
- asset?: AssetMeta
+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': []
+ 'asset-deleted': []
}>()
const cardContainerRef = ref()
@@ -179,14 +195,44 @@ const isMenuOpen = ref(false)
const showVideoControls = ref(false)
const isOverlayHovered = ref(false)
+// Store actual image dimensions
+const imageDimensions = ref<{ width: number; height: number } | undefined>()
+
const isHovered = useElementHover(cardContainerRef)
const actions = useMediaAssetActions()
-const galleryStore = useMediaAssetGalleryStore()
+
+// Get asset type from tags
+const assetType = computed(() => {
+ return getAssetType(asset?.tags)
+})
+
+// Determine file type from extension
+const fileKind = computed((): MediaKind => {
+ return getMediaTypeFromFilename(asset?.name || '') as MediaKind
+})
+
+// Adapt AssetItem to legacy AssetMeta format for existing components
+const adaptedAsset = computed(() => {
+ if (!asset) return undefined
+ return {
+ id: asset.id,
+ name: asset.name,
+ kind: fileKind.value,
+ src: asset.preview_url || '',
+ size: asset.size,
+ tags: asset.tags || [],
+ created_at: asset.created_at,
+ duration: asset.user_metadata?.duration
+ ? Number(asset.user_metadata.duration)
+ : undefined,
+ dimensions: imageDimensions.value
+ }
+})
provide(MediaAssetKey, {
- asset: toRef(() => asset),
- context: toRef(() => context),
+ asset: toRef(() => adaptedAsset.value),
+ context: toRef(() => ({ type: assetType.value })),
isVideoPlaying,
showVideoControls
})
@@ -201,8 +247,16 @@ const containerClasses = computed(() =>
)
const formattedDuration = computed(() => {
- if (!asset?.duration) return ''
- return formatDuration(asset.duration)
+ // Check for execution time first (from history API)
+ const executionTime = asset?.user_metadata?.executionTimeInSeconds
+ if (executionTime !== undefined && executionTime !== null) {
+ return `${Number(executionTime).toFixed(2)}s`
+ }
+
+ // Fall back to duration for media files
+ const duration = asset?.user_metadata?.duration
+ if (!duration) return ''
+ return formatDuration(Number(duration))
})
const fileFormat = computed(() => {
@@ -212,10 +266,10 @@ const fileFormat = computed(() => {
})
const durationChipClasses = computed(() => {
- if (asset?.kind === 'audio') {
+ if (fileKind.value === 'audio') {
return '-translate-y-11'
}
- if (asset?.kind === 'video' && showVideoControls.value) {
+ if (fileKind.value === 'video' && showVideoControls.value) {
return '-translate-y-16'
}
return ''
@@ -238,27 +292,29 @@ const showActionsOverlay = computed(
const showZoomOverlay = computed(
() =>
showHoverActions.value &&
- asset?.kind !== '3D' &&
+ fileKind.value !== '3D' &&
(!isVideoPlaying.value || isCardOrOverlayHovered.value)
)
const showDurationChips = computed(
() =>
!loading &&
- asset?.duration &&
+ (asset?.user_metadata?.executionTimeInSeconds ||
+ asset?.user_metadata?.duration) &&
(!isVideoPlaying.value || isCardOrOverlayHovered.value)
)
-const showOutputCount = computed(
+const showFileFormatChip = computed(
() =>
!loading &&
- context?.outputCount &&
+ !!asset &&
+ !!fileFormat.value &&
(!isVideoPlaying.value || isCardOrOverlayHovered.value)
)
const handleCardClick = () => {
- if (asset) {
- actions.selectAsset(asset)
+ if (adaptedAsset.value) {
+ actions.selectAsset(adaptedAsset.value)
}
}
@@ -272,7 +328,19 @@ const handleOverlayMouseLeave = () => {
const handleZoomClick = () => {
if (asset) {
- galleryStore.openSingle(asset)
+ emit('zoom', asset)
}
}
+
+const handleImageLoaded = (width: number, height: number) => {
+ imageDimensions.value = { width, height }
+}
+
+const handleOutputCountClick = () => {
+ emit('output-count-click')
+}
+
+const handleAssetDelete = () => {
+ emit('asset-deleted')
+}
diff --git a/src/platform/assets/components/MediaAssetMoreMenu.vue b/src/platform/assets/components/MediaAssetMoreMenu.vue
index 62f793aa26..76d193f8ef 100644
--- a/src/platform/assets/components/MediaAssetMoreMenu.vue
+++ b/src/platform/assets/components/MediaAssetMoreMenu.vue
@@ -13,6 +13,7 @@
-
+
-
+
-
+
void
}>()
+const emit = defineEmits<{
+ inspect: []
+ 'asset-deleted': []
+}>()
+
const { asset, context } = inject(MediaAssetKey)!
const actions = useMediaAssetActions()
-const galleryStore = useMediaAssetGalleryStore()
-const showWorkflowOptions = computed(() => context.value.type)
+const assetType = computed(() => {
+ return asset.value?.tags?.[0] || context.value?.type || 'output'
+})
+
+const showWorkflowOptions = computed(() => assetType.value === 'output')
+
+// Only show Copy Job ID for output assets (not for imported/input assets)
+const showCopyJobId = computed(() => {
+ return assetType.value !== 'input'
+})
+
+// Delete button should be shown for:
+// - All output files (can be deleted via history)
+// - Input files only in cloud environment
+const showDeleteButton = computed(() => {
+ return (
+ assetType.value === 'output' || (assetType.value === 'input' && isCloud)
+ )
+})
const handleInspect = () => {
- if (asset.value) {
- galleryStore.openSingle(asset.value)
- }
+ emit('inspect')
close()
}
@@ -124,7 +147,7 @@ const handleAddToWorkflow = () => {
const handleDownload = () => {
if (asset.value) {
- actions.downloadAsset(asset.value.id)
+ actions.downloadAsset()
}
close()
}
@@ -143,17 +166,21 @@ const handleExportWorkflow = () => {
close()
}
-const handleCopyJobId = () => {
+const handleCopyJobId = async () => {
if (asset.value) {
- actions.copyAssetUrl(asset.value.id)
+ await actions.copyJobId()
}
close()
}
-const handleDelete = () => {
- if (asset.value) {
- actions.deleteAsset(asset.value.id)
+const handleDelete = async () => {
+ if (!asset.value) return
+
+ close() // Close the menu first
+
+ const success = await actions.confirmDelete(asset.value)
+ if (success) {
+ emit('asset-deleted')
}
- close()
}
diff --git a/src/platform/assets/components/MediaAudioBottom.vue b/src/platform/assets/components/MediaAudioBottom.vue
index 9b9e103fa1..82373c2828 100644
--- a/src/platform/assets/components/MediaAudioBottom.vue
+++ b/src/platform/assets/components/MediaAudioBottom.vue
@@ -1,11 +1,6 @@
-
- {{ fileName }}
-
+
{{ formatSize(asset.size) }}
@@ -18,6 +13,7 @@ import { computed } from 'vue'
import { formatSize, getFilenameDetails } from '@/utils/formatUtil'
import type { AssetContext, AssetMeta } from '../schemas/mediaAssetSchema'
+import MediaTitle from './MediaTitle.vue'
const { asset } = defineProps<{
asset: AssetMeta
diff --git a/src/platform/assets/components/MediaAudioTop.vue b/src/platform/assets/components/MediaAudioTop.vue
index 038aaf2900..7b82f23b9a 100644
--- a/src/platform/assets/components/MediaAudioTop.vue
+++ b/src/platform/assets/components/MediaAudioTop.vue
@@ -1,7 +1,7 @@
-
+
-
- {{ fileName }}
-
+
- {{ asset.dimensions?.width }}x{{ asset.dimensions?.height }}
+ {{ asset.dimensions?.width }}x{{ asset.dimensions?.height }}
@@ -18,6 +15,7 @@ import { computed } from 'vue'
import { getFilenameDetails } from '@/utils/formatUtil'
import type { AssetContext, AssetMeta } from '../schemas/mediaAssetSchema'
+import MediaTitle from './MediaTitle.vue'
const { asset } = defineProps<{
asset: AssetMeta
diff --git a/src/platform/assets/components/MediaImageTop.vue b/src/platform/assets/components/MediaImageTop.vue
index 1dfb6a2721..f2a7686d20 100644
--- a/src/platform/assets/components/MediaImageTop.vue
+++ b/src/platform/assets/components/MediaImageTop.vue
@@ -1,15 +1,16 @@
-
-
+
@@ -17,11 +18,27 @@
diff --git a/src/platform/assets/components/MediaTitle.vue b/src/platform/assets/components/MediaTitle.vue
new file mode 100644
index 0000000000..c872f77377
--- /dev/null
+++ b/src/platform/assets/components/MediaTitle.vue
@@ -0,0 +1,21 @@
+
+
+ {{ displayName }}
+
+
+
+
diff --git a/src/platform/assets/components/MediaVideoBottom.vue b/src/platform/assets/components/MediaVideoBottom.vue
index 295c5bd6e9..f3586f8a32 100644
--- a/src/platform/assets/components/MediaVideoBottom.vue
+++ b/src/platform/assets/components/MediaVideoBottom.vue
@@ -1,13 +1,8 @@
-
- {{ fileName }}
-
+
- {{ asset.dimensions?.width }}x{{ asset.dimensions?.height }}
+ {{ formatSize(asset.size) }}
@@ -15,9 +10,10 @@