-
+
{{
isCloud
diff --git a/src/components/dialog/content/TopUpCreditsDialogContent.vue b/src/components/dialog/content/TopUpCreditsDialogContent.vue
index ce061300a..0a2884daf 100644
--- a/src/components/dialog/content/TopUpCreditsDialogContent.vue
+++ b/src/components/dialog/content/TopUpCreditsDialogContent.vue
@@ -1,180 +1,259 @@
-
+
-
-
+
+
{{
isInsufficientCredits
? $t('credits.topUp.addMoreCreditsToRun')
: $t('credits.topUp.addMoreCredits')
}}
-
-
-
- {{ $t('credits.topUp.insufficientWorkflowMessage') }}
-
-
-
-
- {{ $t('credits.topUp.creditsDescription') }}
-
-
+
+
+
+ {{ $t('credits.topUp.insufficientWorkflowMessage') }}
+
-
-
-
-
- {{
- $t('credits.creditsAvailable')
- }}
-
-
- {{ $t('credits.refreshes', { date: formattedRenewalDate }) }}
-
-
-
-
-
-
- {{ $t('credits.topUp.howManyCredits') }}
-
-
-
-
-
-
-
+
+
+ {{ $t('credits.topUp.selectAmount') }}
+
+
+
+
+
+
+
+
+
+
+ {{ $t('credits.topUp.youPay') }}
+
+
+
+ $
+
+
-
+
+
+
+ {{ $t('credits.topUp.youGet') }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ $t('credits.topUp.minimumPurchase', {
+ amount: MIN_AMOUNT,
+ credits: usdToCredits(MIN_AMOUNT)
+ })
+ }}
+
+
+ {{
+ $t('credits.topUp.maximumAmount', {
+ amount: formatNumber(MAX_AMOUNT)
+ })
+ }}
+ {{ $t('credits.topUp.needMore') }}
+ {{ $t('credits.topUp.contactUs') }}
+
+
+
-
-
-
-
- {{ t('subscription.videoEstimateExplanation') }}
-
+
-
+
+
+
+
+
+
+
+
+
diff --git a/src/components/input/MultiSelect.vue b/src/components/input/MultiSelect.vue
index aaa372d73..21ba0d6a2 100644
--- a/src/components/input/MultiSelect.vue
+++ b/src/components/input/MultiSelect.vue
@@ -160,7 +160,7 @@
>
diff --git a/src/components/load3d/Load3DControls.vue b/src/components/load3d/Load3DControls.vue
index 1609f39ea..68251c741 100644
--- a/src/components/load3d/Load3DControls.vue
+++ b/src/components/load3d/Load3DControls.vue
@@ -1,6 +1,6 @@
-
+
@@ -169,7 +169,7 @@ const getCategoryIcon = (category: string) => {
export: 'pi pi-download'
}
// @ts-expect-error fixme ts strict error
- return `${icons[category]} text-white text-lg`
+ return `${icons[category]} text-base-foreground text-lg`
}
const emit = defineEmits<{
diff --git a/src/components/load3d/LoadingOverlay.vue b/src/components/load3d/LoadingOverlay.vue
index d5094f9b0..e538b3f91 100644
--- a/src/components/load3d/LoadingOverlay.vue
+++ b/src/components/load3d/LoadingOverlay.vue
@@ -2,11 +2,11 @@
-
diff --git a/src/components/load3d/controls/AnimationControls.vue b/src/components/load3d/controls/AnimationControls.vue
index 16df631e5..5cdfb7b34 100644
--- a/src/components/load3d/controls/AnimationControls.vue
+++ b/src/components/load3d/controls/AnimationControls.vue
@@ -15,7 +15,7 @@
:class="[
'pi',
playing ? 'pi-pause' : 'pi-play',
- 'text-lg text-white'
+ 'text-lg text-base-foreground'
]"
/>
@@ -46,7 +46,7 @@
class="flex-1"
@update:model-value="handleSliderChange"
/>
-
+
{{ formatTime(currentTime) }} / {{ formatTime(animationDuration) }}
diff --git a/src/components/load3d/controls/CameraControls.vue b/src/components/load3d/controls/CameraControls.vue
index 85ab914d8..7922e6f1e 100644
--- a/src/components/load3d/controls/CameraControls.vue
+++ b/src/components/load3d/controls/CameraControls.vue
@@ -11,7 +11,7 @@
:aria-label="$t('load3d.switchCamera')"
@click="switchCamera"
>
-
+
-
+
-
+
diff --git a/src/components/load3d/controls/PopupSlider.vue b/src/components/load3d/controls/PopupSlider.vue
index 5ab1fe3d6..d51a2e5b1 100644
--- a/src/components/load3d/controls/PopupSlider.vue
+++ b/src/components/load3d/controls/PopupSlider.vue
@@ -8,11 +8,11 @@
:aria-label="tooltipText"
@click="toggleSlider"
>
-
+
-
+
@@ -42,7 +42,7 @@
:aria-label="$t('load3d.exportRecording')"
@click="handleExportRecording"
>
-
+
{{ formatDuration(recordingDuration) }}
diff --git a/src/components/load3d/controls/SceneControls.vue b/src/components/load3d/controls/SceneControls.vue
index 4521df3db..9eea9fc27 100644
--- a/src/components/load3d/controls/SceneControls.vue
+++ b/src/components/load3d/controls/SceneControls.vue
@@ -8,7 +8,7 @@
:aria-label="$t('load3d.showGrid')"
@click="toggleGrid"
>
-
+
@@ -23,7 +23,7 @@
:aria-label="$t('load3d.backgroundColor')"
@click="openColorPicker"
>
-
+
-
+
-
+
@@ -98,7 +98,7 @@
:aria-label="$t('load3d.removeBackgroundImage')"
@click="removeBackgroundImage"
>
-
+
diff --git a/src/components/load3d/controls/ViewerControls.vue b/src/components/load3d/controls/ViewerControls.vue
index af751d4f1..7ac99fff8 100644
--- a/src/components/load3d/controls/ViewerControls.vue
+++ b/src/components/load3d/controls/ViewerControls.vue
@@ -1,5 +1,5 @@
-
+
diff --git a/src/components/sidebar/tabs/AssetsSidebarTab.vue b/src/components/sidebar/tabs/AssetsSidebarTab.vue
index 70ec90b6f..032645b4c 100644
--- a/src/components/sidebar/tabs/AssetsSidebarTab.vue
+++ b/src/components/sidebar/tabs/AssetsSidebarTab.vue
@@ -114,11 +114,15 @@
:output-count="getOutputCount(item)"
:show-delete-button="shouldShowDeleteButton"
:open-context-menu-id="openContextMenuId"
+ :selected-assets="getSelectedAssets(displayAssets)"
+ :has-selection="hasSelection"
@click="handleAssetSelect(item)"
@zoom="handleZoomClick(item)"
@output-count-click="enterFolderView(item)"
@asset-deleted="refreshAssets"
@context-menu-opened="openContextMenuId = item.id"
+ @bulk-download="handleBulkDownload"
+ @bulk-delete="handleBulkDelete"
/>
@@ -134,7 +138,6 @@
@@ -80,7 +81,9 @@
>
{{ contentTitle }}
-
+
diff --git a/src/components/widget/nav/NavItem.vue b/src/components/widget/nav/NavItem.vue
index 30ccbb3d9..c564dfcae 100644
--- a/src/components/widget/nav/NavItem.vue
+++ b/src/components/widget/nav/NavItem.vue
@@ -9,7 +9,7 @@
role="button"
@click="onClick"
>
-
+
diff --git a/src/composables/node/useNodePricing.test.ts b/src/composables/node/useNodePricing.test.ts
index a619f5a19..c811a4796 100644
--- a/src/composables/node/useNodePricing.test.ts
+++ b/src/composables/node/useNodePricing.test.ts
@@ -1664,31 +1664,41 @@ describe('useNodePricing', () => {
{
model: 'gemini-2.5-pro-preview-05-06',
expected: creditsListLabel([0.00125, 0.01], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
},
{
model: 'gemini-2.5-pro',
expected: creditsListLabel([0.00125, 0.01], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
},
{
model: 'gemini-3-pro-preview',
expected: creditsListLabel([0.002, 0.012], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
},
{
model: 'gemini-2.5-flash-preview-04-17',
expected: creditsListLabel([0.0003, 0.0025], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
},
{
model: 'gemini-2.5-flash',
expected: creditsListLabel([0.0003, 0.0025], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
},
{ model: 'unknown-gemini-model', expected: 'Token-based' }
@@ -1702,16 +1712,6 @@ describe('useNodePricing', () => {
})
})
- it('should return per-second pricing for Gemini Veo models', () => {
- const { getNodeDisplayPrice } = useNodePricing()
- const node = createMockNode('GeminiNode', [
- { name: 'model', value: 'veo-2.0-generate-001' }
- ])
-
- const price = getNodeDisplayPrice(node)
- expect(price).toBe(creditsLabel(0.5, { suffix: '/second' }))
- })
-
it('should return fallback for GeminiNode without model widget', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('GeminiNode', [])
@@ -1737,73 +1737,97 @@ describe('useNodePricing', () => {
{
model: 'o4-mini',
expected: creditsListLabel([0.0011, 0.0044], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
},
{
model: 'o1-pro',
expected: creditsListLabel([0.15, 0.6], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
},
{
model: 'o1',
expected: creditsListLabel([0.015, 0.06], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
},
{
model: 'o3-mini',
expected: creditsListLabel([0.0011, 0.0044], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
},
{
model: 'o3',
expected: creditsListLabel([0.01, 0.04], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
},
{
model: 'gpt-4o',
expected: creditsListLabel([0.0025, 0.01], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
},
{
model: 'gpt-4.1-nano',
expected: creditsListLabel([0.0001, 0.0004], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
},
{
model: 'gpt-4.1-mini',
expected: creditsListLabel([0.0004, 0.0016], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
},
{
model: 'gpt-4.1',
expected: creditsListLabel([0.002, 0.008], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
},
{
model: 'gpt-5-nano',
expected: creditsListLabel([0.00005, 0.0004], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
},
{
model: 'gpt-5-mini',
expected: creditsListLabel([0.00025, 0.002], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
},
{
model: 'gpt-5',
expected: creditsListLabel([0.00125, 0.01], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
}
]
@@ -1824,37 +1848,49 @@ describe('useNodePricing', () => {
{
model: 'gpt-4.1-nano-test',
expected: creditsListLabel([0.0001, 0.0004], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
},
{
model: 'gpt-4.1-mini-test',
expected: creditsListLabel([0.0004, 0.0016], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
},
{
model: 'gpt-4.1-test',
expected: creditsListLabel([0.002, 0.008], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
},
{
model: 'o1-pro-test',
expected: creditsListLabel([0.15, 0.6], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
},
{
model: 'o1-test',
expected: creditsListLabel([0.015, 0.06], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
},
{
model: 'o3-mini-test',
expected: creditsListLabel([0.0011, 0.0044], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
},
{ model: 'unknown-model', expected: 'Token-based' }
diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts
index cf627b6f9..641d38911 100644
--- a/src/composables/node/useNodePricing.ts
+++ b/src/composables/node/useNodePricing.ts
@@ -1823,28 +1823,35 @@ const apiNodeCosts: Record
=
const model = String(modelWidget.value)
- // Google Veo video generation
- if (model.includes('veo-2.0')) {
- return formatCreditsLabel(0.5, { suffix: '/second' })
- } else if (model.includes('gemini-2.5-flash-preview-04-17')) {
+ if (model.includes('gemini-2.5-flash-preview-04-17')) {
return formatCreditsListLabel([0.0003, 0.0025], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
} else if (model.includes('gemini-2.5-flash')) {
return formatCreditsListLabel([0.0003, 0.0025], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
} else if (model.includes('gemini-2.5-pro-preview-05-06')) {
return formatCreditsListLabel([0.00125, 0.01], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
} else if (model.includes('gemini-2.5-pro')) {
return formatCreditsListLabel([0.00125, 0.01], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
} else if (model.includes('gemini-3-pro-preview')) {
return formatCreditsListLabel([0.002, 0.012], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
}
// For other Gemini models, show token-based pricing info
@@ -1899,51 +1906,75 @@ const apiNodeCosts: Record =
// Specific pricing for exposed models based on official pricing data (converted to per 1K tokens)
if (model.includes('o4-mini')) {
return formatCreditsListLabel([0.0011, 0.0044], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
} else if (model.includes('o1-pro')) {
return formatCreditsListLabel([0.15, 0.6], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
} else if (model.includes('o1')) {
return formatCreditsListLabel([0.015, 0.06], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
} else if (model.includes('o3-mini')) {
return formatCreditsListLabel([0.0011, 0.0044], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
} else if (model.includes('o3')) {
return formatCreditsListLabel([0.01, 0.04], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
} else if (model.includes('gpt-4o')) {
return formatCreditsListLabel([0.0025, 0.01], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
} else if (model.includes('gpt-4.1-nano')) {
return formatCreditsListLabel([0.0001, 0.0004], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
} else if (model.includes('gpt-4.1-mini')) {
return formatCreditsListLabel([0.0004, 0.0016], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
} else if (model.includes('gpt-4.1')) {
return formatCreditsListLabel([0.002, 0.008], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
} else if (model.includes('gpt-5-nano')) {
return formatCreditsListLabel([0.00005, 0.0004], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
} else if (model.includes('gpt-5-mini')) {
return formatCreditsListLabel([0.00025, 0.002], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
} else if (model.includes('gpt-5')) {
return formatCreditsListLabel([0.00125, 0.01], {
- suffix: ' per 1K tokens'
+ suffix: ' per 1K tokens',
+ approximate: true,
+ separator: '-'
})
}
return 'Token-based'
@@ -2101,6 +2132,35 @@ const apiNodeCosts: Record =
},
LtxvApiImageToVideo: {
displayPrice: ltxvPricingCalculator
+ },
+ WanReferenceVideoApi: {
+ displayPrice: (node: LGraphNode): string => {
+ const durationWidget = node.widgets?.find(
+ (w) => w.name === 'duration'
+ ) as IComboWidget
+ const sizeWidget = node.widgets?.find(
+ (w) => w.name === 'size'
+ ) as IComboWidget
+
+ if (!durationWidget || !sizeWidget) {
+ return formatCreditsRangeLabel(0.7, 1.5, {
+ note: '(varies with size & duration)'
+ })
+ }
+
+ const seconds = parseFloat(String(durationWidget.value))
+ const sizeStr = String(sizeWidget.value).toLowerCase()
+
+ const rate = sizeStr.includes('1080p') ? 0.15 : 0.1
+ const inputMin = 2 * rate
+ const inputMax = 5 * rate
+ const outputPrice = seconds * rate
+
+ const minTotal = inputMin + outputPrice
+ const maxTotal = inputMax + outputPrice
+
+ return formatCreditsRangeLabel(minTotal, maxTotal)
+ }
}
}
@@ -2254,6 +2314,7 @@ export const useNodePricing = () => {
ByteDanceImageReferenceNode: ['model', 'duration', 'resolution'],
WanTextToVideoApi: ['duration', 'size'],
WanImageToVideoApi: ['duration', 'resolution'],
+ WanReferenceVideoApi: ['duration', 'size'],
LtxvApiTextToVideo: ['model', 'duration', 'resolution'],
LtxvApiImageToVideo: ['model', 'duration', 'resolution']
}
diff --git a/src/composables/useCoreCommands.ts b/src/composables/useCoreCommands.ts
index 6b2aa63c2..032f1f57b 100644
--- a/src/composables/useCoreCommands.ts
+++ b/src/composables/useCoreCommands.ts
@@ -905,15 +905,6 @@ export function useCoreCommands(): ComfyCommand[] {
})
}
},
- {
- id: 'Comfy.Manager.ToggleManagerProgressDialog',
- icon: 'pi pi-spinner',
- label: 'Toggle the Custom Nodes Manager Progress Bar',
- versionAdded: '1.13.9',
- function: () => {
- dialogService.toggleManagerProgressDialog()
- }
- },
{
id: 'Comfy.User.OpenSignInDialog',
icon: 'pi pi-user',
diff --git a/src/lib/litegraph/src/subgraph/SubgraphNode.ts b/src/lib/litegraph/src/subgraph/SubgraphNode.ts
index 43f59b516..e26e1cd48 100644
--- a/src/lib/litegraph/src/subgraph/SubgraphNode.ts
+++ b/src/lib/litegraph/src/subgraph/SubgraphNode.ts
@@ -28,6 +28,7 @@ import type {
} from '@/lib/litegraph/src/types/serialisation'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import type { UUID } from '@/lib/litegraph/src/utils/uuid'
+import { AssetWidget } from '@/lib/litegraph/src/widgets/AssetWidget'
import { toConcreteWidget } from '@/lib/litegraph/src/widgets/widgetMap'
import { ExecutableNodeDTO } from './ExecutableNodeDTO'
@@ -333,6 +334,8 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
const promotedWidget = toConcreteWidget(widget, this).createCopyForNode(
this
)
+ if (widget instanceof AssetWidget)
+ promotedWidget.options.nodeType ??= widget.node.type
Object.assign(promotedWidget, {
get name() {
diff --git a/src/lib/litegraph/src/types/widgets.ts b/src/lib/litegraph/src/types/widgets.ts
index 1cfb323d8..b2cf11557 100644
--- a/src/lib/litegraph/src/types/widgets.ts
+++ b/src/lib/litegraph/src/types/widgets.ts
@@ -27,6 +27,8 @@ export interface IWidgetOptions {
socketless?: boolean
/** If `true`, the widget will not be rendered by the Vue renderer. */
canvasOnly?: boolean
+ /** Used as a temporary override for determining the asset type in vue mode*/
+ nodeType?: string
values?: TValues
/** Optional function to format values for display (e.g., hash → human-readable name) */
diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index 4cdaf3636..1e709c549 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -10,8 +10,10 @@
"downloadVideo": "Download video",
"editOrMaskImage": "Edit or mask image",
"editImage": "Edit image",
+ "decrement": "Decrement",
"deleteImage": "Delete image",
"deleteAudioFile": "Delete audio file",
+ "increment": "Increment",
"removeImage": "Remove image",
"removeVideo": "Remove video",
"chart": "Chart",
@@ -1918,12 +1920,25 @@
"insufficientWorkflowMessage": "You don't have enough credits to run this workflow.",
"creditsDescription": "Credits are used to run workflows or partner nodes.",
"howManyCredits": "How many credits would you like to add?",
- "videosEstimate": "~{count} videos",
+ "usdAmount": "${amount}",
+ "videosEstimate": "~{count} videos*",
"templateNote": "*Generated with Wan Fun Control template",
"buy": "Buy",
"purchaseError": "Purchase Failed",
"purchaseErrorDetail": "Failed to purchase credits: {error}",
- "unknownError": "An unknown error occurred"
+ "unknownError": "An unknown error occurred",
+ "viewPricing": "View pricing details",
+ "youPay": "Amount (USD)",
+ "youGet": "Credits",
+ "buyCredits": "Continue to payment",
+ "minimumPurchase": "${amount} minimum ({credits} credits)",
+ "maximumAmount": "${amount} max.",
+ "creditsPerDollar": "credits per dollar",
+ "amountToPayLabel": "Amount to pay in dollars",
+ "creditsToReceiveLabel": "Credits to receive",
+ "selectAmount": "Select amount",
+ "needMore": "Need more?",
+ "contactUs": "Contact us"
},
"creditsAvailable": "Credits available",
"refreshes": "Refreshes {date}",
@@ -1960,9 +1975,9 @@
"monthlyBonusDescription": "Monthly credit bonus",
"prepaidDescription": "Pre-paid credits",
"prepaidCreditsInfo": "Pre-paid credits expire after 1 year from purchase date.",
- "creditsRemainingThisMonth": "Credits remaining this month",
- "creditsRemainingThisYear": "Credits remaining this year",
- "creditsYouveAdded": "Credits you've added",
+ "creditsRemainingThisMonth": "Included (Refills {date})",
+ "creditsRemainingThisYear": "Included (Refills {date})",
+ "creditsYouveAdded": "Additional",
"monthlyCreditsInfo": "These credits refresh monthly and don't roll over",
"viewMoreDetailsPlans": "View more details about plans & pricing",
"nextBillingCycle": "next billing cycle",
@@ -2017,7 +2032,7 @@
"subscribeTo": "Subscribe to {plan}",
"monthlyCreditsLabel": "Monthly credits",
"yearlyCreditsLabel": "Total yearly credits",
- "maxDurationLabel": "Max duration of each workflow run",
+ "maxDurationLabel": "Max run duration",
"gpuLabel": "RTX 6000 Pro (96GB VRAM)",
"addCreditsLabel": "Add more credits whenever",
"customLoRAsLabel": "Import your own LoRAs",
@@ -2395,9 +2410,12 @@
},
"selection": {
"selectedCount": "Assets Selected: {count}",
+ "multipleSelectedAssets": "Multiple assets selected",
"deselectAll": "Deselect all",
"downloadSelected": "Download",
+ "downloadSelectedAll": "Download all",
"deleteSelected": "Delete",
+ "deleteSelectedAll": "Delete all",
"downloadStarted": "Downloading {count} files...",
"downloadsStarted": "Started downloading {count} file(s)",
"assetsDeletedSuccessfully": "{count} asset(s) deleted successfully",
@@ -2480,5 +2498,21 @@
"help": {
"recentReleases": "Recent releases",
"helpCenterMenu": "Help Center Menu"
+ },
+ "progressToast": {
+ "importingModels": "Importing Models",
+ "downloadingModel": "Downloading model...",
+ "downloadsFailed": "{count} downloads failed | {count} download failed | {count} downloads failed",
+ "allDownloadsCompleted": "All downloads completed",
+ "noImportsInQueue": "No {filter} in queue",
+ "failed": "Failed",
+ "finished": "Finished",
+ "pending": "Pending",
+ "progressCount": "{completed} of {total}",
+ "filter": {
+ "all": "All",
+ "completed": "Completed",
+ "failed": "Failed"
+ }
}
}
\ No newline at end of file
diff --git a/src/locales/en/nodeDefs.json b/src/locales/en/nodeDefs.json
index c78b7c796..936547f7d 100644
--- a/src/locales/en/nodeDefs.json
+++ b/src/locales/en/nodeDefs.json
@@ -6088,6 +6088,9 @@
},
"ckpt_name": {
"name": "ckpt_name"
+ },
+ "device": {
+ "name": "device"
}
},
"outputs": {
@@ -15365,6 +15368,51 @@
}
}
},
+ "WanReferenceVideoApi": {
+ "display_name": "Wan Reference to Video",
+ "description": "Use the character and voice from input videos, combined with a prompt, to generate a new video that maintains character consistency.",
+ "inputs": {
+ "model": {
+ "name": "model"
+ },
+ "prompt": {
+ "name": "prompt",
+ "tooltip": "Prompt describing the elements and visual features. Supports English and Chinese. Use identifiers such as `character1` and `character2` to refer to the reference characters."
+ },
+ "negative_prompt": {
+ "name": "negative_prompt",
+ "tooltip": "Negative prompt describing what to avoid."
+ },
+ "reference_videos": {
+ "name": "reference_videos"
+ },
+ "size": {
+ "name": "size"
+ },
+ "duration": {
+ "name": "duration"
+ },
+ "seed": {
+ "name": "seed"
+ },
+ "shot_type": {
+ "name": "shot_type",
+ "tooltip": "Specifies the shot type for the generated video, that is, whether the video is a single continuous shot or multiple shots with cuts."
+ },
+ "watermark": {
+ "name": "watermark",
+ "tooltip": "Whether to add an AI-generated watermark to the result."
+ },
+ "control_after_generate": {
+ "name": "control after generate"
+ }
+ },
+ "outputs": {
+ "0": {
+ "tooltip": null
+ }
+ }
+ },
"WanSoundImageToVideo": {
"display_name": "WanSoundImageToVideo",
"inputs": {
diff --git a/src/platform/assets/components/AssetBadgeGroup.vue b/src/platform/assets/components/AssetBadgeGroup.vue
index e78fe3a60..5c85713e4 100644
--- a/src/platform/assets/components/AssetBadgeGroup.vue
+++ b/src/platform/assets/components/AssetBadgeGroup.vue
@@ -5,7 +5,7 @@
:key="badge.label"
:class="
cn(
- 'px-2 py-1 rounded text-xs font-bold uppercase tracking-wider text-modal-card-tag-foreground bg-modal-card-tag-background'
+ 'px-2 py-1 rounded text-xs font-bold uppercase tracking-wider text-modal-card-tag-foreground bg-modal-card-tag-background break-all'
)
"
>
diff --git a/src/platform/assets/components/AssetBrowserModal.vue b/src/platform/assets/components/AssetBrowserModal.vue
index e0c47361d..001387d05 100644
--- a/src/platform/assets/components/AssetBrowserModal.vue
+++ b/src/platform/assets/components/AssetBrowserModal.vue
@@ -32,7 +32,7 @@
diff --git a/src/platform/assets/components/AssetGrid.vue b/src/platform/assets/components/AssetGrid.vue
index 47fcc1fcd..138003393 100644
--- a/src/platform/assets/components/AssetGrid.vue
+++ b/src/platform/assets/components/AssetGrid.vue
@@ -1,28 +1,21 @@
-
-
+
-
@@ -30,24 +23,33 @@
{{ $t('assetBrowser.tryAdjustingFilters') }}
-
-
-
+
+
+
+
+
diff --git a/src/platform/assets/components/MediaAssetCard.vue b/src/platform/assets/components/MediaAssetCard.vue
index 15a423bf9..b61410ca7 100644
--- a/src/platform/assets/components/MediaAssetCard.vue
+++ b/src/platform/assets/components/MediaAssetCard.vue
@@ -139,9 +139,13 @@
:asset-type="assetType"
:file-kind="fileKind"
:show-delete-button="showDeleteButton"
+ :selected-assets="selectedAssets"
+ :is-bulk-mode="hasSelection && (selectedAssets?.length ?? 0) > 1"
@zoom="handleZoomClick"
@asset-deleted="emit('asset-deleted')"
@asset-deleting="isDeleting = $event"
+ @bulk-download="emit('bulk-download', $event)"
+ @bulk-delete="emit('bulk-delete', $event)"
/>
@@ -187,7 +191,9 @@ const {
showOutputCount,
outputCount,
showDeleteButton,
- openContextMenuId
+ openContextMenuId,
+ selectedAssets,
+ hasSelection
} = defineProps<{
asset?: AssetItem
loading?: boolean
@@ -196,6 +202,8 @@ const {
outputCount?: number
showDeleteButton?: boolean
openContextMenuId?: string | null
+ selectedAssets?: AssetItem[]
+ hasSelection?: boolean
}>()
const emit = defineEmits<{
@@ -204,6 +212,8 @@ const emit = defineEmits<{
'output-count-click': []
'asset-deleted': []
'context-menu-opened': []
+ 'bulk-download': [assets: AssetItem[]]
+ 'bulk-delete': [assets: AssetItem[]]
}>()
const cardContainerRef = ref
()
diff --git a/src/platform/assets/components/MediaAssetContextMenu.vue b/src/platform/assets/components/MediaAssetContextMenu.vue
index d10387030..4a79e83ee 100644
--- a/src/platform/assets/components/MediaAssetContextMenu.vue
+++ b/src/platform/assets/components/MediaAssetContextMenu.vue
@@ -15,11 +15,10 @@
-
-
+
+
-
+
@@ -120,60 +120,39 @@
-
-
-
-
- {{ monthlyBonusCredits }}
-
-
-
+
+
+
+ |
+
+ {{ includedCreditsDisplay }}
+ |
+
{{ creditsRemainingLabel }}
-
-
-
-
-
-
- {{ prepaidCredits }}
-
- |
+
+ |
+
+ {{ prepaidCredits }}
+ |
+
{{ $t('subscription.creditsYouveAdded') }}
-
-
-
-
-
-
-
+ |
+
+
+
-
+
{{ $t('subscription.yourPlanIncludes') }}
@@ -288,7 +267,7 @@
diff --git a/src/workbench/extensions/manager/components/ManagerProgressFooter.test.ts b/src/workbench/extensions/manager/components/ManagerProgressFooter.test.ts
deleted file mode 100644
index 024e025b8..000000000
--- a/src/workbench/extensions/manager/components/ManagerProgressFooter.test.ts
+++ /dev/null
@@ -1,486 +0,0 @@
-import { mount } from '@vue/test-utils'
-import { createPinia, setActivePinia } from 'pinia'
-import PrimeVue from 'primevue/config'
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { nextTick } from 'vue'
-import { createI18n } from 'vue-i18n'
-
-import { useSettingStore } from '@/platform/settings/settingStore'
-import { useCommandStore } from '@/stores/commandStore'
-import { useDialogStore } from '@/stores/dialogStore'
-import ManagerProgressFooter from '@/workbench/extensions/manager/components/ManagerProgressFooter.vue'
-import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService'
-import {
- useComfyManagerStore,
- useManagerProgressDialogStore
-} from '@/workbench/extensions/manager/stores/comfyManagerStore'
-import type { TaskLog } from '@/workbench/extensions/manager/types/comfyManagerTypes'
-
-// Mock modules
-vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore')
-vi.mock('@/stores/dialogStore')
-vi.mock('@/platform/settings/settingStore')
-vi.mock('@/stores/commandStore')
-vi.mock('@/workbench/extensions/manager/services/comfyManagerService')
-vi.mock(
- '@/workbench/extensions/manager/composables/useConflictDetection',
- () => ({
- useConflictDetection: vi.fn(() => ({
- conflictedPackages: { value: [] },
- runFullConflictAnalysis: vi.fn().mockResolvedValue(undefined)
- }))
- })
-)
-
-// Mock useEventListener to capture the event handler
-let reconnectHandler: (() => void) | null = null
-vi.mock('@vueuse/core', async () => {
- const actual = await vi.importActual('@vueuse/core')
- return {
- ...actual,
- useEventListener: vi.fn(
- (_target: any, event: string, handler: any, _options: any) => {
- if (event === 'reconnected') {
- reconnectHandler = handler
- }
- }
- )
- }
-})
-vi.mock('@/platform/workflow/core/services/workflowService', () => ({
- useWorkflowService: vi.fn(() => ({
- reloadCurrentWorkflow: vi.fn().mockResolvedValue(undefined)
- }))
-}))
-vi.mock('@/stores/workspace/colorPaletteStore', () => ({
- useColorPaletteStore: vi.fn(() => ({
- completedActivePalette: {
- light_theme: false
- }
- }))
-}))
-
-// Helper function to mount component with required setup
-const mountComponent = (options: { captureError?: boolean } = {}) => {
- const pinia = createPinia()
- setActivePinia(pinia)
-
- const i18n = createI18n({
- legacy: false,
- locale: 'en',
- messages: {
- en: {
- g: {
- close: 'Close',
- progressCountOf: 'of'
- },
- contextMenu: {
- Collapse: 'Collapse',
- Expand: 'Expand'
- },
- manager: {
- clickToFinishSetup: 'Click',
- applyChanges: 'Apply Changes',
- toFinishSetup: 'to finish setup',
- restartingBackend: 'Restarting backend to apply changes...',
- extensionsSuccessfullyInstalled:
- 'Extension(s) successfully installed and are ready to use!',
- restartToApplyChanges: 'To apply changes, please restart ComfyUI',
- installingDependencies: 'Installing dependencies...'
- }
- }
- }
- })
-
- const config: any = {
- global: {
- plugins: [pinia, PrimeVue, i18n]
- }
- }
-
- // Add error handler for tests that expect errors
- if (options.captureError) {
- config.global.config = {
- errorHandler: () => {
- // Suppress error in test
- }
- }
- }
-
- return mount(ManagerProgressFooter, config)
-}
-
-describe('ManagerProgressFooter', () => {
- const mockTaskLogs: TaskLog[] = []
-
- const mockComfyManagerStore = {
- taskLogs: mockTaskLogs,
- allTasksDone: true,
- isProcessingTasks: false,
- succeededTasksIds: [] as string[],
- failedTasksIds: [] as string[],
- taskHistory: {} as Record
,
- taskQueue: null,
- resetTaskState: vi.fn(),
- clearLogs: vi.fn(),
- setStale: vi.fn(),
- // Add other required properties
- isLoading: { value: false },
- error: { value: null },
- statusMessage: { value: 'DONE' },
- installedPacks: {},
- installedPacksIds: new Set(),
- isPackInstalled: vi.fn(),
- isPackEnabled: vi.fn(),
- getInstalledPackVersion: vi.fn(),
- refreshInstalledList: vi.fn(),
- installPack: vi.fn(),
- uninstallPack: vi.fn(),
- updatePack: vi.fn(),
- updateAllPacks: vi.fn(),
- disablePack: vi.fn(),
- enablePack: vi.fn()
- }
-
- const mockDialogStore = {
- closeDialog: vi.fn(),
- // Add other required properties
- dialogStack: { value: [] },
- showDialog: vi.fn(),
- $id: 'dialog',
- $state: {} as any,
- $patch: vi.fn(),
- $reset: vi.fn(),
- $subscribe: vi.fn(),
- $dispose: vi.fn(),
- $onAction: vi.fn()
- }
-
- const mockSettingStore = {
- get: vi.fn().mockReturnValue(false),
- set: vi.fn(),
- // Add other required properties
- settingValues: { value: {} },
- settingsById: { value: {} },
- exists: vi.fn(),
- getDefaultValue: vi.fn(),
- loadSettingValues: vi.fn(),
- updateValue: vi.fn(),
- $id: 'setting',
- $state: {} as any,
- $patch: vi.fn(),
- $reset: vi.fn(),
- $subscribe: vi.fn(),
- $dispose: vi.fn(),
- $onAction: vi.fn()
- }
-
- const mockProgressDialogStore = {
- isExpanded: false,
- toggle: vi.fn(),
- collapse: vi.fn(),
- expand: vi.fn()
- }
-
- const mockCommandStore = {
- execute: vi.fn().mockResolvedValue(undefined)
- }
-
- const mockComfyManagerService = {
- rebootComfyUI: vi.fn().mockResolvedValue(null)
- }
-
- beforeEach(() => {
- vi.clearAllMocks()
- // Create new pinia instance for each test
- const pinia = createPinia()
- setActivePinia(pinia)
-
- // Reset task logs
- mockTaskLogs.length = 0
- mockComfyManagerStore.taskLogs = mockTaskLogs
- // Reset event handler
- reconnectHandler = null
-
- vi.mocked(useComfyManagerStore).mockReturnValue(
- mockComfyManagerStore as any
- )
- vi.mocked(useDialogStore).mockReturnValue(mockDialogStore as any)
- vi.mocked(useSettingStore).mockReturnValue(mockSettingStore as any)
- vi.mocked(useManagerProgressDialogStore).mockReturnValue(
- mockProgressDialogStore as any
- )
- vi.mocked(useCommandStore).mockReturnValue(mockCommandStore as any)
- vi.mocked(useComfyManagerService).mockReturnValue(
- mockComfyManagerService as any
- )
- })
-
- describe('State 1: Queue Running', () => {
- it('should display loading spinner and progress counter when queue is running', async () => {
- // Setup queue running state
- mockComfyManagerStore.isProcessingTasks = true
- mockComfyManagerStore.succeededTasksIds = ['1', '2']
- mockComfyManagerStore.failedTasksIds = []
- mockComfyManagerStore.taskHistory = {
- '1': { taskName: 'Installing pack1' },
- '2': { taskName: 'Installing pack2' },
- '3': { taskName: 'Installing pack3' }
- }
- mockTaskLogs.push(
- { taskName: 'Installing pack1', taskId: '1', logs: [] },
- { taskName: 'Installing pack2', taskId: '2', logs: [] },
- { taskName: 'Installing pack3', taskId: '3', logs: [] }
- )
-
- const wrapper = mountComponent()
-
- // Check loading spinner exists (DotSpinner component)
- expect(wrapper.find('.inline-flex').exists()).toBe(true)
-
- // Check current task name is displayed
- expect(wrapper.text()).toContain('Installing pack3')
-
- // Check progress counter (completed: 2 of 3)
- expect(wrapper.text()).toMatch(/2.*of.*3/)
-
- // Check expand/collapse button exists
- const expandButton = wrapper.find('[aria-label="Expand"]')
- expect(expandButton.exists()).toBe(true)
-
- // Check Apply Changes button is NOT shown
- expect(wrapper.text()).not.toContain('Apply Changes')
- })
-
- it('should toggle expansion when expand button is clicked', async () => {
- mockComfyManagerStore.isProcessingTasks = true
- mockTaskLogs.push({ taskName: 'Installing', taskId: '1', logs: [] })
-
- const wrapper = mountComponent()
-
- const expandButton = wrapper.find('[aria-label="Expand"]')
- await expandButton.trigger('click')
-
- expect(mockProgressDialogStore.toggle).toHaveBeenCalled()
- })
- })
-
- describe('State 2: Tasks Completed (Waiting for Restart)', () => {
- it('should display check mark and Apply Changes button when all tasks are done', async () => {
- // Setup tasks completed state
- mockComfyManagerStore.isProcessingTasks = false
- mockTaskLogs.push(
- { taskName: 'Installed pack1', taskId: '1', logs: [] },
- { taskName: 'Installed pack2', taskId: '2', logs: [] }
- )
- mockComfyManagerStore.allTasksDone = true
-
- const wrapper = mountComponent()
-
- // Check check mark emoji
- expect(wrapper.text()).toContain('✅')
-
- // Check restart message
- expect(wrapper.text()).toContain(
- 'To apply changes, please restart ComfyUI'
- )
- expect(wrapper.text()).toContain('Apply Changes')
-
- // Check Apply Changes button exists
- const applyButton = wrapper
- .findAll('button')
- .find((btn) => btn.text().includes('Apply Changes'))
- expect(applyButton).toBeTruthy()
-
- // Check no progress counter
- expect(wrapper.text()).not.toMatch(/\d+.*of.*\d+/)
- })
- })
-
- describe('State 3: Restarting', () => {
- it('should display restarting message and spinner during restart', async () => {
- // Setup completed state first
- mockComfyManagerStore.isProcessingTasks = false
- mockComfyManagerStore.allTasksDone = true
-
- const wrapper = mountComponent()
-
- // Click Apply Changes to trigger restart
- const applyButton = wrapper
- .findAll('button')
- .find((btn) => btn.text().includes('Apply Changes'))
- await applyButton?.trigger('click')
-
- // Wait for state update
- await nextTick()
-
- // Check restarting message
- expect(wrapper.text()).toContain('Restarting backend to apply changes...')
-
- // Check loading spinner during restart
- expect(wrapper.find('.inline-flex').exists()).toBe(true)
-
- // Check Apply Changes button is hidden
- expect(wrapper.text()).not.toContain('Apply Changes')
- })
- })
-
- describe('State 4: Restart Completed', () => {
- it('should display success message and auto-close after 3 seconds', async () => {
- vi.useFakeTimers()
-
- // Setup completed state
- mockComfyManagerStore.isProcessingTasks = false
- mockComfyManagerStore.allTasksDone = true
-
- const wrapper = mountComponent()
-
- // Trigger restart
- const applyButton = wrapper
- .findAll('button')
- .find((btn) => btn.text().includes('Apply Changes'))
- await applyButton?.trigger('click')
-
- // Wait for event listener to be set up
- await nextTick()
-
- // Trigger the reconnect handler directly
- if (reconnectHandler) {
- await reconnectHandler()
- }
-
- // Wait for restart completed state
- await nextTick()
-
- // Check success message
- expect(wrapper.text()).toContain('🎉')
- expect(wrapper.text()).toContain(
- 'Extension(s) successfully installed and are ready to use!'
- )
-
- // Check dialog closes after 3 seconds
- vi.advanceTimersByTime(3000)
-
- await nextTick()
-
- expect(mockDialogStore.closeDialog).toHaveBeenCalledWith({
- key: 'global-manager-progress-dialog'
- })
- expect(mockComfyManagerStore.resetTaskState).toHaveBeenCalled()
-
- vi.useRealTimers()
- })
- })
-
- describe('Common Features', () => {
- it('should always display close button', async () => {
- const wrapper = mountComponent()
-
- const closeButton = wrapper.find('[aria-label="Close"]')
- expect(closeButton.exists()).toBe(true)
- })
-
- it('should close dialog when close button is clicked', async () => {
- const wrapper = mountComponent()
-
- const closeButton = wrapper.find('[aria-label="Close"]')
- await closeButton.trigger('click')
-
- expect(mockDialogStore.closeDialog).toHaveBeenCalledWith({
- key: 'global-manager-progress-dialog'
- })
- })
- })
-
- describe('Toast Management', () => {
- it('should suppress reconnection toasts during restart', async () => {
- mockComfyManagerStore.isProcessingTasks = false
- mockComfyManagerStore.allTasksDone = true
- mockSettingStore.get.mockReturnValue(false) // Original setting
-
- const wrapper = mountComponent()
-
- // Click Apply Changes
- const applyButton = wrapper
- .findAll('button')
- .find((btn) => btn.text().includes('Apply Changes'))
- await applyButton?.trigger('click')
-
- // Check toast setting was disabled
- expect(mockSettingStore.set).toHaveBeenCalledWith(
- 'Comfy.Toast.DisableReconnectingToast',
- true
- )
- })
-
- it('should restore toast settings after restart completes', async () => {
- mockComfyManagerStore.isProcessingTasks = false
- mockComfyManagerStore.allTasksDone = true
- mockSettingStore.get.mockReturnValue(false) // Original setting
-
- const wrapper = mountComponent()
-
- // Click Apply Changes
- const applyButton = wrapper
- .findAll('button')
- .find((btn) => btn.text().includes('Apply Changes'))
- await applyButton?.trigger('click')
-
- // Wait for event listener to be set up
- await nextTick()
-
- // Trigger the reconnect handler directly
- if (reconnectHandler) {
- await reconnectHandler()
- }
-
- // Wait for settings restoration
- await nextTick()
-
- expect(mockSettingStore.set).toHaveBeenCalledWith(
- 'Comfy.Toast.DisableReconnectingToast',
- false // Restored to original
- )
- })
- })
-
- describe('Error Handling', () => {
- it('should restore state and close dialog on restart error', async () => {
- mockComfyManagerStore.isProcessingTasks = false
- mockComfyManagerStore.allTasksDone = true
-
- // Mock restart to throw error
- mockComfyManagerService.rebootComfyUI.mockRejectedValue(
- new Error('Restart failed')
- )
-
- const wrapper = mountComponent({ captureError: true })
-
- // Click Apply Changes
- const applyButton = wrapper
- .findAll('button')
- .find((btn) => btn.text().includes('Apply Changes'))
-
- expect(applyButton).toBeTruthy()
-
- // The component throws the error but Vue Test Utils catches it
- // We need to check if the error handling logic was executed
- await applyButton!.trigger('click').catch(() => {
- // Error is expected, ignore it
- })
-
- // Wait for error handling
- await nextTick()
-
- // Check dialog was closed on error
- expect(mockDialogStore.closeDialog).toHaveBeenCalled()
- // Check toast settings were restored
- expect(mockSettingStore.set).toHaveBeenCalledWith(
- 'Comfy.Toast.DisableReconnectingToast',
- false
- )
- // Check that the error handler was called
- expect(mockComfyManagerService.rebootComfyUI).toHaveBeenCalled()
- })
- })
-})
diff --git a/src/workbench/extensions/manager/components/ManagerProgressFooter.vue b/src/workbench/extensions/manager/components/ManagerProgressFooter.vue
deleted file mode 100644
index f8fe0052d..000000000
--- a/src/workbench/extensions/manager/components/ManagerProgressFooter.vue
+++ /dev/null
@@ -1,195 +0,0 @@
-
-
-
-
-
-
- {{ currentTaskName }}
-
-
- 🎉
- {{ currentTaskName }}
-
-
- ✅
- {{ $t('manager.restartToApplyChanges') }}
-
-
-
-
-
- {{ completedTasksCount }} {{ $t('g.progressCountOf') }}
- {{ totalTasksCount }}
-
-
-
- {{ $t('manager.applyChanges') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/workbench/extensions/manager/components/ManagerProgressHeader.vue b/src/workbench/extensions/manager/components/ManagerProgressHeader.vue
deleted file mode 100644
index 686d43436..000000000
--- a/src/workbench/extensions/manager/components/ManagerProgressHeader.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/workbench/extensions/manager/components/ManagerProgressToast.vue b/src/workbench/extensions/manager/components/ManagerProgressToast.vue
new file mode 100644
index 000000000..3e6305b7e
--- /dev/null
+++ b/src/workbench/extensions/manager/components/ManagerProgressToast.vue
@@ -0,0 +1,353 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentTaskName }}
+
+
+ 🎉
+ {{ currentTaskName }}
+
+
+ ✅
+ {{ t('manager.restartToApplyChanges') }}
+
+
+
+
+
+ {{ completedTasksCount }} {{ t('g.progressCountOf') }}
+ {{ totalTasksCount }}
+
+
+
+ {{ t('manager.applyChanges') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/workbench/extensions/manager/components/manager/ImportFailedNodeHeader.vue b/src/workbench/extensions/manager/components/manager/ImportFailedNodeHeader.vue
index 0588e8c5f..b21b315a3 100644
--- a/src/workbench/extensions/manager/components/manager/ImportFailedNodeHeader.vue
+++ b/src/workbench/extensions/manager/components/manager/ImportFailedNodeHeader.vue
@@ -1,7 +1,7 @@
-
+
{{ $t('importFailed.title') }}
diff --git a/src/workbench/extensions/manager/components/manager/ManagerDialogContent.vue b/src/workbench/extensions/manager/components/manager/ManagerDialogContent.vue
index d961c026a..c05f375e2 100644
--- a/src/workbench/extensions/manager/components/manager/ManagerDialogContent.vue
+++ b/src/workbench/extensions/manager/components/manager/ManagerDialogContent.vue
@@ -38,7 +38,9 @@
v-if="shouldShowManagerBanner"
class="relative mt-3 mb-4 flex items-center gap-6 rounded-lg bg-yellow-500/20 p-4"
>
-
+
{{ $t('manager.conflicts.warningBanner.title') }}
diff --git a/src/workbench/extensions/manager/components/manager/NodeConflictHeader.vue b/src/workbench/extensions/manager/components/manager/NodeConflictHeader.vue
index 24a1820d3..979b9830a 100644
--- a/src/workbench/extensions/manager/components/manager/NodeConflictHeader.vue
+++ b/src/workbench/extensions/manager/components/manager/NodeConflictHeader.vue
@@ -2,7 +2,7 @@
-
+
{{ $t('manager.conflicts.title') }}
diff --git a/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.test.ts b/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.test.ts
index 6c4976f91..792983c22 100644
--- a/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.test.ts
+++ b/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.test.ts
@@ -411,7 +411,7 @@ describe('PackVersionSelectorPopover', () => {
expect(mockCheckNodeCompatibility).toHaveBeenCalled()
// The warning icon should be shown for incompatible versions
- const warningIcons = wrapper.findAll('.pi-exclamation-triangle')
+ const warningIcons = wrapper.findAll('.icon-\\[lucide--triangle-alert\\]')
expect(warningIcons.length).toBeGreaterThan(0)
})
@@ -536,7 +536,7 @@ describe('PackVersionSelectorPopover', () => {
expect(mockCheckNodeCompatibility).toHaveBeenCalled()
// The warning icon should be shown for version incompatible packages
- const warningIcons = wrapper.findAll('.pi-exclamation-triangle')
+ const warningIcons = wrapper.findAll('.icon-\\[lucide--triangle-alert\\]')
expect(warningIcons.length).toBeGreaterThan(0)
})
@@ -662,7 +662,7 @@ describe('PackVersionSelectorPopover', () => {
await wrapper.vm.$nextTick()
// The warning icon should be shown for banned packages in the dropdown options
- const warningIcons = wrapper.findAll('.pi-exclamation-triangle')
+ const warningIcons = wrapper.findAll('.icon-\\[lucide--triangle-alert\\]')
expect(warningIcons.length).toBeGreaterThan(0)
})
@@ -705,7 +705,7 @@ describe('PackVersionSelectorPopover', () => {
expect(mockCheckNodeCompatibility).toHaveBeenCalled()
// The warning icon should be shown for security pending packages
- const warningIcons = wrapper.findAll('.pi-exclamation-triangle')
+ const warningIcons = wrapper.findAll('.icon-\\[lucide--triangle-alert\\]')
expect(warningIcons.length).toBeGreaterThan(0)
})
})
diff --git a/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.vue b/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.vue
index 46a45abc5..f4a9896c3 100644
--- a/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.vue
+++ b/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.vue
@@ -45,7 +45,7 @@
value: slotProps.option.conflictMessage,
showDelay: 300
}"
- class="pi pi-exclamation-triangle text-yellow-500"
+ class="icon-[lucide--triangle-alert] text-warning-background"
/>
diff --git a/src/workbench/extensions/manager/components/manager/button/PackEnableToggle.test.ts b/src/workbench/extensions/manager/components/manager/button/PackEnableToggle.test.ts
index 80e8417f2..54bbe5bb9 100644
--- a/src/workbench/extensions/manager/components/manager/button/PackEnableToggle.test.ts
+++ b/src/workbench/extensions/manager/components/manager/button/PackEnableToggle.test.ts
@@ -192,9 +192,9 @@ describe('PackEnableToggle', () => {
const wrapper = mountComponent()
// Check if warning icon exists
- const warningIcon = wrapper.find('.pi-exclamation-triangle')
+ const warningIcon = wrapper.find('.icon-\\[lucide--triangle-alert\\]')
expect(warningIcon.exists()).toBe(true)
- expect(warningIcon.classes()).toContain('text-yellow-500')
+ expect(warningIcon.classes()).toContain('text-warning-background')
})
it('should not show warning icon when package has no conflicts', () => {
@@ -204,7 +204,7 @@ describe('PackEnableToggle', () => {
const wrapper = mountComponent()
// Check if warning icon does not exist
- const warningIcon = wrapper.find('.pi-exclamation-triangle')
+ const warningIcon = wrapper.find('.icon-\\[lucide--triangle-alert\\]')
expect(warningIcon.exists()).toBe(false)
})
})
diff --git a/src/workbench/extensions/manager/components/manager/button/PackEnableToggle.vue b/src/workbench/extensions/manager/components/manager/button/PackEnableToggle.vue
index 2317810cf..a43ec76fc 100644
--- a/src/workbench/extensions/manager/components/manager/button/PackEnableToggle.vue
+++ b/src/workbench/extensions/manager/components/manager/button/PackEnableToggle.vue
@@ -9,7 +9,9 @@
class="flex h-6 w-6 cursor-pointer items-center justify-center"
@click="showConflictModal(true)"
>
-
+
({
- useDialogService: () => ({
- showManagerProgressDialog: vi.fn()
- })
-}))
-
// Mock the app API
vi.mock('@/scripts/app', () => ({
app: {
diff --git a/src/workbench/extensions/manager/composables/useManagerQueue.ts b/src/workbench/extensions/manager/composables/useManagerQueue.ts
index 3886b40bf..7ac796414 100644
--- a/src/workbench/extensions/manager/composables/useManagerQueue.ts
+++ b/src/workbench/extensions/manager/composables/useManagerQueue.ts
@@ -4,7 +4,6 @@ import type { Ref } from 'vue'
import { computed, ref } from 'vue'
import { app } from '@/scripts/app'
-import { useDialogService } from '@/services/dialogService'
import { normalizePackKeys } from '@/utils/packUtils'
import type { components } from '@/workbench/extensions/manager/types/generatedManagerTypes'
@@ -27,8 +26,6 @@ export const useManagerQueue = (
taskQueue: Ref,
installedPacks: Ref>
) => {
- const { showManagerProgressDialog } = useDialogService()
-
// Task queue state (read-only from server)
const maxHistoryItems = ref(64)
const isLoading = ref(false)
@@ -113,15 +110,6 @@ export const useManagerQueue = (
(event: CustomEvent) => {
if (event?.type === MANAGER_WS_TASK_DONE_NAME && event.detail?.state) {
updateTaskState(event.detail.state)
-
- // If no more tasks are running/pending, hide the progress dialog
- if (allTasksDone.value) {
- setTimeout(() => {
- if (allTasksDone.value) {
- showManagerProgressDialog()
- }
- }, 1000) // Small delay to let users see completion
- }
}
}
)
@@ -133,9 +121,6 @@ export const useManagerQueue = (
(event: CustomEvent) => {
if (event?.type === MANAGER_WS_TASK_STARTED_NAME && event.detail?.state) {
updateTaskState(event.detail.state)
-
- // Show progress dialog when a task starts
- showManagerProgressDialog()
}
}
)
diff --git a/src/workbench/extensions/manager/stores/comfyManagerStore.test.ts b/src/workbench/extensions/manager/stores/comfyManagerStore.test.ts
index d131a0fc9..d1754d59a 100644
--- a/src/workbench/extensions/manager/stores/comfyManagerStore.test.ts
+++ b/src/workbench/extensions/manager/stores/comfyManagerStore.test.ts
@@ -17,12 +17,6 @@ vi.mock('@/workbench/extensions/manager/services/comfyManagerService', () => ({
useComfyManagerService: vi.fn()
}))
-vi.mock('@/services/dialogService', () => ({
- useDialogService: () => ({
- showManagerProgressDialog: vi.fn()
- })
-}))
-
vi.mock('@/workbench/extensions/manager/composables/useManagerQueue', () => {
const enqueueTaskMock = vi.fn()
diff --git a/src/workbench/extensions/manager/stores/comfyManagerStore.ts b/src/workbench/extensions/manager/stores/comfyManagerStore.ts
index 06097cbe7..df6df758a 100644
--- a/src/workbench/extensions/manager/stores/comfyManagerStore.ts
+++ b/src/workbench/extensions/manager/stores/comfyManagerStore.ts
@@ -8,7 +8,7 @@ import { useCachedRequest } from '@/composables/useCachedRequest'
import { useServerLogs } from '@/composables/useServerLogs'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
-import { useDialogService } from '@/services/dialogService'
+
import { normalizePackKeys } from '@/utils/packUtils'
import { useManagerQueue } from '@/workbench/extensions/manager/composables/useManagerQueue'
import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService'
@@ -32,7 +32,6 @@ type UpdateAllPacksParams = components['schemas']['UpdateAllPacksParams']
export const useComfyManagerStore = defineStore('comfyManager', () => {
const { t } = useI18n()
const managerService = useComfyManagerService()
- const { showManagerProgressDialog } = useDialogService()
const installedPacks = ref({})
const enabledPacksIds = ref>(new Set())
@@ -204,8 +203,6 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
})
try {
- // Show progress dialog immediately when task is queued
- showManagerProgressDialog()
managerQueue.isProcessing.value = true
// Prepare logging hook
@@ -392,44 +389,3 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
enablePack
}
})
-
-/**
- * Store for state of the manager progress dialog content.
- * The dialog itself is managed by the dialog store. This store is used to
- * manage the visibility of the dialog's content, header, footer.
- */
-export const useManagerProgressDialogStore = defineStore(
- 'managerProgressDialog',
- () => {
- const isExpanded = ref(false)
- const activeTabIndex = ref(0)
-
- const setActiveTabIndex = (index: number) => {
- activeTabIndex.value = index
- }
-
- const getActiveTabIndex = () => {
- return activeTabIndex.value
- }
-
- const toggle = () => {
- isExpanded.value = !isExpanded.value
- }
-
- const collapse = () => {
- isExpanded.value = false
- }
-
- const expand = () => {
- isExpanded.value = true
- }
- return {
- isExpanded,
- toggle,
- collapse,
- expand,
- setActiveTabIndex,
- getActiveTabIndex
- }
- }
-)