Skip to content

Commit 887bece

Browse files
authored
fix(video): read the active start frame input when serializing video settings (#9787)
Sub-block conditions in the serializer and on the canvas card did not get the block's canonical mode overrides, so the Video Generator guessed the active start frame input from which member held a value. A frame left in the inactive input hid prompt-only settings (fal aspect ratio, Runway ratio) and dropped them from the request. A shared withCanonicalModes helper now feeds the same condition values to the serializer, canvas card, editor layout, autolayout and dropdown options.
1 parent 06bda4c commit 887bece

8 files changed

Lines changed: 108 additions & 86 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import type { SelectorKey } from '@/lib/selectors/manifest'
1010
import { SEARCH_DEBOUNCE_MS } from '@/lib/url-state'
1111
import { getDependsOnFields } from '@/lib/workflows/subblocks/dependencies'
12+
import { withCanonicalModes } from '@/lib/workflows/subblocks/visibility'
1213
import { staleSelectionOptions } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/stale-selections'
1314
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
1415
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
@@ -172,10 +173,7 @@ export const Dropdown = memo(function Dropdown({
172173
)
173174
const evaluatedOptions = useMemo(() => {
174175
if (typeof options !== 'function') return options ?? EMPTY_OPTIONS
175-
const values = blockValues ?? {}
176-
return options({
177-
values: canonicalModes ? { ...values, __canonicalModes: canonicalModes } : values,
178-
})
176+
return options({ values: withCanonicalModes(blockValues ?? {}, canonicalModes) })
179177
}, [options, blockValues, canonicalModes])
180178

181179
const reactiveOptions = useMemo(

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts‎

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { useCallback, useMemo } from 'react'
22
import {
33
buildCanonicalIndexForSurface,
4+
buildSubBlockValues,
45
evaluateSubBlockCondition,
56
isSubBlockFeatureEnabled,
67
isSubBlockHidden,
78
isSubBlockVisibleForMode,
89
isSubBlockVisibleForTriggerMode,
910
isToolInputOnlySubBlock,
11+
withCanonicalModes,
1012
} from '@/lib/workflows/subblocks/visibility'
1113
import type { BlockConfig, SubBlockConfig } from '@/blocks/types'
1214
import { usePermissionConfig } from '@/hooks/use-permission-config'
@@ -93,24 +95,10 @@ export function useEditorSubblockLayout(
9395
})
9496
}
9597

96-
// Filter visible blocks and those that meet their conditions
97-
const rawValues = Object.entries(stateToUse).reduce<Record<string, unknown>>(
98-
(acc, [key, entry]) => {
99-
acc[key] = entry?.value
100-
return acc
101-
},
102-
{}
103-
)
104-
10598
const canonicalIndex = buildCanonicalIndexForSurface(config.subBlocks || [], displayTriggerMode)
10699
const effectiveAdvanced = displayAdvancedMode
107100
const canonicalModeOverrides = blockData?.canonicalModes
108-
109-
// Expose canonical mode overrides to condition functions so they can
110-
// react to basic/advanced credential toggles (e.g. SERVICE_ACCOUNT_SUBBLOCKS).
111-
if (canonicalModeOverrides) {
112-
rawValues.__canonicalModes = canonicalModeOverrides
113-
}
101+
const rawValues = withCanonicalModes(buildSubBlockValues(stateToUse), canonicalModeOverrides)
114102

115103
const visibleSubBlocks = (config.subBlocks || []).filter((block) => {
116104
if (block.hidden) return false

‎apps/sim/lib/workflows/autolayout/utils.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
isSubBlockVisibleForMode,
3838
isToolInputOnlySubBlock,
3939
isTriggerModeSubBlock,
40+
withCanonicalModes,
4041
} from '@/lib/workflows/subblocks/visibility'
4142
import { getBlock } from '@/blocks'
4243
import type { SubBlockConfig } from '@/blocks/types'
@@ -188,15 +189,14 @@ function getVisiblePreviewSubBlocks(block: BlockState): {
188189
}
189190
}
190191

191-
const rawValues = buildSubBlockValues(block.subBlocks || {})
192192
const canonicalModeOverrides =
193193
typeof block.data?.canonicalModes === 'object' && block.data.canonicalModes !== null
194194
? (block.data.canonicalModes as CanonicalModeOverrides)
195195
: undefined
196-
197-
if (canonicalModeOverrides) {
198-
rawValues.__canonicalModes = canonicalModeOverrides
199-
}
196+
const rawValues = withCanonicalModes(
197+
buildSubBlockValues(block.subBlocks || {}),
198+
canonicalModeOverrides
199+
)
200200

201201
const effectiveAdvanced = Boolean(block.advancedMode)
202202
const effectiveTrigger = Boolean(block.triggerMode)

‎apps/sim/lib/workflows/blocks/canvas-card-fields.test.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,19 @@ describe('getCardSubBlocks', () => {
9595
const onCard = triggerCardIds(BLOCK_REGISTRY.webflow)
9696
expect(onCard).toEqual(expect.arrayContaining(['triggerCredentials', 'triggerSiteId']))
9797
})
98+
99+
it('reads the active start frame member when picking video settings', () => {
100+
// The stale upload is the inactive member, so the card shows the prompt-only settings.
101+
const videoGenerator = BLOCK_REGISTRY.video_generator_v3
102+
const onCard = getCardSubBlocks(videoGenerator, {
103+
advanced: true,
104+
values: {
105+
provider: 'falai',
106+
model: 'alibaba/happy-horse/text-to-video',
107+
visualReferenceUpload: { name: 'frame.png', key: 'k', size: 3, type: 'image/png' },
108+
},
109+
canonicalModeOverrides: { visualReference: 'advanced' },
110+
}).map((subBlock) => subBlock.id)
111+
expect(onCard).toContain('falAspectRatio')
112+
})
98113
})

