Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import type { SelectorKey } from '@/lib/selectors/manifest'
import { SEARCH_DEBOUNCE_MS } from '@/lib/url-state'
import { getDependsOnFields } from '@/lib/workflows/subblocks/dependencies'
import { withCanonicalModes } from '@/lib/workflows/subblocks/visibility'
import { staleSelectionOptions } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/stale-selections'
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
Expand Down Expand Up @@ -172,10 +173,7 @@ export const Dropdown = memo(function Dropdown({
)
const evaluatedOptions = useMemo(() => {
if (typeof options !== 'function') return options ?? EMPTY_OPTIONS
const values = blockValues ?? {}
return options({
values: canonicalModes ? { ...values, __canonicalModes: canonicalModes } : values,
})
return options({ values: withCanonicalModes(blockValues ?? {}, canonicalModes) })
}, [options, blockValues, canonicalModes])

const reactiveOptions = useMemo(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { useCallback, useMemo } from 'react'
import {
buildCanonicalIndexForSurface,
buildSubBlockValues,
evaluateSubBlockCondition,
isSubBlockFeatureEnabled,
isSubBlockHidden,
isSubBlockVisibleForMode,
isSubBlockVisibleForTriggerMode,
isToolInputOnlySubBlock,
withCanonicalModes,
} from '@/lib/workflows/subblocks/visibility'
import type { BlockConfig, SubBlockConfig } from '@/blocks/types'
import { usePermissionConfig } from '@/hooks/use-permission-config'
Expand Down Expand Up @@ -93,24 +95,10 @@ export function useEditorSubblockLayout(
})
}

// Filter visible blocks and those that meet their conditions
const rawValues = Object.entries(stateToUse).reduce<Record<string, unknown>>(
(acc, [key, entry]) => {
acc[key] = entry?.value
return acc
},
{}
)

const canonicalIndex = buildCanonicalIndexForSurface(config.subBlocks || [], displayTriggerMode)
const effectiveAdvanced = displayAdvancedMode
const canonicalModeOverrides = blockData?.canonicalModes

// Expose canonical mode overrides to condition functions so they can
// react to basic/advanced credential toggles (e.g. SERVICE_ACCOUNT_SUBBLOCKS).
if (canonicalModeOverrides) {
rawValues.__canonicalModes = canonicalModeOverrides
}
const rawValues = withCanonicalModes(buildSubBlockValues(stateToUse), canonicalModeOverrides)

const visibleSubBlocks = (config.subBlocks || []).filter((block) => {
if (block.hidden) return false
Expand Down
10 changes: 5 additions & 5 deletions apps/sim/lib/workflows/autolayout/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
isSubBlockVisibleForMode,
isToolInputOnlySubBlock,
isTriggerModeSubBlock,
withCanonicalModes,
} from '@/lib/workflows/subblocks/visibility'
import { getBlock } from '@/blocks'
import type { SubBlockConfig } from '@/blocks/types'
Expand Down Expand Up @@ -188,15 +189,14 @@ function getVisiblePreviewSubBlocks(block: BlockState): {
}
}

const rawValues = buildSubBlockValues(block.subBlocks || {})
const canonicalModeOverrides =
typeof block.data?.canonicalModes === 'object' && block.data.canonicalModes !== null
? (block.data.canonicalModes as CanonicalModeOverrides)
: undefined

if (canonicalModeOverrides) {
rawValues.__canonicalModes = canonicalModeOverrides
}
const rawValues = withCanonicalModes(
buildSubBlockValues(block.subBlocks || {}),
canonicalModeOverrides
)

const effectiveAdvanced = Boolean(block.advancedMode)
const effectiveTrigger = Boolean(block.triggerMode)
Expand Down
15 changes: 15 additions & 0 deletions apps/sim/lib/workflows/blocks/canvas-card-fields.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,19 @@ describe('getCardSubBlocks', () => {
const onCard = triggerCardIds(BLOCK_REGISTRY.webflow)
expect(onCard).toEqual(expect.arrayContaining(['triggerCredentials', 'triggerSiteId']))
})

it('reads the active start frame member when picking video settings', () => {
// The stale upload is the inactive member, so the card shows the prompt-only settings.
const videoGenerator = BLOCK_REGISTRY.video_generator_v3
const onCard = getCardSubBlocks(videoGenerator, {
advanced: true,
values: {
provider: 'falai',
model: 'alibaba/happy-horse/text-to-video',
visualReferenceUpload: { name: 'frame.png', key: 'k', size: 3, type: 'image/png' },
},
canonicalModeOverrides: { visualReference: 'advanced' },
}).map((subBlock) => subBlock.id)
expect(onCard).toContain('falAspectRatio')
})
})
5 changes: 4 additions & 1 deletion apps/sim/lib/workflows/blocks/canvas-card-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
isSubBlockVisibleForMode,
isToolInputOnlySubBlock,
isTriggerModeSubBlock,
withCanonicalModes,
} from '@/lib/workflows/subblocks/visibility'
import type { BlockConfig, SubBlockConfig } from '@/blocks/types'

