-
+
+
{{ label }}
diff --git a/src/components/common/LazyImage.vue b/src/components/common/LazyImage.vue
index dd41247d7..0716beee7 100644
--- a/src/components/common/LazyImage.vue
+++ b/src/components/common/LazyImage.vue
@@ -2,6 +2,7 @@
+ containerClass?: ClassValue
+ imageClass?: ClassValue
imageStyle?: Record
rootMargin?: string
}>()
diff --git a/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue b/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue
index 12e9490ed..c64a8e19d 100644
--- a/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue
+++ b/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue
@@ -141,8 +141,10 @@
@@ -172,9 +174,11 @@
v-for="template in isLoading ? [] : displayTemplates"
:key="template.name"
ref="cardRefs"
- ratio="smallSquare"
- type="workflow-template-card"
+ size="compact"
+ variant="ghost"
+ rounded="lg"
:data-testid="`template-workflow-${template.name}`"
+ class="hover:bg-white dark-theme:hover:bg-zinc-800"
@mouseenter="hoveredTemplate = template.name"
@mouseleave="hoveredTemplate = null"
@click="onLoadWorkflow(template)"
@@ -316,8 +320,10 @@
diff --git a/src/components/widget/SampleModelSelector.vue b/src/components/widget/SampleModelSelector.vue
index 4abc18365..d89413316 100644
--- a/src/components/widget/SampleModelSelector.vue
+++ b/src/components/widget/SampleModelSelector.vue
@@ -88,7 +88,7 @@
-
+
diff --git a/src/platform/assets/components/Media3DBottom.vue b/src/platform/assets/components/Media3DBottom.vue
new file mode 100644
index 000000000..4d3c15c0b
--- /dev/null
+++ b/src/platform/assets/components/Media3DBottom.vue
@@ -0,0 +1,29 @@
+
+
+
+ {{ fileName }}
+
+
+ {{ formatSize(asset.size) }}
+
+
+
+
+
diff --git a/src/platform/assets/components/Media3DTop.vue b/src/platform/assets/components/Media3DTop.vue
new file mode 100644
index 000000000..535e5de6c
--- /dev/null
+++ b/src/platform/assets/components/Media3DTop.vue
@@ -0,0 +1,16 @@
+
+
+
+
+ {{
+ $t('3D Model')
+ }}
+
+
+
+
+
diff --git a/src/platform/assets/components/MediaAssetActions.vue b/src/platform/assets/components/MediaAssetActions.vue
new file mode 100644
index 000000000..73144d4d2
--- /dev/null
+++ b/src/platform/assets/components/MediaAssetActions.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/platform/assets/components/MediaAssetButtonDivider.vue b/src/platform/assets/components/MediaAssetButtonDivider.vue
new file mode 100644
index 000000000..d395ca099
--- /dev/null
+++ b/src/platform/assets/components/MediaAssetButtonDivider.vue
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/platform/assets/components/MediaAssetCard.stories.ts b/src/platform/assets/components/MediaAssetCard.stories.ts
new file mode 100644
index 000000000..fa1d42856
--- /dev/null
+++ b/src/platform/assets/components/MediaAssetCard.stories.ts
@@ -0,0 +1,318 @@
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+
+import type { AssetMeta } from '../schemas/mediaAssetSchema'
+import MediaAssetCard from './MediaAssetCard.vue'
+
+const meta: Meta = {
+ title: 'AssetLibrary/MediaAssetCard',
+ component: MediaAssetCard,
+ argTypes: {
+ context: {
+ control: 'select',
+ options: ['input', 'output']
+ },
+ loading: {
+ control: 'boolean'
+ }
+ }
+}
+
+export default meta
+type Story = StoryObj
+
+// Public sample media URLs
+const SAMPLE_MEDIA = {
+ image1: 'https://i.imgur.com/OB0y6MR.jpg',
+ image2: 'https://i.imgur.com/CzXTtJV.jpg',
+ image3: 'https://farm9.staticflickr.com/8505/8441256181_4e98d8bff5_z_d.jpg',
+ video:
+ 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
+ videoThumbnail:
+ 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg',
+ audio: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'
+}
+
+const sampleAsset: AssetMeta = {
+ 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: []
+}
+
+export const ImageAsset: Story = {
+ decorators: [
+ () => ({
+ template: '
'
+ })
+ ],
+ args: {
+ context: { type: 'output', outputCount: 3 },
+ asset: sampleAsset,
+ loading: false
+ }
+}
+
+export const VideoAsset: Story = {
+ decorators: [
+ () => ({
+ template: '
'
+ })
+ ],
+ 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
+ }
+ }
+ }
+}
+
+export const Model3DAsset: Story = {
+ decorators: [
+ () => ({
+ template: '
'
+ })
+ ],
+ args: {
+ context: { type: 'input' },
+ asset: {
+ ...sampleAsset,
+ id: 'asset-3',
+ name: 'Asset-3d-model.glb',
+ kind: '3D',
+ size: 7340032,
+ src: '',
+ dimensions: undefined,
+ duration: 18023
+ }
+ }
+}
+
+export const AudioAsset: Story = {
+ decorators: [
+ () => ({
+ template: '
'
+ })
+ ],
+ args: {
+ context: { type: 'input' },
+ asset: {
+ ...sampleAsset,
+ id: 'asset-3',
+ name: 'SoundHelix-Song.mp3',
+ kind: 'audio',
+ size: 5242880,
+ src: SAMPLE_MEDIA.audio,
+ dimensions: undefined,
+ duration: 23180
+ }
+ }
+}
+
+export const LoadingState: Story = {
+ decorators: [
+ () => ({
+ template: '
'
+ })
+ ],
+ args: {
+ context: { type: 'input' },
+ asset: sampleAsset,
+ loading: true
+ }
+}
+
+export const LongFileName: Story = {
+ decorators: [
+ () => ({
+ template: '
'
+ })
+ ],
+ args: {
+ context: { type: 'input' },
+ asset: {
+ ...sampleAsset,
+ name: 'very-long-file-name-that-should-be-truncated-in-the-ui-to-prevent-overflow.png'
+ }
+ }
+}
+
+export const SelectedState: Story = {
+ decorators: [
+ () => ({
+ template: '
'
+ })
+ ],
+ args: {
+ context: { type: 'output', outputCount: 2 },
+ asset: sampleAsset,
+ selected: true
+ }
+}
+
+export const WebMVideo: Story = {
+ decorators: [
+ () => ({
+ template: '
'
+ })
+ ],
+ 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: []
+ }
+ }
+}
+
+export const GifAnimation: Story = {
+ decorators: [
+ () => ({
+ template: '
'
+ })
+ ],
+ 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: []
+ }
+ }
+}
+
+export const GridLayout: Story = {
+ render: () => ({
+ components: { MediaAssetCard },
+ setup() {
+ const assets: AssetMeta[] = [
+ {
+ 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: []
+ },
+ {
+ 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: []
+ },
+ {
+ 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: []
+ },
+ {
+ id: 'grid-4',
+ name: 'audio-file.mp3',
+ kind: 'audio',
+ size: 5242880,
+ duration: 180,
+ created_at: Date.now().toString(),
+ src: SAMPLE_MEDIA.audio,
+ tags: []
+ },
+ {
+ 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: []
+ },
+ {
+ id: 'grid-6',
+ name: 'Asset-3d-model.glb',
+ kind: '3D',
+ size: 7340032,
+ src: '',
+ dimensions: undefined,
+ duration: 18023,
+ created_at: Date.now().toString(),
+ tags: []
+ },
+ {
+ 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: []
+ }
+ ]
+ return { assets }
+ },
+ template: `
+
+
+
+ `
+ })
+}
diff --git a/src/platform/assets/components/MediaAssetCard.vue b/src/platform/assets/components/MediaAssetCard.vue
new file mode 100644
index 000000000..8aa58063e
--- /dev/null
+++ b/src/platform/assets/components/MediaAssetCard.vue
@@ -0,0 +1,233 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/platform/assets/components/MediaAssetMoreMenu.vue b/src/platform/assets/components/MediaAssetMoreMenu.vue
new file mode 100644
index 000000000..39e1b150b
--- /dev/null
+++ b/src/platform/assets/components/MediaAssetMoreMenu.vue
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/platform/assets/components/MediaAudioBottom.vue b/src/platform/assets/components/MediaAudioBottom.vue
new file mode 100644
index 000000000..9b9e103fa
--- /dev/null
+++ b/src/platform/assets/components/MediaAudioBottom.vue
@@ -0,0 +1,30 @@
+
+
+
+ {{ fileName }}
+
+
+ {{ formatSize(asset.size) }}
+
+
+
+
+
diff --git a/src/platform/assets/components/MediaAudioTop.vue b/src/platform/assets/components/MediaAudioTop.vue
new file mode 100644
index 000000000..038aaf290
--- /dev/null
+++ b/src/platform/assets/components/MediaAudioTop.vue
@@ -0,0 +1,28 @@
+
+
+
+
+ {{
+ $t('Audio')
+ }}
+
+
+
+
+
+
diff --git a/src/platform/assets/components/MediaImageBottom.vue b/src/platform/assets/components/MediaImageBottom.vue
new file mode 100644
index 000000000..295c5bd6e
--- /dev/null
+++ b/src/platform/assets/components/MediaImageBottom.vue
@@ -0,0 +1,30 @@
+
+
+
+ {{ fileName }}
+
+
+ {{ asset.dimensions?.width }}x{{ asset.dimensions?.height }}
+
+
+
+
+
diff --git a/src/platform/assets/components/MediaImageTop.vue b/src/platform/assets/components/MediaImageTop.vue
new file mode 100644
index 000000000..40adba435
--- /dev/null
+++ b/src/platform/assets/components/MediaImageTop.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
diff --git a/src/platform/assets/components/MediaVideoBottom.vue b/src/platform/assets/components/MediaVideoBottom.vue
new file mode 100644
index 000000000..295c5bd6e
--- /dev/null
+++ b/src/platform/assets/components/MediaVideoBottom.vue
@@ -0,0 +1,30 @@
+
+
+
+ {{ fileName }}
+
+
+ {{ asset.dimensions?.width }}x{{ asset.dimensions?.height }}
+
+
+
+
+
diff --git a/src/platform/assets/components/MediaVideoTop.vue b/src/platform/assets/components/MediaVideoTop.vue
new file mode 100644
index 000000000..ce3af4641
--- /dev/null
+++ b/src/platform/assets/components/MediaVideoTop.vue
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
diff --git a/src/platform/assets/composables/useMediaAssetActions.ts b/src/platform/assets/composables/useMediaAssetActions.ts
new file mode 100644
index 000000000..50be365ae
--- /dev/null
+++ b/src/platform/assets/composables/useMediaAssetActions.ts
@@ -0,0 +1,62 @@
+/* eslint-disable no-console */
+import type { AssetMeta } from '../schemas/mediaAssetSchema'
+
+export function useMediaAssetActions() {
+ const selectAsset = (asset: AssetMeta) => {
+ console.log('Asset selected:', asset)
+ }
+
+ const viewAsset = (assetId: string) => {
+ console.log('Viewing asset:', assetId)
+ }
+
+ const downloadAsset = (assetId: string) => {
+ console.log('Downloading asset:', assetId)
+ }
+
+ const deleteAsset = (assetId: string) => {
+ console.log('Deleting asset:', assetId)
+ }
+
+ const playAsset = (assetId: string) => {
+ console.log('Playing asset:', assetId)
+ }
+
+ const copyAssetUrl = (assetId: string) => {
+ console.log('Copy asset URL:', assetId)
+ }
+
+ const copyJobId = (jobId: string) => {
+ console.log('Copy job ID:', jobId)
+ }
+
+ const addWorkflow = (assetId: string) => {
+ console.log('Adding asset to workflow:', assetId)
+ }
+
+ const openWorkflow = (assetId: string) => {
+ console.log('Opening workflow for asset:', assetId)
+ }
+
+ const exportWorkflow = (assetId: string) => {
+ console.log('Exporting workflow for asset:', assetId)
+ }
+
+ const openMoreOutputs = (assetId: string) => {
+ console.log('Opening more outputs for asset:', assetId)
+ }
+
+ return {
+ selectAsset,
+ viewAsset,
+ downloadAsset,
+ deleteAsset,
+ playAsset,
+ copyAssetUrl,
+ copyJobId,
+ addWorkflow,
+ openWorkflow,
+ exportWorkflow,
+ openMoreOutputs
+ }
+}
diff --git a/src/platform/assets/schemas/mediaAssetSchema.ts b/src/platform/assets/schemas/mediaAssetSchema.ts
new file mode 100644
index 000000000..d35dad6c5
--- /dev/null
+++ b/src/platform/assets/schemas/mediaAssetSchema.ts
@@ -0,0 +1,46 @@
+import type { InjectionKey, Ref } from 'vue'
+import { z } from 'zod'
+
+import { assetItemSchema } from './assetSchema'
+
+const zMediaKindSchema = z.enum(['video', 'audio', 'image', '3D'])
+export type MediaKind = z.infer
+
+const zDimensionsSchema = z.object({
+ width: z.number().positive(),
+ height: z.number().positive()
+})
+
+// Extend the base asset schema with media-specific fields
+const zMediaAssetDisplayItemSchema = assetItemSchema.extend({
+ // New required fields
+ kind: zMediaKindSchema,
+ src: z.string().url(),
+
+ // New optional fields
+ duration: z.number().nonnegative().optional(),
+ dimensions: zDimensionsSchema.optional(),
+ jobId: z.string().optional(),
+ isMulti: z.boolean().optional()
+})
+
+// Asset context schema
+const zAssetContextSchema = z.object({
+ type: z.enum(['input', 'output']),
+ outputCount: z.number().positive().optional() // Only for output context
+})
+
+// Export the inferred types
+export type AssetMeta = z.infer
+export type AssetContext = z.infer
+
+// Injection key for MediaAsset provide/inject pattern
+interface MediaAssetProviderValue {
+ asset: Ref
+ context: Ref
+ isVideoPlaying: Ref
+ showVideoControls: Ref
+}
+
+export const MediaAssetKey: InjectionKey =
+ Symbol('mediaAsset')