‎apps/sim/lib/workflows/blocks/canvas-card-fields.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
isSubBlockVisibleForMode,
88
isToolInputOnlySubBlock,
99
isTriggerModeSubBlock,
10+
withCanonicalModes,
1011
} from '@/lib/workflows/subblocks/visibility'
1112
import type { BlockConfig, SubBlockConfig } from '@/blocks/types'
1213

@@ -88,6 +89,7 @@ export function getCardSubBlocks(
8889
} = options
8990

9091
const canonicalIndex = buildCanonicalIndexForSurface(config.subBlocks, triggerMode)
92+
const conditionValues = withCanonicalModes(values, canonicalModeOverrides)
9193
const isPureTriggerBlock = Boolean(config.triggers?.enabled && config.category === 'triggers')
9294

9395
return config.subBlocks.filter((subBlock) => {
@@ -114,7 +116,8 @@ export function getCardSubBlocks(
114116
return false
115117
}
116118

117-
if (subBlock.condition && !evaluateSubBlockCondition(subBlock.condition, values)) return false
119+
if (subBlock.condition && !evaluateSubBlockCondition(subBlock.condition, conditionValues))
120+
return false
118121

119122
/* The operation is the card's heading here, so it is not also a field. */
120123
if (titleOperationSubBlockId && subBlock.id === titleOperationSubBlockId) return false

‎apps/sim/lib/workflows/subblocks/visibility.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,18 @@ export function buildSubBlockValues(
9494
}, {})
9595
}
9696

97+
/**
98+
* The values a block's sub-block conditions read: its values plus, when set, its canonical mode
99+
* overrides under `__canonicalModes`, so a condition reads the active member of a basic/advanced
100+
* pair instead of guessing it from which member holds a value.
101+
*/
102+
export function withCanonicalModes(
103+
values: Record<string, unknown>,
104+
canonicalModes: CanonicalModeOverrides | undefined
105+
): Record<string, unknown> {
106+
return canonicalModes ? { ...values, __canonicalModes: canonicalModes } : values
107+
}
108+
97109
/**
98110
* Build canonical group indices for a block's subblocks.
99111
*/

‎apps/sim/serializer/field-analysis.test.ts‎

