improvement(starter): added tag dropdown for input fields, fixed response block, remove logs and workflowConnections from api response(#716)

* added start block input fields to tag dropdown

* remove logs and workflowConnections from metadata for API triggered executions

* added field name validation for start block input to prevent JSON/API errors and user error

* fix response stringifcation, reuse input format from starter block for response format, add tag dropdown & connection block handling for response format

* hepler func for filteredResult

* fix response format w builder

* fix stringification of response handler

* expand fields by default

* cleanup
This commit is contained in:
Waleed Latif
2025-07-17 12:59:32 -07:00
committed by GitHub
parent fe5402a6d7
commit 88668fed84
13 changed files with 497 additions and 994 deletions

File diff suppressed because one or more lines are too long

View File

@@ -11,30 +11,17 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
icon={true}
iconSvg={`<svg className="block-icon"
viewBox='-5 0 41 33'
fill='none'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 24 24'
fill='none'
>
<circle cx='16' cy='16' r='14' fill='url(#paint0_linear_87_7225)' />
<circle cx='12' cy='12' r='10' fill='#0088CC' />
<path
d='M22.9866 10.2088C23.1112 9.40332 22.3454 8.76755 21.6292 9.082L7.36482 15.3448C6.85123 15.5703 6.8888 16.3483 7.42147 16.5179L10.3631 17.4547C10.9246 17.6335 11.5325 17.541 12.0228 17.2023L18.655 12.6203C18.855 12.4821 19.073 12.7665 18.9021 12.9426L14.1281 17.8646C13.665 18.3421 13.7569 19.1512 14.314 19.5005L19.659 22.8523C20.2585 23.2282 21.0297 22.8506 21.1418 22.1261L22.9866 10.2088Z'
d='M16.7 8.4c.1-.6-.4-1.1-1-.8l-9.8 4.3c-.4.2-.4.8.1.9l2.1.7c.4.1.8.1 1.1-.2l4.5-3.1c.1-.1.3.1.2.2l-3.2 3.5c-.3.3-.2.8.2 1l3.6 2.3c.4.2.9-.1 1-.5l1.2-7.8Z'
fill='white'
/>
<defs>
<linearGradient
id='paint0_linear_87_7225'
x1='16'
y1='2'
x2='16'
y2='30'
gradientUnits='userSpaceOnUse'
>
<stop stopColor='#37BBFE' />
<stop offset='1' stopColor='#007DBB' />
</linearGradient>
</defs>
</svg>`}
/>

View File

@@ -33,6 +33,20 @@ const EnvVarsSchema = z.record(z.string())
// Use a combination of workflow ID and request ID to allow concurrent executions with different inputs
const runningExecutions = new Set<string>()
// Utility function to filter out logs and workflowConnections from API response
function createFilteredResult(result: any) {
return {
...result,
logs: undefined,
metadata: result.metadata
? {
...result.metadata,
workflowConnections: undefined,
}
: undefined,
}
}
// Custom error class for usage limit exceeded
class UsageLimitError extends Error {
statusCode: number
@@ -358,7 +372,10 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
return createHttpResponseFromBlock(result)
}
return createSuccessResponse(result)
// Filter out logs and workflowConnections from the API response
const filteredResult = createFilteredResult(result)
return createSuccessResponse(filteredResult)
} catch (error: any) {
logger.error(`[${requestId}] Error executing workflow: ${id}`, error)
@@ -418,7 +435,10 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
return createHttpResponseFromBlock(result)
}
return createSuccessResponse(result)
// Filter out logs and workflowConnections from the API response
const filteredResult = createFilteredResult(result)
return createSuccessResponse(filteredResult)
} catch (error: any) {
logger.error(`[${requestId}] Error executing workflow: ${id}`, error)

View File

@@ -6,6 +6,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { ResponseBlockHandler } from '@/executor/handlers/response/response-handler'
import { useSubBlockValue } from '../hooks/use-sub-block-value'
interface DropdownProps {
@@ -34,6 +35,10 @@ export function Dropdown({
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlockId)
const [storeInitialized, setStoreInitialized] = useState(false)
// For response dataMode conversion - get builderData and data sub-blocks
const [builderData] = useSubBlockValue<any[]>(blockId, 'builderData')
const [, setData] = useSubBlockValue<string>(blockId, 'data')
// Use preview value when in preview mode, otherwise use store value or prop value
const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue
@@ -108,6 +113,20 @@ export function Dropdown({
onValueChange={(newValue) => {
// Only update store when not in preview mode and not disabled
if (!isPreview && !disabled) {
// Handle conversion when switching from Builder to Editor mode in response blocks
if (
subBlockId === 'dataMode' &&
storeValue === 'structured' &&
newValue === 'json' &&
builderData &&
Array.isArray(builderData) &&
builderData.length > 0
) {
// Convert builderData to JSON string for editor mode
const jsonString = ResponseBlockHandler.convertBuilderDataToJsonString(builderData)
setData(jsonString)
}
setStoreValue(newValue)
}
}}

View File

@@ -1,236 +0,0 @@
import { ChevronDown, ChevronRight, Plus, Trash } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Input } from '@/components/ui/input'
import { cn } from '@/lib/utils'
import type { JSONProperty } from '../response-format'
import { ValueInput } from './value-input'
const TYPE_ICONS = {
string: 'Aa',
number: '123',
boolean: 'T/F',
object: '{}',
array: '[]',
}
const TYPE_COLORS = {
string: 'text-green-600 dark:text-green-400',
number: 'text-blue-600 dark:text-blue-400',
boolean: 'text-purple-600 dark:text-purple-400',
object: 'text-orange-600 dark:text-orange-400',
array: 'text-pink-600 dark:text-pink-400',
}
interface PropertyRendererProps {
property: JSONProperty
blockId: string
isPreview: boolean
onUpdateProperty: (id: string, updates: Partial<JSONProperty>) => void
onAddProperty: (parentId?: string) => void
onRemoveProperty: (id: string) => void
onAddArrayItem: (arrayPropId: string) => void
onRemoveArrayItem: (arrayPropId: string, index: number) => void
onUpdateArrayItem: (arrayPropId: string, index: number, newValue: any) => void
depth?: number
}
export function PropertyRenderer({
property,
blockId,
isPreview,
onUpdateProperty,
onAddProperty,
onRemoveProperty,
onAddArrayItem,
onRemoveArrayItem,
onUpdateArrayItem,
depth = 0,
}: PropertyRendererProps) {
const isContainer = property.type === 'object'
const indent = depth * 12
// Check if this object is using a variable reference
const isObjectVariable =
property.type === 'object' &&
typeof property.value === 'string' &&
property.value.trim().startsWith('<') &&
property.value.trim().includes('>')
return (
<div className='space-y-1' style={{ marginLeft: `${indent}px` }}>
<div className='rounded border bg-card/50 p-2'>
<div className='flex items-center gap-2'>
{isContainer && !isObjectVariable && (
<Button
variant='ghost'
size='icon'
onClick={() => onUpdateProperty(property.id, { collapsed: !property.collapsed })}
className='h-4 w-4 shrink-0'
disabled={isPreview}
>
{property.collapsed ? (
<ChevronRight className='h-3 w-3' />
) : (
<ChevronDown className='h-3 w-3' />
)}
</Button>
)}
<Badge
variant='outline'
className={cn('shrink-0 px-1 py-0 font-mono text-xs', TYPE_COLORS[property.type])}
>
{TYPE_ICONS[property.type]}
</Badge>
<Input
value={property.key}
onChange={(e) => onUpdateProperty(property.id, { key: e.target.value })}
placeholder='key'
disabled={isPreview}
className='h-6 min-w-0 flex-1 text-xs'
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='outline'
size='sm'
className='h-6 shrink-0 px-2 text-xs'
disabled={isPreview}
>
{property.type}
<ChevronDown className='ml-1 h-3 w-3' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{Object.entries(TYPE_ICONS).map(([type, icon]) => (
<DropdownMenuItem
key={type}
onClick={() => onUpdateProperty(property.id, { type: type as any })}
className='text-xs'
>
<span className='mr-2 font-mono text-xs'>{icon}</span>
{type}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<div className='flex shrink-0 items-center gap-1'>
{isContainer && !isObjectVariable && (
<Button
variant='ghost'
size='icon'
onClick={() => onAddProperty(property.id)}
disabled={isPreview}
className='h-6 w-6'
title='Add property'
>
<Plus className='h-3 w-3' />
</Button>
)}
<Button
variant='ghost'
size='icon'
onClick={() => onRemoveProperty(property.id)}
disabled={isPreview}
className='h-6 w-6 text-muted-foreground hover:text-destructive'
>
<Trash className='h-3 w-3' />
</Button>
</div>
</div>
{/* Show value input for non-container types OR container types using variables */}
{(!isContainer || isObjectVariable) && (
<div className='mt-2'>
<ValueInput
property={property}
blockId={blockId}
isPreview={isPreview}
onUpdateProperty={onUpdateProperty}
onAddArrayItem={onAddArrayItem}
onRemoveArrayItem={onRemoveArrayItem}
onUpdateArrayItem={onUpdateArrayItem}
/>
</div>
)}
{/* Show object variable input for object types */}
{isContainer && !isObjectVariable && (
<div className='mt-2'>
<ValueInput
property={{
...property,
id: `${property.id}-object-variable`,
type: 'string',
value: typeof property.value === 'string' ? property.value : '',
}}
blockId={blockId}
isPreview={isPreview}
onUpdateProperty={(id: string, updates: Partial<JSONProperty>) =>
onUpdateProperty(property.id, updates)
}
onAddArrayItem={onAddArrayItem}
onRemoveArrayItem={onRemoveArrayItem}
onUpdateArrayItem={onUpdateArrayItem}
placeholder='Use <variable.object> or define properties below'
onObjectVariableChange={(newValue: string) => {
if (newValue.startsWith('<')) {
onUpdateProperty(property.id, { value: newValue })
} else if (newValue === '') {
onUpdateProperty(property.id, { value: [] })
}
}}
/>
</div>
)}
</div>
{isContainer && !property.collapsed && !isObjectVariable && (
<div className='ml-1 space-y-1 border-muted/30 border-l-2 pl-2'>
{Array.isArray(property.value) && property.value.length > 0 ? (
property.value.map((childProp: JSONProperty) => (
<PropertyRenderer
key={childProp.id}
property={childProp}
blockId={blockId}
isPreview={isPreview}
onUpdateProperty={onUpdateProperty}
onAddProperty={onAddProperty}
onRemoveProperty={onRemoveProperty}
onAddArrayItem={onAddArrayItem}
onRemoveArrayItem={onRemoveArrayItem}
onUpdateArrayItem={onUpdateArrayItem}
depth={depth + 1}
/>
))
) : (
<div className='rounded border-2 border-muted/50 border-dashed p-2 text-center'>
<p className='text-muted-foreground text-xs'>No properties</p>
<Button
variant='ghost'
size='sm'
onClick={() => onAddProperty(property.id)}
disabled={isPreview}
className='mt-1 h-6 text-xs'
>
<Plus className='mr-1 h-3 w-3' />
Add Property
</Button>
</div>
)}
</div>
)}
</div>
)
}

View File

@@ -1,300 +0,0 @@
import { useRef, useState } from 'react'
import { Plus, Trash } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { checkEnvVarTrigger, EnvVarDropdown } from '@/components/ui/env-var-dropdown'
import { Input } from '@/components/ui/input'
import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
import { createLogger } from '@/lib/logs/console-logger'
import type { JSONProperty } from '../response-format'
const logger = createLogger('ValueInput')
interface ValueInputProps {
property: JSONProperty
blockId: string
isPreview: boolean
onUpdateProperty: (id: string, updates: Partial<JSONProperty>) => void
onAddArrayItem: (arrayPropId: string) => void
onRemoveArrayItem: (arrayPropId: string, index: number) => void
onUpdateArrayItem: (arrayPropId: string, index: number, newValue: any) => void
placeholder?: string
onObjectVariableChange?: (newValue: string) => void
}
export function ValueInput({
property,
blockId,
isPreview,
onUpdateProperty,
onAddArrayItem,
onRemoveArrayItem,
onUpdateArrayItem,
placeholder,
onObjectVariableChange,
}: ValueInputProps) {
const [showEnvVars, setShowEnvVars] = useState(false)
const [showTags, setShowTags] = useState(false)
const [searchTerm, setSearchTerm] = useState('')
const [cursorPosition, setCursorPosition] = useState(0)
const [activeSourceBlockId, setActiveSourceBlockId] = useState<string | null>(null)
const inputRefs = useRef<{ [key: string]: HTMLInputElement | null }>({})
const findPropertyById = (props: JSONProperty[], id: string): JSONProperty | null => {
for (const prop of props) {
if (prop.id === id) return prop
if (prop.type === 'object' && Array.isArray(prop.value)) {
const found = findPropertyById(prop.value, id)
if (found) return found
}
}
return null
}
const handleDragOver = (e: React.DragEvent<HTMLInputElement>) => {
e.preventDefault()
}
const handleDrop = (e: React.DragEvent<HTMLInputElement>, propId: string) => {
if (isPreview) return
e.preventDefault()
try {
const data = JSON.parse(e.dataTransfer.getData('application/json'))
if (data.type !== 'connectionBlock') return
const input = inputRefs.current[propId]
const dropPosition = input?.selectionStart ?? 0
const currentValue = property.value?.toString() ?? ''
const newValue = `${currentValue.slice(0, dropPosition)}<${currentValue.slice(dropPosition)}`
input?.focus()
Promise.resolve().then(() => {
onUpdateProperty(property.id, { value: newValue })
setCursorPosition(dropPosition + 1)
setShowTags(true)
if (data.connectionData?.sourceBlockId) {
setActiveSourceBlockId(data.connectionData.sourceBlockId)
}
setTimeout(() => {
if (input) {
input.selectionStart = dropPosition + 1
input.selectionEnd = dropPosition + 1
}
}, 0)
})
} catch (error) {
logger.error('Failed to parse drop data:', { error })
}
}
const getPlaceholder = () => {
if (placeholder) return placeholder
switch (property.type) {
case 'number':
return '42 or <variable.count>'
case 'boolean':
return 'true/false or <variable.isEnabled>'
case 'array':
return '["item1", "item2"] or <variable.items>'
case 'object':
return '{...} or <variable.object>'
default:
return 'Enter text or <variable.name>'
}
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value
const cursorPos = e.target.selectionStart || 0
if (onObjectVariableChange) {
onObjectVariableChange(newValue.trim())
} else {
onUpdateProperty(property.id, { value: newValue })
}
if (!isPreview) {
const tagTrigger = checkTagTrigger(newValue, cursorPos)
const envVarTrigger = checkEnvVarTrigger(newValue, cursorPos)
setShowTags(tagTrigger.show)
setShowEnvVars(envVarTrigger.show)
setSearchTerm(envVarTrigger.searchTerm || '')
setCursorPosition(cursorPos)
}
}
const handleTagSelect = (newValue: string) => {
if (onObjectVariableChange) {
onObjectVariableChange(newValue)
} else {
onUpdateProperty(property.id, { value: newValue })
}
setShowTags(false)
}
const handleEnvVarSelect = (newValue: string) => {
if (onObjectVariableChange) {
onObjectVariableChange(newValue)
} else {
onUpdateProperty(property.id, { value: newValue })
}
setShowEnvVars(false)
}
const isArrayVariable =
property.type === 'array' &&
typeof property.value === 'string' &&
property.value.trim().startsWith('<') &&
property.value.trim().includes('>')
// Handle array type with individual items
if (property.type === 'array' && !isArrayVariable && Array.isArray(property.value)) {
return (
<div className='space-y-1'>
<div className='relative'>
<Input
ref={(el) => {
inputRefs.current[`${property.id}-array-variable`] = el
}}
value={typeof property.value === 'string' ? property.value : ''}
onChange={(e) => {
const newValue = e.target.value.trim()
if (newValue.startsWith('<') || newValue.startsWith('[')) {
onUpdateProperty(property.id, { value: newValue })
} else if (newValue === '') {
onUpdateProperty(property.id, { value: [] })
}
const cursorPos = e.target.selectionStart || 0
if (!isPreview) {
const tagTrigger = checkTagTrigger(newValue, cursorPos)
const envVarTrigger = checkEnvVarTrigger(newValue, cursorPos)
setShowTags(tagTrigger.show)
setShowEnvVars(envVarTrigger.show)
setSearchTerm(envVarTrigger.searchTerm || '')
setCursorPosition(cursorPos)
}
}}
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, `${property.id}-array-variable`)}
placeholder='Use <variable.items> or define items below'
disabled={isPreview}
className='h-7 text-xs'
/>
{!isPreview && showTags && (
<TagDropdown
visible={showTags}
onSelect={handleTagSelect}
blockId={blockId}
activeSourceBlockId={activeSourceBlockId}
inputValue={typeof property.value === 'string' ? property.value : ''}
cursorPosition={cursorPosition}
onClose={() => setShowTags(false)}
/>
)}
{!isPreview && showEnvVars && (
<EnvVarDropdown
visible={showEnvVars}
onSelect={handleEnvVarSelect}
searchTerm={searchTerm}
inputValue={typeof property.value === 'string' ? property.value : ''}
cursorPosition={cursorPosition}
onClose={() => setShowEnvVars(false)}
/>
)}
</div>
{property.value.length > 0 && (
<>
<div className='mt-2 mb-1 font-medium text-muted-foreground text-xs'>Array Items:</div>
{property.value.map((item: any, index: number) => (
<div key={index} className='flex items-center gap-1'>
<div className='relative flex-1'>
<Input
ref={(el) => {
inputRefs.current[`${property.id}-array-${index}`] = el
}}
value={item || ''}
onChange={(e) => onUpdateArrayItem(property.id, index, e.target.value)}
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, `${property.id}-array-${index}`)}
placeholder={`Item ${index + 1}`}
disabled={isPreview}
className='h-7 text-xs'
/>
</div>
<Button
variant='ghost'
size='icon'
onClick={() => onRemoveArrayItem(property.id, index)}
disabled={isPreview}
className='h-7 w-7'
>
<Trash className='h-3 w-3' />
</Button>
</div>
))}
</>
)}
<Button
variant='outline'
size='sm'
onClick={() => onAddArrayItem(property.id)}
disabled={isPreview}
className='h-7 w-full text-xs'
>
<Plus className='mr-1 h-3 w-3' />
Add Item
</Button>
</div>
)
}
// Handle regular input for all other types
return (
<div className='relative'>
<Input
ref={(el) => {
inputRefs.current[property.id] = el
}}
value={property.value || ''}
onChange={handleInputChange}
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, property.id)}
placeholder={getPlaceholder()}
disabled={isPreview}
className='h-7 text-xs'
/>
{!isPreview && showTags && (
<TagDropdown
visible={showTags}
onSelect={handleTagSelect}
blockId={blockId}
activeSourceBlockId={activeSourceBlockId}
inputValue={property.value || ''}
cursorPosition={cursorPosition}
onClose={() => setShowTags(false)}
/>
)}
{!isPreview && showEnvVars && (
<EnvVarDropdown
visible={showEnvVars}
onSelect={handleEnvVarSelect}
searchTerm={searchTerm}
inputValue={property.value || ''}
cursorPosition={cursorPosition}
onClose={() => setShowEnvVars(false)}
/>
)}
</div>
)
}

View File

@@ -1,15 +1,10 @@
import { useState } from 'react'
import { Code, Eye, Plus } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
import { useSubBlockValue } from '../../hooks/use-sub-block-value'
import { PropertyRenderer } from './components/property-renderer'
import { ResponseFormat as SharedResponseFormat } from '../starter/input-format'
export interface JSONProperty {
id: string
key: string
type: 'string' | 'number' | 'boolean' | 'object' | 'array'
value: any
value?: any
collapsed?: boolean
}
@@ -17,31 +12,10 @@ interface ResponseFormatProps {
blockId: string
subBlockId: string
isPreview?: boolean
previewValue?: JSONProperty[] | null
}
const TYPE_ICONS = {
string: 'Aa',
number: '123',
boolean: 'T/F',
object: '{}',
array: '[]',
}
const TYPE_COLORS = {
string: 'text-green-600 dark:text-green-400',
number: 'text-blue-600 dark:text-blue-400',
boolean: 'text-purple-600 dark:text-purple-400',
object: 'text-orange-600 dark:text-orange-400',
array: 'text-pink-600 dark:text-pink-400',
}
const DEFAULT_PROPERTY: JSONProperty = {
id: crypto.randomUUID(),
key: 'message',
type: 'string',
value: '',
collapsed: false,
previewValue?: any
disabled?: boolean
isConnecting?: boolean
config?: any
}
export function ResponseFormat({
@@ -49,288 +23,19 @@ export function ResponseFormat({
subBlockId,
isPreview = false,
previewValue,
disabled = false,
isConnecting = false,
config,
}: ResponseFormatProps) {
// useSubBlockValue now includes debouncing by default
const [storeValue, setStoreValue] = useSubBlockValue<JSONProperty[]>(blockId, subBlockId, false, {
debounceMs: 200, // Slightly longer debounce for complex structures
})
const [showPreview, setShowPreview] = useState(false)
const value = isPreview ? previewValue : storeValue
const properties: JSONProperty[] = value || [DEFAULT_PROPERTY]
const isVariableReference = (value: any): boolean => {
return typeof value === 'string' && value.trim().startsWith('<') && value.trim().includes('>')
}
const findPropertyById = (props: JSONProperty[], id: string): JSONProperty | null => {
for (const prop of props) {
if (prop.id === id) return prop
if (prop.type === 'object' && Array.isArray(prop.value)) {
const found = findPropertyById(prop.value, id)
if (found) return found
}
}
return null
}
const generateJSON = (props: JSONProperty[]): any => {
const result: any = {}
for (const prop of props) {
if (!prop.key.trim()) return
let value = prop.value
if (prop.type === 'object') {
if (Array.isArray(prop.value)) {
value = generateJSON(prop.value)
} else if (typeof prop.value === 'string' && isVariableReference(prop.value)) {
value = prop.value
} else {
value = {} // Default empty object for non-array, non-variable values
}
} else if (prop.type === 'array' && Array.isArray(prop.value)) {
value = prop.value.map((item: any) => {
if (typeof item === 'object' && item.type) {
if (item.type === 'object' && Array.isArray(item.value)) {
return generateJSON(item.value)
}
if (item.type === 'array' && Array.isArray(item.value)) {
return item.value.map((subItem: any) =>
typeof subItem === 'object' && subItem.type ? subItem.value : subItem
)
}
return item.value
}
return item
})
} else if (prop.type === 'number' && !isVariableReference(value)) {
value = Number.isNaN(Number(value)) ? value : Number(value)
} else if (prop.type === 'boolean' && !isVariableReference(value)) {
const strValue = String(value).toLowerCase().trim()
value = strValue === 'true' || strValue === '1' || strValue === 'yes' || strValue === 'on'
}
result[prop.key] = value
}
return result
}
const updateProperties = (newProperties: JSONProperty[]) => {
if (isPreview) return
setStoreValue(newProperties)
}
const updateProperty = (id: string, updates: Partial<JSONProperty>) => {
const updateRecursive = (props: JSONProperty[]): JSONProperty[] => {
return props.map((prop) => {
if (prop.id === id) {
const updated = { ...prop, ...updates }
if (updates.type && updates.type !== prop.type) {
if (updates.type === 'object') {
updated.value = []
} else if (updates.type === 'array') {
updated.value = []
} else if (updates.type === 'boolean') {
updated.value = 'false'
} else if (updates.type === 'number') {
updated.value = '0'
} else {
updated.value = ''
}
}
return updated
}
if (prop.type === 'object' && Array.isArray(prop.value)) {
return { ...prop, value: updateRecursive(prop.value) }
}
return prop
})
}
updateProperties(updateRecursive(properties))
}
const addProperty = (parentId?: string) => {
const newProp: JSONProperty = {
id: crypto.randomUUID(),
key: '',
type: 'string',
value: '',
collapsed: false,
}
if (parentId) {
const addToParent = (props: JSONProperty[]): JSONProperty[] => {
return props.map((prop) => {
if (prop.id === parentId && prop.type === 'object') {
return { ...prop, value: [...(prop.value || []), newProp] }
}
if (prop.type === 'object' && Array.isArray(prop.value)) {
return { ...prop, value: addToParent(prop.value) }
}
return prop
})
}
updateProperties(addToParent(properties))
} else {
updateProperties([...properties, newProp])
}
}
const removeProperty = (id: string) => {
const removeRecursive = (props: JSONProperty[]): JSONProperty[] => {
return props
.filter((prop) => prop.id !== id)
.map((prop) => {
if (prop.type === 'object' && Array.isArray(prop.value)) {
return { ...prop, value: removeRecursive(prop.value) }
}
return prop
})
}
const newProperties = removeRecursive(properties)
updateProperties(
newProperties.length > 0
? newProperties
: [
{
id: crypto.randomUUID(),
key: '',
type: 'string',
value: '',
collapsed: false,
},
]
)
}
const addArrayItem = (arrayPropId: string) => {
const addItem = (props: JSONProperty[]): JSONProperty[] => {
return props.map((prop) => {
if (prop.id === arrayPropId && prop.type === 'array') {
return { ...prop, value: [...(prop.value || []), ''] }
}
if (prop.type === 'object' && Array.isArray(prop.value)) {
return { ...prop, value: addItem(prop.value) }
}
return prop
})
}
updateProperties(addItem(properties))
}
const removeArrayItem = (arrayPropId: string, index: number) => {
const removeItem = (props: JSONProperty[]): JSONProperty[] => {
return props.map((prop) => {
if (prop.id === arrayPropId && prop.type === 'array') {
const newValue = [...(prop.value || [])]
newValue.splice(index, 1)
return { ...prop, value: newValue }
}
if (prop.type === 'object' && Array.isArray(prop.value)) {
return { ...prop, value: removeItem(prop.value) }
}
return prop
})
}
updateProperties(removeItem(properties))
}
const updateArrayItem = (arrayPropId: string, index: number, newValue: any) => {
const updateItem = (props: JSONProperty[]): JSONProperty[] => {
return props.map((prop) => {
if (prop.id === arrayPropId && prop.type === 'array') {
const updatedValue = [...(prop.value || [])]
updatedValue[index] = newValue
return { ...prop, value: updatedValue }
}
if (prop.type === 'object' && Array.isArray(prop.value)) {
return { ...prop, value: updateItem(prop.value) }
}
return prop
})
}
updateProperties(updateItem(properties))
}
const hasConfiguredProperties = properties.some((prop) => prop.key.trim())
return (
<div className='space-y-2'>
<div className='flex items-center justify-between'>
<Label className='font-medium text-xs'> </Label>
<div className='flex items-center gap-1'>
<Button
variant='outline'
size='sm'
onClick={() => setShowPreview(!showPreview)}
disabled={isPreview}
className='h-6 px-2 text-xs'
>
{showPreview ? <Code className='mr-1 h-3 w-3' /> : <Eye className='mr-1 h-3 w-3' />}
{showPreview ? 'Hide' : 'Preview'}
</Button>
<Button
variant='outline'
size='sm'
onClick={() => addProperty()}
disabled={isPreview}
className='h-6 px-2 text-xs'
>
<Plus className='h-3 w-3' />
</Button>
</div>
</div>
{showPreview && (
<div className='rounded border bg-muted/30 p-2'>
<pre className='max-h-32 overflow-auto text-xs'>
{(() => {
try {
return JSON.stringify(generateJSON(properties), null, 2)
} catch (error) {
return `Error generating preview: ${error instanceof Error ? error.message : 'Unknown error'}`
}
})()}
</pre>
</div>
)}
<div className='space-y-1'>
{properties.map((prop) => (
<PropertyRenderer
key={prop.id}
property={prop}
blockId={blockId}
isPreview={isPreview}
onUpdateProperty={updateProperty}
onAddProperty={addProperty}
onRemoveProperty={removeProperty}
onAddArrayItem={addArrayItem}
onRemoveArrayItem={removeArrayItem}
onUpdateArrayItem={updateArrayItem}
depth={0}
/>
))}
</div>
{!hasConfiguredProperties && (
<div className='py-4 text-center text-muted-foreground'>
<p className='text-xs'>Build your JSON response format</p>
<p className='text-xs'>
Use &lt;variable.name&gt; in values or drag variables from above
</p>
</div>
)}
</div>
<SharedResponseFormat
blockId={blockId}
subBlockId={subBlockId}
isPreview={isPreview}
previewValue={previewValue}
disabled={disabled}
isConnecting={isConnecting}
config={config}
/>
)
}

View File

@@ -1,3 +1,4 @@
import { useRef, useState } from 'react'
import { ChevronDown, Plus, Trash } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
@@ -7,52 +8,83 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { formatDisplayText } from '@/components/ui/formatted-text'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
import { cn } from '@/lib/utils'
import { useSubBlockValue } from '../../hooks/use-sub-block-value'
interface InputField {
interface Field {
id: string
name: string
type: 'string' | 'number' | 'boolean' | 'object' | 'array'
type?: 'string' | 'number' | 'boolean' | 'object' | 'array'
value?: string
collapsed?: boolean
}
interface InputFormatProps {
interface FieldFormatProps {
blockId: string
subBlockId: string
isPreview?: boolean
previewValue?: InputField[] | null
previewValue?: Field[] | null
disabled?: boolean
title?: string
placeholder?: string
emptyMessage?: string
showType?: boolean
showValue?: boolean
valuePlaceholder?: string
isConnecting?: boolean
config?: any
}
// Default values
const DEFAULT_FIELD: InputField = {
const DEFAULT_FIELD: Field = {
id: crypto.randomUUID(),
name: '',
type: 'string',
collapsed: true,
value: '',
collapsed: false,
}
export function InputFormat({
export function FieldFormat({
blockId,
subBlockId,
isPreview = false,
previewValue,
disabled = false,
}: InputFormatProps) {
const [storeValue, setStoreValue] = useSubBlockValue<InputField[]>(blockId, subBlockId)
title = 'Field',
placeholder = 'fieldName',
emptyMessage = 'No fields defined',
showType = true,
showValue = false,
valuePlaceholder = 'Enter value or <variable.name>',
isConnecting = false,
config,
}: FieldFormatProps) {
const [storeValue, setStoreValue] = useSubBlockValue<Field[]>(blockId, subBlockId)
const [tagDropdownStates, setTagDropdownStates] = useState<
Record<
string,
{
visible: boolean
cursorPosition: number
}
>
>({})
const [dragHighlight, setDragHighlight] = useState<Record<string, boolean>>({})
const valueInputRefs = useRef<Record<string, HTMLInputElement>>({})
// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : storeValue
const fields: InputField[] = value || []
const fields: Field[] = value || []
// Field operations
const addField = () => {
if (isPreview || disabled) return
const newField: InputField = {
const newField: Field = {
...DEFAULT_FIELD,
id: crypto.randomUUID(),
}
@@ -61,24 +93,127 @@ export function InputFormat({
const removeField = (id: string) => {
if (isPreview || disabled) return
setStoreValue(fields.filter((field: InputField) => field.id !== id))
setStoreValue(fields.filter((field: Field) => field.id !== id))
}
// Validate field name for API safety
const validateFieldName = (name: string): string => {
// Remove only truly problematic characters for JSON/API usage
// Allow most characters but remove control characters, quotes, and backslashes
return name.replace(/[\x00-\x1F"\\]/g, '').trim()
}
// Tag dropdown handlers
const handleValueInputChange = (fieldId: string, newValue: string) => {
const input = valueInputRefs.current[fieldId]
if (!input) return
const cursorPosition = input.selectionStart || 0
const shouldShow = checkTagTrigger(newValue, cursorPosition)
setTagDropdownStates((prev) => ({
...prev,
[fieldId]: {
visible: shouldShow.show,
cursorPosition,
},
}))
updateField(fieldId, 'value', newValue)
}
const handleTagSelect = (fieldId: string, newValue: string) => {
updateField(fieldId, 'value', newValue)
setTagDropdownStates((prev) => ({
...prev,
[fieldId]: { ...prev[fieldId], visible: false },
}))
}
const handleTagDropdownClose = (fieldId: string) => {
setTagDropdownStates((prev) => ({
...prev,
[fieldId]: { ...prev[fieldId], visible: false },
}))
}
// Drag and drop handlers for connection blocks
const handleDragOver = (e: React.DragEvent, fieldId: string) => {
e.preventDefault()
e.dataTransfer.dropEffect = 'copy'
setDragHighlight((prev) => ({ ...prev, [fieldId]: true }))
}
const handleDragLeave = (e: React.DragEvent, fieldId: string) => {
e.preventDefault()
setDragHighlight((prev) => ({ ...prev, [fieldId]: false }))
}
const handleDrop = (e: React.DragEvent, fieldId: string) => {
e.preventDefault()
setDragHighlight((prev) => ({ ...prev, [fieldId]: false }))
try {
const data = JSON.parse(e.dataTransfer.getData('application/json'))
if (data.type === 'connectionBlock' && data.connectionData) {
const input = valueInputRefs.current[fieldId]
if (!input) return
// Focus the input first
input.focus()
// Get current cursor position or use end of field
const dropPosition = input.selectionStart ?? (input.value?.length || 0)
// Insert '<' at drop position to trigger the dropdown
const currentValue = input.value || ''
const newValue = `${currentValue.slice(0, dropPosition)}<${currentValue.slice(dropPosition)}`
// Update the field value
updateField(fieldId, 'value', newValue)
// Set cursor position and show dropdown
setTimeout(() => {
input.selectionStart = dropPosition + 1
input.selectionEnd = dropPosition + 1
// Trigger dropdown by simulating the tag check
const cursorPosition = dropPosition + 1
const shouldShow = checkTagTrigger(newValue, cursorPosition)
setTagDropdownStates((prev) => ({
...prev,
[fieldId]: {
visible: shouldShow.show,
cursorPosition,
},
}))
}, 0)
}
} catch (error) {
console.error('Error handling drop:', error)
}
}
// Update handlers
const updateField = (id: string, field: keyof InputField, value: any) => {
const updateField = (id: string, field: keyof Field, value: any) => {
if (isPreview || disabled) return
setStoreValue(fields.map((f: InputField) => (f.id === id ? { ...f, [field]: value } : f)))
// Validate field name if it's being updated
if (field === 'name' && typeof value === 'string') {
value = validateFieldName(value)
}
setStoreValue(fields.map((f: Field) => (f.id === id ? { ...f, [field]: value } : f)))
}
const toggleCollapse = (id: string) => {
if (isPreview || disabled) return
setStoreValue(
fields.map((f: InputField) => (f.id === id ? { ...f, collapsed: !f.collapsed } : f))
)
setStoreValue(fields.map((f: Field) => (f.id === id ? { ...f, collapsed: !f.collapsed } : f)))
}
// Field header
const renderFieldHeader = (field: InputField, index: number) => {
const renderFieldHeader = (field: Field, index: number) => {
const isUnconfigured = !field.name || field.name.trim() === ''
return (
@@ -93,9 +228,9 @@ export function InputFormat({
isUnconfigured ? 'text-muted-foreground/50' : 'text-foreground'
)}
>
{field.name ? field.name : `Field ${index + 1}`}
{field.name ? field.name : `${title} ${index + 1}`}
</span>
{field.name && (
{field.name && showType && (
<Badge variant='outline' className='ml-2 h-5 bg-muted py-0 font-normal text-xs'>
{field.type}
</Badge>
@@ -110,7 +245,7 @@ export function InputFormat({
className='h-6 w-6 rounded-full'
>
<Plus className='h-3.5 w-3.5' />
<span className='sr-only'>Add Field</span>
<span className='sr-only'>Add {title}</span>
</Button>
<Button
@@ -128,15 +263,12 @@ export function InputFormat({
)
}
// Check if any fields have been configured
const hasConfiguredFields = fields.some((field) => field.name && field.name.trim() !== '')
// Main render
return (
<div className='space-y-2'>
{fields.length === 0 ? (
<div className='flex flex-col items-center justify-center rounded-md border border-input/50 border-dashed py-8'>
<p className='mb-3 text-muted-foreground text-sm'>No input fields defined</p>
<p className='mb-3 text-muted-foreground text-sm'>{emptyMessage}</p>
<Button
variant='outline'
size='sm'
@@ -145,7 +277,7 @@ export function InputFormat({
className='h-8'
>
<Plus className='mr-1.5 h-3.5 w-3.5' />
Add Field
Add {title}
</Button>
</div>
) : (
@@ -172,78 +304,165 @@ export function InputFormat({
name='name'
value={field.name}
onChange={(e) => updateField(field.id, 'name', e.target.value)}
placeholder='firstName'
placeholder={placeholder}
disabled={isPreview || disabled}
className='h-9 placeholder:text-muted-foreground/50'
/>
</div>
<div className='space-y-1.5'>
<Label className='text-xs'>Type</Label>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='outline'
{showType && (
<div className='space-y-1.5'>
<Label className='text-xs'>Type</Label>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='outline'
disabled={isPreview || disabled}
className='h-9 w-full justify-between font-normal'
>
<div className='flex items-center'>
<span>{field.type}</span>
</div>
<ChevronDown className='h-4 w-4 opacity-50' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end' className='w-[200px]'>
<DropdownMenuItem
onClick={() => updateField(field.id, 'type', 'string')}
className='cursor-pointer'
>
<span className='mr-2 font-mono'>Aa</span>
<span>String</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => updateField(field.id, 'type', 'number')}
className='cursor-pointer'
>
<span className='mr-2 font-mono'>123</span>
<span>Number</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => updateField(field.id, 'type', 'boolean')}
className='cursor-pointer'
>
<span className='mr-2 font-mono'>0/1</span>
<span>Boolean</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => updateField(field.id, 'type', 'object')}
className='cursor-pointer'
>
<span className='mr-2 font-mono'>{'{}'}</span>
<span>Object</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => updateField(field.id, 'type', 'array')}
className='cursor-pointer'
>
<span className='mr-2 font-mono'>[]</span>
<span>Array</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
{showValue && (
<div className='space-y-1.5'>
<Label className='text-xs'>Value</Label>
<div className='relative'>
<Input
ref={(el) => {
if (el) valueInputRefs.current[field.id] = el
}}
name='value'
value={field.value || ''}
onChange={(e) => handleValueInputChange(field.id, e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Escape') {
handleTagDropdownClose(field.id)
}
}}
onDragOver={(e) => handleDragOver(e, field.id)}
onDragLeave={(e) => handleDragLeave(e, field.id)}
onDrop={(e) => handleDrop(e, field.id)}
placeholder={valuePlaceholder}
disabled={isPreview || disabled}
className='h-9 w-full justify-between font-normal'
>
<div className='flex items-center'>
<span>{field.type}</span>
className={cn(
'h-9 text-transparent caret-foreground placeholder:text-muted-foreground/50',
dragHighlight[field.id] && 'ring-2 ring-blue-500 ring-offset-2',
isConnecting &&
config?.connectionDroppable !== false &&
'ring-2 ring-blue-500 ring-offset-2 focus-visible:ring-blue-500'
)}
/>
{field.value && (
<div className='pointer-events-none absolute inset-0 flex items-center px-3 py-2'>
<div className='w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm'>
{formatDisplayText(field.value, true)}
</div>
</div>
<ChevronDown className='h-4 w-4 opacity-50' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end' className='w-[200px]'>
<DropdownMenuItem
onClick={() => updateField(field.id, 'type', 'string')}
className='cursor-pointer'
>
<span className='mr-2 font-mono'>Aa</span>
<span>String</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => updateField(field.id, 'type', 'number')}
className='cursor-pointer'
>
<span className='mr-2 font-mono'>123</span>
<span>Number</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => updateField(field.id, 'type', 'boolean')}
className='cursor-pointer'
>
<span className='mr-2 font-mono'>0/1</span>
<span>Boolean</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => updateField(field.id, 'type', 'object')}
className='cursor-pointer'
>
<span className='mr-2 font-mono'>{'{}'}</span>
<span>Object</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => updateField(field.id, 'type', 'array')}
className='cursor-pointer'
>
<span className='mr-2 font-mono'>[]</span>
<span>Array</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
<TagDropdown
visible={tagDropdownStates[field.id]?.visible || false}
onSelect={(newValue) => handleTagSelect(field.id, newValue)}
blockId={blockId}
activeSourceBlockId={null}
inputValue={field.value || ''}
cursorPosition={tagDropdownStates[field.id]?.cursorPosition || 0}
onClose={() => handleTagDropdownClose(field.id)}
style={{
position: 'absolute',
top: '100%',
left: 0,
right: 0,
zIndex: 9999,
}}
/>
</div>
</div>
)}
</div>
)}
</div>
)
})
)}
{fields.length > 0 && !hasConfiguredFields && (
<div className='mt-1 px-1 text-muted-foreground/70 text-xs italic'>
Define fields above to enable structured API input
</div>
)}
</div>
)
}
// Export specific components for backward compatibility
export function InputFormat(
props: Omit<FieldFormatProps, 'title' | 'placeholder' | 'emptyMessage'>
) {
return (
<FieldFormat
{...props}
title='Field'
placeholder='firstName'
emptyMessage='No input fields defined'
/>
)
}
export function ResponseFormat(
props: Omit<
FieldFormatProps,
'title' | 'placeholder' | 'emptyMessage' | 'showType' | 'showValue' | 'valuePlaceholder'
>
) {
return (
<FieldFormat
{...props}
title='Field'
placeholder='output'
emptyMessage='No response fields defined'
showType={false}
showValue={true}
valuePlaceholder='Enter value or <variable.name>'
/>
)
}
export type { Field as InputField, Field as ResponseField }

View File

@@ -370,6 +370,8 @@ export function SubBlock({
isPreview={isPreview}
previewValue={previewValue}
disabled={isDisabled}
isConnecting={isConnecting}
config={config}
/>
)
}
@@ -380,6 +382,9 @@ export function SubBlock({
subBlockId={config.id}
isPreview={isPreview}
previewValue={previewValue}
isConnecting={isConnecting}
config={config}
disabled={isDisabled}
/>
)
case 'channel-selector':

View File

@@ -2807,8 +2807,8 @@ export const ResponseIcon = (props: SVGProps<SVGSVGElement>) => (
xmlns='http://www.w3.org/2000/svg'
>
<path
d='m1044.768 224 150.827 150.827-306.88 306.88h99.413c508.8 0 922.667 413.866 922.667 922.666v90.667H1697.46v-90.667c0-391.146-318.186-709.333-709.333-709.333h-100.16l307.627 307.733-150.827 150.827-564.8-564.8 564.8-564.8ZM564.8 224l150.827 150.827L301.653 788.8l413.974 413.973L564.8 1353.6 0 788.8 564.8 224Z'
fillRule='evenodd'
d='m1030.975 188 81.249 81.249-429.228 429.228h300.747c516.223 0 936.257 420.034 936.257 936.257v98.028h-114.92v-98.028c0-452.901-368.436-821.337-821.337-821.337H682.996l429.228 429.229-81.25 81.248-567.936-567.937L1030.975 188Zm-463.038.011 81.249 81.25-486.688 486.688 486.688 486.688-81.249 81.249L0 755.949 567.937 188.01Z'
fill-rule='evenodd'
/>
</svg>
)

View File

@@ -182,8 +182,26 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
}
} else if (Object.keys(blockConfig.outputs).length === 0) {
// Handle blocks with no outputs (like starter) - show as just <blockname>
blockTags = [normalizedBlockName]
// Handle blocks with no outputs (like starter) - check for custom input fields
if (sourceBlock.type === 'starter') {
// Check for custom input format fields
const inputFormatValue = useSubBlockStore
.getState()
.getValue(activeSourceBlockId, 'inputFormat')
if (inputFormatValue && Array.isArray(inputFormatValue) && inputFormatValue.length > 0) {
// Use custom input fields if they exist
blockTags = inputFormatValue
.filter((field: any) => field.name && field.name.trim() !== '')
.map((field: any) => `${normalizedBlockName}.${field.name}`)
} else {
// Fallback to just the block name
blockTags = [normalizedBlockName]
}
} else {
// Other blocks with no outputs - show as just <blockname>
blockTags = [normalizedBlockName]
}
} else {
// Use default block outputs
const outputPaths = generateOutputPaths(blockConfig.outputs)
@@ -409,8 +427,26 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
}
} else if (Object.keys(blockConfig.outputs).length === 0) {
// Handle blocks with no outputs (like starter) - show as just <blockname>
blockTags = [normalizedBlockName]
// Handle blocks with no outputs (like starter) - check for custom input fields
if (accessibleBlock.type === 'starter') {
// Check for custom input format fields
const inputFormatValue = useSubBlockStore
.getState()
.getValue(accessibleBlockId, 'inputFormat')
if (inputFormatValue && Array.isArray(inputFormatValue) && inputFormatValue.length > 0) {
// Use custom input fields if they exist
blockTags = inputFormatValue
.filter((field: any) => field.name && field.name.trim() !== '')
.map((field: any) => `${normalizedBlockName}.${field.name}`)
} else {
// Fallback to just the block name
blockTags = [normalizedBlockName]
}
} else {
// Other blocks with no outputs - show as just <blockname>
blockTags = [normalizedBlockName]
}
} else {
// Use default block outputs
const outputPaths = generateOutputPaths(blockConfig.outputs)

View File

@@ -8,7 +8,7 @@ const logger = createLogger('ResponseBlockHandler')
interface JSONProperty {
id: string
key: string
name: string
type: 'string' | 'number' | 'boolean' | 'object' | 'array'
value: any
collapsed?: boolean
@@ -92,17 +92,44 @@ export class ResponseBlockHandler implements BlockHandler {
const result: any = {}
for (const prop of builderData) {
if (!prop.key.trim()) {
return
if (!prop.name || !prop.name.trim()) {
continue
}
const value = this.convertPropertyValue(prop)
result[prop.key] = value
result[prop.name] = value
}
return result
}
// Static method for UI conversion from Builder to Editor mode
static convertBuilderDataToJsonString(builderData: JSONProperty[]): string {
if (!Array.isArray(builderData) || builderData.length === 0) {
return '{\n \n}'
}
const result: any = {}
for (const prop of builderData) {
if (!prop.name || !prop.name.trim()) {
continue
}
// For UI display, keep variable references as-is without processing
result[prop.name] = prop.value
}
// Convert to JSON string, then replace quoted variable references with unquoted ones
let jsonString = JSON.stringify(result, null, 2)
// Replace quoted variable references with unquoted ones
// Pattern: "<variable.name>" -> <variable.name>
jsonString = jsonString.replace(/"(<[^>]+>)"/g, '$1')
return jsonString
}
private convertPropertyValue(prop: JSONProperty): any {
switch (prop.type) {
case 'object':

View File

@@ -439,6 +439,10 @@ export class InputResolver {
else if (blockType === 'condition') {
formattedValue = this.stringifyForCondition(replacementValue)
}
// For response blocks, preserve object structure as-is for proper JSON response
else if (blockType === 'response') {
formattedValue = replacementValue
}
// For all other blocks, stringify objects
else {
// Preserve full JSON structure for objects
@@ -450,10 +454,22 @@ export class InputResolver {
}
} else {
// Standard handling for non-input references
formattedValue =
typeof replacementValue === 'object'
? JSON.stringify(replacementValue)
: String(replacementValue)
const blockType = currentBlock.metadata?.id
if (blockType === 'response') {
// For response blocks, properly quote string values for JSON context
if (typeof replacementValue === 'string') {
// Properly escape and quote the string for JSON
formattedValue = JSON.stringify(replacementValue)
} else {
formattedValue = replacementValue
}
} else {
formattedValue =
typeof replacementValue === 'object'
? JSON.stringify(replacementValue)
: String(replacementValue)
}
}
resolvedValue = resolvedValue.replace(match, formattedValue)
@@ -574,6 +590,8 @@ export class InputResolver {
}
}
const blockType = currentBlock.metadata?.id
let formattedValue: string
if (currentBlock.metadata?.id === 'condition') {
@@ -589,12 +607,22 @@ export class InputResolver {
value.includes('}') &&
value.includes('`')
// For code blocks, use our formatter
formattedValue = this.formatValueForCodeContext(
replacementValue,
currentBlock,
isInTemplateLiteral
)
// For response blocks, properly quote string values for JSON context
if (currentBlock.metadata?.id === 'response') {
if (typeof replacementValue === 'string') {
// Properly escape and quote the string for JSON
formattedValue = JSON.stringify(replacementValue)
} else {
formattedValue = replacementValue
}
} else {
// For code blocks, use our formatter
formattedValue = this.formatValueForCodeContext(
replacementValue,
currentBlock,
isInTemplateLiteral
)
}
} else {
// The function execution API will handle variable resolution within code strings
formattedValue =