11<template >
2- <SidebarTabTemplate :title =" $t('sideToolbar.mediaAssets')" >
2+ <AssetsSidebarTemplate >
3+ <template #top >
4+ <span v-if =" !isInFolderView" class =" font-bold" >
5+ {{ $t('sideToolbar.mediaAssets') }}
6+ </span >
7+ <div v-else class =" flex w-full items-center justify-between gap-2" >
8+ <div class =" flex items-center gap-2" >
9+ <span class =" font-bold" >{{ $t('Job ID') }}:</span >
10+ <span class =" text-sm" >{{ folderPromptId?.substring(0, 8) }}</span >
11+ <button
12+ class =" m-0 cursor-pointer border-0 bg-transparent p-0 outline-0"
13+ role =" button"
14+ @click =" copyJobId"
15+ >
16+ <i class =" mb-1 icon-[lucide--copy] text-sm" ></i >
17+ </button >
18+ </div >
19+ <div >
20+ <span >{{ formattedExecutionTime }}</span >
21+ </div >
22+ </div >
23+ </template >
324 <template #header >
4- <Tabs v-model:value =" activeTab" class =" w-full" >
5- <TabList class =" border-b border-neutral-300" >
6- <Tab value =" input" >{{ $t('sideToolbar.labels.imported') }}</Tab >
7- <Tab value =" output" >{{ $t('sideToolbar.labels.generated') }}</Tab >
8- </TabList >
9- </Tabs >
25+ <!-- Job Detail View Header -->
26+ <div v-if =" isInFolderView" class =" pt-4 pb-2" >
27+ <IconTextButton
28+ :label =" $t('sideToolbar.backToAssets')"
29+ type =" secondary"
30+ @click =" exitFolderView"
31+ >
32+ <template #icon >
33+ <i class =" icon-[lucide--arrow-left] size-4" />
34+ </template >
35+ </IconTextButton >
36+ </div >
37+ <!-- Normal Tab View -->
38+ <TabList v-else v-model =" activeTab" class =" pt-4 pb-1" >
39+ <Tab value =" input" >{{ $t('sideToolbar.labels.imported') }}</Tab >
40+ <Tab value =" output" >{{ $t('sideToolbar.labels.generated') }}</Tab >
41+ </TabList >
1042 </template >
1143 <template #body >
1244 <VirtualGrid
13- v-if =" mediaAssets .length"
45+ v-if =" displayAssets .length"
1446 :items =" mediaAssetsWithKey"
1547 :grid-style =" {
1648 display: 'grid',
2355 <MediaAssetCard
2456 :asset =" item"
2557 :selected =" selectedAsset?.id === item.id"
58+ :show-output-count =" shouldShowOutputCount(item)"
59+ :output-count =" getOutputCount(item)"
2660 @click =" handleAssetSelect(item)"
2761 @zoom =" handleZoomClick(item)"
62+ @output-count-click =" enterFolderView(item)"
2863 />
2964 </template >
3065 </VirtualGrid >
4580 />
4681 </div >
4782 </template >
48- </SidebarTabTemplate >
83+ </AssetsSidebarTemplate >
4984 <ResultGallery
5085 v-model:active-index =" galleryActiveIndex"
5186 :all-gallery-items =" galleryItems"
5489
5590<script setup lang="ts">
5691import ProgressSpinner from ' primevue/progressspinner'
57- import Tab from ' primevue/tab'
58- import TabList from ' primevue/tablist'
59- import Tabs from ' primevue/tabs'
92+ import { useToast } from ' primevue/usetoast'
6093import { computed , ref , watch } from ' vue'
6194
95+ import IconTextButton from ' @/components/button/IconTextButton.vue'
6296import NoResultsPlaceholder from ' @/components/common/NoResultsPlaceholder.vue'
6397import VirtualGrid from ' @/components/common/VirtualGrid.vue'
64- import SidebarTabTemplate from ' @/components/sidebar/tabs/SidebarTabTemplate.vue'
6598import ResultGallery from ' @/components/sidebar/tabs/queue/ResultGallery.vue'
99+ import Tab from ' @/components/tab/Tab.vue'
100+ import TabList from ' @/components/tab/TabList.vue'
101+ import { t } from ' @/i18n'
66102import MediaAssetCard from ' @/platform/assets/components/MediaAssetCard.vue'
67103import { useMediaAssets } from ' @/platform/assets/composables/useMediaAssets'
104+ import { getOutputAssetMetadata } from ' @/platform/assets/schemas/assetMetadataSchema'
68105import type { AssetItem } from ' @/platform/assets/schemas/assetSchema'
69106import { ResultItemImpl } from ' @/stores/queueStore'
70- import { getMediaTypeFromFilename } from ' @/utils/formatUtil'
107+ import { formatDuration , getMediaTypeFromFilename } from ' @/utils/formatUtil'
108+
109+ import AssetsSidebarTemplate from ' ./AssetSidebarTemplate.vue'
71110
72111const activeTab = ref <' input' | ' output' >(' input' )
73112const selectedAsset = ref <AssetItem | null >(null )
113+ const folderPromptId = ref <string | null >(null )
114+ const folderExecutionTime = ref <number | undefined >(undefined )
115+ const isInFolderView = computed (() => folderPromptId .value !== null )
116+
117+ const getOutputCount = (item : AssetItem ): number => {
118+ const count = item .user_metadata ?.outputCount
119+ return typeof count === ' number' && count > 0 ? count : 0
120+ }
121+
122+ const shouldShowOutputCount = (item : AssetItem ): boolean => {
123+ if (activeTab .value !== ' output' || isInFolderView .value ) {
124+ return false
125+ }
126+ return getOutputCount (item ) > 1
127+ }
128+
129+ const formattedExecutionTime = computed (() => {
130+ if (! folderExecutionTime .value ) return ' '
131+ return formatDuration (folderExecutionTime .value * 1000 )
132+ })
133+
134+ const toast = useToast ()
74135
75136const inputAssets = useMediaAssets (' input' )
76137const outputAssets = useMediaAssets (' output' )
@@ -84,7 +145,9 @@ const mediaAssets = computed(() => currentAssets.value.media.value)
84145
85146const galleryActiveIndex = ref (- 1 )
86147const galleryItems = computed (() => {
87- return mediaAssets .value .map ((asset ) => {
148+ // Convert AssetItems to ResultItemImpl format for gallery
149+ // Use displayAssets instead of mediaAssets to show correct items based on view mode
150+ return displayAssets .value .map ((asset ) => {
88151 const mediaType = getMediaTypeFromFilename (asset .name )
89152 const resultItem = new ResultItemImpl ({
90153 filename: asset .name ,
@@ -105,9 +168,23 @@ const galleryItems = computed(() => {
105168 })
106169})
107170
171+ // Store folder view assets separately
172+ const folderAssets = ref <AssetItem []>([])
173+
174+ // Get display assets based on view mode
175+ const displayAssets = computed (() => {
176+ if (isInFolderView .value ) {
177+ // Show all assets from the folder view
178+ return folderAssets .value
179+ }
180+
181+ // Normal view: show grouped assets (already have outputCount from API)
182+ return mediaAssets .value
183+ })
184+
108185// Add key property for VirtualGrid
109186const mediaAssetsWithKey = computed (() => {
110- return mediaAssets .value .map ((asset ) => ({
187+ return displayAssets .value .map ((asset ) => ({
111188 ... asset ,
112189 key: asset .id
113190 }))
@@ -138,9 +215,71 @@ const handleAssetSelect = (asset: AssetItem) => {
138215}
139216
140217const handleZoomClick = (asset : AssetItem ) => {
141- const index = mediaAssets .value .findIndex ((a ) => a .id === asset .id )
218+ // Find the index of the clicked asset
219+ const index = displayAssets .value .findIndex ((a ) => a .id === asset .id )
142220 if (index !== - 1 ) {
143221 galleryActiveIndex .value = index
144222 }
145223}
224+
225+ const enterFolderView = (asset : AssetItem ) => {
226+ const metadata = getOutputAssetMetadata (asset .user_metadata )
227+ if (! metadata ) {
228+ console .warn (' Invalid output asset metadata' )
229+ return
230+ }
231+
232+ const { promptId, allOutputs, executionTimeInSeconds } = metadata
233+
234+ if (! promptId || ! Array .isArray (allOutputs ) || allOutputs .length === 0 ) {
235+ console .warn (' Missing required folder view data' )
236+ return
237+ }
238+
239+ folderPromptId .value = promptId
240+ folderExecutionTime .value = executionTimeInSeconds
241+
242+ folderAssets .value = allOutputs .map ((output ) => ({
243+ id: ` ${promptId }-${output .nodeId }-${output .filename } ` ,
244+ name: output .filename ,
245+ size: 0 ,
246+ created_at: asset .created_at ,
247+ tags: [' output' ],
248+ preview_url: output .url ,
249+ user_metadata: {
250+ promptId ,
251+ nodeId: output .nodeId ,
252+ subfolder: output .subfolder ,
253+ executionTimeInSeconds ,
254+ workflow: metadata .workflow
255+ }
256+ }))
257+ }
258+
259+ const exitFolderView = () => {
260+ folderPromptId .value = null
261+ folderExecutionTime .value = undefined
262+ folderAssets .value = []
263+ }
264+
265+ const copyJobId = async () => {
266+ if (folderPromptId .value ) {
267+ try {
268+ await navigator .clipboard .writeText (folderPromptId .value )
269+ toast .add ({
270+ severity: ' success' ,
271+ summary: t (' mediaAsset.jobIdToast.copied' ),
272+ detail: t (' mediaAsset.jobIdToast.jobIdCopied' ),
273+ life: 2000
274+ })
275+ } catch (error ) {
276+ toast .add ({
277+ severity: ' error' ,
278+ summary: t (' mediaAsset.jobIdToast.error' ),
279+ detail: t (' mediaAsset.jobIdToast.jobIdCopyFailed' ),
280+ life: 3000
281+ })
282+ }
283+ }
284+ }
146285 </script >
0 commit comments