Lines changed: 64 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,27 @@ describe('collectBlockFieldIssues', () => {
138138
})
139139
})
140140

141+
const FRAME_FILE = { id: 'f', name: 'frame.png', url: '', size: 3, type: 'image/png', key: 'k' }
142+
const FAL_VALUES = { provider: 'falai', apiKey: 'test-key', prompt: 'A cinematic sunrise' }
143+
144+
/** Serializes a Video Generator block and maps its params to the video tool body. */
145+
function serializeVideoBlock(
146+
values: Record<string, unknown>,
147+
canonicalModes?: Record<string, string>
148+
) {
149+
svcConfig.value = VideoGeneratorV3Block
150+
const params = extractBlockParams(
151+
block({
152+
type: 'svc',
153+
data: canonicalModes ? { canonicalModes } : {},
154+
subBlocks: Object.fromEntries(
155+
Object.entries(values).map(([id, value]) => [id, { id, type: 'short-input', value }])
156+
),
157+
})
158+
)
159+
return videoToolBodySchema.parse(VideoGeneratorV3Block.tools.config?.params?.(params))
160+
}
161+
141162
describe('extractBlockParams', () => {
142163
it.each([
143164
{
@@ -191,19 +212,8 @@ describe('extractBlockParams', () => {
191212
},
192213
},
193214
])('preserves $name through serialization into the provider request', ({ settings, payload }) => {
194-
svcConfig.value = VideoGeneratorV3Block
195-
const values = { apiKey: 'test-key', prompt: 'A cinematic sunrise', ...settings }
196-
const params = extractBlockParams(
197-
block({
198-
type: 'svc',
199-
subBlocks: Object.fromEntries(
200-
Object.entries(values).map(([id, value]) => [id, { id, type: 'short-input', value }])
201-
),
202-
})
203-
)
204-
const mapped = VideoGeneratorV3Block.tools.config?.params?.(params)
205215
const request = buildDirectVideoRequest({
206-
...videoToolBodySchema.parse(mapped),
216+
...serializeVideoBlock({ apiKey: 'test-key', prompt: 'A cinematic sunrise', ...settings }),
207217
provider: settings.provider,
208218
})
209219
expect(request.body).toMatchObject(payload)
@@ -227,63 +237,58 @@ describe('extractBlockParams', () => {
227237
])(
228238
'sends the model default for a fal setting the endpoint rejects when $name',
229239
({ model, aspectRatio, frame, endpoint }) => {
230-
svcConfig.value = VideoGeneratorV3Block
231-
const frameFile = {
232-
id: 'f',
233-
name: 'frame.png',
234-
url: '',
235-
size: 3,
236-
type: 'image/png',
237-
key: 'k',
238-
}
239-
const values = {
240-
provider: 'falai',
241-
apiKey: 'test-key',
242-
model,
243-
prompt: 'A cinematic sunrise',
244-
falAspectRatio: aspectRatio,
245-
...(frame ? { visualReferenceInput: frameFile } : {}),
246-
}
247-
const params = extractBlockParams(
248-
block({
249-
type: 'svc',
250-
subBlocks: Object.fromEntries(
251-
Object.entries(values).map(([id, value]) => [id, { id, type: 'short-input', value }])
252-
),
253-
})
254-
)
255-
const mapped = VideoGeneratorV3Block.tools.config?.params?.(params)
256240
const request = buildFalVideoRequest({
257-
...videoToolBodySchema.parse(mapped),
241+
...serializeVideoBlock({
242+
...FAL_VALUES,
243+
model,
244+
falAspectRatio: aspectRatio,
245+
...(frame ? { visualReferenceInput: FRAME_FILE } : {}),
246+
}),
258247
provider: 'falai',
259248
})
260249
expect(request.endpoint).toBe(endpoint)
261250
expect(request.body).not.toHaveProperty('aspect_ratio')
262251
}
263252
)
264253

254+
it.each([
255+
{
256+
name: 'a frame left in the inactive upload',
257+
mode: 'advanced',
258+
frameField: 'visualReferenceUpload',
259+
},
260+
{
261+
name: 'a frame left in the inactive reference input',
262+
mode: 'basic',
263+
frameField: 'visualReferenceInput',
264+
},
265+
])('keeps a prompt-only fal setting with $name', ({ mode, frameField }) => {
266+
const request = buildFalVideoRequest({
267+
...serializeVideoBlock(
268+
{
269+
...FAL_VALUES,
270+
model: 'alibaba/happy-horse/text-to-video',
271+
falAspectRatio: '16:9',
272+
[frameField]: FRAME_FILE,
273+
},
274+
{ visualReference: mode }
275+
),
276+
provider: 'falai',
277+
})
278+
expect(request.endpoint).toBe('alibaba/happy-horse/text-to-video')
279+
expect(request.body).toMatchObject({ aspect_ratio: '16:9' })
280+
})
281+
265282
it('sends the model default for a Runway setting the start frame endpoint rejects', () => {
266-
svcConfig.value = VideoGeneratorV3Block
267-
const frameFile = { id: 'f', name: 'frame.png', url: '', size: 3, type: 'image/png', key: 'k' }
268-
const values = {
269-
provider: 'runway',
270-
apiKey: 'test-key',
271-
model: 'kling_4',
272-
prompt: 'A cinematic sunrise',
273-
aspectRatio: '1920:1080',
274-
visualReferenceInput: frameFile,
275-
}
276-
const params = extractBlockParams(
277-
block({
278-
type: 'svc',
279-
subBlocks: Object.fromEntries(
280-
Object.entries(values).map(([id, value]) => [id, { id, type: 'short-input', value }])
281-
),
282-
})
283-
)
284-
const mapped = VideoGeneratorV3Block.tools.config?.params?.(params)
285283
const request = buildDirectVideoRequest({
286-
...videoToolBodySchema.parse(mapped),
284+
...serializeVideoBlock({
285+
provider: 'runway',
286+
apiKey: 'test-key',
287+
model: 'kling_4',
288+
prompt: 'A cinematic sunrise',
289+
aspectRatio: '1920:1080',
290+
visualReferenceInput: FRAME_FILE,
291+
}),
287292
provider: 'runway',
288293
})
289294
expect(request.body).not.toHaveProperty('ratio')

‎apps/sim/serializer/index.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
isSubBlockHidden,
2121
isToolInputOnlySubBlock,
2222
resolveCanonicalMode,
23+
withCanonicalModes,
2324
} from '@/lib/workflows/subblocks/visibility'
2425
import { getBlock } from '@/blocks'
2526
import { isCustomBlockType, RESERVED_PARAMS } from '@/blocks/custom/build-config'
@@ -551,7 +552,7 @@ export function extractBlockParams(block: BlockState): Record<string, any> {
551552
// canonical-index-unscoped: `shouldSerializeSubBlock` drops the inactive surface's members, and
552553
// the collapse below reads `params` (the filtered map), never `allValues`.
553554
const canonicalIndex = buildCanonicalIndex(blockConfig.subBlocks)
554-
const allValues = buildSubBlockValues(block.subBlocks)
555+
const allValues = withCanonicalModes(buildSubBlockValues(block.subBlocks), canonicalModeOverrides)
555556

556557
Object.entries(block.subBlocks).forEach(([id, subBlock]) => {
557558
const matchingConfigs = blockConfig.subBlocks.filter((config) => config.id === id)
@@ -707,7 +708,7 @@ export function collectBlockFieldIssues(
707708
// trigger-mode block returns above before reaching here at all.
708709
const canonicalIndex = buildCanonicalIndex(blockConfig.subBlocks || [])
709710
const canonicalModeOverrides = block.data?.canonicalModes
710-
const allValues = buildSubBlockValues(block.subBlocks)
711+
const allValues = withCanonicalModes(buildSubBlockValues(block.subBlocks), canonicalModeOverrides)
711712

712713
// Get the tool configuration to check parameter visibility
713714
const toolAccess = blockConfig.tools?.access

0 commit comments

Comments
 (0)