Expand Down Expand Up @@ -88,6 +89,7 @@ export function getCardSubBlocks(
} = options

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

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

if (subBlock.condition && !evaluateSubBlockCondition(subBlock.condition, values)) return false
if (subBlock.condition && !evaluateSubBlockCondition(subBlock.condition, conditionValues))
return false

/* The operation is the card's heading here, so it is not also a field. */
if (titleOperationSubBlockId && subBlock.id === titleOperationSubBlockId) return false
Expand Down
12 changes: 12 additions & 0 deletions apps/sim/lib/workflows/subblocks/visibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,18 @@ export function buildSubBlockValues(
}, {})
}

/**
* The values a block's sub-block conditions read: its values plus, when set, its canonical mode
* overrides under `__canonicalModes`, so a condition reads the active member of a basic/advanced
* pair instead of guessing it from which member holds a value.
*/
export function withCanonicalModes(
values: Record<string, unknown>,
canonicalModes: CanonicalModeOverrides | undefined
): Record<string, unknown> {
return canonicalModes ? { ...values, __canonicalModes: canonicalModes } : values
}

/**
* Build canonical group indices for a block's subblocks.
*/
Expand Down
123 changes: 64 additions & 59 deletions apps/sim/serializer/field-analysis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,27 @@ describe('collectBlockFieldIssues', () => {
})
})

const FRAME_FILE = { id: 'f', name: 'frame.png', url: '', size: 3, type: 'image/png', key: 'k' }
const FAL_VALUES = { provider: 'falai', apiKey: 'test-key', prompt: 'A cinematic sunrise' }

/** Serializes a Video Generator block and maps its params to the video tool body. */
function serializeVideoBlock(
values: Record<string, unknown>,
canonicalModes?: Record<string, string>
) {
svcConfig.value = VideoGeneratorV3Block
const params = extractBlockParams(
block({
type: 'svc',
data: canonicalModes ? { canonicalModes } : {},
subBlocks: Object.fromEntries(
Object.entries(values).map(([id, value]) => [id, { id, type: 'short-input', value }])
),
})
)
return videoToolBodySchema.parse(VideoGeneratorV3Block.tools.config?.params?.(params))
}

