mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
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:
File diff suppressed because one or more lines are too long
@@ -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>`}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 <variable.name> 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user