-
+
diff --git a/src/composables/graph/useVueNodeLifecycle.ts b/src/composables/graph/useVueNodeLifecycle.ts
index ff89dfc85..cfcee1d44 100644
--- a/src/composables/graph/useVueNodeLifecycle.ts
+++ b/src/composables/graph/useVueNodeLifecycle.ts
@@ -116,7 +116,7 @@ function useVueNodeLifecycleIndividual() {
slotSyncManager.attemptStart(canvas as LGraphCanvas)
}
},
- { immediate: true }
+ { immediate: true, flush: 'sync' }
)
// Handle case where Vue nodes are enabled but graph starts empty
diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts
index 194e3c0a0..a2590101c 100644
--- a/src/composables/node/useNodePricing.ts
+++ b/src/composables/node/useNodePricing.ts
@@ -169,6 +169,74 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => {
: `$${minCost.toFixed(2)}-$${maxCost.toFixed(2)}/Run`
}
+// ---- constants ----
+const SORA_SIZES = {
+ BASIC: new Set(['720x1280', '1280x720']),
+ PRO: new Set(['1024x1792', '1792x1024'])
+}
+const ALL_SIZES = new Set([...SORA_SIZES.BASIC, ...SORA_SIZES.PRO])
+
+// ---- sora-2 pricing helpers ----
+function validateSora2Selection(
+ modelRaw: string,
+ duration: number,
+ sizeRaw: string
+): string | undefined {
+ const model = modelRaw?.toLowerCase() ?? ''
+ const size = sizeRaw?.toLowerCase() ?? ''
+
+ if (!duration || Number.isNaN(duration)) return 'Set duration (4s / 8s / 12s)'
+ if (!size) return 'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)'
+ if (!ALL_SIZES.has(size))
+ return 'Invalid size. Must be 720x1280, 1280x720, 1024x1792, or 1792x1024.'
+
+ if (model.includes('sora-2-pro')) return undefined
+
+ if (model.includes('sora-2') && !SORA_SIZES.BASIC.has(size))
+ return 'sora-2 supports only 720x1280 or 1280x720'
+
+ if (!model.includes('sora-2')) return 'Unsupported model'
+
+ return undefined
+}
+
+function perSecForSora2(modelRaw: string, sizeRaw: string): number {
+ const model = modelRaw?.toLowerCase() ?? ''
+ const size = sizeRaw?.toLowerCase() ?? ''
+
+ if (model.includes('sora-2-pro')) {
+ return SORA_SIZES.PRO.has(size) ? 0.5 : 0.3
+ }
+ if (model.includes('sora-2')) return 0.1
+
+ return SORA_SIZES.PRO.has(size) ? 0.5 : 0.1
+}
+
+function formatRunPrice(perSec: number, duration: number) {
+ return `$${(perSec * duration).toFixed(2)}/Run`
+}
+
+// ---- pricing calculator ----
+const sora2PricingCalculator: PricingFunction = (node: LGraphNode): string => {
+ const getWidgetValue = (name: string) =>
+ String(node.widgets?.find((w) => w.name === name)?.value ?? '')
+
+ const model = getWidgetValue('model')
+ const size = getWidgetValue('size')
+ const duration = Number(
+ node.widgets?.find((w) => ['duration', 'duration_s'].includes(w.name))
+ ?.value
+ )
+
+ if (!model || !size || !duration) return 'Set model, duration & size'
+
+ const validationError = validateSora2Selection(model, duration, size)
+ if (validationError) return validationError
+
+ const perSec = perSecForSora2(model, size)
+ return formatRunPrice(perSec, duration)
+}
+
/**
* Static pricing data for API nodes, now supporting both strings and functions
*/
@@ -195,6 +263,9 @@ const apiNodeCosts: Record =
FluxProKontextMaxNode: {
displayPrice: '$0.08/Run'
},
+ OpenAIVideoSora2: {
+ displayPrice: sora2PricingCalculator
+ },
IdeogramV1: {
displayPrice: (node: LGraphNode): string => {
const numImagesWidget = node.widgets?.find(
@@ -1658,6 +1729,7 @@ export const useNodePricing = () => {
MinimaxHailuoVideoNode: ['resolution', 'duration'],
OpenAIDalle3: ['size', 'quality'],
OpenAIDalle2: ['size', 'n'],
+ OpenAIVideoSora2: ['model', 'size', 'duration'],
OpenAIGPTImage1: ['quality', 'n'],
IdeogramV1: ['num_images', 'turbo'],
IdeogramV2: ['num_images', 'turbo'],
diff --git a/src/core/graph/subgraph/SubgraphNode.vue b/src/core/graph/subgraph/SubgraphNode.vue
index f163df53f..18e57409e 100644
--- a/src/core/graph/subgraph/SubgraphNode.vue
+++ b/src/core/graph/subgraph/SubgraphNode.vue
@@ -246,7 +246,7 @@ onBeforeUnmount(() => {
/>
@@ -302,7 +302,7 @@ onBeforeUnmount(() => {
diff --git a/src/platform/assets/components/AssetFilterBar.vue b/src/platform/assets/components/AssetFilterBar.vue
index 1d3fb0e7f..5ffb5d0af 100644
--- a/src/platform/assets/components/AssetFilterBar.vue
+++ b/src/platform/assets/components/AssetFilterBar.vue
@@ -32,7 +32,7 @@
@update:model-value="handleFilterChange"
>
-
+
diff --git a/src/platform/assets/components/AssetGrid.vue b/src/platform/assets/components/AssetGrid.vue
index 35122fd52..cbc44054b 100644
--- a/src/platform/assets/components/AssetGrid.vue
+++ b/src/platform/assets/components/AssetGrid.vue
@@ -27,7 +27,7 @@
)
"
>
-
+
{{ $t('assetBrowser.noAssetsFound') }}
@@ -39,7 +39,8 @@
v-if="loading"
class="col-span-full flex items-center justify-center py-16"
>
-
-
+
{{ commandLineArgs }}
diff --git a/src/platform/updates/common/releaseStore.ts b/src/platform/updates/common/releaseStore.ts
index 470c92272..d51d5d026 100644
--- a/src/platform/updates/common/releaseStore.ts
+++ b/src/platform/updates/common/releaseStore.ts
@@ -72,14 +72,10 @@ export const useReleaseStore = defineStore('release', () => {
) === 0
)
- const hasMediumOrHighAttention = computed(() =>
- recentReleases.value
- .slice(0, -1)
- .some(
- (release) =>
- release.attention === 'medium' || release.attention === 'high'
- )
- )
+ const hasMediumOrHighAttention = computed(() => {
+ const attention = recentRelease.value?.attention
+ return attention === 'medium' || attention === 'high'
+ })
// Show toast if needed
const shouldShowToast = computed(() => {
diff --git a/src/platform/updates/components/ReleaseNotificationToast.vue b/src/platform/updates/components/ReleaseNotificationToast.vue
index 1abcf472e..94f21a554 100644
--- a/src/platform/updates/components/ReleaseNotificationToast.vue
+++ b/src/platform/updates/components/ReleaseNotificationToast.vue
@@ -172,7 +172,7 @@ onMounted(async () => {
width: 448px;
padding: 16px 16px 8px;
background: #353535;
- box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
+ box-shadow: 0 4px 4px rgb(0 0 0 / 0.25);
border-radius: 12px;
outline: 1px solid #4e4e4e;
outline-offset: -1px;
@@ -193,7 +193,7 @@ onMounted(async () => {
width: 42px;
height: 42px;
padding: 10px;
- background: rgba(0, 122, 255, 0.2);
+ background: rgb(0 122 255 / 0.2);
border-radius: 8px;
display: flex;
justify-content: center;
diff --git a/src/platform/updates/components/WhatsNewPopup.vue b/src/platform/updates/components/WhatsNewPopup.vue
index 7349d89ef..c4a898faf 100644
--- a/src/platform/updates/components/WhatsNewPopup.vue
+++ b/src/platform/updates/components/WhatsNewPopup.vue
@@ -218,7 +218,7 @@ defineExpose({
width: 400px;
outline: 1px solid #4e4e4e;
outline-offset: -1px;
- box-shadow: 0px 8px 32px rgba(0, 0, 0, 0.3);
+ box-shadow: 0 8px 32px rgb(0 0 0 / 0.3);
position: relative;
}
@@ -293,12 +293,6 @@ defineExpose({
transform: translate(-50%, -50%) rotate(-45deg);
}
-/* Content Section */
-.popup-content {
- display: flex;
- flex-direction: column;
-}
-
.content-text {
color: white;
font-size: 14px;
diff --git a/src/renderer/core/layout/injectionKeys.ts b/src/renderer/core/layout/injectionKeys.ts
index 8e0e0e1d6..8970e55f8 100644
--- a/src/renderer/core/layout/injectionKeys.ts
+++ b/src/renderer/core/layout/injectionKeys.ts
@@ -21,7 +21,7 @@ import type { useTransformState } from '@/renderer/core/layout/transform/useTran
* const state = inject(TransformStateKey)!
* const screen = state.canvasToScreen({ x: 100, y: 50 })
*/
-interface TransformState
+export interface TransformState
extends Pick<
ReturnType
,
'screenToCanvas' | 'canvasToScreen' | 'camera' | 'isNodeInViewport'
diff --git a/src/renderer/extensions/minimap/MiniMap.vue b/src/renderer/extensions/minimap/MiniMap.vue
index 63b0c85d2..db38e697b 100644
--- a/src/renderer/extensions/minimap/MiniMap.vue
+++ b/src/renderer/extensions/minimap/MiniMap.vue
@@ -28,7 +28,7 @@
@click.stop="toggleOptionsPanel"
>
-
+
$emit('updateOption', 'Comfy.Minimap.NodeColors', value)
"
/>
-
+
@@ -27,7 +27,7 @@
(value) => $emit('updateOption', 'Comfy.Minimap.ShowLinks', value)
"
/>
-
+
@@ -41,7 +41,7 @@
(value) => $emit('updateOption', 'Comfy.Minimap.ShowGroups', value)
"
/>
-
+
@@ -56,7 +56,7 @@
$emit('updateOption', 'Comfy.Minimap.RenderBypassState', value)
"
/>
-
+
@@ -71,7 +71,7 @@
$emit('updateOption', 'Comfy.Minimap.RenderErrorState', value)
"
/>
-
+
diff --git a/src/renderer/extensions/vueNodes/VideoPreview.vue b/src/renderer/extensions/vueNodes/VideoPreview.vue
index 334ac55c6..5c95fdaa6 100644
--- a/src/renderer/extensions/vueNodes/VideoPreview.vue
+++ b/src/renderer/extensions/vueNodes/VideoPreview.vue
@@ -19,7 +19,7 @@
v-if="videoError"
class="w-full h-[352px] flex flex-col items-center justify-center text-white text-center bg-gray-800/50"
>
-
+
{{ $t('g.videoFailedToLoad') }}
{{ getVideoFilename(currentVideoUrl) }}
@@ -54,7 +54,7 @@
:aria-label="$t('g.downloadVideo')"
@click="handleDownload"
>
-
+
@@ -64,7 +64,7 @@
:aria-label="$t('g.removeVideo')"
@click="handleRemove"
>
-
+
diff --git a/src/renderer/extensions/vueNodes/components/ImagePreview.vue b/src/renderer/extensions/vueNodes/components/ImagePreview.vue
index 7bc05d437..a31c3633b 100644
--- a/src/renderer/extensions/vueNodes/components/ImagePreview.vue
+++ b/src/renderer/extensions/vueNodes/components/ImagePreview.vue
@@ -19,7 +19,7 @@
v-if="imageError"
class="w-full h-[352px] flex flex-col items-center justify-center text-white text-center bg-gray-800/50"
>
-
+
{{ $t('g.imageFailedToLoad') }}
{{ getImageFilename(currentImageUrl) }}
@@ -53,7 +53,7 @@
:aria-label="$t('g.editOrMaskImage')"
@click="handleEditMask"
>
-
+
@@ -63,7 +63,7 @@
:aria-label="$t('g.downloadImage')"
@click="handleDownload"
>
-
+
@@ -73,7 +73,7 @@
:aria-label="$t('g.removeImage')"
@click="handleRemove"
>
-
+
diff --git a/src/renderer/extensions/vueNodes/components/InputSlot.vue b/src/renderer/extensions/vueNodes/components/InputSlot.vue
index e74688181..c106f5e79 100644
--- a/src/renderer/extensions/vueNodes/components/InputSlot.vue
+++ b/src/renderer/extensions/vueNodes/components/InputSlot.vue
@@ -27,9 +27,7 @@
diff --git a/src/renderer/extensions/vueNodes/components/LGraphNodePreview.vue b/src/renderer/extensions/vueNodes/components/LGraphNodePreview.vue
index a6513f039..3619605f1 100644
--- a/src/renderer/extensions/vueNodes/components/LGraphNodePreview.vue
+++ b/src/renderer/extensions/vueNodes/components/LGraphNodePreview.vue
@@ -1,13 +1,11 @@
-
+
diff --git a/src/renderer/extensions/vueNodes/components/NodeHeader.test.ts b/src/renderer/extensions/vueNodes/components/NodeHeader.test.ts
index 472427e8f..0c755116b 100644
--- a/src/renderer/extensions/vueNodes/components/NodeHeader.test.ts
+++ b/src/renderer/extensions/vueNodes/components/NodeHeader.test.ts
@@ -101,9 +101,6 @@ const createMountConfig = () => {
updated: vi.fn(),
unmounted: vi.fn()
}
- },
- provide: {
- tooltipContainer: { value: document.createElement('div') }
}
}
}
@@ -184,11 +181,11 @@ describe('NodeHeader.vue', () => {
it('renders correct chevron icon based on collapsed prop', async () => {
const wrapper = mountHeader({ collapsed: false })
const expandedIcon = wrapper.get('i')
- expect(expandedIcon.classes()).toContain('pi-chevron-down')
+ expect(expandedIcon.classes()).not.toContain('-rotate-90')
await wrapper.setProps({ collapsed: true })
const collapsedIcon = wrapper.get('i')
- expect(collapsedIcon.classes()).toContain('pi-chevron-right')
+ expect(collapsedIcon.classes()).toContain('-rotate-90')
})
describe('Tooltips', () => {
diff --git a/src/renderer/extensions/vueNodes/components/NodeHeader.vue b/src/renderer/extensions/vueNodes/components/NodeHeader.vue
index b48a54b53..5f0cc03ab 100644
--- a/src/renderer/extensions/vueNodes/components/NodeHeader.vue
+++ b/src/renderer/extensions/vueNodes/components/NodeHeader.vue
@@ -4,65 +4,82 @@
diff --git a/src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuItem.vue b/src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuItem.vue
index 79cdfcb32..1f55d3b03 100644
--- a/src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuItem.vue
+++ b/src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuItem.vue
@@ -93,7 +93,9 @@ function handleVideoLoad(event: Event) {
v-if="selected"
class="rounded-full bg-blue-500 border-1 border-white size-4 absolute top-1 left-1"
>
-
+