mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(copilot): structured metadata context + start block deprecation (#1362)
* progress * progress * deploy command update * add trigger mode modal * fix trigger icons' * fix corners for add trigger card * update serialization error visual in console * works * improvement(copilot-context): structured context for copilot * forgot long description * Update metadata params * progress * add better workflow ux * progress * highlighting works * trigger card * default agent workflow change * fix build error * remove any casts * address greptile comments * Diff input format * address greptile comments * improvement: ui/ux * improvement: changed to vertical scrolling * fix(workflow): ensure new blocks from sidebar click/drag use getUniqueBlockName (with semantic trigger base when applicable) * Validation + build/edit mark complete * fix trigger dropdown * Copilot stuff (lots of it) * Temp update prod dns * fix trigger check * fix * fix trigger mode check * Fix yaml imports * Fix autolayout error * fix deployed chat * Fix copilot input text overflow * fix trigger mode persistence in addBlock with enableTriggerMode flag passed in * Lint * Fix failing tests * Reset ishosted * Lint * input format for legacy starter * Fix executor --------- Co-authored-by: Siddharth Ganesan <siddharthganesan@gmail.com> Co-authored-by: Emir Karabeg <emirkarabeg@berkeley.edu>
This commit is contained in:
co-authored by
Siddharth Ganesan
Emir Karabeg
parent
68df95906f
commit
b7876ca466
@@ -13,6 +13,7 @@ import { hasAdminPermission } from '@/lib/permissions/utils'
|
||||
import { processStreamingBlockLogs } from '@/lib/tokenization'
|
||||
import { getEmailDomain } from '@/lib/urls/utils'
|
||||
import { decryptSecret, generateRequestId } from '@/lib/utils'
|
||||
import { TriggerUtils } from '@/lib/workflows/triggers'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { Executor } from '@/executor'
|
||||
import type { BlockLog, ExecutionResult } from '@/executor/types'
|
||||
@@ -430,9 +431,10 @@ export async function executeWorkflowForChat(
|
||||
(acc, [id, block]) => {
|
||||
const blockConfig = getBlock(block.type)
|
||||
const isTriggerBlock = blockConfig?.category === 'triggers'
|
||||
const isChatTrigger = block.type === 'chat_trigger'
|
||||
|
||||
// Skip trigger blocks during chat execution
|
||||
if (!isTriggerBlock) {
|
||||
// Keep all non-trigger blocks and also keep the chat_trigger block
|
||||
if (!isTriggerBlock || isChatTrigger) {
|
||||
acc[id] = block
|
||||
}
|
||||
return acc
|
||||
@@ -487,8 +489,10 @@ export async function executeWorkflowForChat(
|
||||
|
||||
// Filter edges to exclude connections to/from trigger blocks (same as manual execution)
|
||||
const triggerBlockIds = Object.keys(mergedStates).filter((id) => {
|
||||
const blockConfig = getBlock(mergedStates[id].type)
|
||||
return blockConfig?.category === 'triggers'
|
||||
const type = mergedStates[id].type
|
||||
const blockConfig = getBlock(type)
|
||||
// Exclude chat_trigger from the list so its edges are preserved
|
||||
return blockConfig?.category === 'triggers' && type !== 'chat_trigger'
|
||||
})
|
||||
|
||||
const filteredEdges = edges.filter(
|
||||
@@ -613,9 +617,29 @@ export async function executeWorkflowForChat(
|
||||
// Set up logging on the executor
|
||||
loggingSession.setupExecutor(executor)
|
||||
|
||||
// Determine the start block for chat execution
|
||||
const startBlock = TriggerUtils.findStartBlock(mergedStates, 'chat')
|
||||
|
||||
if (!startBlock) {
|
||||
const errorMessage =
|
||||
'No Chat trigger configured for this workflow. Add a Chat Trigger block to enable chat execution.'
|
||||
logger.error(`[${requestId}] ${errorMessage}`)
|
||||
await loggingSession.safeCompleteWithError({
|
||||
endedAt: new Date().toISOString(),
|
||||
totalDurationMs: 0,
|
||||
error: {
|
||||
message: errorMessage,
|
||||
stackTrace: undefined,
|
||||
},
|
||||
})
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const startBlockId = startBlock.blockId
|
||||
|
||||
let result
|
||||
try {
|
||||
result = await executor.execute(workflowId)
|
||||
result = await executor.execute(workflowId, startBlockId)
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Chat workflow execution failed:`, error)
|
||||
await loggingSession.safeCompleteWithError({
|
||||
|
||||
@@ -220,13 +220,20 @@ describe('Copilot Chat API Route', () => {
|
||||
content: 'Hello',
|
||||
},
|
||||
],
|
||||
chatMessages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello',
|
||||
},
|
||||
],
|
||||
workflowId: 'workflow-123',
|
||||
userId: 'user-123',
|
||||
stream: true,
|
||||
streamToolCalls: true,
|
||||
model: 'gpt-5',
|
||||
mode: 'agent',
|
||||
messageId: 'mock-uuid-1234-5678',
|
||||
depth: 0,
|
||||
version: '1.0.0',
|
||||
chatId: 'chat-123',
|
||||
}),
|
||||
})
|
||||
@@ -284,13 +291,19 @@ describe('Copilot Chat API Route', () => {
|
||||
{ role: 'assistant', content: 'Previous response' },
|
||||
{ role: 'user', content: 'New message' },
|
||||
],
|
||||
chatMessages: [
|
||||
{ role: 'user', content: 'Previous message' },
|
||||
{ role: 'assistant', content: 'Previous response' },
|
||||
{ role: 'user', content: 'New message' },
|
||||
],
|
||||
workflowId: 'workflow-123',
|
||||
userId: 'user-123',
|
||||
stream: true,
|
||||
streamToolCalls: true,
|
||||
model: 'gpt-5',
|
||||
mode: 'agent',
|
||||
messageId: 'mock-uuid-1234-5678',
|
||||
depth: 0,
|
||||
version: '1.0.0',
|
||||
chatId: 'chat-123',
|
||||
}),
|
||||
})
|
||||
@@ -337,13 +350,18 @@ describe('Copilot Chat API Route', () => {
|
||||
{ role: 'system', content: 'User seems confused about the workflow' },
|
||||
{ role: 'user', content: 'Hello' },
|
||||
],
|
||||
chatMessages: [
|
||||
{ role: 'system', content: 'User seems confused about the workflow' },
|
||||
{ role: 'user', content: 'Hello' },
|
||||
],
|
||||
workflowId: 'workflow-123',
|
||||
userId: 'user-123',
|
||||
stream: true,
|
||||
streamToolCalls: true,
|
||||
model: 'gpt-5',
|
||||
mode: 'agent',
|
||||
messageId: 'mock-uuid-1234-5678',
|
||||
depth: 0,
|
||||
version: '1.0.0',
|
||||
chatId: 'chat-123',
|
||||
}),
|
||||
})
|
||||
@@ -427,13 +445,15 @@ describe('Copilot Chat API Route', () => {
|
||||
expect.objectContaining({
|
||||
body: JSON.stringify({
|
||||
messages: [{ role: 'user', content: 'What is this workflow?' }],
|
||||
chatMessages: [{ role: 'user', content: 'What is this workflow?' }],
|
||||
workflowId: 'workflow-123',
|
||||
userId: 'user-123',
|
||||
stream: true,
|
||||
streamToolCalls: true,
|
||||
model: 'gpt-5',
|
||||
mode: 'ask',
|
||||
messageId: 'mock-uuid-1234-5678',
|
||||
depth: 0,
|
||||
version: '1.0.0',
|
||||
chatId: 'chat-123',
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -15,7 +15,7 @@ import { getCopilotModel } from '@/lib/copilot/config'
|
||||
import type { CopilotProviderConfig } from '@/lib/copilot/types'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent'
|
||||
import { SIM_AGENT_API_URL_DEFAULT, SIM_AGENT_VERSION } from '@/lib/sim-agent'
|
||||
import { generateChatTitle } from '@/lib/sim-agent/utils'
|
||||
import { createFileContent, isSupportedFileType } from '@/lib/uploads/file-utils'
|
||||
import { S3_COPILOT_CONFIG } from '@/lib/uploads/setup'
|
||||
@@ -38,8 +38,21 @@ const ChatMessageSchema = z.object({
|
||||
userMessageId: z.string().optional(), // ID from frontend for the user message
|
||||
chatId: z.string().optional(),
|
||||
workflowId: z.string().min(1, 'Workflow ID is required'),
|
||||
model: z
|
||||
.enum([
|
||||
'gpt-5-fast',
|
||||
'gpt-5',
|
||||
'gpt-5-medium',
|
||||
'gpt-5-high',
|
||||
'gpt-4o',
|
||||
'gpt-4.1',
|
||||
'o3',
|
||||
'claude-4-sonnet',
|
||||
'claude-4.1-opus',
|
||||
])
|
||||
.optional()
|
||||
.default('gpt-5'),
|
||||
mode: z.enum(['ask', 'agent']).optional().default('agent'),
|
||||
depth: z.number().int().min(0).max(3).optional().default(0),
|
||||
prefetch: z.boolean().optional(),
|
||||
createNewChat: z.boolean().optional().default(false),
|
||||
stream: z.boolean().optional().default(true),
|
||||
@@ -97,8 +110,8 @@ export async function POST(req: NextRequest) {
|
||||
userMessageId,
|
||||
chatId,
|
||||
workflowId,
|
||||
model,
|
||||
mode,
|
||||
depth,
|
||||
prefetch,
|
||||
createNewChat,
|
||||
stream,
|
||||
@@ -147,19 +160,6 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Consolidation mapping: map negative depths to base depth with prefetch=true
|
||||
let effectiveDepth: number | undefined = typeof depth === 'number' ? depth : undefined
|
||||
let effectivePrefetch: boolean | undefined = prefetch
|
||||
if (typeof effectiveDepth === 'number') {
|
||||
if (effectiveDepth === -2) {
|
||||
effectiveDepth = 1
|
||||
effectivePrefetch = true
|
||||
} else if (effectiveDepth === -1) {
|
||||
effectiveDepth = 0
|
||||
effectivePrefetch = true
|
||||
}
|
||||
}
|
||||
|
||||
// Handle chat context
|
||||
let currentChat: any = null
|
||||
let conversationHistory: any[] = []
|
||||
@@ -366,16 +366,18 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
const requestPayload = {
|
||||
messages: messagesForAgent,
|
||||
chatMessages: messages, // Full unfiltered messages array
|
||||
workflowId,
|
||||
userId: authenticatedUserId,
|
||||
stream: stream,
|
||||
streamToolCalls: true,
|
||||
model: model,
|
||||
mode: mode,
|
||||
messageId: userMessageIdToUse,
|
||||
version: SIM_AGENT_VERSION,
|
||||
...(providerConfig ? { provider: providerConfig } : {}),
|
||||
...(effectiveConversationId ? { conversationId: effectiveConversationId } : {}),
|
||||
...(typeof effectiveDepth === 'number' ? { depth: effectiveDepth } : {}),
|
||||
...(typeof effectivePrefetch === 'boolean' ? { prefetch: effectivePrefetch } : {}),
|
||||
...(typeof prefetch === 'boolean' ? { prefetch: prefetch } : {}),
|
||||
...(session?.user?.name && { userName: session.user.name }),
|
||||
...(agentContexts.length > 0 && { context: agentContexts }),
|
||||
...(actualChatId ? { chatId: actualChatId } : {}),
|
||||
@@ -384,6 +386,9 @@ export async function POST(req: NextRequest) {
|
||||
try {
|
||||
logger.info(`[${tracker.requestId}] About to call Sim Agent with context`, {
|
||||
context: (requestPayload as any).context,
|
||||
messagesCount: messagesForAgent.length,
|
||||
chatMessagesCount: messages.length,
|
||||
hasConversationId: !!effectiveConversationId,
|
||||
})
|
||||
} catch {}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ export async function POST(req: NextRequest) {
|
||||
return createBadRequestResponse('Invalid request body for execute-copilot-server-tool')
|
||||
}
|
||||
logger.error(`[${tracker.requestId}] Failed to execute server tool:`, error)
|
||||
return createInternalServerErrorResponse('Failed to execute server tool')
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to execute server tool'
|
||||
return createInternalServerErrorResponse(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,7 +292,7 @@ describe('Workflow Execution API Route', () => {
|
||||
const Executor = (await import('@/executor')).Executor
|
||||
expect(Executor).toHaveBeenCalled()
|
||||
|
||||
expect(executeMock).toHaveBeenCalledWith('workflow-id')
|
||||
expect(executeMock).toHaveBeenCalledWith('workflow-id', 'starter-id')
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -337,7 +337,7 @@ describe('Workflow Execution API Route', () => {
|
||||
const Executor = (await import('@/executor')).Executor
|
||||
expect(Executor).toHaveBeenCalled()
|
||||
|
||||
expect(executeMock).toHaveBeenCalledWith('workflow-id')
|
||||
expect(executeMock).toHaveBeenCalledWith('workflow-id', 'starter-id')
|
||||
|
||||
expect(Executor).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -14,6 +14,7 @@ import { LoggingSession } from '@/lib/logs/execution/logging-session'
|
||||
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
|
||||
import { decryptSecret, generateRequestId } from '@/lib/utils'
|
||||
import { loadDeployedWorkflowState } from '@/lib/workflows/db-helpers'
|
||||
import { TriggerUtils } from '@/lib/workflows/triggers'
|
||||
import {
|
||||
createHttpResponseFromBlock,
|
||||
updateWorkflowRunCounts,
|
||||
@@ -272,6 +273,32 @@ async function executeWorkflow(
|
||||
true // Enable validation during execution
|
||||
)
|
||||
|
||||
// Determine API trigger start block
|
||||
// Direct API execution ONLY works with API trigger blocks (or legacy starter in api/run mode)
|
||||
const startBlock = TriggerUtils.findStartBlock(mergedStates, 'api', false) // isChildWorkflow = false
|
||||
|
||||
if (!startBlock) {
|
||||
logger.error(`[${requestId}] No API trigger configured for this workflow`)
|
||||
throw new Error(
|
||||
'No API trigger configured for this workflow. Add an API Trigger block or use a Start block in API mode.'
|
||||
)
|
||||
}
|
||||
|
||||
const startBlockId = startBlock.blockId
|
||||
const triggerBlock = startBlock.block
|
||||
|
||||
// Check if the API trigger has any outgoing connections (except for legacy starter blocks)
|
||||
// Legacy starter blocks have their own validation in the executor
|
||||
if (triggerBlock.type !== 'starter') {
|
||||
const outgoingConnections = serializedWorkflow.connections.filter(
|
||||
(conn) => conn.source === startBlockId
|
||||
)
|
||||
if (outgoingConnections.length === 0) {
|
||||
logger.error(`[${requestId}] API trigger has no outgoing connections`)
|
||||
throw new Error('API Trigger block must be connected to other blocks to execute')
|
||||
}
|
||||
}
|
||||
|
||||
const executor = new Executor({
|
||||
workflow: serializedWorkflow,
|
||||
currentBlockStates: processedBlockStates,
|
||||
@@ -287,7 +314,7 @@ async function executeWorkflow(
|
||||
// Set up logging on the executor
|
||||
loggingSession.setupExecutor(executor)
|
||||
|
||||
const result = await executor.execute(workflowId)
|
||||
const result = await executor.execute(workflowId, startBlockId)
|
||||
|
||||
// Check if we got a StreamingExecution result (with stream + execution properties)
|
||||
// For API routes, we only care about the ExecutionResult part, not the stream
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
loadWorkflowFromNormalizedTables,
|
||||
saveWorkflowToNormalizedTables,
|
||||
} from '@/lib/workflows/db-helpers'
|
||||
import { sanitizeAgentToolsInBlocks } from '@/lib/workflows/validation'
|
||||
import { sanitizeAgentToolsInBlocks, validateWorkflowState } from '@/lib/workflows/validation'
|
||||
import { getUserId } from '@/app/api/auth/oauth/utils'
|
||||
import { getAllBlocks, getBlock } from '@/blocks'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
@@ -240,6 +240,65 @@ async function upsertCustomToolsFromBlocks(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert blocks with 'inputs' field to standard 'subBlocks' structure
|
||||
* This handles trigger blocks that may come from YAML/copilot with legacy format
|
||||
*/
|
||||
function normalizeBlockStructure(blocks: Record<string, any>): Record<string, any> {
|
||||
const normalizedBlocks: Record<string, any> = {}
|
||||
|
||||
for (const [blockId, block] of Object.entries(blocks)) {
|
||||
const normalizedBlock = { ...block }
|
||||
|
||||
// Normalize position coordinates (handle both uppercase and lowercase)
|
||||
if (block.position) {
|
||||
normalizedBlock.position = {
|
||||
x: block.position.x ?? block.position.X ?? 0,
|
||||
y: block.position.y ?? block.position.Y ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert any inputs map into subBlocks for consistency (applies to all blocks)
|
||||
if (block.inputs) {
|
||||
// Convert inputs.inputFormat to subBlocks.inputFormat
|
||||
if (block.inputs.inputFormat) {
|
||||
if (!normalizedBlock.subBlocks) {
|
||||
normalizedBlock.subBlocks = {}
|
||||
}
|
||||
|
||||
normalizedBlock.subBlocks.inputFormat = {
|
||||
id: 'inputFormat',
|
||||
type: 'input-format',
|
||||
value: block.inputs.inputFormat,
|
||||
}
|
||||
}
|
||||
|
||||
// Copy all inputs fields to subBlocks (creating entries as needed)
|
||||
for (const [inputKey, inputValue] of Object.entries(block.inputs)) {
|
||||
if (!normalizedBlock.subBlocks) {
|
||||
normalizedBlock.subBlocks = {}
|
||||
}
|
||||
if (!normalizedBlock.subBlocks[inputKey]) {
|
||||
normalizedBlock.subBlocks[inputKey] = {
|
||||
id: inputKey,
|
||||
type: 'short-input', // Default type, may need adjustment based on actual field
|
||||
value: inputValue,
|
||||
}
|
||||
} else {
|
||||
normalizedBlock.subBlocks[inputKey].value = inputValue
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the inputs field after conversion
|
||||
normalizedBlock.inputs = undefined
|
||||
}
|
||||
|
||||
normalizedBlocks[blockId] = normalizedBlock
|
||||
}
|
||||
|
||||
return normalizedBlocks
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/workflows/[id]/yaml
|
||||
* Consolidated YAML workflow saving endpoint
|
||||
@@ -344,39 +403,76 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
})
|
||||
}
|
||||
|
||||
// Normalize blocks that use 'inputs' field to standard 'subBlocks' structure
|
||||
if (workflowState.blocks) {
|
||||
workflowState.blocks = normalizeBlockStructure(workflowState.blocks)
|
||||
}
|
||||
|
||||
// Validate the workflow state before persisting
|
||||
const validation = validateWorkflowState(workflowState, { sanitize: true })
|
||||
|
||||
if (!validation.valid) {
|
||||
logger.error(`[${requestId}] Workflow validation failed`, {
|
||||
errors: validation.errors,
|
||||
warnings: validation.warnings,
|
||||
})
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: 'Invalid workflow structure',
|
||||
errors: validation.errors,
|
||||
warnings: validation.warnings || [],
|
||||
})
|
||||
}
|
||||
|
||||
// Use sanitized state if available
|
||||
const finalWorkflowState = validation.sanitizedState || workflowState
|
||||
|
||||
if (validation.warnings.length > 0) {
|
||||
logger.warn(`[${requestId}] Workflow validation warnings`, {
|
||||
warnings: validation.warnings,
|
||||
})
|
||||
}
|
||||
|
||||
// Ensure all blocks have required fields
|
||||
Object.values(workflowState.blocks).forEach((block: any) => {
|
||||
if (block.enabled === undefined) {
|
||||
block.enabled = true
|
||||
Object.entries(finalWorkflowState.blocks).forEach(([blockId, block]) => {
|
||||
const blockData = block as any
|
||||
if (!blockData.id) blockData.id = blockId
|
||||
if (!blockData.position) {
|
||||
blockData.position = { x: 0, y: 0 }
|
||||
}
|
||||
if (block.horizontalHandles === undefined) {
|
||||
block.horizontalHandles = true
|
||||
if (blockData.enabled === undefined) {
|
||||
blockData.enabled = true
|
||||
}
|
||||
if (block.isWide === undefined) {
|
||||
block.isWide = false
|
||||
if (blockData.horizontalHandles === undefined) {
|
||||
blockData.horizontalHandles = true
|
||||
}
|
||||
if (block.height === undefined) {
|
||||
block.height = 0
|
||||
if (blockData.isWide === undefined) {
|
||||
blockData.isWide = false
|
||||
}
|
||||
if (!block.subBlocks) {
|
||||
block.subBlocks = {}
|
||||
if (blockData.height === undefined) {
|
||||
blockData.height = 0
|
||||
}
|
||||
if (!block.outputs) {
|
||||
block.outputs = {}
|
||||
if (!blockData.subBlocks) {
|
||||
blockData.subBlocks = {}
|
||||
}
|
||||
if (!blockData.outputs) {
|
||||
blockData.outputs = {}
|
||||
}
|
||||
})
|
||||
|
||||
const blocks = Object.values(workflowState.blocks) as Array<{
|
||||
const blocks = Object.values(finalWorkflowState.blocks) as Array<{
|
||||
id: string
|
||||
type: string
|
||||
name: string
|
||||
position: { x: number; y: number }
|
||||
subBlocks?: Record<string, any>
|
||||
inputs?: Record<string, any>
|
||||
triggerMode?: boolean
|
||||
data?: Record<string, any>
|
||||
parentId?: string
|
||||
extent?: string
|
||||
}>
|
||||
const edges = workflowState.edges
|
||||
const edges = finalWorkflowState.edges
|
||||
const warnings = conversionResult.warnings || []
|
||||
|
||||
// Create workflow state
|
||||
@@ -454,6 +550,25 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
})
|
||||
}
|
||||
|
||||
// Handle blocks that have inputs instead of subBlocks (from YAML/copilot format)
|
||||
// This is especially important for trigger configuration
|
||||
if (block.inputs) {
|
||||
Object.entries(block.inputs).forEach(([inputKey, inputValue]) => {
|
||||
const matchingSubBlock = blockConfig.subBlocks.find((sb) => sb.id === inputKey)
|
||||
if (!subBlocks[inputKey]) {
|
||||
subBlocks[inputKey] = {
|
||||
id: inputKey,
|
||||
type:
|
||||
matchingSubBlock?.type ||
|
||||
(inputKey === 'triggerConfig' ? 'trigger-config' : 'short-input'),
|
||||
value: inputValue,
|
||||
}
|
||||
} else if (inputValue !== undefined) {
|
||||
subBlocks[inputKey].value = inputValue
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Set up outputs from block configuration
|
||||
const outputs = resolveOutputType(blockConfig.outputs)
|
||||
|
||||
@@ -476,10 +591,17 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
isWide: false,
|
||||
advancedMode: false,
|
||||
height: 0,
|
||||
triggerMode: block.triggerMode || false, // Preserve triggerMode from imported block
|
||||
data: blockData,
|
||||
}
|
||||
|
||||
logger.debug(`[${requestId}] Processed regular block: ${block.id} -> ${newId}`)
|
||||
logger.debug(`[${requestId}] Processed regular block: ${block.id} -> ${newId}`, {
|
||||
blockType: block.type,
|
||||
hasTriggerMode: block.triggerMode,
|
||||
hasInputs: !!block.inputs,
|
||||
inputKeys: block.inputs ? Object.keys(block.inputs) : [],
|
||||
subBlockKeys: Object.keys(subBlocks),
|
||||
})
|
||||
} else {
|
||||
logger.warn(`[${requestId}] Unknown block type: ${block.type}`)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { db } from '@sim/db'
|
||||
import { workflow, workflowBlocks, workspace } from '@sim/db/schema'
|
||||
import { workflow, workspace } from '@sim/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
@@ -95,132 +95,31 @@ export async function POST(req: NextRequest) {
|
||||
const { name, description, color, workspaceId, folderId } = CreateWorkflowSchema.parse(body)
|
||||
|
||||
const workflowId = crypto.randomUUID()
|
||||
const starterId = crypto.randomUUID()
|
||||
const now = new Date()
|
||||
|
||||
logger.info(`[${requestId}] Creating workflow ${workflowId} for user ${session.user.id}`)
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.insert(workflow).values({
|
||||
id: workflowId,
|
||||
userId: session.user.id,
|
||||
workspaceId: workspaceId || null,
|
||||
folderId: folderId || null,
|
||||
name,
|
||||
description,
|
||||
color,
|
||||
lastSynced: now,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
isDeployed: false,
|
||||
collaborators: [],
|
||||
runCount: 0,
|
||||
variables: {},
|
||||
isPublished: false,
|
||||
marketplaceData: null,
|
||||
})
|
||||
|
||||
await tx.insert(workflowBlocks).values({
|
||||
id: starterId,
|
||||
workflowId: workflowId,
|
||||
type: 'starter',
|
||||
name: 'Start',
|
||||
positionX: '100',
|
||||
positionY: '100',
|
||||
enabled: true,
|
||||
horizontalHandles: true,
|
||||
isWide: false,
|
||||
advancedMode: false,
|
||||
triggerMode: false,
|
||||
height: '95',
|
||||
subBlocks: {
|
||||
startWorkflow: {
|
||||
id: 'startWorkflow',
|
||||
type: 'dropdown',
|
||||
value: 'manual',
|
||||
},
|
||||
webhookPath: {
|
||||
id: 'webhookPath',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
webhookSecret: {
|
||||
id: 'webhookSecret',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
scheduleType: {
|
||||
id: 'scheduleType',
|
||||
type: 'dropdown',
|
||||
value: 'daily',
|
||||
},
|
||||
minutesInterval: {
|
||||
id: 'minutesInterval',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
minutesStartingAt: {
|
||||
id: 'minutesStartingAt',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
hourlyMinute: {
|
||||
id: 'hourlyMinute',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
dailyTime: {
|
||||
id: 'dailyTime',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
weeklyDay: {
|
||||
id: 'weeklyDay',
|
||||
type: 'dropdown',
|
||||
value: 'MON',
|
||||
},
|
||||
weeklyDayTime: {
|
||||
id: 'weeklyDayTime',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
monthlyDay: {
|
||||
id: 'monthlyDay',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
monthlyTime: {
|
||||
id: 'monthlyTime',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
cronExpression: {
|
||||
id: 'cronExpression',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
timezone: {
|
||||
id: 'timezone',
|
||||
type: 'dropdown',
|
||||
value: 'UTC',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
input: 'any',
|
||||
},
|
||||
},
|
||||
},
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Successfully created workflow ${workflowId} with start block in workflow_blocks table`
|
||||
)
|
||||
await db.insert(workflow).values({
|
||||
id: workflowId,
|
||||
userId: session.user.id,
|
||||
workspaceId: workspaceId || null,
|
||||
folderId: folderId || null,
|
||||
name,
|
||||
description,
|
||||
color,
|
||||
lastSynced: now,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
isDeployed: false,
|
||||
collaborators: [],
|
||||
runCount: 0,
|
||||
variables: {},
|
||||
isPublished: false,
|
||||
marketplaceData: null,
|
||||
})
|
||||
|
||||
logger.info(`[${requestId}] Successfully created empty workflow ${workflowId}`)
|
||||
|
||||
return NextResponse.json({
|
||||
id: workflowId,
|
||||
name,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { db } from '@sim/db'
|
||||
import { permissions, workflow, workflowBlocks, workspace } from '@sim/db/schema'
|
||||
import { permissions, workflow, workspace } from '@sim/db/schema'
|
||||
import { and, desc, eq, isNull } from 'drizzle-orm'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
@@ -110,9 +110,7 @@ async function createWorkspace(userId: string, name: string) {
|
||||
updatedAt: now,
|
||||
})
|
||||
|
||||
// Create initial workflow for the workspace with start block
|
||||
const starterId = crypto.randomUUID()
|
||||
|
||||
// Create initial workflow for the workspace (empty canvas)
|
||||
// Create the workflow
|
||||
await tx.insert(workflow).values({
|
||||
id: workflowId,
|
||||
@@ -133,61 +131,7 @@ async function createWorkspace(userId: string, name: string) {
|
||||
marketplaceData: null,
|
||||
})
|
||||
|
||||
// Insert the start block into workflow_blocks table
|
||||
await tx.insert(workflowBlocks).values({
|
||||
id: starterId,
|
||||
workflowId: workflowId,
|
||||
type: 'starter',
|
||||
name: 'Start',
|
||||
positionX: '100',
|
||||
positionY: '100',
|
||||
enabled: true,
|
||||
horizontalHandles: true,
|
||||
isWide: false,
|
||||
advancedMode: false,
|
||||
height: '95',
|
||||
subBlocks: {
|
||||
startWorkflow: {
|
||||
id: 'startWorkflow',
|
||||
type: 'dropdown',
|
||||
value: 'manual',
|
||||
},
|
||||
webhookPath: {
|
||||
id: 'webhookPath',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
webhookSecret: {
|
||||
id: 'webhookSecret',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
scheduleType: {
|
||||
id: 'scheduleType',
|
||||
type: 'dropdown',
|
||||
value: 'daily',
|
||||
},
|
||||
minutesInterval: {
|
||||
id: 'minutesInterval',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
minutesStartingAt: {
|
||||
id: 'minutesStartingAt',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
input: 'any',
|
||||
},
|
||||
},
|
||||
},
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
// No blocks are inserted - empty canvas
|
||||
|
||||
logger.info(
|
||||
`Created workspace ${workspaceId} with initial workflow ${workflowId} for user ${userId}`
|
||||
|
||||
@@ -4,6 +4,7 @@ import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent'
|
||||
import { generateRequestId } from '@/lib/utils'
|
||||
import { validateWorkflowState } from '@/lib/workflows/validation'
|
||||
import { getAllBlocks } from '@/blocks/registry'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { resolveOutputType } from '@/blocks/utils'
|
||||
@@ -61,6 +62,59 @@ const CreateDiffRequestSchema = z.object({
|
||||
.optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Convert blocks with 'inputs' field to standard 'subBlocks' structure
|
||||
* This handles trigger blocks that may come from YAML/copilot with legacy format
|
||||
*/
|
||||
function normalizeBlockStructure(blocks: Record<string, any>): Record<string, any> {
|
||||
const normalizedBlocks: Record<string, any> = {}
|
||||
|
||||
for (const [blockId, block] of Object.entries(blocks)) {
|
||||
const normalizedBlock = { ...block }
|
||||
|
||||
// Check if this is a trigger block with 'inputs' field
|
||||
if (
|
||||
block.inputs &&
|
||||
(block.type === 'api_trigger' ||
|
||||
block.type === 'input_trigger' ||
|
||||
block.type === 'starter' ||
|
||||
block.type === 'chat_trigger' ||
|
||||
block.type === 'generic_webhook')
|
||||
) {
|
||||
// Convert inputs.inputFormat to subBlocks.inputFormat
|
||||
if (block.inputs.inputFormat) {
|
||||
if (!normalizedBlock.subBlocks) {
|
||||
normalizedBlock.subBlocks = {}
|
||||
}
|
||||
|
||||
normalizedBlock.subBlocks.inputFormat = {
|
||||
id: 'inputFormat',
|
||||
type: 'input-format',
|
||||
value: block.inputs.inputFormat,
|
||||
}
|
||||
}
|
||||
|
||||
// Copy any other inputs fields to subBlocks
|
||||
for (const [inputKey, inputValue] of Object.entries(block.inputs)) {
|
||||
if (inputKey !== 'inputFormat' && !normalizedBlock.subBlocks[inputKey]) {
|
||||
normalizedBlock.subBlocks[inputKey] = {
|
||||
id: inputKey,
|
||||
type: 'short-input', // Default type, may need adjustment based on actual field
|
||||
value: inputValue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the inputs field after conversion
|
||||
normalizedBlock.inputs = undefined
|
||||
}
|
||||
|
||||
normalizedBlocks[blockId] = normalizedBlock
|
||||
}
|
||||
|
||||
return normalizedBlocks
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
@@ -202,6 +256,46 @@ export async function POST(request: NextRequest) {
|
||||
const finalResult = result
|
||||
|
||||
if (result.success && result.diff?.proposedState) {
|
||||
// Normalize blocks that use 'inputs' field to standard 'subBlocks' structure
|
||||
if (result.diff.proposedState.blocks) {
|
||||
result.diff.proposedState.blocks = normalizeBlockStructure(result.diff.proposedState.blocks)
|
||||
}
|
||||
|
||||
// Validate the proposed workflow state
|
||||
const validation = validateWorkflowState(result.diff.proposedState, { sanitize: true })
|
||||
|
||||
if (!validation.valid) {
|
||||
logger.error(`[${requestId}] Proposed workflow state validation failed`, {
|
||||
errors: validation.errors,
|
||||
warnings: validation.warnings,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
errors: validation.errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Use sanitized state if available
|
||||
if (validation.sanitizedState) {
|
||||
result.diff.proposedState = validation.sanitizedState
|
||||
}
|
||||
|
||||
if (validation.warnings.length > 0) {
|
||||
logger.warn(`[${requestId}] Proposed workflow validation warnings`, {
|
||||
warnings: validation.warnings,
|
||||
})
|
||||
// Include warnings in the response
|
||||
if (!result.warnings) {
|
||||
result.warnings = []
|
||||
}
|
||||
result.warnings.push(...validation.warnings)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Successfully created diff with normalized and validated blocks`)
|
||||
|
||||
// First, fix parent-child relationships based on edges
|
||||
const blocks = result.diff.proposedState.blocks
|
||||
const edges = result.diff.proposedState.edges || []
|
||||
@@ -271,6 +365,9 @@ export async function POST(request: NextRequest) {
|
||||
if (result.success && result.blocks && !result.diff) {
|
||||
logger.info(`[${requestId}] Transforming sim agent blocks response to diff format`)
|
||||
|
||||
// Normalize blocks that use 'inputs' field to standard 'subBlocks' structure
|
||||
result.blocks = normalizeBlockStructure(result.blocks)
|
||||
|
||||
// First, fix parent-child relationships based on edges
|
||||
const blocks = result.blocks
|
||||
const edges = result.edges || []
|
||||
|
||||
+8
-2
@@ -85,10 +85,16 @@ export function DeployModal({
|
||||
let inputFormatExample = ''
|
||||
try {
|
||||
const blocks = Object.values(useWorkflowStore.getState().blocks)
|
||||
|
||||
// Check for API trigger block first (takes precedence)
|
||||
const apiTriggerBlock = blocks.find((block) => block.type === 'api_trigger')
|
||||
// Fall back to legacy starter block
|
||||
const starterBlock = blocks.find((block) => block.type === 'starter')
|
||||
|
||||
if (starterBlock) {
|
||||
const inputFormat = useSubBlockStore.getState().getValue(starterBlock.id, 'inputFormat')
|
||||
const targetBlock = apiTriggerBlock || starterBlock
|
||||
|
||||
if (targetBlock) {
|
||||
const inputFormat = useSubBlockStore.getState().getValue(targetBlock.id, 'inputFormat')
|
||||
|
||||
if (inputFormat && Array.isArray(inputFormat) && inputFormat.length > 0) {
|
||||
const exampleData: Record<string, any> = {}
|
||||
|
||||
+28
-18
@@ -330,6 +330,25 @@ export function Chat({ chatMessage, setChatMessage }: ChatProps) {
|
||||
|
||||
if (event === 'final' && data) {
|
||||
const result = data as ExecutionResult
|
||||
|
||||
// If final result is a failure, surface error and stop
|
||||
if ('success' in result && !result.success) {
|
||||
addMessage({
|
||||
content: `Error: ${result.error || 'Workflow execution failed'}`,
|
||||
workflowId: activeWorkflowId,
|
||||
type: 'workflow',
|
||||
})
|
||||
|
||||
// Clear any existing message streams
|
||||
for (const msgId of messageIdMap.values()) {
|
||||
finalizeMessageStream(msgId)
|
||||
}
|
||||
messageIdMap.clear()
|
||||
|
||||
// Stop processing
|
||||
return
|
||||
}
|
||||
|
||||
const nonStreamingLogs =
|
||||
result.logs?.filter((log) => !messageIdMap.has(log.blockId)) || []
|
||||
|
||||
@@ -343,34 +362,25 @@ export function Chat({ chatMessage, setChatMessage }: ChatProps) {
|
||||
const blockIdForOutput = extractBlockIdFromOutputId(outputId)
|
||||
const path = extractPathFromOutputId(outputId, blockIdForOutput)
|
||||
const log = nonStreamingLogs.find((l) => l.blockId === blockIdForOutput)
|
||||
|
||||
if (log) {
|
||||
let outputValue: any = log.output
|
||||
|
||||
let output = log.output
|
||||
if (path) {
|
||||
// Parse JSON content safely
|
||||
outputValue = parseOutputContentSafely(outputValue)
|
||||
|
||||
output = parseOutputContentSafely(output)
|
||||
const pathParts = path.split('.')
|
||||
let current = output
|
||||
for (const part of pathParts) {
|
||||
if (
|
||||
outputValue &&
|
||||
typeof outputValue === 'object' &&
|
||||
part in outputValue
|
||||
) {
|
||||
outputValue = outputValue[part]
|
||||
if (current && typeof current === 'object' && part in current) {
|
||||
current = current[part]
|
||||
} else {
|
||||
outputValue = undefined
|
||||
current = undefined
|
||||
break
|
||||
}
|
||||
}
|
||||
output = current
|
||||
}
|
||||
if (outputValue !== undefined) {
|
||||
if (output !== undefined) {
|
||||
addMessage({
|
||||
content:
|
||||
typeof outputValue === 'string'
|
||||
? outputValue
|
||||
: `\`\`\`json\n${JSON.stringify(outputValue, null, 2)}\n\`\`\``,
|
||||
content: typeof output === 'string' ? output : JSON.stringify(output),
|
||||
workflowId: activeWorkflowId,
|
||||
type: 'workflow',
|
||||
})
|
||||
|
||||
+5
-2
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { format } from 'date-fns'
|
||||
import {
|
||||
AlertCircle,
|
||||
AlertTriangle,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
@@ -369,8 +370,10 @@ export function ConsoleEntry({ entry, consoleWidth }: ConsoleEntryProps) {
|
||||
}
|
||||
}, [showCopySuccess])
|
||||
|
||||
const BlockIcon = blockConfig?.icon
|
||||
const blockColor = blockConfig?.bgColor || '#6B7280'
|
||||
// Special handling for serialization errors
|
||||
const BlockIcon = entry.blockType === 'serializer' ? AlertTriangle : blockConfig?.icon
|
||||
const blockColor =
|
||||
entry.blockType === 'serializer' ? '#EF4444' : blockConfig?.bgColor || '#6B7280'
|
||||
|
||||
// Handle image load error callback
|
||||
const handleImageLoadError = (hasError: boolean) => {
|
||||
|
||||
+42
-40
@@ -456,46 +456,6 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
|
||||
) : null}
|
||||
|
||||
<div className='flex items-center justify-end gap-0'>
|
||||
{hasCheckpoints && (
|
||||
<div className='mr-1 inline-flex items-center justify-center'>
|
||||
{showRestoreConfirmation ? (
|
||||
<div className='inline-flex items-center gap-1'>
|
||||
<button
|
||||
onClick={handleConfirmRevert}
|
||||
disabled={isRevertingCheckpoint}
|
||||
className='text-muted-foreground transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50'
|
||||
title='Confirm restore'
|
||||
aria-label='Confirm restore'
|
||||
>
|
||||
{isRevertingCheckpoint ? (
|
||||
<Loader2 className='h-3 w-3 animate-spin' />
|
||||
) : (
|
||||
<Check className='h-3 w-3' />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancelRevert}
|
||||
disabled={isRevertingCheckpoint}
|
||||
className='text-muted-foreground transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50'
|
||||
title='Cancel restore'
|
||||
aria-label='Cancel restore'
|
||||
>
|
||||
<X className='h-3 w-3' />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleRevertToCheckpoint}
|
||||
disabled={isRevertingCheckpoint}
|
||||
className='text-muted-foreground transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50'
|
||||
title='Restore workflow to this checkpoint state'
|
||||
aria-label='Restore'
|
||||
>
|
||||
<RotateCcw className='h-3 w-3' />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className='min-w-0 max-w-[80%]'>
|
||||
{/* Message content in purple box */}
|
||||
<div
|
||||
@@ -544,6 +504,48 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
{hasCheckpoints && (
|
||||
<div className='mt-1 flex h-6 items-center justify-end'>
|
||||
{showRestoreConfirmation ? (
|
||||
<div className='inline-flex items-center gap-1 rounded px-1 py-0.5 text-[11px] text-muted-foreground'>
|
||||
<span>Restore Checkpoint?</span>
|
||||
<button
|
||||
onClick={handleConfirmRevert}
|
||||
disabled={isRevertingCheckpoint}
|
||||
className='transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50'
|
||||
title='Confirm restore'
|
||||
aria-label='Confirm restore'
|
||||
>
|
||||
{isRevertingCheckpoint ? (
|
||||
<Loader2 className='h-3 w-3 animate-spin' />
|
||||
) : (
|
||||
<Check className='h-3 w-3' />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancelRevert}
|
||||
disabled={isRevertingCheckpoint}
|
||||
className='transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50'
|
||||
title='Cancel restore'
|
||||
aria-label='Cancel restore'
|
||||
>
|
||||
<X className='h-3 w-3' />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleRevertToCheckpoint}
|
||||
disabled={isRevertingCheckpoint}
|
||||
className='inline-flex items-center gap-1 text-muted-foreground transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50'
|
||||
title='Restore workflow to this checkpoint state'
|
||||
aria-label='Restore'
|
||||
>
|
||||
<span className='text-[11px]'>Restore</span>
|
||||
<RotateCcw className='h-3 w-3' />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+211
-116
@@ -51,7 +51,6 @@ import {
|
||||
import { useSession } from '@/lib/auth-client'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { CopilotSlider } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/copilot-slider'
|
||||
import { useCopilotStore } from '@/stores/copilot/store'
|
||||
import type { ChatContext } from '@/stores/copilot/types'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
@@ -122,6 +121,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [dragCounter, setDragCounter] = useState(0)
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const overlayRef = useRef<HTMLDivElement>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [showMentionMenu, setShowMentionMenu] = useState(false)
|
||||
const mentionMenuRef = useRef<HTMLDivElement>(null)
|
||||
@@ -319,15 +319,37 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
|
||||
// Auto-resize textarea and toggle vertical scroll when exceeding max height
|
||||
useEffect(() => {
|
||||
const textarea = textareaRef.current
|
||||
const overlay = overlayRef.current
|
||||
if (textarea) {
|
||||
const maxHeight = 120
|
||||
textarea.style.height = 'auto'
|
||||
const nextHeight = Math.min(textarea.scrollHeight, maxHeight)
|
||||
textarea.style.height = `${nextHeight}px`
|
||||
textarea.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden'
|
||||
|
||||
// Also update overlay height to match
|
||||
if (overlay) {
|
||||
overlay.style.height = `${nextHeight}px`
|
||||
overlay.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden'
|
||||
}
|
||||
}
|
||||
}, [message])
|
||||
|
||||
// Sync scroll position between textarea and overlay
|
||||
useEffect(() => {
|
||||
const textarea = textareaRef.current
|
||||
const overlay = overlayRef.current
|
||||
|
||||
if (!textarea || !overlay) return
|
||||
|
||||
const handleScroll = () => {
|
||||
overlay.scrollTop = textarea.scrollTop
|
||||
}
|
||||
|
||||
textarea.addEventListener('scroll', handleScroll)
|
||||
return () => textarea.removeEventListener('scroll', handleScroll)
|
||||
}, [])
|
||||
|
||||
// Close mention menu on outside click
|
||||
useEffect(() => {
|
||||
if (!showMentionMenu) return
|
||||
@@ -1754,55 +1776,44 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
|
||||
return 'Agent'
|
||||
}
|
||||
|
||||
// Depth toggle state comes from global store; access via useCopilotStore
|
||||
const { agentDepth, agentPrefetch, setAgentDepth, setAgentPrefetch } = useCopilotStore()
|
||||
// Model selection state comes from global store; access via useCopilotStore
|
||||
const { selectedModel, agentPrefetch, setSelectedModel, setAgentPrefetch } = useCopilotStore()
|
||||
|
||||
// Ensure MAX mode is off for Fast and Balanced depths
|
||||
useEffect(() => {
|
||||
if (agentDepth < 2 && !agentPrefetch) {
|
||||
setAgentPrefetch(true)
|
||||
}
|
||||
}, [agentDepth, agentPrefetch, setAgentPrefetch])
|
||||
|
||||
const cycleDepth = () => {
|
||||
// 8 modes: depths 0-3, each with prefetch off/on. Cycle depth, then toggle prefetch when wrapping.
|
||||
const nextDepth = agentDepth === 3 ? 0 : ((agentDepth + 1) as 0 | 1 | 2 | 3)
|
||||
if (nextDepth === 0 && agentDepth === 3) {
|
||||
setAgentPrefetch(!agentPrefetch)
|
||||
}
|
||||
setAgentDepth(nextDepth)
|
||||
}
|
||||
// Model configurations with their display names
|
||||
const modelOptions = [
|
||||
{ value: 'gpt-5-fast', label: 'gpt-5-fast' },
|
||||
{ value: 'gpt-5', label: 'gpt-5' },
|
||||
{ value: 'gpt-5-medium', label: 'gpt-5-medium' },
|
||||
{ value: 'gpt-5-high', label: 'gpt-5-high' },
|
||||
{ value: 'gpt-4o', label: 'gpt-4o' },
|
||||
{ value: 'gpt-4.1', label: 'gpt-4.1' },
|
||||
{ value: 'o3', label: 'o3' },
|
||||
{ value: 'claude-4-sonnet', label: 'claude-4-sonnet' },
|
||||
{ value: 'claude-4.1-opus', label: 'claude-4.1-opus' },
|
||||
] as const
|
||||
|
||||
const getCollapsedModeLabel = () => {
|
||||
const base = getDepthLabelFor(agentDepth)
|
||||
return !agentPrefetch ? `${base} MAX` : base
|
||||
const model = modelOptions.find((m) => m.value === selectedModel)
|
||||
return model ? model.label : 'GPT-5 Default'
|
||||
}
|
||||
|
||||
const getDepthLabelFor = (value: 0 | 1 | 2 | 3) => {
|
||||
return value === 0 ? 'Fast' : value === 1 ? 'Balanced' : value === 2 ? 'Advanced' : 'Behemoth'
|
||||
}
|
||||
|
||||
// Removed descriptive suffixes; concise labels only
|
||||
const getDepthDescription = (value: 0 | 1 | 2 | 3) => {
|
||||
if (value === 0)
|
||||
return 'Fastest and cheapest. Good for small edits, simple workflows, and small tasks'
|
||||
if (value === 1) return 'Balances speed and reasoning. Good fit for most tasks'
|
||||
if (value === 2)
|
||||
return 'More reasoning for larger workflows and complex edits, still balanced for speed'
|
||||
return 'Maximum reasoning power. Best for complex workflow building and debugging'
|
||||
}
|
||||
|
||||
const getDepthIconFor = (value: 0 | 1 | 2 | 3) => {
|
||||
const getModelIcon = () => {
|
||||
const colorClass = !agentPrefetch
|
||||
? 'text-[var(--brand-primary-hover-hex)]'
|
||||
: 'text-muted-foreground'
|
||||
if (value === 0) return <Zap className={`h-3 w-3 ${colorClass}`} />
|
||||
if (value === 1) return <InfinityIcon className={`h-3 w-3 ${colorClass}`} />
|
||||
if (value === 2) return <Brain className={`h-3 w-3 ${colorClass}`} />
|
||||
return <BrainCircuit className={`h-3 w-3 ${colorClass}`} />
|
||||
}
|
||||
|
||||
const getDepthIcon = () => getDepthIconFor(agentDepth)
|
||||
// Match the dropdown icon logic exactly
|
||||
if (['gpt-5-high', 'o3', 'claude-4.1-opus'].includes(selectedModel)) {
|
||||
return <BrainCircuit className={`h-3 w-3 ${colorClass}`} />
|
||||
}
|
||||
if (['gpt-5', 'gpt-5-medium', 'claude-4-sonnet'].includes(selectedModel)) {
|
||||
return <Brain className={`h-3 w-3 ${colorClass}`} />
|
||||
}
|
||||
if (['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(selectedModel)) {
|
||||
return <Zap className={`h-3 w-3 ${colorClass}`} />
|
||||
}
|
||||
return <InfinityIcon className={`h-3 w-3 ${colorClass}`} />
|
||||
}
|
||||
|
||||
const scrollActiveItemIntoView = (index: number) => {
|
||||
const container = menuListRef.current
|
||||
@@ -2116,8 +2127,11 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
|
||||
{/* Textarea Field with overlay */}
|
||||
<div className='relative'>
|
||||
{/* Highlight overlay */}
|
||||
<div className='pointer-events-none absolute inset-0 z-[1] px-[2px] py-1'>
|
||||
<pre className='whitespace-pre-wrap font-sans text-foreground text-sm leading-[1.25rem]'>
|
||||
<div
|
||||
ref={overlayRef}
|
||||
className='pointer-events-none absolute inset-0 z-[1] max-h-[120px] overflow-y-auto overflow-x-hidden px-[2px] py-1 [&::-webkit-scrollbar]:hidden'
|
||||
>
|
||||
<pre className='whitespace-pre-wrap break-words font-sans text-foreground text-sm leading-[1.25rem]'>
|
||||
{(() => {
|
||||
const elements: React.ReactNode[] = []
|
||||
const remaining = message
|
||||
@@ -2163,8 +2177,8 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
|
||||
placeholder={isDragging ? 'Drop files here...' : effectivePlaceholder}
|
||||
disabled={disabled}
|
||||
rows={1}
|
||||
className='relative z-[2] mb-2 min-h-[32px] w-full resize-none overflow-y-auto overflow-x-hidden border-0 bg-transparent px-[2px] py-1 font-sans text-sm text-transparent leading-[1.25rem] caret-foreground focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
style={{ height: 'auto' }}
|
||||
className='relative z-[2] mb-2 min-h-[32px] w-full resize-none overflow-y-auto overflow-x-hidden break-words border-0 bg-transparent px-[2px] py-1 font-sans text-sm text-transparent leading-[1.25rem] caret-foreground focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
style={{ height: 'auto', wordBreak: 'break-word' }}
|
||||
/>
|
||||
|
||||
{showMentionMenu && (
|
||||
@@ -3057,7 +3071,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
|
||||
<span>{getModeText()}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='start' className='p-0'>
|
||||
<DropdownMenuContent align='start' side='top' className='p-0'>
|
||||
<TooltipProvider>
|
||||
<div className='w-[160px] p-1'>
|
||||
<Tooltip>
|
||||
@@ -3131,85 +3145,166 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
|
||||
)}
|
||||
title='Choose mode'
|
||||
>
|
||||
{getDepthIcon()}
|
||||
<span>{getCollapsedModeLabel()}</span>
|
||||
{getModelIcon()}
|
||||
<span>
|
||||
{getCollapsedModeLabel()}
|
||||
{!agentPrefetch &&
|
||||
!['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(selectedModel) && (
|
||||
<span className='ml-1 font-semibold'>MAX</span>
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='start' className='p-0'>
|
||||
<DropdownMenuContent align='start' side='top' className='max-h-[400px] p-0'>
|
||||
<TooltipProvider delayDuration={100} skipDelayDuration={0}>
|
||||
<div className='w-[260px] p-3'>
|
||||
<div className='mb-3 flex items-center justify-between'>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<span className='font-medium text-xs'>MAX mode</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type='button'
|
||||
className='h-3.5 w-3.5 rounded text-muted-foreground transition-colors hover:text-foreground'
|
||||
aria-label='MAX mode info'
|
||||
>
|
||||
<Info className='h-3.5 w-3.5' />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side='right'
|
||||
sideOffset={6}
|
||||
align='center'
|
||||
className='max-w-[220px] border bg-popover p-2 text-[11px] text-popover-foreground leading-snug shadow-md'
|
||||
>
|
||||
Significantly increases depth of reasoning
|
||||
<br />
|
||||
<span className='text-[10px] text-muted-foreground italic'>
|
||||
Only available in Advanced and Behemoth modes
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Switch
|
||||
checked={!agentPrefetch}
|
||||
disabled={agentDepth < 2}
|
||||
title={
|
||||
agentDepth < 2
|
||||
? 'MAX mode is only available for Advanced or Expert'
|
||||
: undefined
|
||||
}
|
||||
onCheckedChange={(checked) => {
|
||||
if (agentDepth < 2) return
|
||||
setAgentPrefetch(!checked)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='my-2 flex justify-center'>
|
||||
<div className='h-px w-[100%] bg-border' />
|
||||
</div>
|
||||
<div className='mb-3'>
|
||||
<div className='w-[220px]'>
|
||||
<div className='p-2 pb-0'>
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<span className='font-medium text-xs'>Mode</span>
|
||||
<div className='flex items-center gap-1'>
|
||||
{getDepthIconFor(agentDepth)}
|
||||
<span className='text-muted-foreground text-xs'>
|
||||
{getDepthLabelFor(agentDepth)}
|
||||
</span>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<span className='font-medium text-xs'>MAX mode</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type='button'
|
||||
className='h-3.5 w-3.5 rounded text-muted-foreground transition-colors hover:text-foreground'
|
||||
aria-label='MAX mode info'
|
||||
>
|
||||
<Info className='h-3.5 w-3.5' />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side='right'
|
||||
sideOffset={6}
|
||||
align='center'
|
||||
className='max-w-[220px] border bg-popover p-2 text-[11px] text-popover-foreground leading-snug shadow-md'
|
||||
>
|
||||
Significantly increases depth of reasoning
|
||||
<br />
|
||||
<span className='text-[10px] text-muted-foreground italic'>
|
||||
Only available for advanced models
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className='relative'>
|
||||
<CopilotSlider
|
||||
min={0}
|
||||
max={3}
|
||||
step={1}
|
||||
value={[agentDepth]}
|
||||
onValueChange={(val) =>
|
||||
setAgentDepth((val?.[0] ?? 0) as 0 | 1 | 2 | 3)
|
||||
<Switch
|
||||
checked={!agentPrefetch}
|
||||
disabled={['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(selectedModel)}
|
||||
title={
|
||||
['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(selectedModel)
|
||||
? 'MAX mode is only available for advanced models'
|
||||
: undefined
|
||||
}
|
||||
onCheckedChange={(checked) => {
|
||||
if (['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(selectedModel))
|
||||
return
|
||||
setAgentPrefetch(!checked)
|
||||
}}
|
||||
/>
|
||||
<div className='pointer-events-none absolute inset-0'>
|
||||
<div className='-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-[33.333%] h-2 w-[3px] bg-background' />
|
||||
<div className='-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-[66.667%] h-2 w-[3px] bg-background' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='my-1.5 flex justify-center'>
|
||||
<div className='h-px w-[100%] bg-border' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-3 text-[11px] text-muted-foreground'>
|
||||
{getDepthDescription(agentDepth)}
|
||||
<div className='max-h-[280px] overflow-y-auto px-2 pb-2'>
|
||||
<div>
|
||||
<div className='mb-1'>
|
||||
<span className='font-medium text-xs'>Model</span>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
{/* Helper function to get icon for a model */}
|
||||
{(() => {
|
||||
const getModelIcon = (modelValue: string) => {
|
||||
if (
|
||||
['gpt-5-high', 'o3', 'claude-4.1-opus'].includes(modelValue)
|
||||
) {
|
||||
return (
|
||||
<BrainCircuit className='h-3 w-3 text-muted-foreground' />
|
||||
)
|
||||
}
|
||||
if (
|
||||
['gpt-5', 'gpt-5-medium', 'claude-4-sonnet'].includes(
|
||||
modelValue
|
||||
)
|
||||
) {
|
||||
return <Brain className='h-3 w-3 text-muted-foreground' />
|
||||
}
|
||||
if (['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(modelValue)) {
|
||||
return <Zap className='h-3 w-3 text-muted-foreground' />
|
||||
}
|
||||
return <div className='h-3 w-3' />
|
||||
}
|
||||
|
||||
const renderModelOption = (
|
||||
option: (typeof modelOptions)[number]
|
||||
) => (
|
||||
<DropdownMenuItem
|
||||
key={option.value}
|
||||
onSelect={() => {
|
||||
setSelectedModel(option.value)
|
||||
// Automatically turn off max mode for fast models (Zap icon)
|
||||
if (
|
||||
['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(
|
||||
option.value
|
||||
) &&
|
||||
!agentPrefetch
|
||||
) {
|
||||
setAgentPrefetch(true)
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'flex h-7 items-center gap-1.5 px-2 py-1 text-left text-xs',
|
||||
selectedModel === option.value ? 'bg-muted/50' : ''
|
||||
)}
|
||||
>
|
||||
{getModelIcon(option.value)}
|
||||
<span>{option.label}</span>
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* OpenAI Models */}
|
||||
<div>
|
||||
<div className='px-2 py-1 font-medium text-[10px] text-muted-foreground uppercase'>
|
||||
OpenAI
|
||||
</div>
|
||||
<div className='space-y-0.5'>
|
||||
{modelOptions
|
||||
.filter((option) =>
|
||||
[
|
||||
'gpt-5-fast',
|
||||
'gpt-5',
|
||||
'gpt-5-medium',
|
||||
'gpt-5-high',
|
||||
'gpt-4o',
|
||||
'gpt-4.1',
|
||||
'o3',
|
||||
].includes(option.value)
|
||||
)
|
||||
.map(renderModelOption)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Anthropic Models */}
|
||||
<div>
|
||||
<div className='px-2 py-1 font-medium text-[10px] text-muted-foreground uppercase'>
|
||||
Anthropic
|
||||
</div>
|
||||
<div className='space-y-0.5'>
|
||||
{modelOptions
|
||||
.filter((option) =>
|
||||
['claude-4-sonnet', 'claude-4.1-opus'].includes(
|
||||
option.value
|
||||
)
|
||||
)
|
||||
.map(renderModelOption)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Info, Plus, Search, X } from 'lucide-react'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { getAllTriggerBlocks, getTriggerDisplayName } from '@/lib/workflows/trigger-utils'
|
||||
|
||||
const logger = createLogger('TriggerList')
|
||||
|
||||
interface TriggerListProps {
|
||||
onSelect: (triggerId: string, enableTriggerMode?: boolean) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function TriggerList({ onSelect, className }: TriggerListProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [showList, setShowList] = useState(false)
|
||||
const listRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Get all trigger options from the centralized source
|
||||
const triggerOptions = useMemo(() => getAllTriggerBlocks(), [])
|
||||
|
||||
// Handle escape key
|
||||
useEffect(() => {
|
||||
if (!showList) return
|
||||
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
logger.info('Closing trigger list via escape key')
|
||||
setShowList(false)
|
||||
setSearchQuery('')
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscape)
|
||||
}
|
||||
}, [showList])
|
||||
|
||||
const filteredOptions = useMemo(() => {
|
||||
if (!searchQuery.trim()) return triggerOptions
|
||||
|
||||
const query = searchQuery.toLowerCase()
|
||||
return triggerOptions.filter(
|
||||
(option) =>
|
||||
option.name.toLowerCase().includes(query) ||
|
||||
option.description.toLowerCase().includes(query)
|
||||
)
|
||||
}, [searchQuery, triggerOptions])
|
||||
|
||||
const coreOptions = useMemo(
|
||||
() => filteredOptions.filter((opt) => opt.category === 'core'),
|
||||
[filteredOptions]
|
||||
)
|
||||
|
||||
const integrationOptions = useMemo(
|
||||
() => filteredOptions.filter((opt) => opt.category === 'integration'),
|
||||
[filteredOptions]
|
||||
)
|
||||
|
||||
const handleTriggerClick = (triggerId: string, enableTriggerMode?: boolean) => {
|
||||
logger.info('Trigger selected', { triggerId, enableTriggerMode })
|
||||
onSelect(triggerId, enableTriggerMode)
|
||||
// Reset state after selection
|
||||
setShowList(false)
|
||||
setSearchQuery('')
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
logger.info('Closing trigger list via X button')
|
||||
setShowList(false)
|
||||
setSearchQuery('')
|
||||
}
|
||||
|
||||
const TriggerItem = ({ trigger }: { trigger: (typeof triggerOptions)[0] }) => {
|
||||
const Icon = trigger.icon
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-10 w-[200px] flex-shrink-0 cursor-pointer items-center gap-[10px] rounded-[8px] border px-1.5 transition-all duration-200',
|
||||
'border-border/40 bg-background/60 hover:border-border hover:bg-secondary/80'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
onClick={() => handleTriggerClick(trigger.id, trigger.enableTriggerMode)}
|
||||
className='flex flex-1 items-center gap-[10px]'
|
||||
>
|
||||
<div
|
||||
className='relative flex h-6 w-6 shrink-0 items-center justify-center overflow-hidden rounded-[6px]'
|
||||
style={{ backgroundColor: trigger.color }}
|
||||
>
|
||||
{Icon ? (
|
||||
<Icon className='!h-4 !w-4 text-white' />
|
||||
) : (
|
||||
<div className='h-4 w-4 rounded bg-white/20' />
|
||||
)}
|
||||
</div>
|
||||
<span className='flex-1 truncate font-medium text-sm leading-none'>
|
||||
{getTriggerDisplayName(trigger.id)}
|
||||
</span>
|
||||
</div>
|
||||
<Tooltip delayDuration={300}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className='flex h-6 w-6 items-center justify-center rounded-md'
|
||||
>
|
||||
<Info className='h-3.5 w-3.5 text-muted-foreground' />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side='top'
|
||||
sideOffset={5}
|
||||
className='z-[9999] max-w-[200px]'
|
||||
align='center'
|
||||
avoidCollisions={false}
|
||||
>
|
||||
<p className='text-xs'>{trigger.description}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none absolute inset-0 flex items-center justify-center',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!showList ? (
|
||||
/* Initial Button State */
|
||||
<button
|
||||
onClick={() => {
|
||||
logger.info('Opening trigger list')
|
||||
setShowList(true)
|
||||
}}
|
||||
className={cn(
|
||||
'pointer-events-auto',
|
||||
'flex items-center gap-2',
|
||||
'px-4 py-2',
|
||||
'rounded-lg border border-muted-foreground/50 border-dashed',
|
||||
'bg-background/95 backdrop-blur-sm',
|
||||
'hover:border-muted-foreground hover:bg-muted',
|
||||
'transition-all duration-200',
|
||||
'font-medium text-muted-foreground text-sm'
|
||||
)}
|
||||
>
|
||||
<Plus className='h-4 w-4' />
|
||||
Click to Add Trigger
|
||||
</button>
|
||||
) : (
|
||||
/* Trigger List View */
|
||||
<div
|
||||
ref={listRef}
|
||||
className={cn(
|
||||
'pointer-events-auto',
|
||||
'max-h-[400px] w-[650px]',
|
||||
'rounded-xl border border-border',
|
||||
'bg-background/95 backdrop-blur-sm',
|
||||
'shadow-lg',
|
||||
'flex flex-col',
|
||||
'relative'
|
||||
)}
|
||||
>
|
||||
{/* Search - matching search modal exactly */}
|
||||
<div className='flex items-center border-b px-4 py-1'>
|
||||
<Search className='h-4 w-4 font-sans text-muted-foreground text-xl' />
|
||||
<Input
|
||||
placeholder='Search triggers'
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className='!font-[350] border-0 bg-transparent font-sans text-muted-foreground leading-10 tracking-normal placeholder:text-muted-foreground focus:outline-none focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className='absolute top-4 right-4 h-4 w-4 p-0 text-muted-foreground transition-colors hover:bg-transparent hover:text-foreground focus:outline-none disabled:pointer-events-none'
|
||||
tabIndex={-1}
|
||||
>
|
||||
<X className='h-4 w-4' />
|
||||
<span className='sr-only'>Close</span>
|
||||
</button>
|
||||
|
||||
{/* Trigger List */}
|
||||
<div
|
||||
className='flex-1 overflow-y-auto'
|
||||
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
||||
>
|
||||
<div className='space-y-4 pt-4 pb-4'>
|
||||
{/* Core Triggers Section */}
|
||||
{coreOptions.length > 0 && (
|
||||
<div>
|
||||
<h3 className='mb-2 ml-4 font-normal font-sans text-[13px] text-muted-foreground leading-none tracking-normal'>
|
||||
Core Triggers
|
||||
</h3>
|
||||
<div className='px-4 pb-1'>
|
||||
{/* Display triggers in a 3-column grid */}
|
||||
<div className='grid grid-cols-3 gap-2'>
|
||||
{coreOptions.map((trigger) => (
|
||||
<TriggerItem key={trigger.id} trigger={trigger} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Integration Triggers Section */}
|
||||
{integrationOptions.length > 0 && (
|
||||
<div>
|
||||
<h3 className='mb-2 ml-4 font-normal font-sans text-[13px] text-muted-foreground leading-none tracking-normal'>
|
||||
Integration Triggers
|
||||
</h3>
|
||||
<div
|
||||
className='max-h-[200px] overflow-y-auto px-4 pb-1'
|
||||
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
||||
>
|
||||
{/* Display triggers in a 3-column grid */}
|
||||
<div className='grid grid-cols-3 gap-2'>
|
||||
{integrationOptions.map((trigger) => (
|
||||
<TriggerItem key={trigger.id} trigger={trigger} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredOptions.length === 0 && (
|
||||
<div className='ml-6 py-12 text-center'>
|
||||
<p className='text-muted-foreground'>No results found for "{searchQuery}"</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
|
||||
export enum TriggerWarningType {
|
||||
DUPLICATE_TRIGGER = 'duplicate_trigger',
|
||||
LEGACY_INCOMPATIBILITY = 'legacy_incompatibility',
|
||||
}
|
||||
|
||||
interface TriggerWarningDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
triggerName: string
|
||||
type: TriggerWarningType
|
||||
}
|
||||
|
||||
export function TriggerWarningDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
triggerName,
|
||||
type,
|
||||
}: TriggerWarningDialogProps) {
|
||||
const getTitle = () => {
|
||||
switch (type) {
|
||||
case TriggerWarningType.LEGACY_INCOMPATIBILITY:
|
||||
return 'Cannot mix trigger types'
|
||||
case TriggerWarningType.DUPLICATE_TRIGGER:
|
||||
return `Only one ${triggerName} trigger allowed`
|
||||
}
|
||||
}
|
||||
|
||||
const getDescription = () => {
|
||||
switch (type) {
|
||||
case TriggerWarningType.LEGACY_INCOMPATIBILITY:
|
||||
return 'Cannot add new trigger blocks when a legacy Start block exists. Available in newer workflows.'
|
||||
case TriggerWarningType.DUPLICATE_TRIGGER:
|
||||
return `A workflow can only have one ${triggerName} trigger block. Please remove the existing one before adding a new one.`
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{getTitle()}</AlertDialogTitle>
|
||||
<AlertDialogDescription>{getDescription()}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction onClick={() => onOpenChange(false)}>Got it</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
+1
@@ -10,6 +10,7 @@ export { EvalInput } from './eval-input'
|
||||
export { FileSelectorInput } from './file-selector/file-selector-input'
|
||||
export { FileUpload } from './file-upload'
|
||||
export { FolderSelectorInput } from './folder-selector/components/folder-selector-input'
|
||||
export { InputMapping } from './input-mapping/input-mapping'
|
||||
export { KnowledgeBaseSelector } from './knowledge-base-selector/knowledge-base-selector'
|
||||
export { LongInput } from './long-input'
|
||||
export { McpDynamicArgs } from './mcp-dynamic-args/mcp-dynamic-args'
|
||||
|
||||
+339
@@ -0,0 +1,339 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
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 '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
interface InputFormatField {
|
||||
name: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
interface InputTriggerBlock {
|
||||
type: 'input_trigger'
|
||||
subBlocks?: {
|
||||
inputFormat?: { value?: InputFormatField[] }
|
||||
}
|
||||
}
|
||||
|
||||
interface StarterBlockLegacy {
|
||||
type: 'starter'
|
||||
subBlocks?: {
|
||||
inputFormat?: { value?: InputFormatField[] }
|
||||
}
|
||||
config?: {
|
||||
params?: {
|
||||
inputFormat?: InputFormatField[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isInputTriggerBlock(value: unknown): value is InputTriggerBlock {
|
||||
return (
|
||||
!!value && typeof value === 'object' && (value as { type?: unknown }).type === 'input_trigger'
|
||||
)
|
||||
}
|
||||
|
||||
function isStarterBlock(value: unknown): value is StarterBlockLegacy {
|
||||
return !!value && typeof value === 'object' && (value as { type?: unknown }).type === 'starter'
|
||||
}
|
||||
|
||||
function isInputFormatField(value: unknown): value is InputFormatField {
|
||||
if (typeof value !== 'object' || value === null) return false
|
||||
if (!('name' in value)) return false
|
||||
const { name, type } = value as { name: unknown; type?: unknown }
|
||||
if (typeof name !== 'string' || name.trim() === '') return false
|
||||
if (type !== undefined && typeof type !== 'string') return false
|
||||
return true
|
||||
}
|
||||
|
||||
interface InputMappingProps {
|
||||
blockId: string
|
||||
subBlockId: string
|
||||
isPreview?: boolean
|
||||
previewValue?: any
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
// Simple mapping UI: for each field in child Input Trigger's inputFormat, render an input with TagDropdown support
|
||||
export function InputMapping({
|
||||
blockId,
|
||||
subBlockId,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
disabled = false,
|
||||
}: InputMappingProps) {
|
||||
const [mapping, setMapping] = useSubBlockValue(blockId, subBlockId)
|
||||
const [selectedWorkflowId] = useSubBlockValue(blockId, 'workflowId')
|
||||
|
||||
const { workflows } = useWorkflowRegistry.getState()
|
||||
|
||||
// Fetch child workflow state via registry API endpoint, using cached metadata when possible
|
||||
// Here we rely on live store; the serializer/executor will resolve at runtime too.
|
||||
// We only need the inputFormat from an Input Trigger in the selected child workflow state.
|
||||
const [childInputFields, setChildInputFields] = useState<Array<{ name: string; type?: string }>>(
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true
|
||||
const controller = new AbortController()
|
||||
async function fetchChildSchema() {
|
||||
try {
|
||||
if (!selectedWorkflowId) {
|
||||
if (isMounted) setChildInputFields([])
|
||||
return
|
||||
}
|
||||
const res = await fetch(`/api/workflows/${selectedWorkflowId}`, {
|
||||
signal: controller.signal,
|
||||
})
|
||||
if (!res.ok) {
|
||||
if (isMounted) setChildInputFields([])
|
||||
return
|
||||
}
|
||||
const { data } = await res.json()
|
||||
const blocks = (data?.state?.blocks as Record<string, unknown>) || {}
|
||||
// Prefer new input_trigger
|
||||
const triggerEntry = Object.entries(blocks).find(([, b]) => isInputTriggerBlock(b))
|
||||
if (triggerEntry && isInputTriggerBlock(triggerEntry[1])) {
|
||||
const inputFormat = triggerEntry[1].subBlocks?.inputFormat?.value
|
||||
if (Array.isArray(inputFormat)) {
|
||||
const fields = (inputFormat as unknown[])
|
||||
.filter(isInputFormatField)
|
||||
.map((f) => ({ name: f.name, type: f.type }))
|
||||
if (isMounted) setChildInputFields(fields)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: legacy starter block inputFormat (subBlocks or config.params)
|
||||
const starterEntry = Object.entries(blocks).find(([, b]) => isStarterBlock(b))
|
||||
if (starterEntry && isStarterBlock(starterEntry[1])) {
|
||||
const starter = starterEntry[1]
|
||||
const subBlockFormat = starter.subBlocks?.inputFormat?.value
|
||||
const legacyParamsFormat = starter.config?.params?.inputFormat
|
||||
const chosen = Array.isArray(subBlockFormat) ? subBlockFormat : legacyParamsFormat
|
||||
if (Array.isArray(chosen)) {
|
||||
const fields = (chosen as unknown[])
|
||||
.filter(isInputFormatField)
|
||||
.map((f) => ({ name: f.name, type: f.type }))
|
||||
if (isMounted) setChildInputFields(fields)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (isMounted) setChildInputFields([])
|
||||
} catch {
|
||||
if (isMounted) setChildInputFields([])
|
||||
}
|
||||
}
|
||||
fetchChildSchema()
|
||||
return () => {
|
||||
isMounted = false
|
||||
controller.abort()
|
||||
}
|
||||
}, [selectedWorkflowId])
|
||||
|
||||
const valueObj: Record<string, any> = useMemo(() => {
|
||||
if (isPreview && previewValue && typeof previewValue === 'object') return previewValue
|
||||
if (mapping && typeof mapping === 'object') return mapping as Record<string, any>
|
||||
try {
|
||||
if (typeof mapping === 'string') return JSON.parse(mapping)
|
||||
} catch {}
|
||||
return {}
|
||||
}, [mapping, isPreview, previewValue])
|
||||
|
||||
const update = (field: string, value: string) => {
|
||||
if (disabled) return
|
||||
const updated = { ...valueObj, [field]: value }
|
||||
setMapping(updated)
|
||||
}
|
||||
|
||||
if (!selectedWorkflowId) {
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center rounded-lg border border-border/50 bg-muted/30 p-8 text-center'>
|
||||
<svg
|
||||
className='mb-3 h-10 w-10 text-muted-foreground/60'
|
||||
fill='none'
|
||||
viewBox='0 0 24 24'
|
||||
stroke='currentColor'
|
||||
>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={1.5}
|
||||
d='M13 10V3L4 14h7v7l9-11h-7z'
|
||||
/>
|
||||
</svg>
|
||||
<p className='font-medium text-muted-foreground text-sm'>No workflow selected</p>
|
||||
<p className='mt-1 text-muted-foreground/80 text-xs'>
|
||||
Select a workflow above to configure inputs
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!childInputFields || childInputFields.length === 0) {
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center rounded-lg border border-border/50 bg-muted/30 p-8 text-center'>
|
||||
<svg
|
||||
className='mb-3 h-10 w-10 text-muted-foreground/60'
|
||||
fill='none'
|
||||
viewBox='0 0 24 24'
|
||||
stroke='currentColor'
|
||||
>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={1.5}
|
||||
d='M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z'
|
||||
/>
|
||||
</svg>
|
||||
<p className='font-medium text-muted-foreground text-sm'>No input fields defined</p>
|
||||
<p className='mt-1 max-w-[200px] text-muted-foreground/80 text-xs'>
|
||||
The selected workflow needs an Input Trigger with defined fields
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='space-y-4'>
|
||||
{childInputFields.map((field) => {
|
||||
return (
|
||||
<InputMappingField
|
||||
key={field.name}
|
||||
fieldName={field.name}
|
||||
fieldType={field.type}
|
||||
value={valueObj[field.name] || ''}
|
||||
onChange={(value) => update(field.name, value)}
|
||||
blockId={blockId}
|
||||
subBlockId={subBlockId}
|
||||
disabled={isPreview || disabled}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Individual field component with TagDropdown support
|
||||
function InputMappingField({
|
||||
fieldName,
|
||||
fieldType,
|
||||
value,
|
||||
onChange,
|
||||
blockId,
|
||||
subBlockId,
|
||||
disabled,
|
||||
}: {
|
||||
fieldName: string
|
||||
fieldType?: string
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
blockId: string
|
||||
subBlockId: string
|
||||
disabled: boolean
|
||||
}) {
|
||||
const [showTags, setShowTags] = useState(false)
|
||||
const [cursorPosition, setCursorPosition] = useState(0)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const overlayRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (disabled) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
const newValue = e.target.value
|
||||
const newCursorPosition = e.target.selectionStart ?? 0
|
||||
|
||||
onChange(newValue)
|
||||
setCursorPosition(newCursorPosition)
|
||||
|
||||
// Check for tag trigger
|
||||
const tagTrigger = checkTagTrigger(newValue, newCursorPosition)
|
||||
setShowTags(tagTrigger.show)
|
||||
}
|
||||
|
||||
// Sync scroll position between input and overlay
|
||||
const handleScroll = (e: React.UIEvent<HTMLInputElement>) => {
|
||||
if (overlayRef.current) {
|
||||
overlayRef.current.scrollLeft = e.currentTarget.scrollLeft
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Escape') {
|
||||
setShowTags(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTagSelect = (newValue: string) => {
|
||||
onChange(newValue)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='group relative rounded-lg border border-border/50 bg-background/50 p-3 transition-all hover:border-border hover:bg-background'>
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<Label className='font-medium text-foreground text-xs'>{fieldName}</Label>
|
||||
{fieldType && (
|
||||
<span className='rounded-md bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground'>
|
||||
{fieldType}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className='relative w-full'>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
className={cn(
|
||||
'allow-scroll h-9 w-full overflow-auto border-0 bg-muted/50 text-transparent caret-foreground placeholder:text-muted-foreground/50 focus:bg-background',
|
||||
'transition-colors duration-200'
|
||||
)}
|
||||
type='text'
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onFocus={() => {
|
||||
setShowTags(false)
|
||||
}}
|
||||
onBlur={() => {
|
||||
setShowTags(false)
|
||||
}}
|
||||
onScroll={handleScroll}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoComplete='off'
|
||||
style={{ overflowX: 'auto' }}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div
|
||||
ref={overlayRef}
|
||||
className='pointer-events-none absolute inset-0 flex items-center overflow-x-auto bg-transparent px-3 text-sm'
|
||||
style={{ overflowX: 'auto' }}
|
||||
>
|
||||
<div
|
||||
className='w-full whitespace-pre'
|
||||
style={{ scrollbarWidth: 'none', minWidth: 'fit-content' }}
|
||||
>
|
||||
{formatDisplayText(value)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TagDropdown
|
||||
visible={showTags}
|
||||
onSelect={handleTagSelect}
|
||||
blockId={blockId}
|
||||
activeSourceBlockId={null}
|
||||
inputValue={value}
|
||||
cursorPosition={cursorPosition}
|
||||
onClose={() => {
|
||||
setShowTags(false)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+52
-6
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { PlusIcon, Server, WrenchIcon, XIcon } from 'lucide-react'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { AlertCircle, PlusIcon, Server, WrenchIcon, XIcon } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
@@ -374,6 +374,44 @@ function FileUploadSyncWrapper({
|
||||
)
|
||||
}
|
||||
|
||||
// Error boundary component for tool input
|
||||
class ToolInputErrorBoundary extends React.Component<
|
||||
{ children: React.ReactNode; blockName?: string },
|
||||
{ hasError: boolean; error?: Error }
|
||||
> {
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
this.state = { hasError: false }
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { hasError: true, error }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
||||
console.error('ToolInput error:', error, info)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className='rounded-md bg-red-50 p-4 text-red-800 text-sm dark:bg-red-900/20 dark:text-red-200'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<AlertCircle className='h-4 w-4' />
|
||||
<span className='font-medium'>Tool Configuration Error</span>
|
||||
</div>
|
||||
<p className='mt-1 text-xs opacity-80'>
|
||||
{this.props.blockName ? `Block "${this.props.blockName}": ` : ''}
|
||||
Invalid tool reference. Please check the workflow configuration.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
export function ToolInput({
|
||||
blockId,
|
||||
subBlockId,
|
||||
@@ -475,10 +513,18 @@ export function ToolInput({
|
||||
|
||||
// Fallback: create options from tools.access
|
||||
return block.tools.access.map((toolId) => {
|
||||
const toolParams = getToolParametersConfig(toolId)
|
||||
return {
|
||||
id: toolId,
|
||||
label: toolParams?.toolConfig?.name || toolId,
|
||||
try {
|
||||
const toolParams = getToolParametersConfig(toolId)
|
||||
return {
|
||||
id: toolId,
|
||||
label: toolParams?.toolConfig?.name || toolId,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error getting tool config for ${toolId}:`, error)
|
||||
return {
|
||||
id: toolId,
|
||||
label: toolId,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+12
@@ -18,6 +18,7 @@ import {
|
||||
FileUpload,
|
||||
FolderSelectorInput,
|
||||
InputFormat,
|
||||
InputMapping,
|
||||
KnowledgeBaseSelector,
|
||||
LongInput,
|
||||
McpDynamicArgs,
|
||||
@@ -450,6 +451,17 @@ export function SubBlock({
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'input-mapping': {
|
||||
return (
|
||||
<InputMapping
|
||||
blockId={blockId}
|
||||
subBlockId={config.id}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'response-format':
|
||||
return (
|
||||
<ResponseFormat
|
||||
|
||||
+120
-88
@@ -141,11 +141,32 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
isShowingDiff,
|
||||
id,
|
||||
])
|
||||
// Always call hooks to maintain consistent hook order
|
||||
const storeHorizontalHandles = useWorkflowStore(
|
||||
(state) => state.blocks[id]?.horizontalHandles ?? true
|
||||
)
|
||||
const storeIsWide = useWorkflowStore((state) => state.blocks[id]?.isWide ?? false)
|
||||
const storeBlockHeight = useWorkflowStore((state) => state.blocks[id]?.height ?? 0)
|
||||
const storeBlockAdvancedMode = useWorkflowStore(
|
||||
(state) => state.blocks[id]?.advancedMode ?? false
|
||||
)
|
||||
const storeBlockTriggerMode = useWorkflowStore((state) => state.blocks[id]?.triggerMode ?? false)
|
||||
|
||||
// Get block properties from currentWorkflow when in diff mode, otherwise from workflow store
|
||||
const horizontalHandles = data.isPreview
|
||||
? (data.blockState?.horizontalHandles ?? true) // In preview mode, use blockState and default to horizontal
|
||||
: useWorkflowStore((state) => state.blocks[id]?.horizontalHandles ?? true) // Changed default to true for consistency
|
||||
const isWide = useWorkflowStore((state) => state.blocks[id]?.isWide ?? false)
|
||||
const blockHeight = useWorkflowStore((state) => state.blocks[id]?.height ?? 0)
|
||||
: currentWorkflow.isDiffMode
|
||||
? (currentWorkflow.blocks[id]?.horizontalHandles ?? true)
|
||||
: storeHorizontalHandles
|
||||
|
||||
const isWide = currentWorkflow.isDiffMode
|
||||
? (currentWorkflow.blocks[id]?.isWide ?? false)
|
||||
: storeIsWide
|
||||
|
||||
const blockHeight = currentWorkflow.isDiffMode
|
||||
? (currentWorkflow.blocks[id]?.height ?? 0)
|
||||
: storeBlockHeight
|
||||
|
||||
// Get per-block webhook status by checking if webhook is configured
|
||||
const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
|
||||
|
||||
@@ -157,8 +178,14 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
)
|
||||
const blockWebhookStatus = !!(hasWebhookProvider && hasWebhookPath)
|
||||
|
||||
const blockAdvancedMode = useWorkflowStore((state) => state.blocks[id]?.advancedMode ?? false)
|
||||
const blockTriggerMode = useWorkflowStore((state) => state.blocks[id]?.triggerMode ?? false)
|
||||
const blockAdvancedMode = currentWorkflow.isDiffMode
|
||||
? (currentWorkflow.blocks[id]?.advancedMode ?? false)
|
||||
: storeBlockAdvancedMode
|
||||
|
||||
// Get triggerMode from currentWorkflow blocks when in diff mode, otherwise from workflow store
|
||||
const blockTriggerMode = currentWorkflow.isDiffMode
|
||||
? (currentWorkflow.blocks[id]?.triggerMode ?? false)
|
||||
: storeBlockTriggerMode
|
||||
|
||||
// Local UI state for diff mode controls
|
||||
const [diffIsWide, setDiffIsWide] = useState<boolean>(isWide)
|
||||
@@ -660,7 +687,10 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
|
||||
{/* Block Header */}
|
||||
<div
|
||||
className='workflow-drag-handle flex cursor-grab items-center justify-between border-b p-3 [&:active]:cursor-grabbing'
|
||||
className={cn(
|
||||
'workflow-drag-handle flex cursor-grab items-center justify-between p-3 [&:active]:cursor-grabbing',
|
||||
subBlockRows.length > 0 && 'border-b'
|
||||
)}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
@@ -891,7 +921,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
<p className='mb-1 font-medium text-sm'>Description</p>
|
||||
<p className='text-muted-foreground text-sm'>{config.longDescription}</p>
|
||||
</div>
|
||||
{config.outputs && (
|
||||
{config.outputs && Object.keys(config.outputs).length > 0 && (
|
||||
<div>
|
||||
<p className='mb-1 font-medium text-sm'>Output</p>
|
||||
<div className='text-sm'>
|
||||
@@ -929,90 +959,92 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
</Tooltip>
|
||||
)
|
||||
)}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
if (currentWorkflow.isDiffMode) {
|
||||
setDiffIsWide((prev) => !prev)
|
||||
} else if (userPermissions.canEdit) {
|
||||
collaborativeToggleBlockWide(id)
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'h-7 p-1 text-gray-500',
|
||||
!userPermissions.canEdit &&
|
||||
!currentWorkflow.isDiffMode &&
|
||||
'cursor-not-allowed opacity-50'
|
||||
)}
|
||||
disabled={!userPermissions.canEdit && !currentWorkflow.isDiffMode}
|
||||
>
|
||||
{displayIsWide ? (
|
||||
<RectangleHorizontal className='h-5 w-5' />
|
||||
) : (
|
||||
<RectangleVertical className='h-5 w-5' />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='top'>
|
||||
{!userPermissions.canEdit && !currentWorkflow.isDiffMode
|
||||
? userPermissions.isOfflineMode
|
||||
? 'Connection lost - please refresh'
|
||||
: 'Read-only mode'
|
||||
: displayIsWide
|
||||
? 'Narrow Block'
|
||||
: 'Expand Block'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{subBlockRows.length > 0 && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
if (currentWorkflow.isDiffMode) {
|
||||
setDiffIsWide((prev) => !prev)
|
||||
} else if (userPermissions.canEdit) {
|
||||
collaborativeToggleBlockWide(id)
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'h-7 p-1 text-gray-500',
|
||||
!userPermissions.canEdit &&
|
||||
!currentWorkflow.isDiffMode &&
|
||||
'cursor-not-allowed opacity-50'
|
||||
)}
|
||||
disabled={!userPermissions.canEdit && !currentWorkflow.isDiffMode}
|
||||
>
|
||||
{displayIsWide ? (
|
||||
<RectangleHorizontal className='h-5 w-5' />
|
||||
) : (
|
||||
<RectangleVertical className='h-5 w-5' />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='top'>
|
||||
{!userPermissions.canEdit && !currentWorkflow.isDiffMode
|
||||
? userPermissions.isOfflineMode
|
||||
? 'Connection lost - please refresh'
|
||||
: 'Read-only mode'
|
||||
: displayIsWide
|
||||
? 'Narrow Block'
|
||||
: 'Expand Block'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Block Content */}
|
||||
<div
|
||||
ref={contentRef}
|
||||
className='cursor-pointer space-y-4 px-4 pt-3 pb-4'
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
{subBlockRows.length > 0
|
||||
? subBlockRows.map((row, rowIndex) => (
|
||||
<div key={`row-${rowIndex}`} className='flex gap-4'>
|
||||
{row.map((subBlock, blockIndex) => (
|
||||
<div
|
||||
key={`${id}-${rowIndex}-${blockIndex}`}
|
||||
className={cn('space-y-1', subBlock.layout === 'half' ? 'flex-1' : 'w-full')}
|
||||
>
|
||||
<SubBlock
|
||||
blockId={id}
|
||||
config={subBlock}
|
||||
isConnecting={isConnecting}
|
||||
isPreview={data.isPreview || currentWorkflow.isDiffMode}
|
||||
subBlockValues={
|
||||
data.subBlockValues ||
|
||||
(currentWorkflow.isDiffMode && currentBlock
|
||||
? (currentBlock as any).subBlocks
|
||||
: undefined)
|
||||
}
|
||||
disabled={!userPermissions.canEdit}
|
||||
fieldDiffStatus={
|
||||
fieldDiff?.changed_fields?.includes(subBlock.id)
|
||||
? 'changed'
|
||||
: fieldDiff?.unchanged_fields?.includes(subBlock.id)
|
||||
? 'unchanged'
|
||||
: undefined
|
||||
}
|
||||
allowExpandInPreview={currentWorkflow.isDiffMode}
|
||||
isWide={displayIsWide}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
{/* Block Content - Only render if there are subblocks */}
|
||||
{subBlockRows.length > 0 && (
|
||||
<div
|
||||
ref={contentRef}
|
||||
className='cursor-pointer space-y-4 px-4 pt-3 pb-4'
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
{subBlockRows.map((row, rowIndex) => (
|
||||
<div key={`row-${rowIndex}`} className='flex gap-4'>
|
||||
{row.map((subBlock, blockIndex) => (
|
||||
<div
|
||||
key={`${id}-${rowIndex}-${blockIndex}`}
|
||||
className={cn('space-y-1', subBlock.layout === 'half' ? 'flex-1' : 'w-full')}
|
||||
>
|
||||
<SubBlock
|
||||
blockId={id}
|
||||
config={subBlock}
|
||||
isConnecting={isConnecting}
|
||||
isPreview={data.isPreview || currentWorkflow.isDiffMode}
|
||||
subBlockValues={
|
||||
data.subBlockValues ||
|
||||
(currentWorkflow.isDiffMode && currentBlock
|
||||
? (currentBlock as any).subBlocks
|
||||
: undefined)
|
||||
}
|
||||
disabled={!userPermissions.canEdit}
|
||||
fieldDiffStatus={
|
||||
fieldDiff?.changed_fields?.includes(subBlock.id)
|
||||
? 'changed'
|
||||
: fieldDiff?.unchanged_fields?.includes(subBlock.id)
|
||||
? 'unchanged'
|
||||
: undefined
|
||||
}
|
||||
allowExpandInPreview={currentWorkflow.isDiffMode}
|
||||
isWide={displayIsWide}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Output Handle */}
|
||||
{type !== 'condition' && type !== 'response' && (
|
||||
|
||||
+180
-26
@@ -3,7 +3,7 @@ import { v4 as uuidv4 } from 'uuid'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
|
||||
import { processStreamingBlockLogs } from '@/lib/tokenization'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { TriggerUtils } from '@/lib/workflows/triggers'
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
import { Executor } from '@/executor'
|
||||
import type { BlockLog, ExecutionResult, StreamingExecution } from '@/executor/types'
|
||||
@@ -447,7 +447,30 @@ export function useWorkflowExecution() {
|
||||
)
|
||||
}
|
||||
} catch (error: any) {
|
||||
controller.error(error)
|
||||
// Create a proper error result for logging
|
||||
const errorResult = {
|
||||
success: false,
|
||||
error: error.message || 'Workflow execution failed',
|
||||
output: {},
|
||||
logs: [],
|
||||
metadata: {
|
||||
duration: 0,
|
||||
startTime: new Date().toISOString(),
|
||||
source: 'chat' as const,
|
||||
},
|
||||
}
|
||||
|
||||
// Send the error as final event so downstream handlers can treat it uniformly
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify({ event: 'final', data: errorResult })}\n\n`)
|
||||
)
|
||||
|
||||
// Persist the error to logs so it shows up in the logs page
|
||||
persistLogs(executionId, errorResult).catch((err) =>
|
||||
logger.error('Error persisting error logs:', err)
|
||||
)
|
||||
|
||||
// Do not error the controller to allow consumers to process the final event
|
||||
} finally {
|
||||
controller.close()
|
||||
setIsExecuting(false)
|
||||
@@ -560,22 +583,14 @@ export function useWorkflowExecution() {
|
||||
}
|
||||
})
|
||||
|
||||
// Filter out trigger blocks for manual execution
|
||||
// Do not filter out trigger blocks; executor may need to start from them
|
||||
const filteredStates = Object.entries(mergedStates).reduce(
|
||||
(acc, [id, block]) => {
|
||||
// Skip blocks with undefined type
|
||||
if (!block || !block.type) {
|
||||
logger.warn(`Skipping block with undefined type: ${id}`, block)
|
||||
return acc
|
||||
}
|
||||
|
||||
const blockConfig = getBlock(block.type)
|
||||
const isTriggerBlock = blockConfig?.category === 'triggers'
|
||||
|
||||
// Skip trigger blocks during manual execution
|
||||
if (!isTriggerBlock) {
|
||||
acc[id] = block
|
||||
}
|
||||
acc[id] = block
|
||||
return acc
|
||||
},
|
||||
{} as typeof mergedStates
|
||||
@@ -632,15 +647,8 @@ export function useWorkflowExecution() {
|
||||
{} as Record<string, any>
|
||||
)
|
||||
|
||||
// Filter edges to exclude connections to/from trigger blocks
|
||||
const triggerBlockIds = Object.keys(mergedStates).filter((id) => {
|
||||
const blockConfig = getBlock(mergedStates[id].type)
|
||||
return blockConfig?.category === 'triggers'
|
||||
})
|
||||
|
||||
const filteredEdges = workflowEdges.filter(
|
||||
(edge) => !triggerBlockIds.includes(edge.source) && !triggerBlockIds.includes(edge.target)
|
||||
)
|
||||
// Keep edges intact to allow execution starting from trigger blocks
|
||||
const filteredEdges = workflowEdges
|
||||
|
||||
// Derive subflows from the current filtered graph to avoid stale state
|
||||
const runtimeLoops = generateLoopBlocks(filteredStates)
|
||||
@@ -663,12 +671,158 @@ export function useWorkflowExecution() {
|
||||
selectedOutputIds = chatStore.getState().getSelectedWorkflowOutput(activeWorkflowId)
|
||||
}
|
||||
|
||||
// Create executor options
|
||||
// Determine start block and workflow input based on execution type
|
||||
let startBlockId: string | undefined
|
||||
let finalWorkflowInput = workflowInput
|
||||
|
||||
if (isExecutingFromChat) {
|
||||
// For chat execution, find the appropriate chat trigger
|
||||
const startBlock = TriggerUtils.findStartBlock(filteredStates, 'chat')
|
||||
|
||||
if (!startBlock) {
|
||||
throw new Error(TriggerUtils.getTriggerValidationMessage('chat', 'missing'))
|
||||
}
|
||||
|
||||
startBlockId = startBlock.blockId
|
||||
} else {
|
||||
// For manual editor runs: look for Manual trigger OR API trigger
|
||||
const entries = Object.entries(filteredStates)
|
||||
|
||||
// Find manual triggers and API triggers
|
||||
const manualTriggers = TriggerUtils.findTriggersByType(filteredStates, 'manual')
|
||||
const apiTriggers = TriggerUtils.findTriggersByType(filteredStates, 'api')
|
||||
|
||||
logger.info('Manual run trigger check:', {
|
||||
manualTriggersCount: manualTriggers.length,
|
||||
apiTriggersCount: apiTriggers.length,
|
||||
manualTriggers: manualTriggers.map((t) => ({
|
||||
type: t.type,
|
||||
name: t.name,
|
||||
isLegacy: t.type === 'starter',
|
||||
})),
|
||||
apiTriggers: apiTriggers.map((t) => ({
|
||||
type: t.type,
|
||||
name: t.name,
|
||||
isLegacy: t.type === 'starter',
|
||||
})),
|
||||
})
|
||||
|
||||
let selectedTrigger: any = null
|
||||
let selectedBlockId: string | null = null
|
||||
|
||||
// Check for API triggers first (they take precedence over manual triggers)
|
||||
if (apiTriggers.length === 1) {
|
||||
selectedTrigger = apiTriggers[0]
|
||||
const blockEntry = entries.find(([, block]) => block === selectedTrigger)
|
||||
if (blockEntry) {
|
||||
selectedBlockId = blockEntry[0]
|
||||
|
||||
// Extract test values from the API trigger's inputFormat
|
||||
if (selectedTrigger.type === 'api_trigger' || selectedTrigger.type === 'starter') {
|
||||
const inputFormatValue = selectedTrigger.subBlocks?.inputFormat?.value
|
||||
if (Array.isArray(inputFormatValue)) {
|
||||
const testInput: Record<string, any> = {}
|
||||
inputFormatValue.forEach((field: any) => {
|
||||
if (field && typeof field === 'object' && field.name && field.value !== undefined) {
|
||||
testInput[field.name] = field.value
|
||||
}
|
||||
})
|
||||
|
||||
// Use the test input as workflow input
|
||||
if (Object.keys(testInput).length > 0) {
|
||||
finalWorkflowInput = testInput
|
||||
logger.info('Using API trigger test values for manual run:', testInput)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (apiTriggers.length > 1) {
|
||||
const error = new Error('Multiple API Trigger blocks found. Keep only one.')
|
||||
logger.error('Multiple API triggers found')
|
||||
setIsExecuting(false)
|
||||
throw error
|
||||
} else if (manualTriggers.length === 1) {
|
||||
// No API trigger, check for manual trigger
|
||||
selectedTrigger = manualTriggers[0]
|
||||
const blockEntry = entries.find(([, block]) => block === selectedTrigger)
|
||||
if (blockEntry) {
|
||||
selectedBlockId = blockEntry[0]
|
||||
}
|
||||
} else if (manualTriggers.length > 1) {
|
||||
const error = new Error('Multiple Input Trigger blocks found. Keep only one.')
|
||||
logger.error('Multiple input triggers found')
|
||||
setIsExecuting(false)
|
||||
throw error
|
||||
} else {
|
||||
// Fallback: Check for legacy starter block
|
||||
const starterBlock = Object.values(filteredStates).find((block) => block.type === 'starter')
|
||||
if (starterBlock) {
|
||||
// Found a legacy starter block, use it as a manual trigger
|
||||
const blockEntry = Object.entries(filteredStates).find(
|
||||
([, block]) => block === starterBlock
|
||||
)
|
||||
if (blockEntry) {
|
||||
selectedBlockId = blockEntry[0]
|
||||
selectedTrigger = starterBlock
|
||||
logger.info('Using legacy starter block for manual run')
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedBlockId || !selectedTrigger) {
|
||||
const error = new Error('Manual run requires an Input Trigger or API Trigger block')
|
||||
logger.error('No input or API triggers found for manual run')
|
||||
setIsExecuting(false)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedBlockId && selectedTrigger) {
|
||||
startBlockId = selectedBlockId
|
||||
|
||||
// Check if the trigger has any outgoing connections (except for legacy starter blocks)
|
||||
// Legacy starter blocks have their own validation in the executor
|
||||
if (selectedTrigger.type !== 'starter') {
|
||||
const outgoingConnections = workflowEdges.filter((edge) => edge.source === startBlockId)
|
||||
if (outgoingConnections.length === 0) {
|
||||
const triggerName = selectedTrigger.name || selectedTrigger.type
|
||||
const error = new Error(`${triggerName} must be connected to other blocks to execute`)
|
||||
logger.error('Trigger has no outgoing connections', { triggerName, startBlockId })
|
||||
setIsExecuting(false)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Trigger found for manual run:', {
|
||||
startBlockId,
|
||||
triggerType: selectedTrigger.type,
|
||||
triggerName: selectedTrigger.name,
|
||||
isLegacyStarter: selectedTrigger.type === 'starter',
|
||||
usingTestValues: selectedTrigger.type === 'api_trigger',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have a valid startBlockId at this point, throw an error
|
||||
if (!startBlockId) {
|
||||
const error = new Error('No valid trigger block found to start execution')
|
||||
logger.error('No startBlockId found after trigger search')
|
||||
setIsExecuting(false)
|
||||
throw error
|
||||
}
|
||||
|
||||
// Log the final startBlockId
|
||||
logger.info('Final execution setup:', {
|
||||
startBlockId,
|
||||
isExecutingFromChat,
|
||||
hasWorkflowInput: !!workflowInput,
|
||||
})
|
||||
|
||||
// Create executor options with the final workflow input
|
||||
const executorOptions: ExecutorOptions = {
|
||||
workflow,
|
||||
currentBlockStates,
|
||||
envVarValues,
|
||||
workflowInput,
|
||||
workflowInput: finalWorkflowInput,
|
||||
workflowVariables,
|
||||
contextExtensions: {
|
||||
stream: isExecutingFromChat,
|
||||
@@ -687,8 +841,8 @@ export function useWorkflowExecution() {
|
||||
const newExecutor = new Executor(executorOptions)
|
||||
setExecutor(newExecutor)
|
||||
|
||||
// Execute workflow
|
||||
return newExecutor.execute(activeWorkflowId || '')
|
||||
// Execute workflow with the determined start block
|
||||
return newExecutor.execute(activeWorkflowId || '', startBlockId)
|
||||
}
|
||||
|
||||
const handleExecutionError = (error: any, options?: { executionId?: string }) => {
|
||||
@@ -729,7 +883,7 @@ export function useWorkflowExecution() {
|
||||
try {
|
||||
// Prefer attributing to specific subflow if we have a structured error
|
||||
let blockId = 'serialization'
|
||||
let blockName = 'Serialization'
|
||||
let blockName = 'Workflow'
|
||||
let blockType = 'serializer'
|
||||
if (error instanceof WorkflowValidationError) {
|
||||
blockId = error.blockId || blockId
|
||||
|
||||
+6
-30
@@ -6,7 +6,6 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
|
||||
import { getBlock } from '@/blocks'
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
import { Executor } from '@/executor'
|
||||
import type { ExecutionResult, StreamingExecution } from '@/executor/types'
|
||||
@@ -131,26 +130,9 @@ export async function executeWorkflowWithLogging(
|
||||
// Merge subblock states from the appropriate store
|
||||
const mergedStates = mergeSubblockState(validBlocks)
|
||||
|
||||
// Filter out trigger blocks for manual execution
|
||||
const filteredStates = Object.entries(mergedStates).reduce(
|
||||
(acc, [id, block]) => {
|
||||
// Skip blocks with undefined type
|
||||
if (!block || !block.type) {
|
||||
logger.warn(`Skipping block with undefined type: ${id}`, block)
|
||||
return acc
|
||||
}
|
||||
|
||||
const blockConfig = getBlock(block.type)
|
||||
const isTriggerBlock = blockConfig?.category === 'triggers'
|
||||
|
||||
// Skip trigger blocks during manual execution
|
||||
if (!isTriggerBlock) {
|
||||
acc[id] = block
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as typeof mergedStates
|
||||
)
|
||||
// Don't filter out trigger blocks - let the executor handle them properly
|
||||
// The standard executor has TriggerBlockHandler that knows how to handle triggers
|
||||
const filteredStates = mergedStates
|
||||
|
||||
const currentBlockStates = Object.entries(filteredStates).reduce(
|
||||
(acc, [id, block]) => {
|
||||
@@ -186,15 +168,9 @@ export async function executeWorkflowWithLogging(
|
||||
{} as Record<string, any>
|
||||
)
|
||||
|
||||
// Filter edges to exclude connections to/from trigger blocks
|
||||
const triggerBlockIds = Object.keys(mergedStates).filter((id) => {
|
||||
const blockConfig = getBlock(mergedStates[id].type)
|
||||
return blockConfig?.category === 'triggers'
|
||||
})
|
||||
|
||||
const filteredEdges = workflowEdges.filter(
|
||||
(edge: any) => !triggerBlockIds.includes(edge.source) && !triggerBlockIds.includes(edge.target)
|
||||
)
|
||||
// Don't filter edges - let all connections remain intact
|
||||
// The executor's routing system will handle execution paths properly
|
||||
const filteredEdges = workflowEdges
|
||||
|
||||
// Create serialized workflow with filtered blocks and edges
|
||||
const workflow = new Serializer().serializeWorkflow(
|
||||
|
||||
@@ -206,6 +206,18 @@ export async function applyAutoLayoutAndUpdateStore(
|
||||
loops: newWorkflowState.loops || {},
|
||||
parallels: newWorkflowState.parallels || {},
|
||||
deploymentStatuses: newWorkflowState.deploymentStatuses || {},
|
||||
// Sanitize edges: remove null/empty handle fields to satisfy schema (optional strings)
|
||||
edges: (newWorkflowState.edges || []).map((edge: any) => {
|
||||
const { sourceHandle, targetHandle, ...rest } = edge || {}
|
||||
const sanitized: any = { ...rest }
|
||||
if (typeof sourceHandle === 'string' && sourceHandle.length > 0) {
|
||||
sanitized.sourceHandle = sourceHandle
|
||||
}
|
||||
if (typeof targetHandle === 'string' && targetHandle.length > 0) {
|
||||
sanitized.targetHandle = targetHandle
|
||||
}
|
||||
return sanitized
|
||||
}),
|
||||
}
|
||||
|
||||
// Save the updated workflow state to the database
|
||||
|
||||
@@ -13,6 +13,7 @@ import ReactFlow, {
|
||||
} from 'reactflow'
|
||||
import 'reactflow/dist/style.css'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { TriggerUtils } from '@/lib/workflows/triggers'
|
||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
import { ControlBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar'
|
||||
import { DiffControls } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls'
|
||||
@@ -20,6 +21,11 @@ import { ErrorBoundary } from '@/app/workspace/[workspaceId]/w/[workflowId]/comp
|
||||
import { FloatingControls } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/floating-controls/floating-controls'
|
||||
import { Panel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel'
|
||||
import { SubflowNodeComponent } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node'
|
||||
import { TriggerList } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-list/trigger-list'
|
||||
import {
|
||||
TriggerWarningDialog,
|
||||
TriggerWarningType,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-warning-dialog'
|
||||
import { WorkflowBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block'
|
||||
import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge'
|
||||
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
|
||||
@@ -77,6 +83,17 @@ const WorkflowContent = React.memo(() => {
|
||||
// Enhanced edge selection with parent context and unique identifier
|
||||
const [selectedEdgeInfo, setSelectedEdgeInfo] = useState<SelectedEdgeInfo | null>(null)
|
||||
|
||||
// State for trigger warning dialog
|
||||
const [triggerWarning, setTriggerWarning] = useState<{
|
||||
open: boolean
|
||||
triggerName: string
|
||||
type: TriggerWarningType
|
||||
}>({
|
||||
open: false,
|
||||
triggerName: '',
|
||||
type: TriggerWarningType.DUPLICATE_TRIGGER,
|
||||
})
|
||||
|
||||
// Hooks
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
@@ -107,6 +124,11 @@ const WorkflowContent = React.memo(() => {
|
||||
// Extract workflow data from the abstraction
|
||||
const { blocks, edges, loops, parallels, isDiffMode } = currentWorkflow
|
||||
|
||||
// Check if workflow is empty (no blocks)
|
||||
const isWorkflowEmpty = useMemo(() => {
|
||||
return Object.keys(blocks).length === 0
|
||||
}, [blocks])
|
||||
|
||||
// Get diff analysis for edge reconstruction
|
||||
const { diffAnalysis, isShowingDiff, isDiffReady } = useWorkflowDiffStore()
|
||||
|
||||
@@ -565,7 +587,7 @@ const WorkflowContent = React.memo(() => {
|
||||
return
|
||||
}
|
||||
|
||||
const { type } = event.detail
|
||||
const { type, enableTriggerMode } = event.detail
|
||||
|
||||
if (!type) return
|
||||
if (type === 'connectionBlock') return
|
||||
@@ -637,7 +659,10 @@ const WorkflowContent = React.memo(() => {
|
||||
|
||||
// Create a new block with a unique ID
|
||||
const id = crypto.randomUUID()
|
||||
const name = getUniqueBlockName(blockConfig.name, blocks)
|
||||
// Prefer semantic default names for triggers; then ensure unique numbering centrally
|
||||
const defaultTriggerName = TriggerUtils.getDefaultTriggerName(type)
|
||||
const baseName = defaultTriggerName || blockConfig.name
|
||||
const name = getUniqueBlockName(baseName, blocks)
|
||||
|
||||
// Auto-connect logic
|
||||
const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled
|
||||
@@ -661,8 +686,38 @@ const WorkflowContent = React.memo(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// Centralized trigger constraints
|
||||
const additionIssue = TriggerUtils.getTriggerAdditionIssue(blocks, type)
|
||||
if (additionIssue) {
|
||||
if (additionIssue.issue === 'legacy') {
|
||||
setTriggerWarning({
|
||||
open: true,
|
||||
triggerName: additionIssue.triggerName,
|
||||
type: TriggerWarningType.LEGACY_INCOMPATIBILITY,
|
||||
})
|
||||
} else {
|
||||
setTriggerWarning({
|
||||
open: true,
|
||||
triggerName: additionIssue.triggerName,
|
||||
type: TriggerWarningType.DUPLICATE_TRIGGER,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Add the block to the workflow with auto-connect edge
|
||||
addBlock(id, type, name, centerPosition, undefined, undefined, undefined, autoConnectEdge)
|
||||
// Enable trigger mode if this is a trigger-capable block from the triggers tab
|
||||
addBlock(
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
centerPosition,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
autoConnectEdge,
|
||||
enableTriggerMode
|
||||
)
|
||||
}
|
||||
|
||||
window.addEventListener('add-block-from-toolbar', handleAddBlockFromToolbar as EventListener)
|
||||
@@ -681,8 +736,35 @@ const WorkflowContent = React.memo(() => {
|
||||
findClosestOutput,
|
||||
determineSourceHandle,
|
||||
effectivePermissions.canEdit,
|
||||
setTriggerWarning,
|
||||
])
|
||||
|
||||
// Handler for trigger selection from list
|
||||
const handleTriggerSelect = useCallback(
|
||||
(triggerId: string, enableTriggerMode?: boolean) => {
|
||||
// Get the trigger name
|
||||
const triggerName = TriggerUtils.getDefaultTriggerName(triggerId) || triggerId
|
||||
|
||||
// Create the trigger block at the center of the viewport
|
||||
const centerPosition = project({ x: window.innerWidth / 2, y: window.innerHeight / 2 })
|
||||
const id = `${triggerId}_${Date.now()}`
|
||||
|
||||
// Add the trigger block with trigger mode if specified
|
||||
addBlock(
|
||||
id,
|
||||
triggerId,
|
||||
triggerName,
|
||||
centerPosition,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
enableTriggerMode || false
|
||||
)
|
||||
},
|
||||
[project, addBlock]
|
||||
)
|
||||
|
||||
// Update the onDrop handler
|
||||
const onDrop = useCallback(
|
||||
(event: React.DragEvent) => {
|
||||
@@ -784,8 +866,14 @@ const WorkflowContent = React.memo(() => {
|
||||
|
||||
// Generate id and name here so they're available in all code paths
|
||||
const id = crypto.randomUUID()
|
||||
// Prefer semantic default names for triggers; then ensure unique numbering centrally
|
||||
const defaultTriggerNameDrop = TriggerUtils.getDefaultTriggerName(data.type)
|
||||
const baseName =
|
||||
data.type === 'loop' ? 'Loop' : data.type === 'parallel' ? 'Parallel' : blockConfig!.name
|
||||
data.type === 'loop'
|
||||
? 'Loop'
|
||||
: data.type === 'parallel'
|
||||
? 'Parallel'
|
||||
: defaultTriggerNameDrop || blockConfig!.name
|
||||
const name = getUniqueBlockName(baseName, blocks)
|
||||
|
||||
if (containerInfo) {
|
||||
@@ -868,6 +956,20 @@ const WorkflowContent = React.memo(() => {
|
||||
// Immediate resize without delay
|
||||
resizeLoopNodesWrapper()
|
||||
} else {
|
||||
// Centralized trigger constraints
|
||||
const dropIssue = TriggerUtils.getTriggerAdditionIssue(blocks, data.type)
|
||||
if (dropIssue) {
|
||||
setTriggerWarning({
|
||||
open: true,
|
||||
triggerName: dropIssue.triggerName,
|
||||
type:
|
||||
dropIssue.issue === 'legacy'
|
||||
? TriggerWarningType.LEGACY_INCOMPATIBILITY
|
||||
: TriggerWarningType.DUPLICATE_TRIGGER,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Regular auto-connect logic
|
||||
const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled
|
||||
let autoConnectEdge
|
||||
@@ -888,7 +990,19 @@ const WorkflowContent = React.memo(() => {
|
||||
}
|
||||
|
||||
// Regular canvas drop with auto-connect edge
|
||||
addBlock(id, data.type, name, position, undefined, undefined, undefined, autoConnectEdge)
|
||||
// Use enableTriggerMode from drag data if present (when dragging from Triggers tab)
|
||||
const enableTriggerMode = data.enableTriggerMode || false
|
||||
addBlock(
|
||||
id,
|
||||
data.type,
|
||||
name,
|
||||
position,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
autoConnectEdge,
|
||||
enableTriggerMode
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Error dropping block:', { err })
|
||||
@@ -903,6 +1017,7 @@ const WorkflowContent = React.memo(() => {
|
||||
determineSourceHandle,
|
||||
isPointInLoopNodeWrapper,
|
||||
getNodes,
|
||||
setTriggerWarning,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1815,6 +1930,19 @@ const WorkflowContent = React.memo(() => {
|
||||
|
||||
{/* Show DiffControls if diff is available (regardless of current view mode) */}
|
||||
<DiffControls />
|
||||
|
||||
{/* Trigger warning dialog */}
|
||||
<TriggerWarningDialog
|
||||
open={triggerWarning.open}
|
||||
onOpenChange={(open) => setTriggerWarning({ ...triggerWarning, open })}
|
||||
triggerName={triggerWarning.triggerName}
|
||||
type={triggerWarning.type}
|
||||
/>
|
||||
|
||||
{/* Trigger list for empty workflows - only show after workflow has loaded */}
|
||||
{isWorkflowReady && isWorkflowEmpty && effectivePermissions.canEdit && (
|
||||
<TriggerList onSelect={handleTriggerSelect} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+15
-3
@@ -7,9 +7,14 @@ import type { BlockConfig } from '@/blocks/types'
|
||||
export type ToolbarBlockProps = {
|
||||
config: BlockConfig
|
||||
disabled?: boolean
|
||||
enableTriggerMode?: boolean
|
||||
}
|
||||
|
||||
export function ToolbarBlock({ config, disabled = false }: ToolbarBlockProps) {
|
||||
export function ToolbarBlock({
|
||||
config,
|
||||
disabled = false,
|
||||
enableTriggerMode = false,
|
||||
}: ToolbarBlockProps) {
|
||||
const userPermissions = useUserPermissionsContext()
|
||||
|
||||
const handleDragStart = (e: React.DragEvent) => {
|
||||
@@ -17,7 +22,13 @@ export function ToolbarBlock({ config, disabled = false }: ToolbarBlockProps) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
e.dataTransfer.setData('application/json', JSON.stringify({ type: config.type }))
|
||||
e.dataTransfer.setData(
|
||||
'application/json',
|
||||
JSON.stringify({
|
||||
type: config.type,
|
||||
enableTriggerMode,
|
||||
})
|
||||
)
|
||||
e.dataTransfer.effectAllowed = 'move'
|
||||
}
|
||||
|
||||
@@ -29,10 +40,11 @@ export function ToolbarBlock({ config, disabled = false }: ToolbarBlockProps) {
|
||||
const event = new CustomEvent('add-block-from-toolbar', {
|
||||
detail: {
|
||||
type: config.type,
|
||||
enableTriggerMode,
|
||||
},
|
||||
})
|
||||
window.dispatchEvent(event)
|
||||
}, [config.type, disabled])
|
||||
}, [config.type, disabled, enableTriggerMode])
|
||||
|
||||
const blockContent = (
|
||||
<div
|
||||
|
||||
+128
-75
@@ -4,10 +4,15 @@ import { useMemo, useState } from 'react'
|
||||
import { Search } from 'lucide-react'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Tabs, TabsContent } from '@/components/ui/tabs'
|
||||
import {
|
||||
getBlocksForSidebar,
|
||||
getTriggersForSidebar,
|
||||
hasTriggerCapability,
|
||||
} from '@/lib/workflows/trigger-utils'
|
||||
import { ToolbarBlock } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/components/toolbar-block/toolbar-block'
|
||||
import LoopToolbarItem from '@/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/components/toolbar-loop-block/toolbar-loop-block'
|
||||
import ParallelToolbarItem from '@/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/components/toolbar-parallel-block/toolbar-parallel-block'
|
||||
import { getAllBlocks } from '@/blocks'
|
||||
import type { WorkspaceUserPermissions } from '@/hooks/use-user-permissions'
|
||||
|
||||
interface ToolbarProps {
|
||||
@@ -24,25 +29,30 @@ interface BlockItem {
|
||||
|
||||
export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }: ToolbarProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [activeTab, setActiveTab] = useState('blocks')
|
||||
|
||||
const { regularBlocks, specialBlocks, tools, triggers } = useMemo(() => {
|
||||
const allBlocks = getAllBlocks()
|
||||
// Get blocks based on the active tab using centralized logic
|
||||
const sourceBlocks = activeTab === 'blocks' ? getBlocksForSidebar() : getTriggersForSidebar()
|
||||
|
||||
// Filter blocks based on search query
|
||||
const filteredBlocks = allBlocks.filter((block) => {
|
||||
if (block.type === 'starter' || block.hideFromToolbar) return false
|
||||
|
||||
return (
|
||||
const filteredBlocks = sourceBlocks.filter((block) => {
|
||||
const matchesSearch =
|
||||
!searchQuery.trim() ||
|
||||
block.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
block.description.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
|
||||
return matchesSearch
|
||||
})
|
||||
|
||||
// Separate blocks by category: 'blocks', 'tools', and 'triggers'
|
||||
// Separate blocks by category
|
||||
const regularBlockConfigs = filteredBlocks.filter((block) => block.category === 'blocks')
|
||||
const toolConfigs = filteredBlocks.filter((block) => block.category === 'tools')
|
||||
const triggerConfigs = filteredBlocks.filter((block) => block.category === 'triggers')
|
||||
// For triggers tab, include both 'triggers' category and tools with trigger capability
|
||||
const triggerConfigs =
|
||||
activeTab === 'triggers'
|
||||
? filteredBlocks
|
||||
: filteredBlocks.filter((block) => block.category === 'triggers')
|
||||
|
||||
// Create regular block items and sort alphabetically
|
||||
const regularBlockItems: BlockItem[] = regularBlockConfigs
|
||||
@@ -54,23 +64,25 @@ export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }:
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
// Create special blocks (loop and parallel) if they match search
|
||||
// Create special blocks (loop and parallel) only for blocks tab
|
||||
const specialBlockItems: BlockItem[] = []
|
||||
|
||||
if (!searchQuery.trim() || 'loop'.toLowerCase().includes(searchQuery.toLowerCase())) {
|
||||
specialBlockItems.push({
|
||||
name: 'Loop',
|
||||
type: 'loop',
|
||||
isCustom: true,
|
||||
})
|
||||
}
|
||||
if (activeTab === 'blocks') {
|
||||
if (!searchQuery.trim() || 'loop'.toLowerCase().includes(searchQuery.toLowerCase())) {
|
||||
specialBlockItems.push({
|
||||
name: 'Loop',
|
||||
type: 'loop',
|
||||
isCustom: true,
|
||||
})
|
||||
}
|
||||
|
||||
if (!searchQuery.trim() || 'parallel'.toLowerCase().includes(searchQuery.toLowerCase())) {
|
||||
specialBlockItems.push({
|
||||
name: 'Parallel',
|
||||
type: 'parallel',
|
||||
isCustom: true,
|
||||
})
|
||||
if (!searchQuery.trim() || 'parallel'.toLowerCase().includes(searchQuery.toLowerCase())) {
|
||||
specialBlockItems.push({
|
||||
name: 'Parallel',
|
||||
type: 'parallel',
|
||||
isCustom: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort special blocks alphabetically
|
||||
@@ -95,65 +107,106 @@ export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }:
|
||||
tools: toolConfigs,
|
||||
triggers: triggerBlockItems,
|
||||
}
|
||||
}, [searchQuery])
|
||||
}, [searchQuery, activeTab])
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col'>
|
||||
{/* Search */}
|
||||
<div className='flex-shrink-0 p-2'>
|
||||
<div className='flex h-9 items-center gap-2 rounded-[8px] border bg-background pr-2 pl-3'>
|
||||
<Search className='h-4 w-4 text-muted-foreground' strokeWidth={2} />
|
||||
<Input
|
||||
placeholder='Search blocks...'
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className='h-6 flex-1 border-0 bg-transparent px-0 text-muted-foreground text-sm leading-none placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
spellCheck='false'
|
||||
/>
|
||||
{/* Tabs */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className='flex h-full flex-col'>
|
||||
<div className='flex-shrink-0 px-2 pt-2'>
|
||||
<div className='flex h-9 w-full items-center gap-1 rounded-[10px] border bg-card px-[2.5px] py-1 shadow-xs'>
|
||||
<button
|
||||
onClick={() => setActiveTab('blocks')}
|
||||
className={`panel-tab-base inline-flex flex-1 cursor-pointer items-center justify-center rounded-[8px] border border-transparent py-1 font-[450] text-sm outline-none transition-colors duration-200 ${
|
||||
activeTab === 'blocks' ? 'panel-tab-active' : 'panel-tab-inactive'
|
||||
}`}
|
||||
>
|
||||
Blocks
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('triggers')}
|
||||
className={`panel-tab-base inline-flex flex-1 cursor-pointer items-center justify-center rounded-[8px] border border-transparent py-1 font-[450] text-sm outline-none transition-colors duration-200 ${
|
||||
activeTab === 'triggers' ? 'panel-tab-active' : 'panel-tab-inactive'
|
||||
}`}
|
||||
>
|
||||
Triggers
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<ScrollArea className='flex-1 px-2 pb-[0.26px]' hideScrollbar={true}>
|
||||
<div className='space-y-1 pb-2'>
|
||||
{/* Regular Blocks Section */}
|
||||
{regularBlocks.map((block) => (
|
||||
<ToolbarBlock
|
||||
key={block.type}
|
||||
config={block.config}
|
||||
disabled={!userPermissions.canEdit}
|
||||
{/* Search */}
|
||||
<div className='flex-shrink-0 p-2'>
|
||||
<div className='flex h-9 items-center gap-2 rounded-[8px] border bg-background pr-2 pl-3'>
|
||||
<Search className='h-4 w-4 text-muted-foreground' strokeWidth={2} />
|
||||
<Input
|
||||
placeholder={activeTab === 'blocks' ? 'Search blocks...' : 'Search triggers...'}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className='h-6 flex-1 border-0 bg-transparent px-0 text-muted-foreground text-sm leading-none placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
spellCheck='false'
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Special Blocks Section (Loop & Parallel) */}
|
||||
{specialBlocks.map((block) => {
|
||||
if (block.type === 'loop') {
|
||||
return <LoopToolbarItem key={block.type} disabled={!userPermissions.canEdit} />
|
||||
}
|
||||
if (block.type === 'parallel') {
|
||||
return <ParallelToolbarItem key={block.type} disabled={!userPermissions.canEdit} />
|
||||
}
|
||||
return null
|
||||
})}
|
||||
|
||||
{/* Triggers Section */}
|
||||
{triggers.map((trigger) => (
|
||||
<ToolbarBlock
|
||||
key={trigger.type}
|
||||
config={trigger.config}
|
||||
disabled={!userPermissions.canEdit}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Tools Section */}
|
||||
{tools.map((tool) => (
|
||||
<ToolbarBlock key={tool.type} config={tool} disabled={!userPermissions.canEdit} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Blocks Tab Content */}
|
||||
<TabsContent value='blocks' className='mt-0 flex-1 overflow-hidden'>
|
||||
<ScrollArea className='h-full px-2' hideScrollbar={true}>
|
||||
<div className='space-y-1 pb-2'>
|
||||
{/* Regular Blocks */}
|
||||
{regularBlocks.map((block) => (
|
||||
<ToolbarBlock
|
||||
key={block.type}
|
||||
config={block.config}
|
||||
disabled={!userPermissions.canEdit}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Special Blocks (Loop & Parallel) */}
|
||||
{specialBlocks.map((block) => {
|
||||
if (block.type === 'loop') {
|
||||
return <LoopToolbarItem key={block.type} disabled={!userPermissions.canEdit} />
|
||||
}
|
||||
if (block.type === 'parallel') {
|
||||
return (
|
||||
<ParallelToolbarItem key={block.type} disabled={!userPermissions.canEdit} />
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
|
||||
{/* Tools */}
|
||||
{tools.map((tool) => (
|
||||
<ToolbarBlock key={tool.type} config={tool} disabled={!userPermissions.canEdit} />
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
{/* Triggers Tab Content */}
|
||||
<TabsContent value='triggers' className='mt-0 flex-1 overflow-hidden'>
|
||||
<ScrollArea className='h-full px-2' hideScrollbar={true}>
|
||||
<div className='space-y-1 pb-2'>
|
||||
{triggers.length > 0 ? (
|
||||
triggers.map((trigger) => (
|
||||
<ToolbarBlock
|
||||
key={trigger.type}
|
||||
config={trigger.config}
|
||||
disabled={!userPermissions.canEdit}
|
||||
enableTriggerMode={hasTriggerCapability(trigger.config)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className='py-8 text-center text-muted-foreground text-sm'>
|
||||
{searchQuery ? 'No triggers found' : 'Add triggers from the workflow canvas'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AgentIcon } from '@/components/icons'
|
||||
import { isHosted } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import {
|
||||
getAllModelProviders,
|
||||
getBaseModelProviders,
|
||||
@@ -61,6 +62,7 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
|
||||
type: 'agent',
|
||||
name: 'Agent',
|
||||
description: 'Build an agent',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription:
|
||||
'The Agent block is a core workflow block that is a wrapper around an LLM. It takes in system/user prompts and calls an LLM provider. It can also make tool calls by directly containing tools inside of its tool input. It can additionally return structured output.',
|
||||
docsLink: 'https://docs.sim.ai/blocks/agent',
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { AirtableIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { AirtableResponse } from '@/tools/airtable/types'
|
||||
|
||||
export const AirtableBlock: BlockConfig<AirtableResponse> = {
|
||||
type: 'airtable',
|
||||
name: 'Airtable',
|
||||
description: 'Read, create, and update Airtable',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription:
|
||||
'Integrates Airtable into the workflow. Can create, get, list, or update Airtable records. Requires OAuth. Can be used in trigger mode to trigger a workflow when an update is made to an Airtable table.',
|
||||
'Integrates Airtable into the workflow. Can create, get, list, or update Airtable records. Can be used in trigger mode to trigger a workflow when an update is made to an Airtable table.',
|
||||
docsLink: 'https://docs.sim.ai/tools/airtable',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ApiIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
|
||||
export const ApiTriggerBlock: BlockConfig = {
|
||||
type: 'api_trigger',
|
||||
name: 'API',
|
||||
description: 'Expose as HTTP API endpoint',
|
||||
longDescription:
|
||||
'API trigger to start the workflow via authenticated HTTP calls with structured input.',
|
||||
category: 'triggers',
|
||||
bgColor: '#2F55FF',
|
||||
icon: ApiIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'inputFormat',
|
||||
title: 'Input Format',
|
||||
type: 'input-format',
|
||||
layout: 'full',
|
||||
description: 'Define the JSON input schema accepted by the API endpoint.',
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [],
|
||||
},
|
||||
inputs: {},
|
||||
outputs: {
|
||||
// Dynamic outputs will be added from inputFormat at runtime
|
||||
// Always includes 'input' field plus any fields defined in inputFormat
|
||||
},
|
||||
triggers: {
|
||||
enabled: true,
|
||||
available: ['api'],
|
||||
},
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import { BrowserUseIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
import type { BrowserUseResponse } from '@/tools/browser_use/types'
|
||||
|
||||
export const BrowserUseBlock: BlockConfig<BrowserUseResponse> = {
|
||||
type: 'browser_use',
|
||||
name: 'Browser Use',
|
||||
description: 'Run browser automation tasks',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription:
|
||||
'Integrate Browser Use into the workflow. Can navigate the web and perform actions as if a real user was interacting with the browser. Requires API Key.',
|
||||
'Integrate Browser Use into the workflow. Can navigate the web and perform actions as if a real user was interacting with the browser.',
|
||||
docsLink: 'https://docs.sim.ai/tools/browser_use',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { SVGProps } from 'react'
|
||||
import { createElement } from 'react'
|
||||
import { MessageCircle } from 'lucide-react'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
|
||||
const ChatTriggerIcon = (props: SVGProps<SVGSVGElement>) => createElement(MessageCircle, props)
|
||||
|
||||
export const ChatTriggerBlock: BlockConfig = {
|
||||
type: 'chat_trigger',
|
||||
name: 'Chat',
|
||||
description: 'Start workflow from a chat deployment',
|
||||
longDescription: 'Chat trigger to run the workflow via deployed chat interfaces.',
|
||||
category: 'triggers',
|
||||
bgColor: '#6F3DFA',
|
||||
icon: ChatTriggerIcon,
|
||||
subBlocks: [],
|
||||
tools: {
|
||||
access: [],
|
||||
},
|
||||
inputs: {},
|
||||
outputs: {
|
||||
input: { type: 'string', description: 'User message' },
|
||||
conversationId: { type: 'string', description: 'Conversation ID' },
|
||||
files: { type: 'array', description: 'Uploaded files' },
|
||||
},
|
||||
triggers: {
|
||||
enabled: true,
|
||||
available: ['chat'],
|
||||
},
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { ClayIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
import type { ClayPopulateResponse } from '@/tools/clay/types'
|
||||
|
||||
export const ClayBlock: BlockConfig<ClayPopulateResponse> = {
|
||||
type: 'clay',
|
||||
name: 'Clay',
|
||||
description: 'Populate Clay workbook',
|
||||
longDescription:
|
||||
'Integrate Clay into the workflow. Can populate a table with data. Requires an API Key.',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription: 'Integrate Clay into the workflow. Can populate a table with data.',
|
||||
docsLink: 'https://docs.sim.ai/tools/clay',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { ConfluenceIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { ConfluenceResponse } from '@/tools/confluence/types'
|
||||
|
||||
export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
|
||||
type: 'confluence',
|
||||
name: 'Confluence',
|
||||
description: 'Interact with Confluence',
|
||||
longDescription:
|
||||
'Integrate Confluence into the workflow. Can read and update a page. Requires OAuth.',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription: 'Integrate Confluence into the workflow. Can read and update a page.',
|
||||
docsLink: 'https://docs.sim.ai/tools/confluence',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { DiscordIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { DiscordResponse } from '@/tools/discord/types'
|
||||
|
||||
export const DiscordBlock: BlockConfig<DiscordResponse> = {
|
||||
type: 'discord',
|
||||
name: 'Discord',
|
||||
description: 'Interact with Discord',
|
||||
authMode: AuthMode.BotToken,
|
||||
longDescription:
|
||||
'Integrate Discord into the workflow. Can send and get messages, get server information, and get a user’s information. Requires bot API key.',
|
||||
'Integrate Discord into the workflow. Can send and get messages, get server information, and get a user’s information.',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
icon: DiscordIcon,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { ElevenLabsIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
import type { ElevenLabsBlockResponse } from '@/tools/elevenlabs/types'
|
||||
|
||||
export const ElevenLabsBlock: BlockConfig<ElevenLabsBlockResponse> = {
|
||||
type: 'elevenlabs',
|
||||
name: 'ElevenLabs',
|
||||
description: 'Convert TTS using ElevenLabs',
|
||||
longDescription:
|
||||
'Integrate ElevenLabs into the workflow. Can convert text to speech. Requires API key.',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription: 'Integrate ElevenLabs into the workflow. Can convert text to speech.',
|
||||
docsLink: 'https://docs.sim.ai/tools/elevenlabs',
|
||||
category: 'tools',
|
||||
bgColor: '#181C1E',
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { ExaAIIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { ExaResponse } from '@/tools/exa/types'
|
||||
|
||||
export const ExaBlock: BlockConfig<ExaResponse> = {
|
||||
type: 'exa',
|
||||
name: 'Exa',
|
||||
description: 'Search with Exa AI',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription:
|
||||
'Integrate Exa into the workflow. Can search, get contents, find similar links, answer a question, and perform research. Requires API Key.',
|
||||
'Integrate Exa into the workflow. Can search, get contents, find similar links, answer a question, and perform research.',
|
||||
docsLink: 'https://docs.sim.ai/tools/exa',
|
||||
category: 'tools',
|
||||
bgColor: '#1F40ED',
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { FirecrawlIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { FirecrawlResponse } from '@/tools/firecrawl/types'
|
||||
|
||||
export const FirecrawlBlock: BlockConfig<FirecrawlResponse> = {
|
||||
type: 'firecrawl',
|
||||
name: 'Firecrawl',
|
||||
description: 'Scrape or search the web',
|
||||
longDescription:
|
||||
'Integrate Firecrawl into the workflow. Can search, scrape, or crawl websites. Requires API Key.',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription: 'Integrate Firecrawl into the workflow. Can search, scrape, or crawl websites.',
|
||||
docsLink: 'https://docs.sim.ai/tools/firecrawl',
|
||||
category: 'tools',
|
||||
bgColor: '#181C1E',
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { WebhookIcon } from '@/components/icons'
|
||||
import type { SVGProps } from 'react'
|
||||
import { createElement } from 'react'
|
||||
import { Webhook } from 'lucide-react'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
|
||||
const WebhookIcon = (props: SVGProps<SVGSVGElement>) => createElement(Webhook, props)
|
||||
|
||||
export const GenericWebhookBlock: BlockConfig = {
|
||||
type: 'generic_webhook',
|
||||
name: 'Webhook',
|
||||
@@ -8,6 +12,7 @@ export const GenericWebhookBlock: BlockConfig = {
|
||||
category: 'triggers',
|
||||
icon: WebhookIcon,
|
||||
bgColor: '#10B981', // Green color for triggers
|
||||
triggerAllowed: true,
|
||||
|
||||
subBlocks: [
|
||||
// Generic webhook configuration - always visible
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { GithubIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { GitHubResponse } from '@/tools/github/types'
|
||||
|
||||
export const GitHubBlock: BlockConfig<GitHubResponse> = {
|
||||
type: 'github',
|
||||
name: 'GitHub',
|
||||
description: 'Interact with GitHub or trigger workflows from GitHub events',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription:
|
||||
'Integrate Github into the workflow. Can get get PR details, create PR comment, get repository info, and get latest commit. Requires github token API Key. Can be used in trigger mode to trigger a workflow when a PR is created, commented on, or a commit is pushed.',
|
||||
'Integrate Github into the workflow. Can get get PR details, create PR comment, get repository info, and get latest commit. Can be used in trigger mode to trigger a workflow when a PR is created, commented on, or a commit is pushed.',
|
||||
docsLink: 'https://docs.sim.ai/tools/github',
|
||||
category: 'tools',
|
||||
bgColor: '#181C1E',
|
||||
icon: GithubIcon,
|
||||
triggerAllowed: true,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { GmailIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { GmailToolResponse } from '@/tools/gmail/types'
|
||||
|
||||
export const GmailBlock: BlockConfig<GmailToolResponse> = {
|
||||
type: 'gmail',
|
||||
name: 'Gmail',
|
||||
description: 'Send Gmail or trigger workflows from Gmail events',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription:
|
||||
'Integrate Gmail into the workflow. Can send, read, and search emails. Requires OAuth. Can be used in trigger mode to trigger a workflow when a new email is received.',
|
||||
'Integrate Gmail into the workflow. Can send, read, and search emails. Can be used in trigger mode to trigger a workflow when a new email is received.',
|
||||
docsLink: 'https://docs.sim.ai/tools/gmail',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
icon: GmailIcon,
|
||||
triggerAllowed: true,
|
||||
subBlocks: [
|
||||
// Operation selector
|
||||
{
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { GoogleIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { GoogleSearchResponse } from '@/tools/google/types'
|
||||
|
||||
export const GoogleSearchBlock: BlockConfig<GoogleSearchResponse> = {
|
||||
type: 'google_search',
|
||||
name: 'Google Search',
|
||||
description: 'Search the web',
|
||||
longDescription:
|
||||
'Integrate Google Search into the workflow. Can search the web. Requires API Key.',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription: 'Integrate Google Search into the workflow. Can search the web.',
|
||||
docsLink: 'https://docs.sim.ai/tools/google_search',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { GoogleCalendarIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { GoogleCalendarResponse } from '@/tools/google_calendar/types'
|
||||
|
||||
export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
type: 'google_calendar',
|
||||
name: 'Google Calendar',
|
||||
description: 'Manage Google Calendar events',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription:
|
||||
'Integrate Google Calendar into the workflow. Can create, read, update, and list calendar events. Requires OAuth.',
|
||||
'Integrate Google Calendar into the workflow. Can create, read, update, and list calendar events.',
|
||||
docsLink: 'https://docs.sim.ai/tools/google_calendar',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { GoogleDocsIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { GoogleDocsResponse } from '@/tools/google_docs/types'
|
||||
|
||||
export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
|
||||
type: 'google_docs',
|
||||
name: 'Google Docs',
|
||||
description: 'Read, write, and create documents',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription:
|
||||
'Integrate Google Docs into the workflow. Can read, write, and create documents. Requires OAuth.',
|
||||
'Integrate Google Docs into the workflow. Can read, write, and create documents.',
|
||||
docsLink: 'https://docs.sim.ai/tools/google_docs',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { GoogleDriveIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { GoogleDriveResponse } from '@/tools/google_drive/types'
|
||||
|
||||
export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
|
||||
type: 'google_drive',
|
||||
name: 'Google Drive',
|
||||
description: 'Create, upload, and list files',
|
||||
longDescription:
|
||||
'Integrate Google Drive into the workflow. Can create, upload, and list files. Requires OAuth.',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription: 'Integrate Google Drive into the workflow. Can create, upload, and list files.',
|
||||
docsLink: 'https://docs.sim.ai/tools/google_drive',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { GoogleSheetsIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { GoogleSheetsResponse } from '@/tools/google_sheets/types'
|
||||
|
||||
export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
|
||||
type: 'google_sheets',
|
||||
name: 'Google Sheets',
|
||||
description: 'Read, write, and update data',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription:
|
||||
'Integrate Google Sheets into the workflow. Can read, write, append, and update data. Requires OAuth.',
|
||||
'Integrate Google Sheets into the workflow. Can read, write, append, and update data.',
|
||||
docsLink: 'https://docs.sim.ai/tools/google_sheets',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { HuggingFaceIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { HuggingFaceChatResponse } from '@/tools/huggingface/types'
|
||||
|
||||
export const HuggingFaceBlock: BlockConfig<HuggingFaceChatResponse> = {
|
||||
type: 'huggingface',
|
||||
name: 'Hugging Face',
|
||||
description: 'Use Hugging Face Inference API',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription:
|
||||
'Integrate Hugging Face into the workflow. Can generate completions using the Hugging Face Inference API. Requires API Key.',
|
||||
'Integrate Hugging Face into the workflow. Can generate completions using the Hugging Face Inference API.',
|
||||
docsLink: 'https://docs.sim.ai/tools/huggingface',
|
||||
category: 'tools',
|
||||
bgColor: '#0B0F19',
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { HunterIOIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
import type { HunterResponse } from '@/tools/hunter/types'
|
||||
|
||||
export const HunterBlock: BlockConfig<HunterResponse> = {
|
||||
type: 'hunter',
|
||||
name: 'Hunter io',
|
||||
description: 'Find and verify professional email addresses',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription:
|
||||
'Integrate Hunter into the workflow. Can search domains, find email addresses, verify email addresses, discover companies, find companies, and count email addresses. Requires API Key.',
|
||||
'Integrate Hunter into the workflow. Can search domains, find email addresses, verify email addresses, discover companies, find companies, and count email addresses.',
|
||||
docsLink: 'https://docs.sim.ai/tools/hunter',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { ImageIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
import type { DalleResponse } from '@/tools/openai/types'
|
||||
|
||||
export const ImageGeneratorBlock: BlockConfig<DalleResponse> = {
|
||||
type: 'image_generator',
|
||||
name: 'Image Generator',
|
||||
description: 'Generate images',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription:
|
||||
'Integrate Image Generator into the workflow. Can generate images using DALL-E 3 or GPT Image. Requires API Key.',
|
||||
'Integrate Image Generator into the workflow. Can generate images using DALL-E 3 or GPT Image.',
|
||||
docsLink: 'https://docs.sim.ai/tools/image_generator',
|
||||
category: 'tools',
|
||||
bgColor: '#4D5FFF',
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { SVGProps } from 'react'
|
||||
import { createElement } from 'react'
|
||||
import { Play } from 'lucide-react'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
|
||||
const InputTriggerIcon = (props: SVGProps<SVGSVGElement>) => createElement(Play, props)
|
||||
|
||||
export const InputTriggerBlock: BlockConfig = {
|
||||
type: 'input_trigger',
|
||||
name: 'Input Form',
|
||||
description: 'Start workflow manually with a defined input schema',
|
||||
longDescription:
|
||||
'Manually trigger the workflow from the editor with a structured input schema. This enables typed inputs for parent workflows to map into.',
|
||||
category: 'triggers',
|
||||
bgColor: '#3B82F6',
|
||||
icon: InputTriggerIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'inputFormat',
|
||||
title: 'Input Format',
|
||||
type: 'input-format',
|
||||
layout: 'full',
|
||||
description: 'Define the JSON input schema for this workflow when run manually.',
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [],
|
||||
},
|
||||
inputs: {},
|
||||
outputs: {
|
||||
// Dynamic outputs will be derived from inputFormat
|
||||
},
|
||||
triggers: {
|
||||
enabled: true,
|
||||
available: ['manual'],
|
||||
},
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { JinaAIIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
import type { ReadUrlResponse } from '@/tools/jina/types'
|
||||
|
||||
export const JinaBlock: BlockConfig<ReadUrlResponse> = {
|
||||
type: 'jina',
|
||||
name: 'Jina',
|
||||
description: 'Convert website content into text',
|
||||
longDescription:
|
||||
'Integrate Jina into the workflow. Extracts content from websites. Requires API Key.',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription: 'Integrate Jina into the workflow. Extracts content from websites.',
|
||||
docsLink: 'https://docs.sim.ai/tools/jina',
|
||||
category: 'tools',
|
||||
bgColor: '#333333',
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { JiraIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { JiraResponse } from '@/tools/jira/types'
|
||||
|
||||
export const JiraBlock: BlockConfig<JiraResponse> = {
|
||||
type: 'jira',
|
||||
name: 'Jira',
|
||||
description: 'Interact with Jira',
|
||||
longDescription:
|
||||
'Integrate Jira into the workflow. Can read, write, and update issues. Requires OAuth.',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription: 'Integrate Jira into the workflow. Can read, write, and update issues.',
|
||||
docsLink: 'https://docs.sim.ai/tools/jira',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { LinearIcon } from '@/components/icons'
|
||||
import type { BlockConfig, BlockIcon } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { LinearResponse } from '@/tools/linear/types'
|
||||
|
||||
const LinearBlockIcon: BlockIcon = (props) => LinearIcon(props as any)
|
||||
@@ -8,8 +9,8 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
|
||||
type: 'linear',
|
||||
name: 'Linear',
|
||||
description: 'Read and create issues in Linear',
|
||||
longDescription:
|
||||
'Integrate Linear into the workflow. Can read and create issues. Requires OAuth.',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription: 'Integrate Linear into the workflow. Can read and create issues.',
|
||||
category: 'tools',
|
||||
icon: LinearBlockIcon,
|
||||
bgColor: '#5E6AD2',
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { LinkupIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
import type { LinkupSearchToolResponse } from '@/tools/linkup/types'
|
||||
|
||||
export const LinkupBlock: BlockConfig<LinkupSearchToolResponse> = {
|
||||
type: 'linkup',
|
||||
name: 'Linkup',
|
||||
description: 'Search the web with Linkup',
|
||||
longDescription: 'Integrate Linkup into the workflow. Can search the web. Requires API Key.',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription: 'Integrate Linkup into the workflow. Can search the web.',
|
||||
docsLink: 'https://docs.sim.ai/tools/linkup',
|
||||
category: 'tools',
|
||||
bgColor: '#D6D3C7',
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Mem0Icon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
import type { Mem0Response } from '@/tools/mem0/types'
|
||||
|
||||
export const Mem0Block: BlockConfig<Mem0Response> = {
|
||||
type: 'mem0',
|
||||
name: 'Mem0',
|
||||
description: 'Agent memory management',
|
||||
longDescription:
|
||||
'Integrate Mem0 into the workflow. Can add, search, and retrieve memories. Requires API Key.',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription: 'Integrate Mem0 into the workflow. Can add, search, and retrieve memories.',
|
||||
bgColor: '#181C1E',
|
||||
icon: Mem0Icon,
|
||||
category: 'tools',
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { MicrosoftExcelIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { MicrosoftExcelResponse } from '@/tools/microsoft_excel/types'
|
||||
|
||||
export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
|
||||
type: 'microsoft_excel',
|
||||
name: 'Microsoft Excel',
|
||||
description: 'Read, write, and update data',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription:
|
||||
'Integrate Microsoft Excel into the workflow. Can read, write, update, and add to table. Requires OAuth.',
|
||||
'Integrate Microsoft Excel into the workflow. Can read, write, update, and add to table.',
|
||||
docsLink: 'https://docs.sim.ai/tools/microsoft_excel',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MicrosoftPlannerIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { MicrosoftPlannerResponse } from '@/tools/microsoft_planner/types'
|
||||
|
||||
interface MicrosoftPlannerBlockParams {
|
||||
@@ -19,8 +20,8 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
|
||||
type: 'microsoft_planner',
|
||||
name: 'Microsoft Planner',
|
||||
description: 'Read and create tasks in Microsoft Planner',
|
||||
longDescription:
|
||||
'Integrate Microsoft Planner into the workflow. Can read and create tasks. Requires OAuth.',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription: 'Integrate Microsoft Planner into the workflow. Can read and create tasks.',
|
||||
docsLink: 'https://docs.sim.ai/tools/microsoft_planner',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { MicrosoftTeamsIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { MicrosoftTeamsResponse } from '@/tools/microsoft_teams/types'
|
||||
|
||||
export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
|
||||
type: 'microsoft_teams',
|
||||
name: 'Microsoft Teams',
|
||||
description: 'Read, write, and create messages',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription:
|
||||
'Integrate Microsoft Teams into the workflow. Can read and write chat messages, and read and write channel messages. Requires OAuth. Can be used in trigger mode to trigger a workflow when a message is sent to a chat or channel.',
|
||||
'Integrate Microsoft Teams into the workflow. Can read and write chat messages, and read and write channel messages. Can be used in trigger mode to trigger a workflow when a message is sent to a chat or channel.',
|
||||
docsLink: 'https://docs.sim.ai/tools/microsoft_teams',
|
||||
category: 'tools',
|
||||
triggerAllowed: true,
|
||||
bgColor: '#E0E0E0',
|
||||
icon: MicrosoftTeamsIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { MistralIcon } from '@/components/icons'
|
||||
import type { BlockConfig, SubBlockLayout, SubBlockType } from '@/blocks/types'
|
||||
import { AuthMode, type BlockConfig, type SubBlockLayout, type SubBlockType } from '@/blocks/types'
|
||||
import type { MistralParserOutput } from '@/tools/mistral/types'
|
||||
|
||||
export const MistralParseBlock: BlockConfig<MistralParserOutput> = {
|
||||
type: 'mistral_parse',
|
||||
name: 'Mistral Parser',
|
||||
description: 'Extract text from PDF documents',
|
||||
longDescription: `Integrate Mistral Parse into the workflow. Can extract text from uploaded PDF documents, or from a URL. Requires API Key.`,
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription: `Integrate Mistral Parse into the workflow. Can extract text from uploaded PDF documents, or from a URL.`,
|
||||
docsLink: 'https://docs.sim.ai/tools/mistral_parse',
|
||||
category: 'tools',
|
||||
bgColor: '#000000',
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { NotionIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { NotionResponse } from '@/tools/notion/types'
|
||||
|
||||
export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
type: 'notion',
|
||||
name: 'Notion',
|
||||
description: 'Manage Notion pages',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription:
|
||||
'Integrate with Notion into the workflow. Can read page, read database, create page, create database, append content, query database, and search workspace. Requires OAuth.',
|
||||
'Integrate with Notion into the workflow. Can read page, read database, create page, create database, append content, query database, and search workspace.',
|
||||
docsLink: 'https://docs.sim.ai/tools/notion',
|
||||
category: 'tools',
|
||||
bgColor: '#181C1E',
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { MicrosoftOneDriveIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { OneDriveResponse } from '@/tools/onedrive/types'
|
||||
|
||||
export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
type: 'onedrive',
|
||||
name: 'OneDrive',
|
||||
description: 'Create, upload, and list files',
|
||||
longDescription:
|
||||
'Integrate OneDrive into the workflow. Can create, upload, and list files. Requires OAuth.',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription: 'Integrate OneDrive into the workflow. Can create, upload, and list files.',
|
||||
docsLink: 'https://docs.sim.ai/tools/onedrive',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { OpenAIIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
|
||||
export const OpenAIBlock: BlockConfig = {
|
||||
type: 'openai',
|
||||
name: 'Embeddings',
|
||||
description: 'Generate Open AI embeddings',
|
||||
longDescription:
|
||||
'Integrate Embeddings into the workflow. Can generate embeddings from text. Requires API Key.',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription: 'Integrate Embeddings into the workflow. Can generate embeddings from text.',
|
||||
category: 'tools',
|
||||
docsLink: 'https://docs.sim.ai/tools/openai',
|
||||
bgColor: '#10a37f',
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { OutlookIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { OutlookResponse } from '@/tools/outlook/types'
|
||||
|
||||
export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
||||
type: 'outlook',
|
||||
name: 'Outlook',
|
||||
description: 'Access Outlook',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription:
|
||||
'Integrate Outlook into the workflow. Can read, draft, and send email messages. Requires OAuth. Can be used in trigger mode to trigger a workflow when a new email is received.',
|
||||
'Integrate Outlook into the workflow. Can read, draft, and send email messages. Can be used in trigger mode to trigger a workflow when a new email is received.',
|
||||
docsLink: 'https://docs.sim.ai/tools/outlook',
|
||||
category: 'tools',
|
||||
triggerAllowed: true,
|
||||
bgColor: '#E0E0E0',
|
||||
icon: OutlookIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { ParallelIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
export const ParallelBlock: BlockConfig<ToolResponse> = {
|
||||
type: 'parallel_ai',
|
||||
name: 'Parallel AI',
|
||||
description: 'Search with Parallel AI',
|
||||
longDescription: 'Integrate Parallel AI into the workflow. Can search the web. Requires API Key.',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription: 'Integrate Parallel AI into the workflow. Can search the web.',
|
||||
docsLink: 'https://docs.parallel.ai/search-api/search-quickstart',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PerplexityIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
import type { PerplexityChatResponse } from '@/tools/perplexity/types'
|
||||
|
||||
export const PerplexityBlock: BlockConfig<PerplexityChatResponse> = {
|
||||
@@ -7,7 +7,8 @@ export const PerplexityBlock: BlockConfig<PerplexityChatResponse> = {
|
||||
name: 'Perplexity',
|
||||
description: 'Use Perplexity AI chat models',
|
||||
longDescription:
|
||||
'Integrate Perplexity into the workflow. Can generate completions using Perplexity AI chat models. Requires API Key.',
|
||||
'Integrate Perplexity into the workflow. Can generate completions using Perplexity AI chat models.',
|
||||
authMode: AuthMode.ApiKey,
|
||||
docsLink: 'https://docs.sim.ai/tools/perplexity',
|
||||
category: 'tools',
|
||||
bgColor: '#20808D', // Perplexity turquoise color
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { PineconeIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { PineconeResponse } from '@/tools/pinecone/types'
|
||||
|
||||
export const PineconeBlock: BlockConfig<PineconeResponse> = {
|
||||
type: 'pinecone',
|
||||
name: 'Pinecone',
|
||||
description: 'Use Pinecone vector database',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription:
|
||||
'Integrate Pinecone into the workflow. Can generate embeddings, upsert text, search with text, fetch vectors, and search with vectors. Requires API Key.',
|
||||
'Integrate Pinecone into the workflow. Can generate embeddings, upsert text, search with text, fetch vectors, and search with vectors.',
|
||||
docsLink: 'https://docs.sim.ai/tools/pinecone',
|
||||
category: 'tools',
|
||||
bgColor: '#0D1117',
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { QdrantIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { QdrantResponse } from '@/tools/qdrant/types'
|
||||
|
||||
export const QdrantBlock: BlockConfig<QdrantResponse> = {
|
||||
type: 'qdrant',
|
||||
name: 'Qdrant',
|
||||
description: 'Use Qdrant vector database',
|
||||
longDescription:
|
||||
'Integrate Qdrant into the workflow. Can upsert, search, and fetch points. Requires API Key.',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription: 'Integrate Qdrant into the workflow. Can upsert, search, and fetch points.',
|
||||
docsLink: 'https://qdrant.tech/documentation/',
|
||||
category: 'tools',
|
||||
bgColor: '#1A223F',
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { RedditIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { RedditResponse } from '@/tools/reddit/types'
|
||||
|
||||
export const RedditBlock: BlockConfig<RedditResponse> = {
|
||||
type: 'reddit',
|
||||
name: 'Reddit',
|
||||
description: 'Access Reddit data and content',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription:
|
||||
'Integrate Reddit into the workflow. Can get posts and comments from a subreddit. Requires OAuth.',
|
||||
'Integrate Reddit into the workflow. Can get posts and comments from a subreddit.',
|
||||
docsLink: 'https://docs.sim.ai/tools/reddit',
|
||||
category: 'tools',
|
||||
bgColor: '#FF5700',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ConnectIcon } from '@/components/icons'
|
||||
import { isHosted } from '@/lib/environment'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
import type { ProviderId } from '@/providers/types'
|
||||
import {
|
||||
getAllModelProviders,
|
||||
@@ -108,6 +108,7 @@ export const RouterBlock: BlockConfig<RouterResponse> = {
|
||||
type: 'router',
|
||||
name: 'Router',
|
||||
description: 'Route workflow',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription:
|
||||
'This is a core workflow block. Intelligently direct workflow execution to different paths based on input analysis. Use natural language to instruct the router to route to certain blocks based on the input.',
|
||||
category: 'blocks',
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { S3Icon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { S3Response } from '@/tools/s3/types'
|
||||
|
||||
export const S3Block: BlockConfig<S3Response> = {
|
||||
type: 's3',
|
||||
name: 'S3',
|
||||
description: 'View S3 files',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription:
|
||||
'Integrate S3 into the workflow. Can get presigned URLs for S3 objects. Requires access key and secret access key.',
|
||||
docsLink: 'https://docs.sim.ai/tools/s3',
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { ScheduleIcon } from '@/components/icons'
|
||||
import type { SVGProps } from 'react'
|
||||
import { createElement } from 'react'
|
||||
import { Clock } from 'lucide-react'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
|
||||
const ScheduleIcon = (props: SVGProps<SVGSVGElement>) => createElement(Clock, props)
|
||||
|
||||
export const ScheduleBlock: BlockConfig = {
|
||||
type: 'schedule',
|
||||
name: 'Schedule',
|
||||
@@ -8,7 +12,7 @@ export const ScheduleBlock: BlockConfig = {
|
||||
longDescription:
|
||||
'Integrate Schedule into the workflow. Can trigger a workflow on a schedule configuration.',
|
||||
category: 'triggers',
|
||||
bgColor: '#7B68EE',
|
||||
bgColor: '#6366F1',
|
||||
icon: ScheduleIcon,
|
||||
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { SerperIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { SearchResponse } from '@/tools/serper/types'
|
||||
|
||||
export const SerperBlock: BlockConfig<SearchResponse> = {
|
||||
type: 'serper',
|
||||
name: 'Serper',
|
||||
description: 'Search the web using Serper',
|
||||
longDescription: 'Integrate Serper into the workflow. Can search the web. Requires API Key.',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription: 'Integrate Serper into the workflow. Can search the web.',
|
||||
docsLink: 'https://docs.sim.ai/tools/serper',
|
||||
category: 'tools',
|
||||
bgColor: '#2B3543',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MicrosoftSharepointIcon } from '@/components/icons'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { SharepointResponse } from '@/tools/sharepoint/types'
|
||||
|
||||
const logger = createLogger('SharepointBlock')
|
||||
@@ -9,6 +10,7 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
||||
type: 'sharepoint',
|
||||
name: 'Sharepoint',
|
||||
description: 'Work with pages and lists',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription:
|
||||
'Integrate SharePoint into the workflow. Read/create pages, list sites, and work with lists (read, create, update items). Requires OAuth.',
|
||||
docsLink: 'https://docs.sim.ai/tools/sharepoint',
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { SlackIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { SlackResponse } from '@/tools/slack/types'
|
||||
|
||||
export const SlackBlock: BlockConfig<SlackResponse> = {
|
||||
type: 'slack',
|
||||
name: 'Slack',
|
||||
description: 'Send messages to Slack or trigger workflows from Slack events',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription:
|
||||
'Integrate Slack into the workflow. Can send messages, create canvases, and read messages. Requires OAuth. Can be used in trigger mode to trigger a workflow when a message is sent to a channel.',
|
||||
'Integrate Slack into the workflow. Can send messages, create canvases, and read messages. Requires Bot Token instead of OAuth in advanced mode. Can be used in trigger mode to trigger a workflow when a message is sent to a channel.',
|
||||
docsLink: 'https://docs.sim.ai/tools/slack',
|
||||
category: 'tools',
|
||||
bgColor: '#611f69',
|
||||
icon: SlackIcon,
|
||||
triggerAllowed: true,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { StagehandIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface StagehandExtractResponse extends ToolResponse {
|
||||
@@ -12,8 +12,9 @@ export const StagehandBlock: BlockConfig<StagehandExtractResponse> = {
|
||||
type: 'stagehand',
|
||||
name: 'Stagehand Extract',
|
||||
description: 'Extract data from websites',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription:
|
||||
'Integrate Stagehand into the workflow. Can extract structured data from webpages. Requires API Key.',
|
||||
'Integrate Stagehand into the workflow. Can extract structured data from webpages.',
|
||||
docsLink: 'https://docs.sim.ai/tools/stagehand',
|
||||
category: 'tools',
|
||||
bgColor: '#FFC83C',
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { StagehandIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
import type { StagehandAgentResponse } from '@/tools/stagehand/types'
|
||||
|
||||
export const StagehandAgentBlock: BlockConfig<StagehandAgentResponse> = {
|
||||
type: 'stagehand_agent',
|
||||
name: 'Stagehand Agent',
|
||||
description: 'Autonomous web browsing agent',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription:
|
||||
'Integrate Stagehand Agent into the workflow. Can navigate the web and perform tasks. Requires API Key.',
|
||||
'Integrate Stagehand Agent into the workflow. Can navigate the web and perform tasks.',
|
||||
docsLink: 'https://docs.sim.ai/tools/stagehand_agent',
|
||||
category: 'tools',
|
||||
bgColor: '#FFC83C',
|
||||
|
||||
@@ -9,6 +9,7 @@ export const StarterBlock: BlockConfig = {
|
||||
category: 'blocks',
|
||||
bgColor: '#2FB3FF',
|
||||
icon: StartIcon,
|
||||
hideFromToolbar: true,
|
||||
subBlocks: [
|
||||
// Main trigger selector
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SupabaseIcon } from '@/components/icons'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
import type { SupabaseResponse } from '@/tools/supabase/types'
|
||||
|
||||
const logger = createLogger('SupabaseBlock')
|
||||
@@ -9,6 +9,7 @@ export const SupabaseBlock: BlockConfig<SupabaseResponse> = {
|
||||
type: 'supabase',
|
||||
name: 'Supabase',
|
||||
description: 'Use Supabase database',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription:
|
||||
'Integrate Supabase into the workflow. Can get many rows, get, create, update, delete, and upsert a row.',
|
||||
docsLink: 'https://docs.sim.ai/tools/supabase',
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { TavilyIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { TavilyResponse } from '@/tools/tavily/types'
|
||||
|
||||
export const TavilyBlock: BlockConfig<TavilyResponse> = {
|
||||
type: 'tavily',
|
||||
name: 'Tavily',
|
||||
description: 'Search and extract information',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription:
|
||||
'Integrate Tavily into the workflow. Can search the web and extract content from specific URLs. Requires API Key.',
|
||||
category: 'tools',
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { TelegramIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { TelegramMessageResponse } from '@/tools/telegram/types'
|
||||
|
||||
export const TelegramBlock: BlockConfig<TelegramMessageResponse> = {
|
||||
type: 'telegram',
|
||||
name: 'Telegram',
|
||||
description: 'Send messages through Telegram or trigger workflows from Telegram events',
|
||||
authMode: AuthMode.BotToken,
|
||||
longDescription:
|
||||
'Integrate Telegram into the workflow. Can send messages. Can be used in trigger mode to trigger a workflow when a message is sent to a chat.',
|
||||
docsLink: 'https://docs.sim.ai/tools/telegram',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
icon: TelegramIcon,
|
||||
triggerAllowed: true,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'botToken',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TranslateIcon } from '@/components/icons'
|
||||
import { isHosted } from '@/lib/environment'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
import {
|
||||
getAllModelProviders,
|
||||
getBaseModelProviders,
|
||||
@@ -29,6 +29,7 @@ export const TranslateBlock: BlockConfig = {
|
||||
type: 'translate',
|
||||
name: 'Translate',
|
||||
description: 'Translate text to any language',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription: 'Integrate Translate into the workflow. Can translate text to any language.',
|
||||
docsLink: 'https://docs.sim.ai/tools/translate',
|
||||
category: 'tools',
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { TwilioIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { TwilioSMSBlockOutput } from '@/tools/twilio/types'
|
||||
|
||||
export const TwilioSMSBlock: BlockConfig<TwilioSMSBlockOutput> = {
|
||||
type: 'twilio_sms',
|
||||
name: 'Twilio SMS',
|
||||
description: 'Send SMS messages',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription: 'Integrate Twilio into the workflow. Can send SMS messages.',
|
||||
category: 'tools',
|
||||
bgColor: '#F22F46', // Twilio brand color
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { TypeformIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { TypeformResponse } from '@/tools/typeform/types'
|
||||
|
||||
export const TypeformBlock: BlockConfig<TypeformResponse> = {
|
||||
type: 'typeform',
|
||||
name: 'Typeform',
|
||||
description: 'Interact with Typeform',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription:
|
||||
'Integrate Typeform into the workflow. Can retrieve responses, download files, and get form insights. Requires API Key.',
|
||||
docsLink: 'https://docs.sim.ai/tools/typeform',
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { EyeIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { VisionResponse } from '@/tools/vision/types'
|
||||
|
||||
export const VisionBlock: BlockConfig<VisionResponse> = {
|
||||
type: 'vision',
|
||||
name: 'Vision',
|
||||
description: 'Analyze images with vision models',
|
||||
longDescription:
|
||||
'Integrate Vision into the workflow. Can analyze images with vision models. Requires API Key.',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription: 'Integrate Vision into the workflow. Can analyze images with vision models.',
|
||||
docsLink: 'https://docs.sim.ai/tools/vision',
|
||||
category: 'tools',
|
||||
bgColor: '#4D5FFF',
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { WealthboxIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { WealthboxResponse } from '@/tools/wealthbox/types'
|
||||
|
||||
export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
|
||||
type: 'wealthbox',
|
||||
name: 'Wealthbox',
|
||||
description: 'Interact with Wealthbox',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription:
|
||||
'Integrate Wealthbox into the workflow. Can read and write notes, read and write contacts, and read and write tasks. Requires OAuth.',
|
||||
'Integrate Wealthbox into the workflow. Can read and write notes, read and write contacts, and read and write tasks.',
|
||||
docsLink: 'https://docs.sim.ai/tools/wealthbox',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
WhatsAppIcon,
|
||||
} from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
|
||||
const getWebhookProviderIcon = (provider: string) => {
|
||||
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
@@ -36,9 +37,11 @@ export const WebhookBlock: BlockConfig = {
|
||||
type: 'webhook',
|
||||
name: 'Webhook',
|
||||
description: 'Trigger workflow execution from external webhooks',
|
||||
authMode: AuthMode.OAuth,
|
||||
category: 'triggers',
|
||||
icon: WebhookIcon,
|
||||
bgColor: '#10B981', // Green color for triggers
|
||||
triggerAllowed: true,
|
||||
hideFromToolbar: true, // Hidden for backwards compatibility - use generic webhook trigger instead
|
||||
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { WhatsAppIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { WhatsAppResponse } from '@/tools/whatsapp/types'
|
||||
|
||||
export const WhatsAppBlock: BlockConfig<WhatsAppResponse> = {
|
||||
type: 'whatsapp',
|
||||
name: 'WhatsApp',
|
||||
description: 'Send WhatsApp messages',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription: 'Integrate WhatsApp into the workflow. Can send messages.',
|
||||
docsLink: 'https://docs.sim.ai/tools/whatsapp',
|
||||
category: 'tools',
|
||||
bgColor: '#25D366',
|
||||
icon: WhatsAppIcon,
|
||||
triggerAllowed: true,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'phoneNumber',
|
||||
|
||||
@@ -80,4 +80,5 @@ export const WorkflowBlock: BlockConfig = {
|
||||
result: { type: 'json', description: 'Workflow execution result' },
|
||||
error: { type: 'string', description: 'Error message' },
|
||||
},
|
||||
hideFromToolbar: true,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { WorkflowIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
// Helper: list workflows excluding self
|
||||
const getAvailableWorkflows = (): Array<{ label: string; id: string }> => {
|
||||
try {
|
||||
const { workflows, activeWorkflowId } = useWorkflowRegistry.getState()
|
||||
return Object.entries(workflows)
|
||||
.filter(([id]) => id !== activeWorkflowId)
|
||||
.map(([id, w]) => ({ label: w.name || `Workflow ${id.slice(0, 8)}`, id }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// New workflow block variant that visualizes child Input Trigger schema for mapping
|
||||
export const WorkflowInputBlock: BlockConfig = {
|
||||
type: 'workflow_input',
|
||||
name: 'Workflow',
|
||||
description: 'Execute another workflow and map variables to its Input Trigger schema.',
|
||||
category: 'blocks',
|
||||
bgColor: '#6366F1', // Indigo - modern and professional
|
||||
icon: WorkflowIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'workflowId',
|
||||
title: 'Select Workflow',
|
||||
type: 'dropdown',
|
||||
options: getAvailableWorkflows,
|
||||
required: true,
|
||||
},
|
||||
// Renders dynamic mapping UI based on selected child workflow's Input Trigger inputFormat
|
||||
{
|
||||
id: 'inputMapping',
|
||||
title: 'Input Mapping',
|
||||
type: 'input-mapping',
|
||||
layout: 'full',
|
||||
description:
|
||||
"Map fields defined in the child workflow's Input Trigger to variables/values in this workflow.",
|
||||
dependsOn: ['workflowId'],
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: ['workflow_executor'],
|
||||
},
|
||||
inputs: {
|
||||
workflowId: { type: 'string', description: 'ID of the child workflow' },
|
||||
inputMapping: { type: 'json', description: 'Mapping of input fields to values' },
|
||||
},
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Execution success status' },
|
||||
childWorkflowName: { type: 'string', description: 'Child workflow name' },
|
||||
result: { type: 'json', description: 'Workflow execution result' },
|
||||
error: { type: 'string', description: 'Error message' },
|
||||
},
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
import { xIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { XResponse } from '@/tools/x/types'
|
||||
|
||||
export const XBlock: BlockConfig<XResponse> = {
|
||||
type: 'x',
|
||||
name: 'X',
|
||||
description: 'Interact with X',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription:
|
||||
'Integrate X into the workflow. Can post a new tweet, get tweet details, search tweets, and get user profile. Requires OAuth.',
|
||||
'Integrate X into the workflow. Can post a new tweet, get tweet details, search tweets, and get user profile.',
|
||||
docsLink: 'https://docs.sim.ai/tools/x',
|
||||
category: 'tools',
|
||||
bgColor: '#000000', // X's black color
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { YouTubeIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import type { BlockConfig, BlockIcon } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { YouTubeSearchResponse } from '@/tools/youtube/types'
|
||||
|
||||
const YouTubeBlockIcon: BlockIcon = (props) => YouTubeIcon(props as any)
|
||||
|
||||
export const YouTubeBlock: BlockConfig<YouTubeSearchResponse> = {
|
||||
type: 'youtube',
|
||||
name: 'YouTube',
|
||||
description: 'Search for videos on YouTube',
|
||||
longDescription: 'Integrate YouTube into the workflow. Can search for videos. Requires API Key.',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription: 'Integrate YouTube into the workflow. Can search for videos.',
|
||||
docsLink: 'https://docs.sim.ai/tools/youtube',
|
||||
category: 'tools',
|
||||
bgColor: '#FF0000',
|
||||
icon: YouTubeIcon,
|
||||
icon: YouTubeBlockIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'query',
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
import { AgentBlock } from '@/blocks/blocks/agent'
|
||||
import { AirtableBlock } from '@/blocks/blocks/airtable'
|
||||
import { ApiBlock } from '@/blocks/blocks/api'
|
||||
import { ApiTriggerBlock } from '@/blocks/blocks/api_trigger'
|
||||
import { ArxivBlock } from '@/blocks/blocks/arxiv'
|
||||
import { BrowserUseBlock } from '@/blocks/blocks/browser_use'
|
||||
import { ChatTriggerBlock } from '@/blocks/blocks/chat_trigger'
|
||||
import { ClayBlock } from '@/blocks/blocks/clay'
|
||||
import { ConditionBlock } from '@/blocks/blocks/condition'
|
||||
import { ConfluenceBlock } from '@/blocks/blocks/confluence'
|
||||
@@ -30,6 +32,7 @@ import { GoogleSheetsBlock } from '@/blocks/blocks/google_sheets'
|
||||
import { HuggingFaceBlock } from '@/blocks/blocks/huggingface'
|
||||
import { HunterBlock } from '@/blocks/blocks/hunter'
|
||||
import { ImageGeneratorBlock } from '@/blocks/blocks/image_generator'
|
||||
import { InputTriggerBlock } from '@/blocks/blocks/input_trigger'
|
||||
import { JinaBlock } from '@/blocks/blocks/jina'
|
||||
import { JiraBlock } from '@/blocks/blocks/jira'
|
||||
import { KnowledgeBlock } from '@/blocks/blocks/knowledge'
|
||||
@@ -78,6 +81,7 @@ import { WebhookBlock } from '@/blocks/blocks/webhook'
|
||||
import { WhatsAppBlock } from '@/blocks/blocks/whatsapp'
|
||||
import { WikipediaBlock } from '@/blocks/blocks/wikipedia'
|
||||
import { WorkflowBlock } from '@/blocks/blocks/workflow'
|
||||
import { WorkflowInputBlock } from '@/blocks/blocks/workflow_input'
|
||||
import { XBlock } from '@/blocks/blocks/x'
|
||||
import { YouTubeBlock } from '@/blocks/blocks/youtube'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
@@ -147,6 +151,9 @@ export const registry: Record<string, BlockConfig> = {
|
||||
stagehand_agent: StagehandAgentBlock,
|
||||
slack: SlackBlock,
|
||||
starter: StarterBlock,
|
||||
input_trigger: InputTriggerBlock,
|
||||
chat_trigger: ChatTriggerBlock,
|
||||
api_trigger: ApiTriggerBlock,
|
||||
supabase: SupabaseBlock,
|
||||
tavily: TavilyBlock,
|
||||
telegram: TelegramBlock,
|
||||
@@ -160,6 +167,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
whatsapp: WhatsAppBlock,
|
||||
wikipedia: WikipediaBlock,
|
||||
workflow: WorkflowBlock,
|
||||
workflow_input: WorkflowInputBlock,
|
||||
x: XBlock,
|
||||
youtube: YouTubeBlock,
|
||||
}
|
||||
|
||||
@@ -7,6 +7,13 @@ export type PrimitiveValueType = 'string' | 'number' | 'boolean' | 'json' | 'arr
|
||||
|
||||
export type BlockCategory = 'blocks' | 'tools' | 'triggers'
|
||||
|
||||
// Authentication modes for sub-blocks and summaries
|
||||
export enum AuthMode {
|
||||
OAuth = 'oauth',
|
||||
ApiKey = 'api_key',
|
||||
BotToken = 'bot_token',
|
||||
}
|
||||
|
||||
export type GenerationType =
|
||||
| 'javascript-function-body'
|
||||
| 'typescript-function-body'
|
||||
@@ -54,6 +61,7 @@ export type SubBlockType =
|
||||
| 'input-format' // Input structure format
|
||||
| 'response-format' // Response structure format
|
||||
| 'file-upload' // File uploader
|
||||
| 'input-mapping' // Map parent variables to child workflow input schema
|
||||
|
||||
export type SubBlockLayout = 'full' | 'half'
|
||||
|
||||
@@ -186,6 +194,8 @@ export interface BlockConfig<T extends ToolResponse = ToolResponse> {
|
||||
bgColor: string
|
||||
icon: BlockIcon
|
||||
subBlocks: SubBlockConfig[]
|
||||
triggerAllowed?: boolean
|
||||
authMode?: AuthMode
|
||||
tools: {
|
||||
access: string[]
|
||||
config?: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ChevronRight } from 'lucide-react'
|
||||
import { BlockPathCalculator } from '@/lib/block-path-calculator'
|
||||
import { extractFieldsFromSchema, parseResponseFormatSafely } from '@/lib/response-format'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { getBlockOutputPaths, getBlockOutputType } from '@/lib/workflows/block-outputs'
|
||||
import { getBlock } from '@/blocks'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { Serializer } from '@/serializer'
|
||||
@@ -101,7 +102,8 @@ const getOutputTypeForPath = (
|
||||
block: BlockState,
|
||||
blockConfig: BlockConfig | null,
|
||||
blockId: string,
|
||||
outputPath: string
|
||||
outputPath: string,
|
||||
mergedSubBlocksOverride?: Record<string, any>
|
||||
): string => {
|
||||
if (block?.triggerMode && blockConfig?.triggers?.enabled) {
|
||||
const triggerId = blockConfig?.triggers?.available?.[0]
|
||||
@@ -125,7 +127,8 @@ const getOutputTypeForPath = (
|
||||
}
|
||||
} else if (block?.type === 'starter') {
|
||||
// Handle starter block specific outputs
|
||||
const startWorkflowValue = getSubBlockValue(blockId, 'startWorkflow')
|
||||
const startWorkflowValue =
|
||||
mergedSubBlocksOverride?.startWorkflow?.value ?? getSubBlockValue(blockId, 'startWorkflow')
|
||||
|
||||
if (startWorkflowValue === 'chat') {
|
||||
// Define types for chat mode outputs
|
||||
@@ -137,7 +140,8 @@ const getOutputTypeForPath = (
|
||||
return chatModeTypes[outputPath] || 'any'
|
||||
}
|
||||
// For API mode, check inputFormat for custom field types
|
||||
const inputFormatValue = getSubBlockValue(blockId, 'inputFormat')
|
||||
const inputFormatValue =
|
||||
mergedSubBlocksOverride?.inputFormat?.value ?? getSubBlockValue(blockId, 'inputFormat')
|
||||
if (inputFormatValue && Array.isArray(inputFormatValue)) {
|
||||
const field = inputFormatValue.find(
|
||||
(f: { name?: string; type?: string }) => f.name === outputPath
|
||||
@@ -146,6 +150,11 @@ const getOutputTypeForPath = (
|
||||
return field.type
|
||||
}
|
||||
}
|
||||
} else if (blockConfig?.category === 'triggers') {
|
||||
// For trigger blocks, use the dynamic output helper
|
||||
const blockState = useWorkflowStore.getState().blocks[blockId]
|
||||
const subBlocks = mergedSubBlocksOverride ?? (blockState?.subBlocks || {})
|
||||
return getBlockOutputType(block.type, outputPath, subBlocks)
|
||||
} else {
|
||||
const operationValue = getSubBlockValue(blockId, 'operation')
|
||||
if (blockConfig && operationValue) {
|
||||
@@ -297,6 +306,24 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
const edges = useWorkflowStore((state) => state.edges)
|
||||
const workflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
|
||||
|
||||
// Subscribe to live subblock values for the active workflow to react to input format changes
|
||||
const workflowSubBlockValues = useSubBlockStore((state) =>
|
||||
workflowId ? (state.workflowValues[workflowId] ?? {}) : {}
|
||||
)
|
||||
|
||||
const getMergedSubBlocks = useCallback(
|
||||
(targetBlockId: string): Record<string, any> => {
|
||||
const base = blocks[targetBlockId]?.subBlocks || {}
|
||||
const live = workflowSubBlockValues?.[targetBlockId] || {}
|
||||
const merged: Record<string, any> = { ...base }
|
||||
for (const [subId, liveVal] of Object.entries(live)) {
|
||||
merged[subId] = { ...(base[subId] || {}), value: liveVal }
|
||||
}
|
||||
return merged
|
||||
},
|
||||
[blocks, workflowSubBlockValues]
|
||||
)
|
||||
|
||||
const getVariablesByWorkflowId = useVariablesStore((state) => state.getVariablesByWorkflowId)
|
||||
const variables = useVariablesStore((state) => state.variables)
|
||||
const workflowVariables = workflowId ? getVariablesByWorkflowId(workflowId) : []
|
||||
@@ -355,7 +382,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
const blockName = sourceBlock.name || sourceBlock.type
|
||||
const normalizedBlockName = normalizeBlockName(blockName)
|
||||
|
||||
const responseFormatValue = getSubBlockValue(activeSourceBlockId, 'responseFormat')
|
||||
const mergedSubBlocks = getMergedSubBlocks(activeSourceBlockId)
|
||||
const responseFormatValue = mergedSubBlocks?.responseFormat?.value
|
||||
const responseFormat = parseResponseFormatSafely(responseFormatValue, activeSourceBlockId)
|
||||
|
||||
let blockTags: string[]
|
||||
@@ -382,7 +410,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
}
|
||||
} else if (!blockConfig.outputs || Object.keys(blockConfig.outputs).length === 0) {
|
||||
if (sourceBlock.type === 'starter') {
|
||||
const startWorkflowValue = getSubBlockValue(activeSourceBlockId, 'startWorkflow')
|
||||
const startWorkflowValue = mergedSubBlocks?.startWorkflow?.value
|
||||
|
||||
if (startWorkflowValue === 'chat') {
|
||||
// For chat mode, provide input, conversationId, and files
|
||||
@@ -392,7 +420,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
`${normalizedBlockName}.files`,
|
||||
]
|
||||
} else {
|
||||
const inputFormatValue = getSubBlockValue(activeSourceBlockId, 'inputFormat')
|
||||
const inputFormatValue = mergedSubBlocks?.inputFormat?.value
|
||||
|
||||
if (
|
||||
inputFormatValue &&
|
||||
@@ -410,7 +438,17 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
blockTags = [normalizedBlockName]
|
||||
}
|
||||
} else {
|
||||
if (sourceBlock?.triggerMode && blockConfig.triggers?.enabled) {
|
||||
// For triggers and starter blocks, use dynamic outputs based on live subblock values
|
||||
if (blockConfig.category === 'triggers' || sourceBlock.type === 'starter') {
|
||||
const dynamicOutputs = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks)
|
||||
if (dynamicOutputs.length > 0) {
|
||||
blockTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`)
|
||||
} else if (sourceBlock.type === 'starter') {
|
||||
blockTags = [normalizedBlockName]
|
||||
} else {
|
||||
blockTags = []
|
||||
}
|
||||
} else if (sourceBlock?.triggerMode && blockConfig.triggers?.enabled) {
|
||||
const triggerId = blockConfig?.triggers?.available?.[0]
|
||||
const firstTrigger = triggerId
|
||||
? getTrigger(triggerId)
|
||||
@@ -426,7 +464,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
}
|
||||
} else {
|
||||
// Check for tool-specific outputs first
|
||||
const operationValue = getSubBlockValue(activeSourceBlockId, 'operation')
|
||||
const operationValue =
|
||||
mergedSubBlocks?.operation?.value ?? getSubBlockValue(activeSourceBlockId, 'operation')
|
||||
const toolOutputPaths = operationValue
|
||||
? generateToolOutputPaths(blockConfig, operationValue)
|
||||
: []
|
||||
@@ -625,12 +664,34 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
const blockName = accessibleBlock.name || accessibleBlock.type
|
||||
const normalizedBlockName = normalizeBlockName(blockName)
|
||||
|
||||
const responseFormatValue = getSubBlockValue(accessibleBlockId, 'responseFormat')
|
||||
const mergedSubBlocks = getMergedSubBlocks(accessibleBlockId)
|
||||
const responseFormatValue = mergedSubBlocks?.responseFormat?.value
|
||||
const responseFormat = parseResponseFormatSafely(responseFormatValue, accessibleBlockId)
|
||||
|
||||
let blockTags: string[]
|
||||
|
||||
if (accessibleBlock.type === 'evaluator') {
|
||||
// For trigger blocks, use the dynamic output helper
|
||||
if (blockConfig.category === 'triggers' || accessibleBlock.type === 'starter') {
|
||||
const dynamicOutputs = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks)
|
||||
|
||||
if (dynamicOutputs.length > 0) {
|
||||
blockTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`)
|
||||
} else if (accessibleBlock.type === 'starter') {
|
||||
// Legacy starter block fallback
|
||||
const startWorkflowValue = mergedSubBlocks?.startWorkflow?.value
|
||||
if (startWorkflowValue === 'chat') {
|
||||
blockTags = [
|
||||
`${normalizedBlockName}.input`,
|
||||
`${normalizedBlockName}.conversationId`,
|
||||
`${normalizedBlockName}.files`,
|
||||
]
|
||||
} else {
|
||||
blockTags = [normalizedBlockName]
|
||||
}
|
||||
} else {
|
||||
blockTags = []
|
||||
}
|
||||
} else if (accessibleBlock.type === 'evaluator') {
|
||||
const metricsValue = getSubBlockValue(accessibleBlockId, 'metrics')
|
||||
|
||||
if (metricsValue && Array.isArray(metricsValue) && metricsValue.length > 0) {
|
||||
@@ -651,34 +712,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
||||
}
|
||||
} else if (!blockConfig.outputs || Object.keys(blockConfig.outputs).length === 0) {
|
||||
if (accessibleBlock.type === 'starter') {
|
||||
const startWorkflowValue = getSubBlockValue(accessibleBlockId, 'startWorkflow')
|
||||
|
||||
if (startWorkflowValue === 'chat') {
|
||||
// For chat mode, provide input, conversationId, and files
|
||||
blockTags = [
|
||||
`${normalizedBlockName}.input`,
|
||||
`${normalizedBlockName}.conversationId`,
|
||||
`${normalizedBlockName}.files`,
|
||||
]
|
||||
} else {
|
||||
const inputFormatValue = getSubBlockValue(accessibleBlockId, 'inputFormat')
|
||||
|
||||
if (
|
||||
inputFormatValue &&
|
||||
Array.isArray(inputFormatValue) &&
|
||||
inputFormatValue.length > 0
|
||||
) {
|
||||
blockTags = inputFormatValue
|
||||
.filter((field: { name?: string }) => field.name && field.name.trim() !== '')
|
||||
.map((field: { name: string }) => `${normalizedBlockName}.${field.name}`)
|
||||
} else {
|
||||
blockTags = [normalizedBlockName]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
blockTags = [normalizedBlockName]
|
||||
}
|
||||
blockTags = [normalizedBlockName]
|
||||
} else {
|
||||
const blockState = blocks[accessibleBlockId]
|
||||
if (blockState?.triggerMode && blockConfig.triggers?.enabled) {
|
||||
@@ -697,7 +731,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
}
|
||||
} else {
|
||||
// Check for tool-specific outputs first
|
||||
const operationValue = getSubBlockValue(accessibleBlockId, 'operation')
|
||||
const operationValue =
|
||||
mergedSubBlocks?.operation?.value ?? getSubBlockValue(accessibleBlockId, 'operation')
|
||||
const toolOutputPaths = operationValue
|
||||
? generateToolOutputPaths(blockConfig, operationValue)
|
||||
: []
|
||||
@@ -746,7 +781,17 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
variableInfoMap,
|
||||
blockTagGroups: finalBlockTagGroups,
|
||||
}
|
||||
}, [blocks, edges, loops, parallels, blockId, activeSourceBlockId, workflowVariables])
|
||||
}, [
|
||||
blocks,
|
||||
edges,
|
||||
loops,
|
||||
parallels,
|
||||
blockId,
|
||||
activeSourceBlockId,
|
||||
workflowVariables,
|
||||
workflowSubBlockValues,
|
||||
getMergedSubBlocks,
|
||||
])
|
||||
|
||||
const filteredTags = useMemo(() => {
|
||||
if (!searchTerm) return tags
|
||||
@@ -1328,12 +1373,14 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
)
|
||||
if (block) {
|
||||
const blockConfig = getBlock(block.type)
|
||||
const mergedSubBlocks = getMergedSubBlocks(group.blockId)
|
||||
|
||||
tagDescription = getOutputTypeForPath(
|
||||
block,
|
||||
blockConfig || null,
|
||||
group.blockId,
|
||||
outputPath
|
||||
outputPath,
|
||||
mergedSubBlocks
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1468,12 +1515,14 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
)
|
||||
if (block) {
|
||||
const blockConfig = getBlock(block.type)
|
||||
const mergedSubBlocks = getMergedSubBlocks(group.blockId)
|
||||
|
||||
childType = getOutputTypeForPath(
|
||||
block,
|
||||
blockConfig || null,
|
||||
group.blockId,
|
||||
childOutputPath
|
||||
childOutputPath,
|
||||
mergedSubBlocks
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,20 @@ const MAX_WORKFLOW_DEPTH = 10
|
||||
export class WorkflowBlockHandler implements BlockHandler {
|
||||
private serializer = new Serializer()
|
||||
|
||||
// Tolerant JSON parser for mapping values
|
||||
// Keeps handler self-contained without introducing utilities
|
||||
private safeParse(input: unknown): unknown {
|
||||
if (typeof input !== 'string') return input
|
||||
try {
|
||||
return JSON.parse(input)
|
||||
} catch {
|
||||
return input
|
||||
}
|
||||
}
|
||||
|
||||
canHandle(block: SerializedBlock): boolean {
|
||||
return block.metadata?.id === BlockType.WORKFLOW
|
||||
const id = block.metadata?.id
|
||||
return id === BlockType.WORKFLOW || id === 'workflow_input'
|
||||
}
|
||||
|
||||
async execute(
|
||||
@@ -63,13 +75,22 @@ export class WorkflowBlockHandler implements BlockHandler {
|
||||
)
|
||||
|
||||
// Prepare the input for the child workflow
|
||||
// The input from this block should be passed as start.input to the child workflow
|
||||
let childWorkflowInput = {}
|
||||
// Prefer structured mapping if provided; otherwise fall back to legacy 'input' passthrough
|
||||
let childWorkflowInput: Record<string, any> = {}
|
||||
|
||||
if (inputs.input !== undefined) {
|
||||
// If input is provided, use it directly
|
||||
if (inputs.inputMapping !== undefined && inputs.inputMapping !== null) {
|
||||
// Handle inputMapping - could be object or stringified JSON
|
||||
const raw = inputs.inputMapping
|
||||
const normalized = this.safeParse(raw)
|
||||
|
||||
if (normalized && typeof normalized === 'object' && !Array.isArray(normalized)) {
|
||||
childWorkflowInput = normalized as Record<string, any>
|
||||
} else {
|
||||
childWorkflowInput = {}
|
||||
}
|
||||
} else if (inputs.input !== undefined) {
|
||||
// Legacy behavior: pass under start.input
|
||||
childWorkflowInput = inputs.input
|
||||
logger.info(`Passing input to child workflow: ${JSON.stringify(childWorkflowInput)}`)
|
||||
}
|
||||
|
||||
// Remove the workflowId from the input to avoid confusion
|
||||
@@ -308,10 +329,11 @@ export class WorkflowBlockHandler implements BlockHandler {
|
||||
}
|
||||
return failure as Record<string, any>
|
||||
}
|
||||
let result = childResult
|
||||
if (childResult?.output) {
|
||||
result = childResult.output
|
||||
}
|
||||
|
||||
// childResult is an ExecutionResult with structure { success, output, metadata, logs }
|
||||
// We want the actual output from the execution
|
||||
const result = childResult.output || {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
childWorkflowName,
|
||||
|
||||
+158
-70
@@ -630,17 +630,31 @@ export class Executor {
|
||||
*/
|
||||
private validateWorkflow(startBlockId?: string): void {
|
||||
if (startBlockId) {
|
||||
// If starting from a specific block (webhook trigger or schedule trigger), validate that block exists
|
||||
const startBlock = this.actualWorkflow.blocks.find((block) => block.id === startBlockId)
|
||||
if (!startBlock || !startBlock.enabled) {
|
||||
throw new Error(`Start block ${startBlockId} not found or disabled`)
|
||||
}
|
||||
// Trigger blocks (webhook and schedule) can have incoming connections, so no need to check that
|
||||
return
|
||||
}
|
||||
|
||||
const starterBlock = this.actualWorkflow.blocks.find(
|
||||
(block) => block.metadata?.id === BlockType.STARTER
|
||||
)
|
||||
|
||||
// Check for any type of trigger block (dedicated triggers or trigger-mode blocks)
|
||||
const hasTriggerBlocks = this.actualWorkflow.blocks.some((block) => {
|
||||
// Check if it's a dedicated trigger block (category: 'triggers')
|
||||
if (block.metadata?.category === 'triggers') return true
|
||||
// Check if it's a block with trigger mode enabled
|
||||
if (block.config?.params?.triggerMode === true) return true
|
||||
return false
|
||||
})
|
||||
|
||||
if (hasTriggerBlocks) {
|
||||
// When triggers exist (either dedicated or trigger-mode), we allow execution without a starter block
|
||||
// The actual start block will be determined at runtime based on the execution context
|
||||
} else {
|
||||
// Default validation for starter block
|
||||
const starterBlock = this.actualWorkflow.blocks.find(
|
||||
(block) => block.metadata?.id === BlockType.STARTER
|
||||
)
|
||||
// Legacy workflows: require a valid starter block and basic connection checks
|
||||
if (!starterBlock || !starterBlock.enabled) {
|
||||
throw new Error('Workflow must have an enabled starter block')
|
||||
}
|
||||
@@ -652,22 +666,15 @@ export class Executor {
|
||||
throw new Error('Starter block cannot have incoming connections')
|
||||
}
|
||||
|
||||
// Check if there are any trigger blocks on the canvas
|
||||
const hasTriggerBlocks = this.actualWorkflow.blocks.some((block) => {
|
||||
return block.metadata?.category === 'triggers' || block.config?.params?.triggerMode === true
|
||||
})
|
||||
|
||||
// Only check outgoing connections for starter blocks if there are no trigger blocks
|
||||
if (!hasTriggerBlocks) {
|
||||
const outgoingFromStarter = this.actualWorkflow.connections.filter(
|
||||
(conn) => conn.source === starterBlock.id
|
||||
)
|
||||
if (outgoingFromStarter.length === 0) {
|
||||
throw new Error('Starter block must have at least one outgoing connection')
|
||||
}
|
||||
const outgoingFromStarter = this.actualWorkflow.connections.filter(
|
||||
(conn) => conn.source === starterBlock.id
|
||||
)
|
||||
if (outgoingFromStarter.length === 0) {
|
||||
throw new Error('Starter block must have at least one outgoing connection')
|
||||
}
|
||||
}
|
||||
|
||||
// General graph validations
|
||||
const blockIds = new Set(this.actualWorkflow.blocks.map((block) => block.id))
|
||||
for (const conn of this.actualWorkflow.connections) {
|
||||
if (!blockIds.has(conn.source)) {
|
||||
@@ -762,20 +769,54 @@ export class Executor {
|
||||
// Determine which block to initialize as the starting point
|
||||
let initBlock: SerializedBlock | undefined
|
||||
if (startBlockId) {
|
||||
// Starting from a specific block (webhook trigger or schedule trigger)
|
||||
// Starting from a specific block (webhook trigger, schedule trigger, or new trigger blocks)
|
||||
initBlock = this.actualWorkflow.blocks.find((block) => block.id === startBlockId)
|
||||
} else {
|
||||
// Default to starter block
|
||||
// Default to starter block (legacy) or find any trigger block
|
||||
initBlock = this.actualWorkflow.blocks.find(
|
||||
(block) => block.metadata?.id === BlockType.STARTER
|
||||
)
|
||||
|
||||
// If no starter block, look for appropriate trigger block based on context
|
||||
if (!initBlock) {
|
||||
if (this.isChildExecution) {
|
||||
const inputTriggerBlocks = this.actualWorkflow.blocks.filter(
|
||||
(block) => block.metadata?.id === 'input_trigger'
|
||||
)
|
||||
if (inputTriggerBlocks.length === 1) {
|
||||
initBlock = inputTriggerBlocks[0]
|
||||
} else if (inputTriggerBlocks.length > 1) {
|
||||
throw new Error('Child workflow has multiple Input Trigger blocks. Keep only one.')
|
||||
}
|
||||
} else {
|
||||
// Parent workflows can use any trigger block (dedicated or trigger-mode)
|
||||
const triggerBlocks = this.actualWorkflow.blocks.filter(
|
||||
(block) =>
|
||||
block.metadata?.id === 'input_trigger' ||
|
||||
block.metadata?.id === 'api_trigger' ||
|
||||
block.metadata?.id === 'chat_trigger' ||
|
||||
block.metadata?.category === 'triggers' ||
|
||||
block.config?.params?.triggerMode === true
|
||||
)
|
||||
if (triggerBlocks.length > 0) {
|
||||
initBlock = triggerBlocks[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (initBlock) {
|
||||
// Initialize the starting block with the workflow input
|
||||
try {
|
||||
// Get inputFormat from either old location (config.params) or new location (metadata.subBlocks)
|
||||
const blockParams = initBlock.config.params
|
||||
const inputFormat = blockParams?.inputFormat
|
||||
let inputFormat = blockParams?.inputFormat
|
||||
|
||||
// For new trigger blocks (api_trigger, etc), inputFormat is in metadata.subBlocks
|
||||
const metadataWithSubBlocks = initBlock.metadata as any
|
||||
if (!inputFormat && metadataWithSubBlocks?.subBlocks?.inputFormat?.value) {
|
||||
inputFormat = metadataWithSubBlocks.subBlocks.inputFormat.value
|
||||
}
|
||||
|
||||
// If input format is defined, structure the input according to the schema
|
||||
if (inputFormat && Array.isArray(inputFormat) && inputFormat.length > 0) {
|
||||
@@ -841,11 +882,31 @@ export class Executor {
|
||||
// Use the structured input if we processed fields, otherwise use raw input
|
||||
const finalInput = hasProcessedFields ? structuredInput : rawInputData
|
||||
|
||||
// Initialize the starting block with structured input (flattened)
|
||||
const blockOutput = {
|
||||
input: finalInput,
|
||||
conversationId: this.workflowInput?.conversationId, // Add conversationId to root
|
||||
...finalInput, // Add input fields directly at top level
|
||||
// Initialize the starting block with structured input
|
||||
let blockOutput: any
|
||||
|
||||
// For API/Input triggers, normalize primitives and mirror objects under input
|
||||
if (
|
||||
initBlock.metadata?.id === 'api_trigger' ||
|
||||
initBlock.metadata?.id === 'input_trigger'
|
||||
) {
|
||||
const isObject =
|
||||
finalInput !== null && typeof finalInput === 'object' && !Array.isArray(finalInput)
|
||||
if (isObject) {
|
||||
blockOutput = { ...finalInput }
|
||||
// Provide a mirrored input object for universal <start.input> references
|
||||
blockOutput.input = { ...finalInput }
|
||||
} else {
|
||||
// Primitive input: only expose under input
|
||||
blockOutput = { input: finalInput }
|
||||
}
|
||||
} else {
|
||||
// For legacy starter blocks, keep the old behavior
|
||||
blockOutput = {
|
||||
input: finalInput,
|
||||
conversationId: this.workflowInput?.conversationId, // Add conversationId to root
|
||||
...finalInput, // Add input fields directly at top level
|
||||
}
|
||||
}
|
||||
|
||||
// Add files if present (for all trigger types)
|
||||
@@ -863,54 +924,81 @@ export class Executor {
|
||||
// This ensures files are captured in trace spans and execution logs
|
||||
this.createStartedBlockWithFilesLog(initBlock, blockOutput, context)
|
||||
} else {
|
||||
// Handle structured input (like API calls or chat messages)
|
||||
if (this.workflowInput && typeof this.workflowInput === 'object') {
|
||||
// Check if this is a chat workflow input (has both input and conversationId)
|
||||
if (
|
||||
Object.hasOwn(this.workflowInput, 'input') &&
|
||||
Object.hasOwn(this.workflowInput, 'conversationId')
|
||||
) {
|
||||
// Chat workflow: extract input, conversationId, and files to root level
|
||||
const starterOutput: any = {
|
||||
input: this.workflowInput.input,
|
||||
conversationId: this.workflowInput.conversationId,
|
||||
// Handle triggers without inputFormat
|
||||
let starterOutput: any
|
||||
|
||||
// Handle different trigger types
|
||||
if (initBlock.metadata?.id === 'chat_trigger') {
|
||||
// Chat trigger: extract input, conversationId, and files
|
||||
starterOutput = {
|
||||
input: this.workflowInput?.input || '',
|
||||
conversationId: this.workflowInput?.conversationId || '',
|
||||
}
|
||||
|
||||
if (this.workflowInput?.files && Array.isArray(this.workflowInput.files)) {
|
||||
starterOutput.files = this.workflowInput.files
|
||||
}
|
||||
} else if (
|
||||
initBlock.metadata?.id === 'api_trigger' ||
|
||||
initBlock.metadata?.id === 'input_trigger'
|
||||
) {
|
||||
// API/Input trigger without inputFormat: normalize primitives and mirror objects under input
|
||||
const rawCandidate =
|
||||
this.workflowInput?.input !== undefined
|
||||
? this.workflowInput.input
|
||||
: this.workflowInput
|
||||
const isObject =
|
||||
rawCandidate !== null &&
|
||||
typeof rawCandidate === 'object' &&
|
||||
!Array.isArray(rawCandidate)
|
||||
if (isObject) {
|
||||
starterOutput = {
|
||||
...(rawCandidate as Record<string, any>),
|
||||
input: { ...(rawCandidate as Record<string, any>) },
|
||||
}
|
||||
|
||||
// Add files if present
|
||||
if (this.workflowInput.files && Array.isArray(this.workflowInput.files)) {
|
||||
starterOutput.files = this.workflowInput.files
|
||||
}
|
||||
|
||||
context.blockStates.set(initBlock.id, {
|
||||
output: starterOutput,
|
||||
executed: true,
|
||||
executionTime: 0,
|
||||
})
|
||||
|
||||
// Create a block log for the starter block if it has files
|
||||
// This ensures files are captured in trace spans and execution logs
|
||||
this.createStartedBlockWithFilesLog(initBlock, starterOutput, context)
|
||||
} else {
|
||||
// API workflow: spread the raw data directly (no wrapping)
|
||||
const starterOutput = { ...this.workflowInput }
|
||||
|
||||
context.blockStates.set(initBlock.id, {
|
||||
output: starterOutput,
|
||||
executed: true,
|
||||
executionTime: 0,
|
||||
})
|
||||
starterOutput = { input: rawCandidate }
|
||||
}
|
||||
} else {
|
||||
// Fallback for primitive input values
|
||||
const starterOutput = {
|
||||
input: this.workflowInput,
|
||||
}
|
||||
// Legacy starter block handling
|
||||
if (this.workflowInput && typeof this.workflowInput === 'object') {
|
||||
// Check if this is a chat workflow input (has both input and conversationId)
|
||||
if (
|
||||
Object.hasOwn(this.workflowInput, 'input') &&
|
||||
Object.hasOwn(this.workflowInput, 'conversationId')
|
||||
) {
|
||||
// Chat workflow: extract input, conversationId, and files to root level
|
||||
starterOutput = {
|
||||
input: this.workflowInput.input,
|
||||
conversationId: this.workflowInput.conversationId,
|
||||
}
|
||||
|
||||
context.blockStates.set(initBlock.id, {
|
||||
output: starterOutput,
|
||||
executed: true,
|
||||
executionTime: 0,
|
||||
})
|
||||
// Add files if present
|
||||
if (this.workflowInput.files && Array.isArray(this.workflowInput.files)) {
|
||||
starterOutput.files = this.workflowInput.files
|
||||
}
|
||||
} else {
|
||||
// API workflow: spread the raw data directly (no wrapping)
|
||||
starterOutput = { ...this.workflowInput }
|
||||
}
|
||||
} else {
|
||||
// Fallback for primitive input values
|
||||
starterOutput = {
|
||||
input: this.workflowInput,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.blockStates.set(initBlock.id, {
|
||||
output: starterOutput,
|
||||
executed: true,
|
||||
executionTime: 0,
|
||||
})
|
||||
|
||||
// Create a block log for the starter block if it has files
|
||||
// This ensures files are captured in trace spans and execution logs
|
||||
if (starterOutput.files) {
|
||||
this.createStartedBlockWithFilesLog(initBlock, starterOutput, context)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BlockPathCalculator } from '@/lib/block-path-calculator'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { VariableManager } from '@/lib/variables/variable-manager'
|
||||
import { TRIGGER_REFERENCE_ALIAS_MAP } from '@/lib/workflows/triggers'
|
||||
import { getBlock } from '@/blocks/index'
|
||||
import type { LoopManager } from '@/executor/loops/loops'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
@@ -520,15 +521,19 @@ export class InputResolver {
|
||||
// System references and regular block references are both processed
|
||||
// Accessibility validation happens later in validateBlockReference
|
||||
|
||||
// Special case for "start" references
|
||||
if (blockRef.toLowerCase() === 'start') {
|
||||
// Find the starter block
|
||||
const starterBlock = this.workflow.blocks.find((block) => block.metadata?.id === 'starter')
|
||||
if (starterBlock) {
|
||||
const blockState = context.blockStates.get(starterBlock.id)
|
||||
// Special case for trigger block references (start, api, chat, manual)
|
||||
const blockRefLower = blockRef.toLowerCase()
|
||||
const triggerType =
|
||||
TRIGGER_REFERENCE_ALIAS_MAP[blockRefLower as keyof typeof TRIGGER_REFERENCE_ALIAS_MAP]
|
||||
if (triggerType) {
|
||||
const triggerBlock = this.workflow.blocks.find(
|
||||
(block) => block.metadata?.id === triggerType
|
||||
)
|
||||
if (triggerBlock) {
|
||||
const blockState = context.blockStates.get(triggerBlock.id)
|
||||
if (blockState) {
|
||||
// For starter block, start directly with the flattened output
|
||||
// This enables direct access to <start.input> and <start.conversationId>
|
||||
// For trigger blocks, start directly with the flattened output
|
||||
// This enables direct access to <start.input>, <api.fieldName>, <chat.input> etc
|
||||
let replacementValue: any = blockState.output
|
||||
|
||||
for (const part of pathParts) {
|
||||
@@ -537,7 +542,7 @@ export class InputResolver {
|
||||
`[resolveBlockReferences] Invalid path "${part}" - replacementValue is not an object:`,
|
||||
replacementValue
|
||||
)
|
||||
throw new Error(`Invalid path "${part}" in "${path}" for starter block.`)
|
||||
throw new Error(`Invalid path "${part}" in "${path}" for trigger block.`)
|
||||
}
|
||||
|
||||
// Handle array indexing syntax like "files[0]" or "items[1]"
|
||||
@@ -550,14 +555,14 @@ export class InputResolver {
|
||||
const arrayValue = replacementValue[arrayName]
|
||||
if (!Array.isArray(arrayValue)) {
|
||||
throw new Error(
|
||||
`Property "${arrayName}" is not an array in path "${path}" for starter block.`
|
||||
`Property "${arrayName}" is not an array in path "${path}" for trigger block.`
|
||||
)
|
||||
}
|
||||
|
||||
// Then access the array element
|
||||
if (index < 0 || index >= arrayValue.length) {
|
||||
throw new Error(
|
||||
`Array index ${index} is out of bounds for "${arrayName}" (length: ${arrayValue.length}) in path "${path}" for starter block.`
|
||||
`Array index ${index} is out of bounds for "${arrayName}" (length: ${arrayValue.length}) in path "${path}" for trigger block.`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -577,17 +582,22 @@ export class InputResolver {
|
||||
|
||||
if (replacementValue === undefined) {
|
||||
logger.warn(
|
||||
`[resolveBlockReferences] No value found at path "${part}" in starter block.`
|
||||
`[resolveBlockReferences] No value found at path "${part}" in trigger block.`
|
||||
)
|
||||
throw new Error(`No value found at path "${path}" in starter block.`)
|
||||
throw new Error(`No value found at path "${path}" in trigger block.`)
|
||||
}
|
||||
}
|
||||
|
||||
// Format the value based on block type and path
|
||||
let formattedValue: string
|
||||
|
||||
// Special handling for all blocks referencing starter input
|
||||
if (blockRef.toLowerCase() === 'start' && pathParts.join('.').includes('input')) {
|
||||
// Special handling for all blocks referencing trigger input
|
||||
// For starter and chat triggers, check for 'input' field. For API trigger, any field access counts
|
||||
const isTriggerInputRef =
|
||||
(blockRefLower === 'start' && pathParts.join('.').includes('input')) ||
|
||||
(blockRefLower === 'chat' && pathParts.join('.').includes('input')) ||
|
||||
(blockRefLower === 'api' && pathParts.length > 0)
|
||||
if (isTriggerInputRef) {
|
||||
const blockType = currentBlock.metadata?.id
|
||||
|
||||
// Format based on which block is consuming this value
|
||||
|
||||
@@ -662,7 +662,8 @@ export function useCollaborativeWorkflow() {
|
||||
data?: Record<string, any>,
|
||||
parentId?: string,
|
||||
extent?: 'parent',
|
||||
autoConnectEdge?: Edge
|
||||
autoConnectEdge?: Edge,
|
||||
triggerMode?: boolean
|
||||
) => {
|
||||
// Skip socket operations when in diff mode
|
||||
if (isShowingDiff) {
|
||||
@@ -695,6 +696,7 @@ export function useCollaborativeWorkflow() {
|
||||
horizontalHandles: true,
|
||||
isWide: false,
|
||||
advancedMode: false,
|
||||
triggerMode: triggerMode || false,
|
||||
height: 0,
|
||||
parentId,
|
||||
extent,
|
||||
@@ -704,7 +706,7 @@ export function useCollaborativeWorkflow() {
|
||||
// Skip if applying remote changes
|
||||
if (isApplyingRemoteChange.current) {
|
||||
workflowStore.addBlock(id, type, name, position, data, parentId, extent, {
|
||||
triggerMode: false,
|
||||
triggerMode: triggerMode || false,
|
||||
})
|
||||
if (autoConnectEdge) {
|
||||
workflowStore.addEdge(autoConnectEdge)
|
||||
@@ -729,7 +731,7 @@ export function useCollaborativeWorkflow() {
|
||||
|
||||
// Apply locally first (immediate UI feedback)
|
||||
workflowStore.addBlock(id, type, name, position, data, parentId, extent, {
|
||||
triggerMode: false,
|
||||
triggerMode: triggerMode || false,
|
||||
})
|
||||
if (autoConnectEdge) {
|
||||
workflowStore.addEdge(autoConnectEdge)
|
||||
@@ -774,7 +776,7 @@ export function useCollaborativeWorkflow() {
|
||||
horizontalHandles: true,
|
||||
isWide: false,
|
||||
advancedMode: false,
|
||||
triggerMode: false,
|
||||
triggerMode: triggerMode || false,
|
||||
height: 0, // Default height, will be set by the UI
|
||||
parentId,
|
||||
extent,
|
||||
@@ -801,7 +803,7 @@ export function useCollaborativeWorkflow() {
|
||||
|
||||
// Apply locally
|
||||
workflowStore.addBlock(id, type, name, position, data, parentId, extent, {
|
||||
triggerMode: false,
|
||||
triggerMode: triggerMode || false,
|
||||
})
|
||||
if (autoConnectEdge) {
|
||||
workflowStore.addEdge(autoConnectEdge)
|
||||
|
||||
@@ -57,7 +57,16 @@ export interface SendMessageRequest {
|
||||
chatId?: string
|
||||
workflowId?: string
|
||||
mode?: 'ask' | 'agent'
|
||||
depth?: 0 | 1 | 2 | 3
|
||||
model?:
|
||||
| 'gpt-5-fast'
|
||||
| 'gpt-5'
|
||||
| 'gpt-5-medium'
|
||||
| 'gpt-5-high'
|
||||
| 'gpt-4o'
|
||||
| 'gpt-4.1'
|
||||
| 'o3'
|
||||
| 'claude-4-sonnet'
|
||||
| 'claude-4.1-opus'
|
||||
prefetch?: boolean
|
||||
createNewChat?: boolean
|
||||
stream?: boolean
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user