describe('extractBlockParams', () => {
it.each([
{
Expand Down Expand Up @@ -191,19 +212,8 @@ describe('extractBlockParams', () => {
},
},
])('preserves $name through serialization into the provider request', ({ settings, payload }) => {
svcConfig.value = VideoGeneratorV3Block
const values = { apiKey: 'test-key', prompt: 'A cinematic sunrise', ...settings }
const params = extractBlockParams(
block({
type: 'svc',
subBlocks: Object.fromEntries(
Object.entries(values).map(([id, value]) => [id, { id, type: 'short-input', value }])
),
})
)
const mapped = VideoGeneratorV3Block.tools.config?.params?.(params)
const request = buildDirectVideoRequest({
...videoToolBodySchema.parse(mapped),
...serializeVideoBlock({ apiKey: 'test-key', prompt: 'A cinematic sunrise', ...settings }),
provider: settings.provider,
})
expect(request.body).toMatchObject(payload)
Expand All @@ -227,63 +237,58 @@ describe('extractBlockParams', () => {
])(
'sends the model default for a fal setting the endpoint rejects when $name',
({ model, aspectRatio, frame, endpoint }) => {
svcConfig.value = VideoGeneratorV3Block
const frameFile = {
id: 'f',
name: 'frame.png',
url: '',
size: 3,
type: 'image/png',
key: 'k',
}
const values = {
provider: 'falai',
apiKey: 'test-key',
model,
prompt: 'A cinematic sunrise',
falAspectRatio: aspectRatio,
...(frame ? { visualReferenceInput: frameFile } : {}),
}
const params = extractBlockParams(
block({
type: 'svc',
subBlocks: Object.fromEntries(
Object.entries(values).map(([id, value]) => [id, { id, type: 'short-input', value }])
),
})
)
const mapped = VideoGeneratorV3Block.tools.config?.params?.(params)
const request = buildFalVideoRequest({
...videoToolBodySchema.parse(mapped),
...serializeVideoBlock({
...FAL_VALUES,
model,
falAspectRatio: aspectRatio,
...(frame ? { visualReferenceInput: FRAME_FILE } : {}),
}),
provider: 'falai',
})
expect(request.endpoint).toBe(endpoint)
expect(request.body).not.toHaveProperty('aspect_ratio')
}
)

it.each([
{
name: 'a frame left in the inactive upload',
mode: 'advanced',
frameField: 'visualReferenceUpload',
},
{
name: 'a frame left in the inactive reference input',
mode: 'basic',
frameField: 'visualReferenceInput',
},
])('keeps a prompt-only fal setting with $name', ({ mode, frameField }) => {
const request = buildFalVideoRequest({
...serializeVideoBlock(
{
...FAL_VALUES,
model: 'alibaba/happy-horse/text-to-video',
falAspectRatio: '16:9',
[frameField]: FRAME_FILE,
},
{ visualReference: mode }
),
provider: 'falai',
})
expect(request.endpoint).toBe('alibaba/happy-horse/text-to-video')
expect(request.body).toMatchObject({ aspect_ratio: '16:9' })
})

it('sends the model default for a Runway setting the start frame endpoint rejects', () => {
svcConfig.value = VideoGeneratorV3Block
const frameFile = { id: 'f', name: 'frame.png', url: '', size: 3, type: 'image/png', key: 'k' }
const values = {
provider: 'runway',
apiKey: 'test-key',
model: 'kling_4',
prompt: 'A cinematic sunrise',
aspectRatio: '1920:1080',
visualReferenceInput: frameFile,
}
const params = extractBlockParams(
block({
type: 'svc',
subBlocks: Object.fromEntries(
Object.entries(values).map(([id, value]) => [id, { id, type: 'short-input', value }])
),
})
)
const mapped = VideoGeneratorV3Block.tools.config?.params?.(params)
const request = buildDirectVideoRequest({
...videoToolBodySchema.parse(mapped),
...serializeVideoBlock({
provider: 'runway',
apiKey: 'test-key',
model: 'kling_4',
prompt: 'A cinematic sunrise',
aspectRatio: '1920:1080',
visualReferenceInput: FRAME_FILE,
}),
provider: 'runway',
})
expect(request.body).not.toHaveProperty('ratio')
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/serializer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
isSubBlockHidden,
isToolInputOnlySubBlock,
resolveCanonicalMode,
withCanonicalModes,
} from '@/lib/workflows/subblocks/visibility'
import { getBlock } from '@/blocks'
import { isCustomBlockType, RESERVED_PARAMS } from '@/blocks/custom/build-config'
Expand Down Expand Up @@ -551,7 +552,7 @@ export function extractBlockParams(block: BlockState): Record<string, any> {
// canonical-index-unscoped: `shouldSerializeSubBlock` drops the inactive surface's members, and
// the collapse below reads `params` (the filtered map), never `allValues`.
const canonicalIndex = buildCanonicalIndex(blockConfig.subBlocks)
const allValues = buildSubBlockValues(block.subBlocks)
const allValues = withCanonicalModes(buildSubBlockValues(block.subBlocks), canonicalModeOverrides)

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

// Get the tool configuration to check parameter visibility
const toolAccess = blockConfig.tools?.access
Expand Down
Loading