From f843d779c294b4f343cbf494c89e0729181b9e9a Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Sat, 10 Jan 2026 20:51:16 +0200 Subject: [PATCH 01/63] feat(price-badges): add price badges for Vidu2 nodes (#7927) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Price badges for the new nodes. ## Screenshots (if applicable) Screenshot From 2026-01-09
13-29-59 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7927-feat-price-badges-add-price-badges-for-Vidu2-nodes-2e36d73d365081a0b6f4de768dce11e4) by [Unito](https://www.unito.io) --- src/composables/node/useNodePricing.ts | 238 ++++++++++++++++++++++++- 1 file changed, 237 insertions(+), 1 deletion(-) diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts index 641d38911..643dd676b 100644 --- a/src/composables/node/useNodePricing.ts +++ b/src/composables/node/useNodePricing.ts @@ -2161,6 +2161,238 @@ const apiNodeCosts: Record = return formatCreditsRangeLabel(minTotal, maxTotal) } + }, + Vidu2TextToVideoNode: { + displayPrice: (node: LGraphNode): string => { + const durationWidget = node.widgets?.find( + (w) => w.name === 'duration' + ) as IComboWidget + const resolutionWidget = node.widgets?.find( + (w) => w.name === 'resolution' + ) as IComboWidget + + if (!durationWidget || !resolutionWidget) { + return formatCreditsRangeLabel(0.075, 0.6, { + note: '(varies with duration & resolution)' + }) + } + + const duration = parseFloat(String(durationWidget.value)) + const resolution = String(resolutionWidget.value).toLowerCase() + + // Text-to-Video uses Q2 model only + // 720P: Starts at $0.075, +$0.025/sec + // 1080P: Starts at $0.10, +$0.05/sec + let basePrice: number + let pricePerSecond: number + + if (resolution.includes('1080')) { + basePrice = 0.1 + pricePerSecond = 0.05 + } else { + // 720P default + basePrice = 0.075 + pricePerSecond = 0.025 + } + + if (!Number.isFinite(duration) || duration <= 0) { + return formatCreditsRangeLabel(0.075, 0.6, { + note: '(varies with duration & resolution)' + }) + } + + const cost = basePrice + pricePerSecond * (duration - 1) + return formatCreditsLabel(cost) + } + }, + Vidu2ImageToVideoNode: { + displayPrice: (node: LGraphNode): string => { + const modelWidget = node.widgets?.find( + (w) => w.name === 'model' + ) as IComboWidget + const durationWidget = node.widgets?.find( + (w) => w.name === 'duration' + ) as IComboWidget + const resolutionWidget = node.widgets?.find( + (w) => w.name === 'resolution' + ) as IComboWidget + + if (!modelWidget || !durationWidget || !resolutionWidget) { + return formatCreditsRangeLabel(0.04, 1.0, { + note: '(varies with model, duration & resolution)' + }) + } + + const model = String(modelWidget.value).toLowerCase() + const duration = parseFloat(String(durationWidget.value)) + const resolution = String(resolutionWidget.value).toLowerCase() + const is1080p = resolution.includes('1080') + + let basePrice: number + let pricePerSecond: number + + if (model.includes('q2-pro-fast')) { + // Q2-pro-fast: 720P $0.04+$0.01/sec, 1080P $0.08+$0.02/sec + basePrice = is1080p ? 0.08 : 0.04 + pricePerSecond = is1080p ? 0.02 : 0.01 + } else if (model.includes('q2-pro')) { + // Q2-pro: 720P $0.075+$0.05/sec, 1080P $0.275+$0.075/sec + basePrice = is1080p ? 0.275 : 0.075 + pricePerSecond = is1080p ? 0.075 : 0.05 + } else if (model.includes('q2-turbo')) { + // Q2-turbo: 720P special pricing, 1080P $0.175+$0.05/sec + if (is1080p) { + basePrice = 0.175 + pricePerSecond = 0.05 + } else { + // 720P: $0.04 at 1s, $0.05 at 2s, +$0.05/sec beyond 2s + if (duration <= 1) { + return formatCreditsLabel(0.04) + } + if (duration <= 2) { + return formatCreditsLabel(0.05) + } + const cost = 0.05 + 0.05 * (duration - 2) + return formatCreditsLabel(cost) + } + } else { + return formatCreditsRangeLabel(0.04, 1.0, { + note: '(varies with model, duration & resolution)' + }) + } + + if (!Number.isFinite(duration) || duration <= 0) { + return formatCreditsRangeLabel(0.04, 1.0, { + note: '(varies with model, duration & resolution)' + }) + } + + const cost = basePrice + pricePerSecond * (duration - 1) + return formatCreditsLabel(cost) + } + }, + Vidu2ReferenceVideoNode: { + displayPrice: (node: LGraphNode): string => { + const durationWidget = node.widgets?.find( + (w) => w.name === 'duration' + ) as IComboWidget + const resolutionWidget = node.widgets?.find( + (w) => w.name === 'resolution' + ) as IComboWidget + const audioWidget = node.widgets?.find( + (w) => w.name === 'audio' + ) as IComboWidget + + if (!durationWidget) { + return formatCreditsRangeLabel(0.125, 1.5, { + note: '(varies with duration, resolution & audio)' + }) + } + + const duration = parseFloat(String(durationWidget.value)) + const resolution = String(resolutionWidget?.value ?? '').toLowerCase() + const is1080p = resolution.includes('1080') + + // Check if audio is enabled (adds $0.75) + const audioValue = audioWidget?.value + const hasAudio = + audioValue !== undefined && + audioValue !== null && + String(audioValue).toLowerCase() !== 'false' && + String(audioValue).toLowerCase() !== 'none' && + audioValue !== '' + + // Reference-to-Video uses Q2 model + // 720P: Starts at $0.125, +$0.025/sec + // 1080P: Starts at $0.375, +$0.05/sec + let basePrice: number + let pricePerSecond: number + + if (is1080p) { + basePrice = 0.375 + pricePerSecond = 0.05 + } else { + // 720P default + basePrice = 0.125 + pricePerSecond = 0.025 + } + + let cost = basePrice + if (Number.isFinite(duration) && duration > 0) { + cost = basePrice + pricePerSecond * (duration - 1) + } + + // Audio adds $0.75 on top + if (hasAudio) { + cost += 0.075 + } + + return formatCreditsLabel(cost) + } + }, + Vidu2StartEndToVideoNode: { + displayPrice: (node: LGraphNode): string => { + const modelWidget = node.widgets?.find( + (w) => w.name === 'model' + ) as IComboWidget + const durationWidget = node.widgets?.find( + (w) => w.name === 'duration' + ) as IComboWidget + const resolutionWidget = node.widgets?.find( + (w) => w.name === 'resolution' + ) as IComboWidget + + if (!modelWidget || !durationWidget || !resolutionWidget) { + return formatCreditsRangeLabel(0.04, 1.0, { + note: '(varies with model, duration & resolution)' + }) + } + + const model = String(modelWidget.value).toLowerCase() + const duration = parseFloat(String(durationWidget.value)) + const resolution = String(resolutionWidget.value).toLowerCase() + const is1080p = resolution.includes('1080') + + let basePrice: number + let pricePerSecond: number + + if (model.includes('q2-pro-fast')) { + // Q2-pro-fast: 720P $0.04+$0.01/sec, 1080P $0.08+$0.02/sec + basePrice = is1080p ? 0.08 : 0.04 + pricePerSecond = is1080p ? 0.02 : 0.01 + } else if (model.includes('q2-pro')) { + // Q2-pro: 720P $0.075+$0.05/sec, 1080P $0.275+$0.075/sec + basePrice = is1080p ? 0.275 : 0.075 + pricePerSecond = is1080p ? 0.075 : 0.05 + } else if (model.includes('q2-turbo')) { + // Q2-turbo: 720P special pricing, 1080P $0.175+$0.05/sec + if (is1080p) { + basePrice = 0.175 + pricePerSecond = 0.05 + } else { + // 720P: $0.04 at 1s, $0.05 at 2s, +$0.05/sec beyond 2s + if (!Number.isFinite(duration) || duration <= 1) { + return formatCreditsLabel(0.04) + } + if (duration <= 2) { + return formatCreditsLabel(0.05) + } + const cost = 0.05 + 0.05 * (duration - 2) + return formatCreditsLabel(cost) + } + } else { + return formatCreditsRangeLabel(0.04, 1.0, { + note: '(varies with model, duration & resolution)' + }) + } + + if (!Number.isFinite(duration) || duration <= 0) { + return formatCreditsLabel(basePrice) + } + + const cost = basePrice + pricePerSecond * (duration - 1) + return formatCreditsLabel(cost) + } } } @@ -2316,7 +2548,11 @@ export const useNodePricing = () => { WanImageToVideoApi: ['duration', 'resolution'], WanReferenceVideoApi: ['duration', 'size'], LtxvApiTextToVideo: ['model', 'duration', 'resolution'], - LtxvApiImageToVideo: ['model', 'duration', 'resolution'] + LtxvApiImageToVideo: ['model', 'duration', 'resolution'], + Vidu2TextToVideoNode: ['model', 'duration', 'resolution'], + Vidu2ImageToVideoNode: ['model', 'duration', 'resolution'], + Vidu2ReferenceVideoNode: ['audio', 'duration', 'resolution'], + Vidu2StartEndToVideoNode: ['model', 'duration', 'resolution'] } return widgetMap[nodeType] || [] } From 8086f977c98eeb0b0bb8929229dfc30ff36fb6f9 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Sat, 10 Jan 2026 10:56:29 -0800 Subject: [PATCH 02/63] [QPOv2] Add list view to assets sidepanel (#7737) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds the list view to the media assets sidepanel, while also adding the active jobs to be displayed right now. The design for this is actually changing, which is why it is in draft right now. There are technical limitations of the virtual grid that doesn't make it easy for both the active jobs and generated assets to exist on the same container. Currently WIP right now. Part of the QPO v2 iteration, figma design can be found [here](https://www.figma.com/design/LVilZgHGk5RwWOkVN6yCEK/Queue-Progress-Modal?node-id=3330-37286&m=dev). This will be implemented in a series of stacked PRs that can be reviewed and merged individually. main <-- #7737, #7743, #7745 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7737-QPOv2-Add-list-view-to-assets-sidepanel-2d26d73d365081858e22c48902bd56e2) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action --- src/components/queue/job/QueueJobItem.vue | 29 ++- .../sidebar/tabs/AssetsSidebarListView.vue | 203 ++++++++++++++++++ .../sidebar/tabs/AssetsSidebarTab.vue | 30 ++- src/composables/queue/useJobActions.ts | 59 +++++ src/composables/queue/useJobMenu.ts | 123 +++++------ src/composables/useProgressBarBackground.ts | 47 ++++ src/locales/en/main.json | 1 + .../components/AssetsListItem.stories.ts | 99 +++++++++ .../assets/components/AssetsListItem.vue | 116 ++++++++++ src/platform/assets/utils/mediaIconUtil.ts | 14 ++ 10 files changed, 650 insertions(+), 71 deletions(-) create mode 100644 src/components/sidebar/tabs/AssetsSidebarListView.vue create mode 100644 src/composables/queue/useJobActions.ts create mode 100644 src/composables/useProgressBarBackground.ts create mode 100644 src/platform/assets/components/AssetsListItem.stories.ts create mode 100644 src/platform/assets/components/AssetsListItem.vue create mode 100644 src/platform/assets/utils/mediaIconUtil.ts diff --git a/src/components/queue/job/QueueJobItem.vue b/src/components/queue/job/QueueJobItem.vue index 7e71840bc..b1809dac2 100644 --- a/src/components/queue/job/QueueJobItem.vue +++ b/src/components/queue/job/QueueJobItem.vue @@ -50,20 +50,22 @@
@@ -201,6 +203,7 @@ import { useI18n } from 'vue-i18n' import JobDetailsPopover from '@/components/queue/job/JobDetailsPopover.vue' import QueueAssetPreview from '@/components/queue/job/QueueAssetPreview.vue' import Button from '@/components/ui/button/Button.vue' +import { useProgressBarBackground } from '@/composables/useProgressBarBackground' import { buildTooltipConfig } from '@/composables/useTooltipConfig' import type { JobState } from '@/types/queue' import { iconForJobState } from '@/utils/queueDisplay' @@ -245,6 +248,14 @@ const emit = defineEmits<{ }>() const { t } = useI18n() +const { + progressBarContainerClass, + progressBarPrimaryClass, + progressBarSecondaryClass, + hasProgressPercent, + hasAnyProgressPercent, + progressPercentStyle +} = useProgressBarBackground() const cancelTooltipConfig = computed(() => buildTooltipConfig(t('g.cancel'))) const deleteTooltipConfig = computed(() => buildTooltipConfig(t('g.delete'))) diff --git a/src/components/sidebar/tabs/AssetsSidebarListView.vue b/src/components/sidebar/tabs/AssetsSidebarListView.vue new file mode 100644 index 000000000..f750661ba --- /dev/null +++ b/src/components/sidebar/tabs/AssetsSidebarListView.vue @@ -0,0 +1,203 @@ + + + diff --git a/src/components/sidebar/tabs/AssetsSidebarTab.vue b/src/components/sidebar/tabs/AssetsSidebarTab.vue index 032645b4c..7da25bb8e 100644 --- a/src/components/sidebar/tabs/AssetsSidebarTab.vue +++ b/src/components/sidebar/tabs/AssetsSidebarTab.vue @@ -79,10 +79,10 @@ @@ -196,6 +190,21 @@ v-model:active-index="galleryActiveIndex" :all-gallery-items="galleryItems" /> + diff --git a/src/platform/assets/components/MediaAssetContextMenu.vue b/src/platform/assets/components/MediaAssetContextMenu.vue index 9325f7a17..fe49c2ca1 100644 --- a/src/platform/assets/components/MediaAssetContextMenu.vue +++ b/src/platform/assets/components/MediaAssetContextMenu.vue @@ -11,6 +11,7 @@ ) } }" + @hide="emit('hide')" >