mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-12 07:24:55 -05: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:
committed by
GitHub
parent
68df95906f
commit
b7876ca466
@@ -13,6 +13,7 @@ import { hasAdminPermission } from '@/lib/permissions/utils'
|
|||||||
import { processStreamingBlockLogs } from '@/lib/tokenization'
|
import { processStreamingBlockLogs } from '@/lib/tokenization'
|
||||||
import { getEmailDomain } from '@/lib/urls/utils'
|
import { getEmailDomain } from '@/lib/urls/utils'
|
||||||
import { decryptSecret, generateRequestId } from '@/lib/utils'
|
import { decryptSecret, generateRequestId } from '@/lib/utils'
|
||||||
|
import { TriggerUtils } from '@/lib/workflows/triggers'
|
||||||
import { getBlock } from '@/blocks'
|
import { getBlock } from '@/blocks'
|
||||||
import { Executor } from '@/executor'
|
import { Executor } from '@/executor'
|
||||||
import type { BlockLog, ExecutionResult } from '@/executor/types'
|
import type { BlockLog, ExecutionResult } from '@/executor/types'
|
||||||
@@ -430,9 +431,10 @@ export async function executeWorkflowForChat(
|
|||||||
(acc, [id, block]) => {
|
(acc, [id, block]) => {
|
||||||
const blockConfig = getBlock(block.type)
|
const blockConfig = getBlock(block.type)
|
||||||
const isTriggerBlock = blockConfig?.category === 'triggers'
|
const isTriggerBlock = blockConfig?.category === 'triggers'
|
||||||
|
const isChatTrigger = block.type === 'chat_trigger'
|
||||||
|
|
||||||
// Skip trigger blocks during chat execution
|
// Keep all non-trigger blocks and also keep the chat_trigger block
|
||||||
if (!isTriggerBlock) {
|
if (!isTriggerBlock || isChatTrigger) {
|
||||||
acc[id] = block
|
acc[id] = block
|
||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
@@ -487,8 +489,10 @@ export async function executeWorkflowForChat(
|
|||||||
|
|
||||||
// Filter edges to exclude connections to/from trigger blocks (same as manual execution)
|
// Filter edges to exclude connections to/from trigger blocks (same as manual execution)
|
||||||
const triggerBlockIds = Object.keys(mergedStates).filter((id) => {
|
const triggerBlockIds = Object.keys(mergedStates).filter((id) => {
|
||||||
const blockConfig = getBlock(mergedStates[id].type)
|
const type = mergedStates[id].type
|
||||||
return blockConfig?.category === 'triggers'
|
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(
|
const filteredEdges = edges.filter(
|
||||||
@@ -613,9 +617,29 @@ export async function executeWorkflowForChat(
|
|||||||
// Set up logging on the executor
|
// Set up logging on the executor
|
||||||
loggingSession.setupExecutor(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
|
let result
|
||||||
try {
|
try {
|
||||||
result = await executor.execute(workflowId)
|
result = await executor.execute(workflowId, startBlockId)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error(`[${requestId}] Chat workflow execution failed:`, error)
|
logger.error(`[${requestId}] Chat workflow execution failed:`, error)
|
||||||
await loggingSession.safeCompleteWithError({
|
await loggingSession.safeCompleteWithError({
|
||||||
|
|||||||
@@ -220,13 +220,20 @@ describe('Copilot Chat API Route', () => {
|
|||||||
content: 'Hello',
|
content: 'Hello',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
chatMessages: [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: 'Hello',
|
||||||
|
},
|
||||||
|
],
|
||||||
workflowId: 'workflow-123',
|
workflowId: 'workflow-123',
|
||||||
userId: 'user-123',
|
userId: 'user-123',
|
||||||
stream: true,
|
stream: true,
|
||||||
streamToolCalls: true,
|
streamToolCalls: true,
|
||||||
|
model: 'gpt-5',
|
||||||
mode: 'agent',
|
mode: 'agent',
|
||||||
messageId: 'mock-uuid-1234-5678',
|
messageId: 'mock-uuid-1234-5678',
|
||||||
depth: 0,
|
version: '1.0.0',
|
||||||
chatId: 'chat-123',
|
chatId: 'chat-123',
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -284,13 +291,19 @@ describe('Copilot Chat API Route', () => {
|
|||||||
{ role: 'assistant', content: 'Previous response' },
|
{ role: 'assistant', content: 'Previous response' },
|
||||||
{ role: 'user', content: 'New message' },
|
{ role: 'user', content: 'New message' },
|
||||||
],
|
],
|
||||||
|
chatMessages: [
|
||||||
|
{ role: 'user', content: 'Previous message' },
|
||||||
|
{ role: 'assistant', content: 'Previous response' },
|
||||||
|
{ role: 'user', content: 'New message' },
|
||||||
|
],
|
||||||
workflowId: 'workflow-123',
|
workflowId: 'workflow-123',
|
||||||
userId: 'user-123',
|
userId: 'user-123',
|
||||||
stream: true,
|
stream: true,
|
||||||
streamToolCalls: true,
|
streamToolCalls: true,
|
||||||
|
model: 'gpt-5',
|
||||||
mode: 'agent',
|
mode: 'agent',
|
||||||
messageId: 'mock-uuid-1234-5678',
|
messageId: 'mock-uuid-1234-5678',
|
||||||
depth: 0,
|
version: '1.0.0',
|
||||||
chatId: 'chat-123',
|
chatId: 'chat-123',
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -337,13 +350,18 @@ describe('Copilot Chat API Route', () => {
|
|||||||
{ role: 'system', content: 'User seems confused about the workflow' },
|
{ role: 'system', content: 'User seems confused about the workflow' },
|
||||||
{ role: 'user', content: 'Hello' },
|
{ role: 'user', content: 'Hello' },
|
||||||
],
|
],
|
||||||
|
chatMessages: [
|
||||||
|
{ role: 'system', content: 'User seems confused about the workflow' },
|
||||||
|
{ role: 'user', content: 'Hello' },
|
||||||
|
],
|
||||||
workflowId: 'workflow-123',
|
workflowId: 'workflow-123',
|
||||||
userId: 'user-123',
|
userId: 'user-123',
|
||||||
stream: true,
|
stream: true,
|
||||||
streamToolCalls: true,
|
streamToolCalls: true,
|
||||||
|
model: 'gpt-5',
|
||||||
mode: 'agent',
|
mode: 'agent',
|
||||||
messageId: 'mock-uuid-1234-5678',
|
messageId: 'mock-uuid-1234-5678',
|
||||||
depth: 0,
|
version: '1.0.0',
|
||||||
chatId: 'chat-123',
|
chatId: 'chat-123',
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -427,13 +445,15 @@ describe('Copilot Chat API Route', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
messages: [{ role: 'user', content: 'What is this workflow?' }],
|
messages: [{ role: 'user', content: 'What is this workflow?' }],
|
||||||
|
chatMessages: [{ role: 'user', content: 'What is this workflow?' }],
|
||||||
workflowId: 'workflow-123',
|
workflowId: 'workflow-123',
|
||||||
userId: 'user-123',
|
userId: 'user-123',
|
||||||
stream: true,
|
stream: true,
|
||||||
streamToolCalls: true,
|
streamToolCalls: true,
|
||||||
|
model: 'gpt-5',
|
||||||
mode: 'ask',
|
mode: 'ask',
|
||||||
messageId: 'mock-uuid-1234-5678',
|
messageId: 'mock-uuid-1234-5678',
|
||||||
depth: 0,
|
version: '1.0.0',
|
||||||
chatId: 'chat-123',
|
chatId: 'chat-123',
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { getCopilotModel } from '@/lib/copilot/config'
|
|||||||
import type { CopilotProviderConfig } from '@/lib/copilot/types'
|
import type { CopilotProviderConfig } from '@/lib/copilot/types'
|
||||||
import { env } from '@/lib/env'
|
import { env } from '@/lib/env'
|
||||||
import { createLogger } from '@/lib/logs/console/logger'
|
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 { generateChatTitle } from '@/lib/sim-agent/utils'
|
||||||
import { createFileContent, isSupportedFileType } from '@/lib/uploads/file-utils'
|
import { createFileContent, isSupportedFileType } from '@/lib/uploads/file-utils'
|
||||||
import { S3_COPILOT_CONFIG } from '@/lib/uploads/setup'
|
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
|
userMessageId: z.string().optional(), // ID from frontend for the user message
|
||||||
chatId: z.string().optional(),
|
chatId: z.string().optional(),
|
||||||
workflowId: z.string().min(1, 'Workflow ID is required'),
|
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'),
|
mode: z.enum(['ask', 'agent']).optional().default('agent'),
|
||||||
depth: z.number().int().min(0).max(3).optional().default(0),
|
|
||||||
prefetch: z.boolean().optional(),
|
prefetch: z.boolean().optional(),
|
||||||
createNewChat: z.boolean().optional().default(false),
|
createNewChat: z.boolean().optional().default(false),
|
||||||
stream: z.boolean().optional().default(true),
|
stream: z.boolean().optional().default(true),
|
||||||
@@ -97,8 +110,8 @@ export async function POST(req: NextRequest) {
|
|||||||
userMessageId,
|
userMessageId,
|
||||||
chatId,
|
chatId,
|
||||||
workflowId,
|
workflowId,
|
||||||
|
model,
|
||||||
mode,
|
mode,
|
||||||
depth,
|
|
||||||
prefetch,
|
prefetch,
|
||||||
createNewChat,
|
createNewChat,
|
||||||
stream,
|
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
|
// Handle chat context
|
||||||
let currentChat: any = null
|
let currentChat: any = null
|
||||||
let conversationHistory: any[] = []
|
let conversationHistory: any[] = []
|
||||||
@@ -366,16 +366,18 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
const requestPayload = {
|
const requestPayload = {
|
||||||
messages: messagesForAgent,
|
messages: messagesForAgent,
|
||||||
|
chatMessages: messages, // Full unfiltered messages array
|
||||||
workflowId,
|
workflowId,
|
||||||
userId: authenticatedUserId,
|
userId: authenticatedUserId,
|
||||||
stream: stream,
|
stream: stream,
|
||||||
streamToolCalls: true,
|
streamToolCalls: true,
|
||||||
|
model: model,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
messageId: userMessageIdToUse,
|
messageId: userMessageIdToUse,
|
||||||
|
version: SIM_AGENT_VERSION,
|
||||||
...(providerConfig ? { provider: providerConfig } : {}),
|
...(providerConfig ? { provider: providerConfig } : {}),
|
||||||
...(effectiveConversationId ? { conversationId: effectiveConversationId } : {}),
|
...(effectiveConversationId ? { conversationId: effectiveConversationId } : {}),
|
||||||
...(typeof effectiveDepth === 'number' ? { depth: effectiveDepth } : {}),
|
...(typeof prefetch === 'boolean' ? { prefetch: prefetch } : {}),
|
||||||
...(typeof effectivePrefetch === 'boolean' ? { prefetch: effectivePrefetch } : {}),
|
|
||||||
...(session?.user?.name && { userName: session.user.name }),
|
...(session?.user?.name && { userName: session.user.name }),
|
||||||
...(agentContexts.length > 0 && { context: agentContexts }),
|
...(agentContexts.length > 0 && { context: agentContexts }),
|
||||||
...(actualChatId ? { chatId: actualChatId } : {}),
|
...(actualChatId ? { chatId: actualChatId } : {}),
|
||||||
@@ -384,6 +386,9 @@ export async function POST(req: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
logger.info(`[${tracker.requestId}] About to call Sim Agent with context`, {
|
logger.info(`[${tracker.requestId}] About to call Sim Agent with context`, {
|
||||||
context: (requestPayload as any).context,
|
context: (requestPayload as any).context,
|
||||||
|
messagesCount: messagesForAgent.length,
|
||||||
|
chatMessagesCount: messages.length,
|
||||||
|
hasConversationId: !!effectiveConversationId,
|
||||||
})
|
})
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export async function POST(req: NextRequest) {
|
|||||||
return createBadRequestResponse('Invalid request body for execute-copilot-server-tool')
|
return createBadRequestResponse('Invalid request body for execute-copilot-server-tool')
|
||||||
}
|
}
|
||||||
logger.error(`[${tracker.requestId}] Failed to execute server tool:`, error)
|
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
|
const Executor = (await import('@/executor')).Executor
|
||||||
expect(Executor).toHaveBeenCalled()
|
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
|
const Executor = (await import('@/executor')).Executor
|
||||||
expect(Executor).toHaveBeenCalled()
|
expect(Executor).toHaveBeenCalled()
|
||||||
|
|
||||||
expect(executeMock).toHaveBeenCalledWith('workflow-id')
|
expect(executeMock).toHaveBeenCalledWith('workflow-id', 'starter-id')
|
||||||
|
|
||||||
expect(Executor).toHaveBeenCalledWith(
|
expect(Executor).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
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 { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
|
||||||
import { decryptSecret, generateRequestId } from '@/lib/utils'
|
import { decryptSecret, generateRequestId } from '@/lib/utils'
|
||||||
import { loadDeployedWorkflowState } from '@/lib/workflows/db-helpers'
|
import { loadDeployedWorkflowState } from '@/lib/workflows/db-helpers'
|
||||||
|
import { TriggerUtils } from '@/lib/workflows/triggers'
|
||||||
import {
|
import {
|
||||||
createHttpResponseFromBlock,
|
createHttpResponseFromBlock,
|
||||||
updateWorkflowRunCounts,
|
updateWorkflowRunCounts,
|
||||||
@@ -272,6 +273,32 @@ async function executeWorkflow(
|
|||||||
true // Enable validation during execution
|
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({
|
const executor = new Executor({
|
||||||
workflow: serializedWorkflow,
|
workflow: serializedWorkflow,
|
||||||
currentBlockStates: processedBlockStates,
|
currentBlockStates: processedBlockStates,
|
||||||
@@ -287,7 +314,7 @@ async function executeWorkflow(
|
|||||||
// Set up logging on the executor
|
// Set up logging on the executor
|
||||||
loggingSession.setupExecutor(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)
|
// Check if we got a StreamingExecution result (with stream + execution properties)
|
||||||
// For API routes, we only care about the ExecutionResult part, not the stream
|
// For API routes, we only care about the ExecutionResult part, not the stream
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
loadWorkflowFromNormalizedTables,
|
loadWorkflowFromNormalizedTables,
|
||||||
saveWorkflowToNormalizedTables,
|
saveWorkflowToNormalizedTables,
|
||||||
} from '@/lib/workflows/db-helpers'
|
} 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 { getUserId } from '@/app/api/auth/oauth/utils'
|
||||||
import { getAllBlocks, getBlock } from '@/blocks'
|
import { getAllBlocks, getBlock } from '@/blocks'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
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
|
* PUT /api/workflows/[id]/yaml
|
||||||
* Consolidated YAML workflow saving endpoint
|
* 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
|
// Ensure all blocks have required fields
|
||||||
Object.values(workflowState.blocks).forEach((block: any) => {
|
Object.entries(finalWorkflowState.blocks).forEach(([blockId, block]) => {
|
||||||
if (block.enabled === undefined) {
|
const blockData = block as any
|
||||||
block.enabled = true
|
if (!blockData.id) blockData.id = blockId
|
||||||
|
if (!blockData.position) {
|
||||||
|
blockData.position = { x: 0, y: 0 }
|
||||||
}
|
}
|
||||||
if (block.horizontalHandles === undefined) {
|
if (blockData.enabled === undefined) {
|
||||||
block.horizontalHandles = true
|
blockData.enabled = true
|
||||||
}
|
}
|
||||||
if (block.isWide === undefined) {
|
if (blockData.horizontalHandles === undefined) {
|
||||||
block.isWide = false
|
blockData.horizontalHandles = true
|
||||||
}
|
}
|
||||||
if (block.height === undefined) {
|
if (blockData.isWide === undefined) {
|
||||||
block.height = 0
|
blockData.isWide = false
|
||||||
}
|
}
|
||||||
if (!block.subBlocks) {
|
if (blockData.height === undefined) {
|
||||||
block.subBlocks = {}
|
blockData.height = 0
|
||||||
}
|
}
|
||||||
if (!block.outputs) {
|
if (!blockData.subBlocks) {
|
||||||
block.outputs = {}
|
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
|
id: string
|
||||||
type: string
|
type: string
|
||||||
name: string
|
name: string
|
||||||
position: { x: number; y: number }
|
position: { x: number; y: number }
|
||||||
subBlocks?: Record<string, any>
|
subBlocks?: Record<string, any>
|
||||||
|
inputs?: Record<string, any>
|
||||||
|
triggerMode?: boolean
|
||||||
data?: Record<string, any>
|
data?: Record<string, any>
|
||||||
parentId?: string
|
parentId?: string
|
||||||
extent?: string
|
extent?: string
|
||||||
}>
|
}>
|
||||||
const edges = workflowState.edges
|
const edges = finalWorkflowState.edges
|
||||||
const warnings = conversionResult.warnings || []
|
const warnings = conversionResult.warnings || []
|
||||||
|
|
||||||
// Create workflow state
|
// 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
|
// Set up outputs from block configuration
|
||||||
const outputs = resolveOutputType(blockConfig.outputs)
|
const outputs = resolveOutputType(blockConfig.outputs)
|
||||||
|
|
||||||
@@ -476,10 +591,17 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|||||||
isWide: false,
|
isWide: false,
|
||||||
advancedMode: false,
|
advancedMode: false,
|
||||||
height: 0,
|
height: 0,
|
||||||
|
triggerMode: block.triggerMode || false, // Preserve triggerMode from imported block
|
||||||
data: blockData,
|
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 {
|
} else {
|
||||||
logger.warn(`[${requestId}] Unknown block type: ${block.type}`)
|
logger.warn(`[${requestId}] Unknown block type: ${block.type}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { db } from '@sim/db'
|
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 { eq } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
@@ -95,132 +95,31 @@ export async function POST(req: NextRequest) {
|
|||||||
const { name, description, color, workspaceId, folderId } = CreateWorkflowSchema.parse(body)
|
const { name, description, color, workspaceId, folderId } = CreateWorkflowSchema.parse(body)
|
||||||
|
|
||||||
const workflowId = crypto.randomUUID()
|
const workflowId = crypto.randomUUID()
|
||||||
const starterId = crypto.randomUUID()
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|
||||||
logger.info(`[${requestId}] Creating workflow ${workflowId} for user ${session.user.id}`)
|
logger.info(`[${requestId}] Creating workflow ${workflowId} for user ${session.user.id}`)
|
||||||
|
|
||||||
await db.transaction(async (tx) => {
|
await db.insert(workflow).values({
|
||||||
await tx.insert(workflow).values({
|
id: workflowId,
|
||||||
id: workflowId,
|
userId: session.user.id,
|
||||||
userId: session.user.id,
|
workspaceId: workspaceId || null,
|
||||||
workspaceId: workspaceId || null,
|
folderId: folderId || null,
|
||||||
folderId: folderId || null,
|
name,
|
||||||
name,
|
description,
|
||||||
description,
|
color,
|
||||||
color,
|
lastSynced: now,
|
||||||
lastSynced: now,
|
createdAt: now,
|
||||||
createdAt: now,
|
updatedAt: now,
|
||||||
updatedAt: now,
|
isDeployed: false,
|
||||||
isDeployed: false,
|
collaborators: [],
|
||||||
collaborators: [],
|
runCount: 0,
|
||||||
runCount: 0,
|
variables: {},
|
||||||
variables: {},
|
isPublished: false,
|
||||||
isPublished: false,
|
marketplaceData: null,
|
||||||
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`
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
logger.info(`[${requestId}] Successfully created empty workflow ${workflowId}`)
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
id: workflowId,
|
id: workflowId,
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { db } from '@sim/db'
|
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 { and, desc, eq, isNull } from 'drizzle-orm'
|
||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
@@ -110,9 +110,7 @@ async function createWorkspace(userId: string, name: string) {
|
|||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create initial workflow for the workspace with start block
|
// Create initial workflow for the workspace (empty canvas)
|
||||||
const starterId = crypto.randomUUID()
|
|
||||||
|
|
||||||
// Create the workflow
|
// Create the workflow
|
||||||
await tx.insert(workflow).values({
|
await tx.insert(workflow).values({
|
||||||
id: workflowId,
|
id: workflowId,
|
||||||
@@ -133,61 +131,7 @@ async function createWorkspace(userId: string, name: string) {
|
|||||||
marketplaceData: null,
|
marketplaceData: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Insert the start block into workflow_blocks table
|
// No blocks are inserted - empty canvas
|
||||||
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,
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Created workspace ${workspaceId} with initial workflow ${workflowId} for user ${userId}`
|
`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 { createLogger } from '@/lib/logs/console/logger'
|
||||||
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent'
|
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent'
|
||||||
import { generateRequestId } from '@/lib/utils'
|
import { generateRequestId } from '@/lib/utils'
|
||||||
|
import { validateWorkflowState } from '@/lib/workflows/validation'
|
||||||
import { getAllBlocks } from '@/blocks/registry'
|
import { getAllBlocks } from '@/blocks/registry'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
import { resolveOutputType } from '@/blocks/utils'
|
import { resolveOutputType } from '@/blocks/utils'
|
||||||
@@ -61,6 +62,59 @@ const CreateDiffRequestSchema = z.object({
|
|||||||
.optional(),
|
.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) {
|
export async function POST(request: NextRequest) {
|
||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
@@ -202,6 +256,46 @@ export async function POST(request: NextRequest) {
|
|||||||
const finalResult = result
|
const finalResult = result
|
||||||
|
|
||||||
if (result.success && result.diff?.proposedState) {
|
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
|
// First, fix parent-child relationships based on edges
|
||||||
const blocks = result.diff.proposedState.blocks
|
const blocks = result.diff.proposedState.blocks
|
||||||
const edges = result.diff.proposedState.edges || []
|
const edges = result.diff.proposedState.edges || []
|
||||||
@@ -271,6 +365,9 @@ export async function POST(request: NextRequest) {
|
|||||||
if (result.success && result.blocks && !result.diff) {
|
if (result.success && result.blocks && !result.diff) {
|
||||||
logger.info(`[${requestId}] Transforming sim agent blocks response to diff format`)
|
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
|
// First, fix parent-child relationships based on edges
|
||||||
const blocks = result.blocks
|
const blocks = result.blocks
|
||||||
const edges = result.edges || []
|
const edges = result.edges || []
|
||||||
|
|||||||
@@ -85,10 +85,16 @@ export function DeployModal({
|
|||||||
let inputFormatExample = ''
|
let inputFormatExample = ''
|
||||||
try {
|
try {
|
||||||
const blocks = Object.values(useWorkflowStore.getState().blocks)
|
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')
|
const starterBlock = blocks.find((block) => block.type === 'starter')
|
||||||
|
|
||||||
if (starterBlock) {
|
const targetBlock = apiTriggerBlock || starterBlock
|
||||||
const inputFormat = useSubBlockStore.getState().getValue(starterBlock.id, 'inputFormat')
|
|
||||||
|
if (targetBlock) {
|
||||||
|
const inputFormat = useSubBlockStore.getState().getValue(targetBlock.id, 'inputFormat')
|
||||||
|
|
||||||
if (inputFormat && Array.isArray(inputFormat) && inputFormat.length > 0) {
|
if (inputFormat && Array.isArray(inputFormat) && inputFormat.length > 0) {
|
||||||
const exampleData: Record<string, any> = {}
|
const exampleData: Record<string, any> = {}
|
||||||
|
|||||||
@@ -330,6 +330,25 @@ export function Chat({ chatMessage, setChatMessage }: ChatProps) {
|
|||||||
|
|
||||||
if (event === 'final' && data) {
|
if (event === 'final' && data) {
|
||||||
const result = data as ExecutionResult
|
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 =
|
const nonStreamingLogs =
|
||||||
result.logs?.filter((log) => !messageIdMap.has(log.blockId)) || []
|
result.logs?.filter((log) => !messageIdMap.has(log.blockId)) || []
|
||||||
|
|
||||||
@@ -343,34 +362,25 @@ export function Chat({ chatMessage, setChatMessage }: ChatProps) {
|
|||||||
const blockIdForOutput = extractBlockIdFromOutputId(outputId)
|
const blockIdForOutput = extractBlockIdFromOutputId(outputId)
|
||||||
const path = extractPathFromOutputId(outputId, blockIdForOutput)
|
const path = extractPathFromOutputId(outputId, blockIdForOutput)
|
||||||
const log = nonStreamingLogs.find((l) => l.blockId === blockIdForOutput)
|
const log = nonStreamingLogs.find((l) => l.blockId === blockIdForOutput)
|
||||||
|
|
||||||
if (log) {
|
if (log) {
|
||||||
let outputValue: any = log.output
|
let output = log.output
|
||||||
|
|
||||||
if (path) {
|
if (path) {
|
||||||
// Parse JSON content safely
|
output = parseOutputContentSafely(output)
|
||||||
outputValue = parseOutputContentSafely(outputValue)
|
|
||||||
|
|
||||||
const pathParts = path.split('.')
|
const pathParts = path.split('.')
|
||||||
|
let current = output
|
||||||
for (const part of pathParts) {
|
for (const part of pathParts) {
|
||||||
if (
|
if (current && typeof current === 'object' && part in current) {
|
||||||
outputValue &&
|
current = current[part]
|
||||||
typeof outputValue === 'object' &&
|
|
||||||
part in outputValue
|
|
||||||
) {
|
|
||||||
outputValue = outputValue[part]
|
|
||||||
} else {
|
} else {
|
||||||
outputValue = undefined
|
current = undefined
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
output = current
|
||||||
}
|
}
|
||||||
if (outputValue !== undefined) {
|
if (output !== undefined) {
|
||||||
addMessage({
|
addMessage({
|
||||||
content:
|
content: typeof output === 'string' ? output : JSON.stringify(output),
|
||||||
typeof outputValue === 'string'
|
|
||||||
? outputValue
|
|
||||||
: `\`\`\`json\n${JSON.stringify(outputValue, null, 2)}\n\`\`\``,
|
|
||||||
workflowId: activeWorkflowId,
|
workflowId: activeWorkflowId,
|
||||||
type: 'workflow',
|
type: 'workflow',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'
|
|||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
|
AlertTriangle,
|
||||||
Check,
|
Check,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
@@ -369,8 +370,10 @@ export function ConsoleEntry({ entry, consoleWidth }: ConsoleEntryProps) {
|
|||||||
}
|
}
|
||||||
}, [showCopySuccess])
|
}, [showCopySuccess])
|
||||||
|
|
||||||
const BlockIcon = blockConfig?.icon
|
// Special handling for serialization errors
|
||||||
const blockColor = blockConfig?.bgColor || '#6B7280'
|
const BlockIcon = entry.blockType === 'serializer' ? AlertTriangle : blockConfig?.icon
|
||||||
|
const blockColor =
|
||||||
|
entry.blockType === 'serializer' ? '#EF4444' : blockConfig?.bgColor || '#6B7280'
|
||||||
|
|
||||||
// Handle image load error callback
|
// Handle image load error callback
|
||||||
const handleImageLoadError = (hasError: boolean) => {
|
const handleImageLoadError = (hasError: boolean) => {
|
||||||
|
|||||||
@@ -456,46 +456,6 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className='flex items-center justify-end gap-0'>
|
<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%]'>
|
<div className='min-w-0 max-w-[80%]'>
|
||||||
{/* Message content in purple box */}
|
{/* Message content in purple box */}
|
||||||
<div
|
<div
|
||||||
@@ -544,6 +504,48 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ import {
|
|||||||
import { useSession } from '@/lib/auth-client'
|
import { useSession } from '@/lib/auth-client'
|
||||||
import { createLogger } from '@/lib/logs/console/logger'
|
import { createLogger } from '@/lib/logs/console/logger'
|
||||||
import { cn } from '@/lib/utils'
|
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 { useCopilotStore } from '@/stores/copilot/store'
|
||||||
import type { ChatContext } from '@/stores/copilot/types'
|
import type { ChatContext } from '@/stores/copilot/types'
|
||||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||||
@@ -122,6 +121,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
|
|||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
const [dragCounter, setDragCounter] = useState(0)
|
const [dragCounter, setDragCounter] = useState(0)
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
|
const overlayRef = useRef<HTMLDivElement>(null)
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
const [showMentionMenu, setShowMentionMenu] = useState(false)
|
const [showMentionMenu, setShowMentionMenu] = useState(false)
|
||||||
const mentionMenuRef = useRef<HTMLDivElement>(null)
|
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
|
// Auto-resize textarea and toggle vertical scroll when exceeding max height
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const textarea = textareaRef.current
|
const textarea = textareaRef.current
|
||||||
|
const overlay = overlayRef.current
|
||||||
if (textarea) {
|
if (textarea) {
|
||||||
const maxHeight = 120
|
const maxHeight = 120
|
||||||
textarea.style.height = 'auto'
|
textarea.style.height = 'auto'
|
||||||
const nextHeight = Math.min(textarea.scrollHeight, maxHeight)
|
const nextHeight = Math.min(textarea.scrollHeight, maxHeight)
|
||||||
textarea.style.height = `${nextHeight}px`
|
textarea.style.height = `${nextHeight}px`
|
||||||
textarea.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden'
|
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])
|
}, [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
|
// Close mention menu on outside click
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showMentionMenu) return
|
if (!showMentionMenu) return
|
||||||
@@ -1754,55 +1776,44 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
|
|||||||
return 'Agent'
|
return 'Agent'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Depth toggle state comes from global store; access via useCopilotStore
|
// Model selection state comes from global store; access via useCopilotStore
|
||||||
const { agentDepth, agentPrefetch, setAgentDepth, setAgentPrefetch } = useCopilotStore()
|
const { selectedModel, agentPrefetch, setSelectedModel, setAgentPrefetch } = useCopilotStore()
|
||||||
|
|
||||||
// Ensure MAX mode is off for Fast and Balanced depths
|
// Model configurations with their display names
|
||||||
useEffect(() => {
|
const modelOptions = [
|
||||||
if (agentDepth < 2 && !agentPrefetch) {
|
{ value: 'gpt-5-fast', label: 'gpt-5-fast' },
|
||||||
setAgentPrefetch(true)
|
{ value: 'gpt-5', label: 'gpt-5' },
|
||||||
}
|
{ value: 'gpt-5-medium', label: 'gpt-5-medium' },
|
||||||
}, [agentDepth, agentPrefetch, setAgentPrefetch])
|
{ value: 'gpt-5-high', label: 'gpt-5-high' },
|
||||||
|
{ value: 'gpt-4o', label: 'gpt-4o' },
|
||||||
const cycleDepth = () => {
|
{ value: 'gpt-4.1', label: 'gpt-4.1' },
|
||||||
// 8 modes: depths 0-3, each with prefetch off/on. Cycle depth, then toggle prefetch when wrapping.
|
{ value: 'o3', label: 'o3' },
|
||||||
const nextDepth = agentDepth === 3 ? 0 : ((agentDepth + 1) as 0 | 1 | 2 | 3)
|
{ value: 'claude-4-sonnet', label: 'claude-4-sonnet' },
|
||||||
if (nextDepth === 0 && agentDepth === 3) {
|
{ value: 'claude-4.1-opus', label: 'claude-4.1-opus' },
|
||||||
setAgentPrefetch(!agentPrefetch)
|
] as const
|
||||||
}
|
|
||||||
setAgentDepth(nextDepth)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCollapsedModeLabel = () => {
|
const getCollapsedModeLabel = () => {
|
||||||
const base = getDepthLabelFor(agentDepth)
|
const model = modelOptions.find((m) => m.value === selectedModel)
|
||||||
return !agentPrefetch ? `${base} MAX` : base
|
return model ? model.label : 'GPT-5 Default'
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDepthLabelFor = (value: 0 | 1 | 2 | 3) => {
|
const getModelIcon = () => {
|
||||||
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 colorClass = !agentPrefetch
|
const colorClass = !agentPrefetch
|
||||||
? 'text-[var(--brand-primary-hover-hex)]'
|
? 'text-[var(--brand-primary-hover-hex)]'
|
||||||
: 'text-muted-foreground'
|
: '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 scrollActiveItemIntoView = (index: number) => {
|
||||||
const container = menuListRef.current
|
const container = menuListRef.current
|
||||||
@@ -2116,8 +2127,11 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
|
|||||||
{/* Textarea Field with overlay */}
|
{/* Textarea Field with overlay */}
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
{/* Highlight overlay */}
|
{/* Highlight overlay */}
|
||||||
<div className='pointer-events-none absolute inset-0 z-[1] px-[2px] py-1'>
|
<div
|
||||||
<pre className='whitespace-pre-wrap font-sans text-foreground text-sm leading-[1.25rem]'>
|
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 elements: React.ReactNode[] = []
|
||||||
const remaining = message
|
const remaining = message
|
||||||
@@ -2163,8 +2177,8 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
|
|||||||
placeholder={isDragging ? 'Drop files here...' : effectivePlaceholder}
|
placeholder={isDragging ? 'Drop files here...' : effectivePlaceholder}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
rows={1}
|
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'
|
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' }}
|
style={{ height: 'auto', wordBreak: 'break-word' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showMentionMenu && (
|
{showMentionMenu && (
|
||||||
@@ -3057,7 +3071,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
|
|||||||
<span>{getModeText()}</span>
|
<span>{getModeText()}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align='start' className='p-0'>
|
<DropdownMenuContent align='start' side='top' className='p-0'>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className='w-[160px] p-1'>
|
<div className='w-[160px] p-1'>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -3131,85 +3145,166 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
|
|||||||
)}
|
)}
|
||||||
title='Choose mode'
|
title='Choose mode'
|
||||||
>
|
>
|
||||||
{getDepthIcon()}
|
{getModelIcon()}
|
||||||
<span>{getCollapsedModeLabel()}</span>
|
<span>
|
||||||
|
{getCollapsedModeLabel()}
|
||||||
|
{!agentPrefetch &&
|
||||||
|
!['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(selectedModel) && (
|
||||||
|
<span className='ml-1 font-semibold'>MAX</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align='start' className='p-0'>
|
<DropdownMenuContent align='start' side='top' className='max-h-[400px] p-0'>
|
||||||
<TooltipProvider delayDuration={100} skipDelayDuration={0}>
|
<TooltipProvider delayDuration={100} skipDelayDuration={0}>
|
||||||
<div className='w-[260px] p-3'>
|
<div className='w-[220px]'>
|
||||||
<div className='mb-3 flex items-center justify-between'>
|
<div className='p-2 pb-0'>
|
||||||
<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='mb-2 flex items-center justify-between'>
|
<div className='mb-2 flex items-center justify-between'>
|
||||||
<span className='font-medium text-xs'>Mode</span>
|
<div className='flex items-center gap-1.5'>
|
||||||
<div className='flex items-center gap-1'>
|
<span className='font-medium text-xs'>MAX mode</span>
|
||||||
{getDepthIconFor(agentDepth)}
|
<Tooltip>
|
||||||
<span className='text-muted-foreground text-xs'>
|
<TooltipTrigger asChild>
|
||||||
{getDepthLabelFor(agentDepth)}
|
<button
|
||||||
</span>
|
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>
|
<Switch
|
||||||
<div className='relative'>
|
checked={!agentPrefetch}
|
||||||
<CopilotSlider
|
disabled={['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(selectedModel)}
|
||||||
min={0}
|
title={
|
||||||
max={3}
|
['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(selectedModel)
|
||||||
step={1}
|
? 'MAX mode is only available for advanced models'
|
||||||
value={[agentDepth]}
|
: undefined
|
||||||
onValueChange={(val) =>
|
|
||||||
setAgentDepth((val?.[0] ?? 0) as 0 | 1 | 2 | 3)
|
|
||||||
}
|
}
|
||||||
|
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>
|
||||||
<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='my-1.5 flex justify-center'>
|
||||||
<div className='-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-[66.667%] h-2 w-[3px] bg-background' />
|
<div className='h-px w-[100%] bg-border' />
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-3 text-[11px] text-muted-foreground'>
|
<div className='max-h-[280px] overflow-y-auto px-2 pb-2'>
|
||||||
{getDepthDescription(agentDepth)}
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ export { EvalInput } from './eval-input'
|
|||||||
export { FileSelectorInput } from './file-selector/file-selector-input'
|
export { FileSelectorInput } from './file-selector/file-selector-input'
|
||||||
export { FileUpload } from './file-upload'
|
export { FileUpload } from './file-upload'
|
||||||
export { FolderSelectorInput } from './folder-selector/components/folder-selector-input'
|
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 { KnowledgeBaseSelector } from './knowledge-base-selector/knowledge-base-selector'
|
||||||
export { LongInput } from './long-input'
|
export { LongInput } from './long-input'
|
||||||
export { McpDynamicArgs } from './mcp-dynamic-args/mcp-dynamic-args'
|
export { McpDynamicArgs } from './mcp-dynamic-args/mcp-dynamic-args'
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { PlusIcon, Server, WrenchIcon, XIcon } from 'lucide-react'
|
import { AlertCircle, PlusIcon, Server, WrenchIcon, XIcon } from 'lucide-react'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
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({
|
export function ToolInput({
|
||||||
blockId,
|
blockId,
|
||||||
subBlockId,
|
subBlockId,
|
||||||
@@ -475,10 +513,18 @@ export function ToolInput({
|
|||||||
|
|
||||||
// Fallback: create options from tools.access
|
// Fallback: create options from tools.access
|
||||||
return block.tools.access.map((toolId) => {
|
return block.tools.access.map((toolId) => {
|
||||||
const toolParams = getToolParametersConfig(toolId)
|
try {
|
||||||
return {
|
const toolParams = getToolParametersConfig(toolId)
|
||||||
id: toolId,
|
return {
|
||||||
label: toolParams?.toolConfig?.name || toolId,
|
id: toolId,
|
||||||
|
label: toolParams?.toolConfig?.name || toolId,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error getting tool config for ${toolId}:`, error)
|
||||||
|
return {
|
||||||
|
id: toolId,
|
||||||
|
label: toolId,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
FileUpload,
|
FileUpload,
|
||||||
FolderSelectorInput,
|
FolderSelectorInput,
|
||||||
InputFormat,
|
InputFormat,
|
||||||
|
InputMapping,
|
||||||
KnowledgeBaseSelector,
|
KnowledgeBaseSelector,
|
||||||
LongInput,
|
LongInput,
|
||||||
McpDynamicArgs,
|
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':
|
case 'response-format':
|
||||||
return (
|
return (
|
||||||
<ResponseFormat
|
<ResponseFormat
|
||||||
|
|||||||
@@ -141,11 +141,32 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
|||||||
isShowingDiff,
|
isShowingDiff,
|
||||||
id,
|
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
|
const horizontalHandles = data.isPreview
|
||||||
? (data.blockState?.horizontalHandles ?? true) // In preview mode, use blockState and default to horizontal
|
? (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
|
: currentWorkflow.isDiffMode
|
||||||
const isWide = useWorkflowStore((state) => state.blocks[id]?.isWide ?? false)
|
? (currentWorkflow.blocks[id]?.horizontalHandles ?? true)
|
||||||
const blockHeight = useWorkflowStore((state) => state.blocks[id]?.height ?? 0)
|
: 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
|
// Get per-block webhook status by checking if webhook is configured
|
||||||
const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
|
const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
|
||||||
|
|
||||||
@@ -157,8 +178,14 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
|||||||
)
|
)
|
||||||
const blockWebhookStatus = !!(hasWebhookProvider && hasWebhookPath)
|
const blockWebhookStatus = !!(hasWebhookProvider && hasWebhookPath)
|
||||||
|
|
||||||
const blockAdvancedMode = useWorkflowStore((state) => state.blocks[id]?.advancedMode ?? false)
|
const blockAdvancedMode = currentWorkflow.isDiffMode
|
||||||
const blockTriggerMode = useWorkflowStore((state) => state.blocks[id]?.triggerMode ?? false)
|
? (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
|
// Local UI state for diff mode controls
|
||||||
const [diffIsWide, setDiffIsWide] = useState<boolean>(isWide)
|
const [diffIsWide, setDiffIsWide] = useState<boolean>(isWide)
|
||||||
@@ -660,7 +687,10 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
|||||||
|
|
||||||
{/* Block Header */}
|
{/* Block Header */}
|
||||||
<div
|
<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) => {
|
onMouseDown={(e) => {
|
||||||
e.stopPropagation()
|
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='mb-1 font-medium text-sm'>Description</p>
|
||||||
<p className='text-muted-foreground text-sm'>{config.longDescription}</p>
|
<p className='text-muted-foreground text-sm'>{config.longDescription}</p>
|
||||||
</div>
|
</div>
|
||||||
{config.outputs && (
|
{config.outputs && Object.keys(config.outputs).length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<p className='mb-1 font-medium text-sm'>Output</p>
|
<p className='mb-1 font-medium text-sm'>Output</p>
|
||||||
<div className='text-sm'>
|
<div className='text-sm'>
|
||||||
@@ -929,90 +959,92 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<Tooltip>
|
{subBlockRows.length > 0 && (
|
||||||
<TooltipTrigger asChild>
|
<Tooltip>
|
||||||
<Button
|
<TooltipTrigger asChild>
|
||||||
variant='ghost'
|
<Button
|
||||||
size='sm'
|
variant='ghost'
|
||||||
onClick={() => {
|
size='sm'
|
||||||
if (currentWorkflow.isDiffMode) {
|
onClick={() => {
|
||||||
setDiffIsWide((prev) => !prev)
|
if (currentWorkflow.isDiffMode) {
|
||||||
} else if (userPermissions.canEdit) {
|
setDiffIsWide((prev) => !prev)
|
||||||
collaborativeToggleBlockWide(id)
|
} else if (userPermissions.canEdit) {
|
||||||
}
|
collaborativeToggleBlockWide(id)
|
||||||
}}
|
}
|
||||||
className={cn(
|
}}
|
||||||
'h-7 p-1 text-gray-500',
|
className={cn(
|
||||||
!userPermissions.canEdit &&
|
'h-7 p-1 text-gray-500',
|
||||||
!currentWorkflow.isDiffMode &&
|
!userPermissions.canEdit &&
|
||||||
'cursor-not-allowed opacity-50'
|
!currentWorkflow.isDiffMode &&
|
||||||
)}
|
'cursor-not-allowed opacity-50'
|
||||||
disabled={!userPermissions.canEdit && !currentWorkflow.isDiffMode}
|
)}
|
||||||
>
|
disabled={!userPermissions.canEdit && !currentWorkflow.isDiffMode}
|
||||||
{displayIsWide ? (
|
>
|
||||||
<RectangleHorizontal className='h-5 w-5' />
|
{displayIsWide ? (
|
||||||
) : (
|
<RectangleHorizontal className='h-5 w-5' />
|
||||||
<RectangleVertical className='h-5 w-5' />
|
) : (
|
||||||
)}
|
<RectangleVertical className='h-5 w-5' />
|
||||||
</Button>
|
)}
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipContent side='top'>
|
</TooltipTrigger>
|
||||||
{!userPermissions.canEdit && !currentWorkflow.isDiffMode
|
<TooltipContent side='top'>
|
||||||
? userPermissions.isOfflineMode
|
{!userPermissions.canEdit && !currentWorkflow.isDiffMode
|
||||||
? 'Connection lost - please refresh'
|
? userPermissions.isOfflineMode
|
||||||
: 'Read-only mode'
|
? 'Connection lost - please refresh'
|
||||||
: displayIsWide
|
: 'Read-only mode'
|
||||||
? 'Narrow Block'
|
: displayIsWide
|
||||||
: 'Expand Block'}
|
? 'Narrow Block'
|
||||||
</TooltipContent>
|
: 'Expand Block'}
|
||||||
</Tooltip>
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Block Content */}
|
{/* Block Content - Only render if there are subblocks */}
|
||||||
<div
|
{subBlockRows.length > 0 && (
|
||||||
ref={contentRef}
|
<div
|
||||||
className='cursor-pointer space-y-4 px-4 pt-3 pb-4'
|
ref={contentRef}
|
||||||
onMouseDown={(e) => {
|
className='cursor-pointer space-y-4 px-4 pt-3 pb-4'
|
||||||
e.stopPropagation()
|
onMouseDown={(e) => {
|
||||||
}}
|
e.stopPropagation()
|
||||||
>
|
}}
|
||||||
{subBlockRows.length > 0
|
>
|
||||||
? subBlockRows.map((row, rowIndex) => (
|
{subBlockRows.map((row, rowIndex) => (
|
||||||
<div key={`row-${rowIndex}`} className='flex gap-4'>
|
<div key={`row-${rowIndex}`} className='flex gap-4'>
|
||||||
{row.map((subBlock, blockIndex) => (
|
{row.map((subBlock, blockIndex) => (
|
||||||
<div
|
<div
|
||||||
key={`${id}-${rowIndex}-${blockIndex}`}
|
key={`${id}-${rowIndex}-${blockIndex}`}
|
||||||
className={cn('space-y-1', subBlock.layout === 'half' ? 'flex-1' : 'w-full')}
|
className={cn('space-y-1', subBlock.layout === 'half' ? 'flex-1' : 'w-full')}
|
||||||
>
|
>
|
||||||
<SubBlock
|
<SubBlock
|
||||||
blockId={id}
|
blockId={id}
|
||||||
config={subBlock}
|
config={subBlock}
|
||||||
isConnecting={isConnecting}
|
isConnecting={isConnecting}
|
||||||
isPreview={data.isPreview || currentWorkflow.isDiffMode}
|
isPreview={data.isPreview || currentWorkflow.isDiffMode}
|
||||||
subBlockValues={
|
subBlockValues={
|
||||||
data.subBlockValues ||
|
data.subBlockValues ||
|
||||||
(currentWorkflow.isDiffMode && currentBlock
|
(currentWorkflow.isDiffMode && currentBlock
|
||||||
? (currentBlock as any).subBlocks
|
? (currentBlock as any).subBlocks
|
||||||
: undefined)
|
: undefined)
|
||||||
}
|
}
|
||||||
disabled={!userPermissions.canEdit}
|
disabled={!userPermissions.canEdit}
|
||||||
fieldDiffStatus={
|
fieldDiffStatus={
|
||||||
fieldDiff?.changed_fields?.includes(subBlock.id)
|
fieldDiff?.changed_fields?.includes(subBlock.id)
|
||||||
? 'changed'
|
? 'changed'
|
||||||
: fieldDiff?.unchanged_fields?.includes(subBlock.id)
|
: fieldDiff?.unchanged_fields?.includes(subBlock.id)
|
||||||
? 'unchanged'
|
? 'unchanged'
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
allowExpandInPreview={currentWorkflow.isDiffMode}
|
allowExpandInPreview={currentWorkflow.isDiffMode}
|
||||||
isWide={displayIsWide}
|
isWide={displayIsWide}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
))
|
))}
|
||||||
: null}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Output Handle */}
|
{/* Output Handle */}
|
||||||
{type !== 'condition' && type !== 'response' && (
|
{type !== 'condition' && type !== 'response' && (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { v4 as uuidv4 } from 'uuid'
|
|||||||
import { createLogger } from '@/lib/logs/console/logger'
|
import { createLogger } from '@/lib/logs/console/logger'
|
||||||
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
|
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
|
||||||
import { processStreamingBlockLogs } from '@/lib/tokenization'
|
import { processStreamingBlockLogs } from '@/lib/tokenization'
|
||||||
import { getBlock } from '@/blocks'
|
import { TriggerUtils } from '@/lib/workflows/triggers'
|
||||||
import type { BlockOutput } from '@/blocks/types'
|
import type { BlockOutput } from '@/blocks/types'
|
||||||
import { Executor } from '@/executor'
|
import { Executor } from '@/executor'
|
||||||
import type { BlockLog, ExecutionResult, StreamingExecution } from '@/executor/types'
|
import type { BlockLog, ExecutionResult, StreamingExecution } from '@/executor/types'
|
||||||
@@ -447,7 +447,30 @@ export function useWorkflowExecution() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} 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 {
|
} finally {
|
||||||
controller.close()
|
controller.close()
|
||||||
setIsExecuting(false)
|
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(
|
const filteredStates = Object.entries(mergedStates).reduce(
|
||||||
(acc, [id, block]) => {
|
(acc, [id, block]) => {
|
||||||
// Skip blocks with undefined type
|
|
||||||
if (!block || !block.type) {
|
if (!block || !block.type) {
|
||||||
logger.warn(`Skipping block with undefined type: ${id}`, block)
|
logger.warn(`Skipping block with undefined type: ${id}`, block)
|
||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
|
acc[id] = block
|
||||||
const blockConfig = getBlock(block.type)
|
|
||||||
const isTriggerBlock = blockConfig?.category === 'triggers'
|
|
||||||
|
|
||||||
// Skip trigger blocks during manual execution
|
|
||||||
if (!isTriggerBlock) {
|
|
||||||
acc[id] = block
|
|
||||||
}
|
|
||||||
return acc
|
return acc
|
||||||
},
|
},
|
||||||
{} as typeof mergedStates
|
{} as typeof mergedStates
|
||||||
@@ -632,15 +647,8 @@ export function useWorkflowExecution() {
|
|||||||
{} as Record<string, any>
|
{} as Record<string, any>
|
||||||
)
|
)
|
||||||
|
|
||||||
// Filter edges to exclude connections to/from trigger blocks
|
// Keep edges intact to allow execution starting from trigger blocks
|
||||||
const triggerBlockIds = Object.keys(mergedStates).filter((id) => {
|
const filteredEdges = workflowEdges
|
||||||
const blockConfig = getBlock(mergedStates[id].type)
|
|
||||||
return blockConfig?.category === 'triggers'
|
|
||||||
})
|
|
||||||
|
|
||||||
const filteredEdges = workflowEdges.filter(
|
|
||||||
(edge) => !triggerBlockIds.includes(edge.source) && !triggerBlockIds.includes(edge.target)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Derive subflows from the current filtered graph to avoid stale state
|
// Derive subflows from the current filtered graph to avoid stale state
|
||||||
const runtimeLoops = generateLoopBlocks(filteredStates)
|
const runtimeLoops = generateLoopBlocks(filteredStates)
|
||||||
@@ -663,12 +671,158 @@ export function useWorkflowExecution() {
|
|||||||
selectedOutputIds = chatStore.getState().getSelectedWorkflowOutput(activeWorkflowId)
|
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 = {
|
const executorOptions: ExecutorOptions = {
|
||||||
workflow,
|
workflow,
|
||||||
currentBlockStates,
|
currentBlockStates,
|
||||||
envVarValues,
|
envVarValues,
|
||||||
workflowInput,
|
workflowInput: finalWorkflowInput,
|
||||||
workflowVariables,
|
workflowVariables,
|
||||||
contextExtensions: {
|
contextExtensions: {
|
||||||
stream: isExecutingFromChat,
|
stream: isExecutingFromChat,
|
||||||
@@ -687,8 +841,8 @@ export function useWorkflowExecution() {
|
|||||||
const newExecutor = new Executor(executorOptions)
|
const newExecutor = new Executor(executorOptions)
|
||||||
setExecutor(newExecutor)
|
setExecutor(newExecutor)
|
||||||
|
|
||||||
// Execute workflow
|
// Execute workflow with the determined start block
|
||||||
return newExecutor.execute(activeWorkflowId || '')
|
return newExecutor.execute(activeWorkflowId || '', startBlockId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExecutionError = (error: any, options?: { executionId?: string }) => {
|
const handleExecutionError = (error: any, options?: { executionId?: string }) => {
|
||||||
@@ -729,7 +883,7 @@ export function useWorkflowExecution() {
|
|||||||
try {
|
try {
|
||||||
// Prefer attributing to specific subflow if we have a structured error
|
// Prefer attributing to specific subflow if we have a structured error
|
||||||
let blockId = 'serialization'
|
let blockId = 'serialization'
|
||||||
let blockName = 'Serialization'
|
let blockName = 'Workflow'
|
||||||
let blockType = 'serializer'
|
let blockType = 'serializer'
|
||||||
if (error instanceof WorkflowValidationError) {
|
if (error instanceof WorkflowValidationError) {
|
||||||
blockId = error.blockId || blockId
|
blockId = error.blockId || blockId
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { createLogger } from '@/lib/logs/console/logger'
|
import { createLogger } from '@/lib/logs/console/logger'
|
||||||
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
|
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
|
||||||
import { getBlock } from '@/blocks'
|
|
||||||
import type { BlockOutput } from '@/blocks/types'
|
import type { BlockOutput } from '@/blocks/types'
|
||||||
import { Executor } from '@/executor'
|
import { Executor } from '@/executor'
|
||||||
import type { ExecutionResult, StreamingExecution } from '@/executor/types'
|
import type { ExecutionResult, StreamingExecution } from '@/executor/types'
|
||||||
@@ -131,26 +130,9 @@ export async function executeWorkflowWithLogging(
|
|||||||
// Merge subblock states from the appropriate store
|
// Merge subblock states from the appropriate store
|
||||||
const mergedStates = mergeSubblockState(validBlocks)
|
const mergedStates = mergeSubblockState(validBlocks)
|
||||||
|
|
||||||
// Filter out trigger blocks for manual execution
|
// Don't filter out trigger blocks - let the executor handle them properly
|
||||||
const filteredStates = Object.entries(mergedStates).reduce(
|
// The standard executor has TriggerBlockHandler that knows how to handle triggers
|
||||||
(acc, [id, block]) => {
|
const filteredStates = mergedStates
|
||||||
// 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
|
|
||||||
)
|
|
||||||
|
|
||||||
const currentBlockStates = Object.entries(filteredStates).reduce(
|
const currentBlockStates = Object.entries(filteredStates).reduce(
|
||||||
(acc, [id, block]) => {
|
(acc, [id, block]) => {
|
||||||
@@ -186,15 +168,9 @@ export async function executeWorkflowWithLogging(
|
|||||||
{} as Record<string, any>
|
{} as Record<string, any>
|
||||||
)
|
)
|
||||||
|
|
||||||
// Filter edges to exclude connections to/from trigger blocks
|
// Don't filter edges - let all connections remain intact
|
||||||
const triggerBlockIds = Object.keys(mergedStates).filter((id) => {
|
// The executor's routing system will handle execution paths properly
|
||||||
const blockConfig = getBlock(mergedStates[id].type)
|
const filteredEdges = workflowEdges
|
||||||
return blockConfig?.category === 'triggers'
|
|
||||||
})
|
|
||||||
|
|
||||||
const filteredEdges = workflowEdges.filter(
|
|
||||||
(edge: any) => !triggerBlockIds.includes(edge.source) && !triggerBlockIds.includes(edge.target)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create serialized workflow with filtered blocks and edges
|
// Create serialized workflow with filtered blocks and edges
|
||||||
const workflow = new Serializer().serializeWorkflow(
|
const workflow = new Serializer().serializeWorkflow(
|
||||||
|
|||||||
@@ -206,6 +206,18 @@ export async function applyAutoLayoutAndUpdateStore(
|
|||||||
loops: newWorkflowState.loops || {},
|
loops: newWorkflowState.loops || {},
|
||||||
parallels: newWorkflowState.parallels || {},
|
parallels: newWorkflowState.parallels || {},
|
||||||
deploymentStatuses: newWorkflowState.deploymentStatuses || {},
|
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
|
// Save the updated workflow state to the database
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import ReactFlow, {
|
|||||||
} from 'reactflow'
|
} from 'reactflow'
|
||||||
import 'reactflow/dist/style.css'
|
import 'reactflow/dist/style.css'
|
||||||
import { createLogger } from '@/lib/logs/console/logger'
|
import { createLogger } from '@/lib/logs/console/logger'
|
||||||
|
import { TriggerUtils } from '@/lib/workflows/triggers'
|
||||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||||
import { ControlBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar'
|
import { ControlBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar'
|
||||||
import { DiffControls } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls'
|
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 { FloatingControls } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/floating-controls/floating-controls'
|
||||||
import { Panel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel'
|
import { Panel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel'
|
||||||
import { SubflowNodeComponent } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node'
|
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 { 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 { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge'
|
||||||
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
|
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
|
// Enhanced edge selection with parent context and unique identifier
|
||||||
const [selectedEdgeInfo, setSelectedEdgeInfo] = useState<SelectedEdgeInfo | null>(null)
|
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
|
// Hooks
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -107,6 +124,11 @@ const WorkflowContent = React.memo(() => {
|
|||||||
// Extract workflow data from the abstraction
|
// Extract workflow data from the abstraction
|
||||||
const { blocks, edges, loops, parallels, isDiffMode } = currentWorkflow
|
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
|
// Get diff analysis for edge reconstruction
|
||||||
const { diffAnalysis, isShowingDiff, isDiffReady } = useWorkflowDiffStore()
|
const { diffAnalysis, isShowingDiff, isDiffReady } = useWorkflowDiffStore()
|
||||||
|
|
||||||
@@ -565,7 +587,7 @@ const WorkflowContent = React.memo(() => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { type } = event.detail
|
const { type, enableTriggerMode } = event.detail
|
||||||
|
|
||||||
if (!type) return
|
if (!type) return
|
||||||
if (type === 'connectionBlock') return
|
if (type === 'connectionBlock') return
|
||||||
@@ -637,7 +659,10 @@ const WorkflowContent = React.memo(() => {
|
|||||||
|
|
||||||
// Create a new block with a unique ID
|
// Create a new block with a unique ID
|
||||||
const id = crypto.randomUUID()
|
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
|
// Auto-connect logic
|
||||||
const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled
|
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
|
// 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)
|
window.addEventListener('add-block-from-toolbar', handleAddBlockFromToolbar as EventListener)
|
||||||
@@ -681,8 +736,35 @@ const WorkflowContent = React.memo(() => {
|
|||||||
findClosestOutput,
|
findClosestOutput,
|
||||||
determineSourceHandle,
|
determineSourceHandle,
|
||||||
effectivePermissions.canEdit,
|
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
|
// Update the onDrop handler
|
||||||
const onDrop = useCallback(
|
const onDrop = useCallback(
|
||||||
(event: React.DragEvent) => {
|
(event: React.DragEvent) => {
|
||||||
@@ -784,8 +866,14 @@ const WorkflowContent = React.memo(() => {
|
|||||||
|
|
||||||
// Generate id and name here so they're available in all code paths
|
// Generate id and name here so they're available in all code paths
|
||||||
const id = crypto.randomUUID()
|
const id = crypto.randomUUID()
|
||||||
|
// Prefer semantic default names for triggers; then ensure unique numbering centrally
|
||||||
|
const defaultTriggerNameDrop = TriggerUtils.getDefaultTriggerName(data.type)
|
||||||
const baseName =
|
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)
|
const name = getUniqueBlockName(baseName, blocks)
|
||||||
|
|
||||||
if (containerInfo) {
|
if (containerInfo) {
|
||||||
@@ -868,6 +956,20 @@ const WorkflowContent = React.memo(() => {
|
|||||||
// Immediate resize without delay
|
// Immediate resize without delay
|
||||||
resizeLoopNodesWrapper()
|
resizeLoopNodesWrapper()
|
||||||
} else {
|
} 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
|
// Regular auto-connect logic
|
||||||
const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled
|
const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled
|
||||||
let autoConnectEdge
|
let autoConnectEdge
|
||||||
@@ -888,7 +990,19 @@ const WorkflowContent = React.memo(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Regular canvas drop with auto-connect edge
|
// 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) {
|
} catch (err) {
|
||||||
logger.error('Error dropping block:', { err })
|
logger.error('Error dropping block:', { err })
|
||||||
@@ -903,6 +1017,7 @@ const WorkflowContent = React.memo(() => {
|
|||||||
determineSourceHandle,
|
determineSourceHandle,
|
||||||
isPointInLoopNodeWrapper,
|
isPointInLoopNodeWrapper,
|
||||||
getNodes,
|
getNodes,
|
||||||
|
setTriggerWarning,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1815,6 +1930,19 @@ const WorkflowContent = React.memo(() => {
|
|||||||
|
|
||||||
{/* Show DiffControls if diff is available (regardless of current view mode) */}
|
{/* Show DiffControls if diff is available (regardless of current view mode) */}
|
||||||
<DiffControls />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,9 +7,14 @@ import type { BlockConfig } from '@/blocks/types'
|
|||||||
export type ToolbarBlockProps = {
|
export type ToolbarBlockProps = {
|
||||||
config: BlockConfig
|
config: BlockConfig
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
enableTriggerMode?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolbarBlock({ config, disabled = false }: ToolbarBlockProps) {
|
export function ToolbarBlock({
|
||||||
|
config,
|
||||||
|
disabled = false,
|
||||||
|
enableTriggerMode = false,
|
||||||
|
}: ToolbarBlockProps) {
|
||||||
const userPermissions = useUserPermissionsContext()
|
const userPermissions = useUserPermissionsContext()
|
||||||
|
|
||||||
const handleDragStart = (e: React.DragEvent) => {
|
const handleDragStart = (e: React.DragEvent) => {
|
||||||
@@ -17,7 +22,13 @@ export function ToolbarBlock({ config, disabled = false }: ToolbarBlockProps) {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
return
|
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'
|
e.dataTransfer.effectAllowed = 'move'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,10 +40,11 @@ export function ToolbarBlock({ config, disabled = false }: ToolbarBlockProps) {
|
|||||||
const event = new CustomEvent('add-block-from-toolbar', {
|
const event = new CustomEvent('add-block-from-toolbar', {
|
||||||
detail: {
|
detail: {
|
||||||
type: config.type,
|
type: config.type,
|
||||||
|
enableTriggerMode,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
window.dispatchEvent(event)
|
window.dispatchEvent(event)
|
||||||
}, [config.type, disabled])
|
}, [config.type, disabled, enableTriggerMode])
|
||||||
|
|
||||||
const blockContent = (
|
const blockContent = (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -4,10 +4,15 @@ import { useMemo, useState } from 'react'
|
|||||||
import { Search } from 'lucide-react'
|
import { Search } from 'lucide-react'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
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 { 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 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 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'
|
import type { WorkspaceUserPermissions } from '@/hooks/use-user-permissions'
|
||||||
|
|
||||||
interface ToolbarProps {
|
interface ToolbarProps {
|
||||||
@@ -24,25 +29,30 @@ interface BlockItem {
|
|||||||
|
|
||||||
export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }: ToolbarProps) {
|
export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }: ToolbarProps) {
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
const [activeTab, setActiveTab] = useState('blocks')
|
||||||
|
|
||||||
const { regularBlocks, specialBlocks, tools, triggers } = useMemo(() => {
|
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
|
// Filter blocks based on search query
|
||||||
const filteredBlocks = allBlocks.filter((block) => {
|
const filteredBlocks = sourceBlocks.filter((block) => {
|
||||||
if (block.type === 'starter' || block.hideFromToolbar) return false
|
const matchesSearch =
|
||||||
|
|
||||||
return (
|
|
||||||
!searchQuery.trim() ||
|
!searchQuery.trim() ||
|
||||||
block.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
block.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
block.description.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 regularBlockConfigs = filteredBlocks.filter((block) => block.category === 'blocks')
|
||||||
const toolConfigs = filteredBlocks.filter((block) => block.category === 'tools')
|
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
|
// Create regular block items and sort alphabetically
|
||||||
const regularBlockItems: BlockItem[] = regularBlockConfigs
|
const regularBlockItems: BlockItem[] = regularBlockConfigs
|
||||||
@@ -54,23 +64,25 @@ export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }:
|
|||||||
}))
|
}))
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.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[] = []
|
const specialBlockItems: BlockItem[] = []
|
||||||
|
|
||||||
if (!searchQuery.trim() || 'loop'.toLowerCase().includes(searchQuery.toLowerCase())) {
|
if (activeTab === 'blocks') {
|
||||||
specialBlockItems.push({
|
if (!searchQuery.trim() || 'loop'.toLowerCase().includes(searchQuery.toLowerCase())) {
|
||||||
name: 'Loop',
|
specialBlockItems.push({
|
||||||
type: 'loop',
|
name: 'Loop',
|
||||||
isCustom: true,
|
type: 'loop',
|
||||||
})
|
isCustom: true,
|
||||||
}
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (!searchQuery.trim() || 'parallel'.toLowerCase().includes(searchQuery.toLowerCase())) {
|
if (!searchQuery.trim() || 'parallel'.toLowerCase().includes(searchQuery.toLowerCase())) {
|
||||||
specialBlockItems.push({
|
specialBlockItems.push({
|
||||||
name: 'Parallel',
|
name: 'Parallel',
|
||||||
type: 'parallel',
|
type: 'parallel',
|
||||||
isCustom: true,
|
isCustom: true,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort special blocks alphabetically
|
// Sort special blocks alphabetically
|
||||||
@@ -95,65 +107,106 @@ export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }:
|
|||||||
tools: toolConfigs,
|
tools: toolConfigs,
|
||||||
triggers: triggerBlockItems,
|
triggers: triggerBlockItems,
|
||||||
}
|
}
|
||||||
}, [searchQuery])
|
}, [searchQuery, activeTab])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex h-full flex-col'>
|
<div className='flex h-full flex-col'>
|
||||||
{/* Search */}
|
{/* Tabs */}
|
||||||
<div className='flex-shrink-0 p-2'>
|
<Tabs value={activeTab} onValueChange={setActiveTab} className='flex h-full flex-col'>
|
||||||
<div className='flex h-9 items-center gap-2 rounded-[8px] border bg-background pr-2 pl-3'>
|
<div className='flex-shrink-0 px-2 pt-2'>
|
||||||
<Search className='h-4 w-4 text-muted-foreground' strokeWidth={2} />
|
<div className='flex h-9 w-full items-center gap-1 rounded-[10px] border bg-card px-[2.5px] py-1 shadow-xs'>
|
||||||
<Input
|
<button
|
||||||
placeholder='Search blocks...'
|
onClick={() => setActiveTab('blocks')}
|
||||||
value={searchQuery}
|
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 ${
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
activeTab === 'blocks' ? 'panel-tab-active' : 'panel-tab-inactive'
|
||||||
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'
|
Blocks
|
||||||
autoCapitalize='off'
|
</button>
|
||||||
spellCheck='false'
|
<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>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
{/* Search */}
|
||||||
<ScrollArea className='flex-1 px-2 pb-[0.26px]' hideScrollbar={true}>
|
<div className='flex-shrink-0 p-2'>
|
||||||
<div className='space-y-1 pb-2'>
|
<div className='flex h-9 items-center gap-2 rounded-[8px] border bg-background pr-2 pl-3'>
|
||||||
{/* Regular Blocks Section */}
|
<Search className='h-4 w-4 text-muted-foreground' strokeWidth={2} />
|
||||||
{regularBlocks.map((block) => (
|
<Input
|
||||||
<ToolbarBlock
|
placeholder={activeTab === 'blocks' ? 'Search blocks...' : 'Search triggers...'}
|
||||||
key={block.type}
|
value={searchQuery}
|
||||||
config={block.config}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
disabled={!userPermissions.canEdit}
|
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'
|
||||||
/>
|
/>
|
||||||
))}
|
</div>
|
||||||
|
|
||||||
{/* 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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { AgentIcon } from '@/components/icons'
|
|||||||
import { isHosted } from '@/lib/environment'
|
import { isHosted } from '@/lib/environment'
|
||||||
import { createLogger } from '@/lib/logs/console/logger'
|
import { createLogger } from '@/lib/logs/console/logger'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import {
|
import {
|
||||||
getAllModelProviders,
|
getAllModelProviders,
|
||||||
getBaseModelProviders,
|
getBaseModelProviders,
|
||||||
@@ -61,6 +62,7 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
|
|||||||
type: 'agent',
|
type: 'agent',
|
||||||
name: 'Agent',
|
name: 'Agent',
|
||||||
description: 'Build an agent',
|
description: 'Build an agent',
|
||||||
|
authMode: AuthMode.ApiKey,
|
||||||
longDescription:
|
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.',
|
'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',
|
docsLink: 'https://docs.sim.ai/blocks/agent',
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { AirtableIcon } from '@/components/icons'
|
import { AirtableIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { AirtableResponse } from '@/tools/airtable/types'
|
import type { AirtableResponse } from '@/tools/airtable/types'
|
||||||
|
|
||||||
export const AirtableBlock: BlockConfig<AirtableResponse> = {
|
export const AirtableBlock: BlockConfig<AirtableResponse> = {
|
||||||
type: 'airtable',
|
type: 'airtable',
|
||||||
name: 'Airtable',
|
name: 'Airtable',
|
||||||
description: 'Read, create, and update Airtable',
|
description: 'Read, create, and update Airtable',
|
||||||
|
authMode: AuthMode.OAuth,
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/airtable',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#E0E0E0',
|
bgColor: '#E0E0E0',
|
||||||
|
|||||||
34
apps/sim/blocks/blocks/api_trigger.ts
Normal file
34
apps/sim/blocks/blocks/api_trigger.ts
Normal file
@@ -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 { 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'
|
import type { BrowserUseResponse } from '@/tools/browser_use/types'
|
||||||
|
|
||||||
export const BrowserUseBlock: BlockConfig<BrowserUseResponse> = {
|
export const BrowserUseBlock: BlockConfig<BrowserUseResponse> = {
|
||||||
type: 'browser_use',
|
type: 'browser_use',
|
||||||
name: 'Browser Use',
|
name: 'Browser Use',
|
||||||
description: 'Run browser automation tasks',
|
description: 'Run browser automation tasks',
|
||||||
|
authMode: AuthMode.ApiKey,
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/browser_use',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#E0E0E0',
|
bgColor: '#E0E0E0',
|
||||||
|
|||||||
30
apps/sim/blocks/blocks/chat_trigger.ts
Normal file
30
apps/sim/blocks/blocks/chat_trigger.ts
Normal file
@@ -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 { ClayIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||||
import type { ClayPopulateResponse } from '@/tools/clay/types'
|
import type { ClayPopulateResponse } from '@/tools/clay/types'
|
||||||
|
|
||||||
export const ClayBlock: BlockConfig<ClayPopulateResponse> = {
|
export const ClayBlock: BlockConfig<ClayPopulateResponse> = {
|
||||||
type: 'clay',
|
type: 'clay',
|
||||||
name: 'Clay',
|
name: 'Clay',
|
||||||
description: 'Populate Clay workbook',
|
description: 'Populate Clay workbook',
|
||||||
longDescription:
|
authMode: AuthMode.ApiKey,
|
||||||
'Integrate Clay into the workflow. Can populate a table with data. Requires an API Key.',
|
longDescription: 'Integrate Clay into the workflow. Can populate a table with data.',
|
||||||
docsLink: 'https://docs.sim.ai/tools/clay',
|
docsLink: 'https://docs.sim.ai/tools/clay',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#E0E0E0',
|
bgColor: '#E0E0E0',
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { ConfluenceIcon } from '@/components/icons'
|
import { ConfluenceIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { ConfluenceResponse } from '@/tools/confluence/types'
|
import type { ConfluenceResponse } from '@/tools/confluence/types'
|
||||||
|
|
||||||
export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
|
export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
|
||||||
type: 'confluence',
|
type: 'confluence',
|
||||||
name: 'Confluence',
|
name: 'Confluence',
|
||||||
description: 'Interact with Confluence',
|
description: 'Interact with Confluence',
|
||||||
longDescription:
|
authMode: AuthMode.OAuth,
|
||||||
'Integrate Confluence into the workflow. Can read and update a page. Requires OAuth.',
|
longDescription: 'Integrate Confluence into the workflow. Can read and update a page.',
|
||||||
docsLink: 'https://docs.sim.ai/tools/confluence',
|
docsLink: 'https://docs.sim.ai/tools/confluence',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#E0E0E0',
|
bgColor: '#E0E0E0',
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { DiscordIcon } from '@/components/icons'
|
import { DiscordIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { DiscordResponse } from '@/tools/discord/types'
|
import type { DiscordResponse } from '@/tools/discord/types'
|
||||||
|
|
||||||
export const DiscordBlock: BlockConfig<DiscordResponse> = {
|
export const DiscordBlock: BlockConfig<DiscordResponse> = {
|
||||||
type: 'discord',
|
type: 'discord',
|
||||||
name: 'Discord',
|
name: 'Discord',
|
||||||
description: 'Interact with Discord',
|
description: 'Interact with Discord',
|
||||||
|
authMode: AuthMode.BotToken,
|
||||||
longDescription:
|
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',
|
category: 'tools',
|
||||||
bgColor: '#E0E0E0',
|
bgColor: '#E0E0E0',
|
||||||
icon: DiscordIcon,
|
icon: DiscordIcon,
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { ElevenLabsIcon } from '@/components/icons'
|
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'
|
import type { ElevenLabsBlockResponse } from '@/tools/elevenlabs/types'
|
||||||
|
|
||||||
export const ElevenLabsBlock: BlockConfig<ElevenLabsBlockResponse> = {
|
export const ElevenLabsBlock: BlockConfig<ElevenLabsBlockResponse> = {
|
||||||
type: 'elevenlabs',
|
type: 'elevenlabs',
|
||||||
name: 'ElevenLabs',
|
name: 'ElevenLabs',
|
||||||
description: 'Convert TTS using ElevenLabs',
|
description: 'Convert TTS using ElevenLabs',
|
||||||
longDescription:
|
authMode: AuthMode.ApiKey,
|
||||||
'Integrate ElevenLabs into the workflow. Can convert text to speech. Requires API key.',
|
longDescription: 'Integrate ElevenLabs into the workflow. Can convert text to speech.',
|
||||||
docsLink: 'https://docs.sim.ai/tools/elevenlabs',
|
docsLink: 'https://docs.sim.ai/tools/elevenlabs',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#181C1E',
|
bgColor: '#181C1E',
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { ExaAIIcon } from '@/components/icons'
|
import { ExaAIIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { ExaResponse } from '@/tools/exa/types'
|
import type { ExaResponse } from '@/tools/exa/types'
|
||||||
|
|
||||||
export const ExaBlock: BlockConfig<ExaResponse> = {
|
export const ExaBlock: BlockConfig<ExaResponse> = {
|
||||||
type: 'exa',
|
type: 'exa',
|
||||||
name: 'Exa',
|
name: 'Exa',
|
||||||
description: 'Search with Exa AI',
|
description: 'Search with Exa AI',
|
||||||
|
authMode: AuthMode.ApiKey,
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/exa',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#1F40ED',
|
bgColor: '#1F40ED',
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { FirecrawlIcon } from '@/components/icons'
|
import { FirecrawlIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { FirecrawlResponse } from '@/tools/firecrawl/types'
|
import type { FirecrawlResponse } from '@/tools/firecrawl/types'
|
||||||
|
|
||||||
export const FirecrawlBlock: BlockConfig<FirecrawlResponse> = {
|
export const FirecrawlBlock: BlockConfig<FirecrawlResponse> = {
|
||||||
type: 'firecrawl',
|
type: 'firecrawl',
|
||||||
name: 'Firecrawl',
|
name: 'Firecrawl',
|
||||||
description: 'Scrape or search the web',
|
description: 'Scrape or search the web',
|
||||||
longDescription:
|
authMode: AuthMode.ApiKey,
|
||||||
'Integrate Firecrawl into the workflow. Can search, scrape, or crawl websites. Requires API Key.',
|
longDescription: 'Integrate Firecrawl into the workflow. Can search, scrape, or crawl websites.',
|
||||||
docsLink: 'https://docs.sim.ai/tools/firecrawl',
|
docsLink: 'https://docs.sim.ai/tools/firecrawl',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#181C1E',
|
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'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
|
||||||
|
const WebhookIcon = (props: SVGProps<SVGSVGElement>) => createElement(Webhook, props)
|
||||||
|
|
||||||
export const GenericWebhookBlock: BlockConfig = {
|
export const GenericWebhookBlock: BlockConfig = {
|
||||||
type: 'generic_webhook',
|
type: 'generic_webhook',
|
||||||
name: 'Webhook',
|
name: 'Webhook',
|
||||||
@@ -8,6 +12,7 @@ export const GenericWebhookBlock: BlockConfig = {
|
|||||||
category: 'triggers',
|
category: 'triggers',
|
||||||
icon: WebhookIcon,
|
icon: WebhookIcon,
|
||||||
bgColor: '#10B981', // Green color for triggers
|
bgColor: '#10B981', // Green color for triggers
|
||||||
|
triggerAllowed: true,
|
||||||
|
|
||||||
subBlocks: [
|
subBlocks: [
|
||||||
// Generic webhook configuration - always visible
|
// Generic webhook configuration - always visible
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { GithubIcon } from '@/components/icons'
|
import { GithubIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { GitHubResponse } from '@/tools/github/types'
|
import type { GitHubResponse } from '@/tools/github/types'
|
||||||
|
|
||||||
export const GitHubBlock: BlockConfig<GitHubResponse> = {
|
export const GitHubBlock: BlockConfig<GitHubResponse> = {
|
||||||
type: 'github',
|
type: 'github',
|
||||||
name: 'GitHub',
|
name: 'GitHub',
|
||||||
description: 'Interact with GitHub or trigger workflows from GitHub events',
|
description: 'Interact with GitHub or trigger workflows from GitHub events',
|
||||||
|
authMode: AuthMode.ApiKey,
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/github',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#181C1E',
|
bgColor: '#181C1E',
|
||||||
icon: GithubIcon,
|
icon: GithubIcon,
|
||||||
|
triggerAllowed: true,
|
||||||
subBlocks: [
|
subBlocks: [
|
||||||
{
|
{
|
||||||
id: 'operation',
|
id: 'operation',
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { GmailIcon } from '@/components/icons'
|
import { GmailIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { GmailToolResponse } from '@/tools/gmail/types'
|
import type { GmailToolResponse } from '@/tools/gmail/types'
|
||||||
|
|
||||||
export const GmailBlock: BlockConfig<GmailToolResponse> = {
|
export const GmailBlock: BlockConfig<GmailToolResponse> = {
|
||||||
type: 'gmail',
|
type: 'gmail',
|
||||||
name: 'Gmail',
|
name: 'Gmail',
|
||||||
description: 'Send Gmail or trigger workflows from Gmail events',
|
description: 'Send Gmail or trigger workflows from Gmail events',
|
||||||
|
authMode: AuthMode.OAuth,
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/gmail',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#E0E0E0',
|
bgColor: '#E0E0E0',
|
||||||
icon: GmailIcon,
|
icon: GmailIcon,
|
||||||
|
triggerAllowed: true,
|
||||||
subBlocks: [
|
subBlocks: [
|
||||||
// Operation selector
|
// Operation selector
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { GoogleIcon } from '@/components/icons'
|
import { GoogleIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { GoogleSearchResponse } from '@/tools/google/types'
|
import type { GoogleSearchResponse } from '@/tools/google/types'
|
||||||
|
|
||||||
export const GoogleSearchBlock: BlockConfig<GoogleSearchResponse> = {
|
export const GoogleSearchBlock: BlockConfig<GoogleSearchResponse> = {
|
||||||
type: 'google_search',
|
type: 'google_search',
|
||||||
name: 'Google Search',
|
name: 'Google Search',
|
||||||
description: 'Search the web',
|
description: 'Search the web',
|
||||||
longDescription:
|
authMode: AuthMode.ApiKey,
|
||||||
'Integrate Google Search into the workflow. Can search the web. Requires API Key.',
|
longDescription: 'Integrate Google Search into the workflow. Can search the web.',
|
||||||
docsLink: 'https://docs.sim.ai/tools/google_search',
|
docsLink: 'https://docs.sim.ai/tools/google_search',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#E0E0E0',
|
bgColor: '#E0E0E0',
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { GoogleCalendarIcon } from '@/components/icons'
|
import { GoogleCalendarIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { GoogleCalendarResponse } from '@/tools/google_calendar/types'
|
import type { GoogleCalendarResponse } from '@/tools/google_calendar/types'
|
||||||
|
|
||||||
export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||||
type: 'google_calendar',
|
type: 'google_calendar',
|
||||||
name: 'Google Calendar',
|
name: 'Google Calendar',
|
||||||
description: 'Manage Google Calendar events',
|
description: 'Manage Google Calendar events',
|
||||||
|
authMode: AuthMode.OAuth,
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/google_calendar',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#E0E0E0',
|
bgColor: '#E0E0E0',
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { GoogleDocsIcon } from '@/components/icons'
|
import { GoogleDocsIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { GoogleDocsResponse } from '@/tools/google_docs/types'
|
import type { GoogleDocsResponse } from '@/tools/google_docs/types'
|
||||||
|
|
||||||
export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
|
export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
|
||||||
type: 'google_docs',
|
type: 'google_docs',
|
||||||
name: 'Google Docs',
|
name: 'Google Docs',
|
||||||
description: 'Read, write, and create documents',
|
description: 'Read, write, and create documents',
|
||||||
|
authMode: AuthMode.OAuth,
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/google_docs',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#E0E0E0',
|
bgColor: '#E0E0E0',
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { GoogleDriveIcon } from '@/components/icons'
|
import { GoogleDriveIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { GoogleDriveResponse } from '@/tools/google_drive/types'
|
import type { GoogleDriveResponse } from '@/tools/google_drive/types'
|
||||||
|
|
||||||
export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
|
export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
|
||||||
type: 'google_drive',
|
type: 'google_drive',
|
||||||
name: 'Google Drive',
|
name: 'Google Drive',
|
||||||
description: 'Create, upload, and list files',
|
description: 'Create, upload, and list files',
|
||||||
longDescription:
|
authMode: AuthMode.OAuth,
|
||||||
'Integrate Google Drive into the workflow. Can create, upload, and list files. Requires OAuth.',
|
longDescription: 'Integrate Google Drive into the workflow. Can create, upload, and list files.',
|
||||||
docsLink: 'https://docs.sim.ai/tools/google_drive',
|
docsLink: 'https://docs.sim.ai/tools/google_drive',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#E0E0E0',
|
bgColor: '#E0E0E0',
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { GoogleSheetsIcon } from '@/components/icons'
|
import { GoogleSheetsIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { GoogleSheetsResponse } from '@/tools/google_sheets/types'
|
import type { GoogleSheetsResponse } from '@/tools/google_sheets/types'
|
||||||
|
|
||||||
export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
|
export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
|
||||||
type: 'google_sheets',
|
type: 'google_sheets',
|
||||||
name: 'Google Sheets',
|
name: 'Google Sheets',
|
||||||
description: 'Read, write, and update data',
|
description: 'Read, write, and update data',
|
||||||
|
authMode: AuthMode.OAuth,
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/google_sheets',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#E0E0E0',
|
bgColor: '#E0E0E0',
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { HuggingFaceIcon } from '@/components/icons'
|
import { HuggingFaceIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { HuggingFaceChatResponse } from '@/tools/huggingface/types'
|
import type { HuggingFaceChatResponse } from '@/tools/huggingface/types'
|
||||||
|
|
||||||
export const HuggingFaceBlock: BlockConfig<HuggingFaceChatResponse> = {
|
export const HuggingFaceBlock: BlockConfig<HuggingFaceChatResponse> = {
|
||||||
type: 'huggingface',
|
type: 'huggingface',
|
||||||
name: 'Hugging Face',
|
name: 'Hugging Face',
|
||||||
description: 'Use Hugging Face Inference API',
|
description: 'Use Hugging Face Inference API',
|
||||||
|
authMode: AuthMode.ApiKey,
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/huggingface',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#0B0F19',
|
bgColor: '#0B0F19',
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { HunterIOIcon } from '@/components/icons'
|
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'
|
import type { HunterResponse } from '@/tools/hunter/types'
|
||||||
|
|
||||||
export const HunterBlock: BlockConfig<HunterResponse> = {
|
export const HunterBlock: BlockConfig<HunterResponse> = {
|
||||||
type: 'hunter',
|
type: 'hunter',
|
||||||
name: 'Hunter io',
|
name: 'Hunter io',
|
||||||
description: 'Find and verify professional email addresses',
|
description: 'Find and verify professional email addresses',
|
||||||
|
authMode: AuthMode.ApiKey,
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/hunter',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#E0E0E0',
|
bgColor: '#E0E0E0',
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { ImageIcon } from '@/components/icons'
|
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'
|
import type { DalleResponse } from '@/tools/openai/types'
|
||||||
|
|
||||||
export const ImageGeneratorBlock: BlockConfig<DalleResponse> = {
|
export const ImageGeneratorBlock: BlockConfig<DalleResponse> = {
|
||||||
type: 'image_generator',
|
type: 'image_generator',
|
||||||
name: 'Image Generator',
|
name: 'Image Generator',
|
||||||
description: 'Generate images',
|
description: 'Generate images',
|
||||||
|
authMode: AuthMode.ApiKey,
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/image_generator',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#4D5FFF',
|
bgColor: '#4D5FFF',
|
||||||
|
|||||||
37
apps/sim/blocks/blocks/input_trigger.ts
Normal file
37
apps/sim/blocks/blocks/input_trigger.ts
Normal file
@@ -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 { JinaAIIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||||
import type { ReadUrlResponse } from '@/tools/jina/types'
|
import type { ReadUrlResponse } from '@/tools/jina/types'
|
||||||
|
|
||||||
export const JinaBlock: BlockConfig<ReadUrlResponse> = {
|
export const JinaBlock: BlockConfig<ReadUrlResponse> = {
|
||||||
type: 'jina',
|
type: 'jina',
|
||||||
name: 'Jina',
|
name: 'Jina',
|
||||||
description: 'Convert website content into text',
|
description: 'Convert website content into text',
|
||||||
longDescription:
|
authMode: AuthMode.ApiKey,
|
||||||
'Integrate Jina into the workflow. Extracts content from websites. Requires API Key.',
|
longDescription: 'Integrate Jina into the workflow. Extracts content from websites.',
|
||||||
docsLink: 'https://docs.sim.ai/tools/jina',
|
docsLink: 'https://docs.sim.ai/tools/jina',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#333333',
|
bgColor: '#333333',
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { JiraIcon } from '@/components/icons'
|
import { JiraIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { JiraResponse } from '@/tools/jira/types'
|
import type { JiraResponse } from '@/tools/jira/types'
|
||||||
|
|
||||||
export const JiraBlock: BlockConfig<JiraResponse> = {
|
export const JiraBlock: BlockConfig<JiraResponse> = {
|
||||||
type: 'jira',
|
type: 'jira',
|
||||||
name: 'Jira',
|
name: 'Jira',
|
||||||
description: 'Interact with Jira',
|
description: 'Interact with Jira',
|
||||||
longDescription:
|
authMode: AuthMode.OAuth,
|
||||||
'Integrate Jira into the workflow. Can read, write, and update issues. Requires OAuth.',
|
longDescription: 'Integrate Jira into the workflow. Can read, write, and update issues.',
|
||||||
docsLink: 'https://docs.sim.ai/tools/jira',
|
docsLink: 'https://docs.sim.ai/tools/jira',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#E0E0E0',
|
bgColor: '#E0E0E0',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { LinearIcon } from '@/components/icons'
|
import { LinearIcon } from '@/components/icons'
|
||||||
import type { BlockConfig, BlockIcon } from '@/blocks/types'
|
import type { BlockConfig, BlockIcon } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { LinearResponse } from '@/tools/linear/types'
|
import type { LinearResponse } from '@/tools/linear/types'
|
||||||
|
|
||||||
const LinearBlockIcon: BlockIcon = (props) => LinearIcon(props as any)
|
const LinearBlockIcon: BlockIcon = (props) => LinearIcon(props as any)
|
||||||
@@ -8,8 +9,8 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
|
|||||||
type: 'linear',
|
type: 'linear',
|
||||||
name: 'Linear',
|
name: 'Linear',
|
||||||
description: 'Read and create issues in Linear',
|
description: 'Read and create issues in Linear',
|
||||||
longDescription:
|
authMode: AuthMode.OAuth,
|
||||||
'Integrate Linear into the workflow. Can read and create issues. Requires OAuth.',
|
longDescription: 'Integrate Linear into the workflow. Can read and create issues.',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
icon: LinearBlockIcon,
|
icon: LinearBlockIcon,
|
||||||
bgColor: '#5E6AD2',
|
bgColor: '#5E6AD2',
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { LinkupIcon } from '@/components/icons'
|
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'
|
import type { LinkupSearchToolResponse } from '@/tools/linkup/types'
|
||||||
|
|
||||||
export const LinkupBlock: BlockConfig<LinkupSearchToolResponse> = {
|
export const LinkupBlock: BlockConfig<LinkupSearchToolResponse> = {
|
||||||
type: 'linkup',
|
type: 'linkup',
|
||||||
name: 'Linkup',
|
name: 'Linkup',
|
||||||
description: 'Search the web with 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',
|
docsLink: 'https://docs.sim.ai/tools/linkup',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#D6D3C7',
|
bgColor: '#D6D3C7',
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Mem0Icon } from '@/components/icons'
|
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'
|
import type { Mem0Response } from '@/tools/mem0/types'
|
||||||
|
|
||||||
export const Mem0Block: BlockConfig<Mem0Response> = {
|
export const Mem0Block: BlockConfig<Mem0Response> = {
|
||||||
type: 'mem0',
|
type: 'mem0',
|
||||||
name: 'Mem0',
|
name: 'Mem0',
|
||||||
description: 'Agent memory management',
|
description: 'Agent memory management',
|
||||||
longDescription:
|
authMode: AuthMode.ApiKey,
|
||||||
'Integrate Mem0 into the workflow. Can add, search, and retrieve memories. Requires API Key.',
|
longDescription: 'Integrate Mem0 into the workflow. Can add, search, and retrieve memories.',
|
||||||
bgColor: '#181C1E',
|
bgColor: '#181C1E',
|
||||||
icon: Mem0Icon,
|
icon: Mem0Icon,
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { MicrosoftExcelIcon } from '@/components/icons'
|
import { MicrosoftExcelIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { MicrosoftExcelResponse } from '@/tools/microsoft_excel/types'
|
import type { MicrosoftExcelResponse } from '@/tools/microsoft_excel/types'
|
||||||
|
|
||||||
export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
|
export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
|
||||||
type: 'microsoft_excel',
|
type: 'microsoft_excel',
|
||||||
name: 'Microsoft Excel',
|
name: 'Microsoft Excel',
|
||||||
description: 'Read, write, and update data',
|
description: 'Read, write, and update data',
|
||||||
|
authMode: AuthMode.OAuth,
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/microsoft_excel',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#E0E0E0',
|
bgColor: '#E0E0E0',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { MicrosoftPlannerIcon } from '@/components/icons'
|
import { MicrosoftPlannerIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { MicrosoftPlannerResponse } from '@/tools/microsoft_planner/types'
|
import type { MicrosoftPlannerResponse } from '@/tools/microsoft_planner/types'
|
||||||
|
|
||||||
interface MicrosoftPlannerBlockParams {
|
interface MicrosoftPlannerBlockParams {
|
||||||
@@ -19,8 +20,8 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
|
|||||||
type: 'microsoft_planner',
|
type: 'microsoft_planner',
|
||||||
name: 'Microsoft Planner',
|
name: 'Microsoft Planner',
|
||||||
description: 'Read and create tasks in Microsoft Planner',
|
description: 'Read and create tasks in Microsoft Planner',
|
||||||
longDescription:
|
authMode: AuthMode.OAuth,
|
||||||
'Integrate Microsoft Planner into the workflow. Can read and create tasks. Requires OAuth.',
|
longDescription: 'Integrate Microsoft Planner into the workflow. Can read and create tasks.',
|
||||||
docsLink: 'https://docs.sim.ai/tools/microsoft_planner',
|
docsLink: 'https://docs.sim.ai/tools/microsoft_planner',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#E0E0E0',
|
bgColor: '#E0E0E0',
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import { MicrosoftTeamsIcon } from '@/components/icons'
|
import { MicrosoftTeamsIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { MicrosoftTeamsResponse } from '@/tools/microsoft_teams/types'
|
import type { MicrosoftTeamsResponse } from '@/tools/microsoft_teams/types'
|
||||||
|
|
||||||
export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
|
export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
|
||||||
type: 'microsoft_teams',
|
type: 'microsoft_teams',
|
||||||
name: 'Microsoft Teams',
|
name: 'Microsoft Teams',
|
||||||
description: 'Read, write, and create messages',
|
description: 'Read, write, and create messages',
|
||||||
|
authMode: AuthMode.OAuth,
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/microsoft_teams',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
|
triggerAllowed: true,
|
||||||
bgColor: '#E0E0E0',
|
bgColor: '#E0E0E0',
|
||||||
icon: MicrosoftTeamsIcon,
|
icon: MicrosoftTeamsIcon,
|
||||||
subBlocks: [
|
subBlocks: [
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { MistralIcon } from '@/components/icons'
|
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'
|
import type { MistralParserOutput } from '@/tools/mistral/types'
|
||||||
|
|
||||||
export const MistralParseBlock: BlockConfig<MistralParserOutput> = {
|
export const MistralParseBlock: BlockConfig<MistralParserOutput> = {
|
||||||
type: 'mistral_parse',
|
type: 'mistral_parse',
|
||||||
name: 'Mistral Parser',
|
name: 'Mistral Parser',
|
||||||
description: 'Extract text from PDF documents',
|
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',
|
docsLink: 'https://docs.sim.ai/tools/mistral_parse',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#000000',
|
bgColor: '#000000',
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { NotionIcon } from '@/components/icons'
|
import { NotionIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { NotionResponse } from '@/tools/notion/types'
|
import type { NotionResponse } from '@/tools/notion/types'
|
||||||
|
|
||||||
export const NotionBlock: BlockConfig<NotionResponse> = {
|
export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||||
type: 'notion',
|
type: 'notion',
|
||||||
name: 'Notion',
|
name: 'Notion',
|
||||||
description: 'Manage Notion pages',
|
description: 'Manage Notion pages',
|
||||||
|
authMode: AuthMode.OAuth,
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/notion',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#181C1E',
|
bgColor: '#181C1E',
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { MicrosoftOneDriveIcon } from '@/components/icons'
|
import { MicrosoftOneDriveIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { OneDriveResponse } from '@/tools/onedrive/types'
|
import type { OneDriveResponse } from '@/tools/onedrive/types'
|
||||||
|
|
||||||
export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||||
type: 'onedrive',
|
type: 'onedrive',
|
||||||
name: 'OneDrive',
|
name: 'OneDrive',
|
||||||
description: 'Create, upload, and list files',
|
description: 'Create, upload, and list files',
|
||||||
longDescription:
|
authMode: AuthMode.OAuth,
|
||||||
'Integrate OneDrive into the workflow. Can create, upload, and list files. Requires OAuth.',
|
longDescription: 'Integrate OneDrive into the workflow. Can create, upload, and list files.',
|
||||||
docsLink: 'https://docs.sim.ai/tools/onedrive',
|
docsLink: 'https://docs.sim.ai/tools/onedrive',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#E0E0E0',
|
bgColor: '#E0E0E0',
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { OpenAIIcon } from '@/components/icons'
|
import { OpenAIIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
|
|
||||||
export const OpenAIBlock: BlockConfig = {
|
export const OpenAIBlock: BlockConfig = {
|
||||||
type: 'openai',
|
type: 'openai',
|
||||||
name: 'Embeddings',
|
name: 'Embeddings',
|
||||||
description: 'Generate Open AI embeddings',
|
description: 'Generate Open AI embeddings',
|
||||||
longDescription:
|
authMode: AuthMode.ApiKey,
|
||||||
'Integrate Embeddings into the workflow. Can generate embeddings from text. Requires API Key.',
|
longDescription: 'Integrate Embeddings into the workflow. Can generate embeddings from text.',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
docsLink: 'https://docs.sim.ai/tools/openai',
|
docsLink: 'https://docs.sim.ai/tools/openai',
|
||||||
bgColor: '#10a37f',
|
bgColor: '#10a37f',
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import { OutlookIcon } from '@/components/icons'
|
import { OutlookIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { OutlookResponse } from '@/tools/outlook/types'
|
import type { OutlookResponse } from '@/tools/outlook/types'
|
||||||
|
|
||||||
export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
||||||
type: 'outlook',
|
type: 'outlook',
|
||||||
name: 'Outlook',
|
name: 'Outlook',
|
||||||
description: 'Access Outlook',
|
description: 'Access Outlook',
|
||||||
|
authMode: AuthMode.OAuth,
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/outlook',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
|
triggerAllowed: true,
|
||||||
bgColor: '#E0E0E0',
|
bgColor: '#E0E0E0',
|
||||||
icon: OutlookIcon,
|
icon: OutlookIcon,
|
||||||
subBlocks: [
|
subBlocks: [
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { ParallelIcon } from '@/components/icons'
|
import { ParallelIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||||
import type { ToolResponse } from '@/tools/types'
|
import type { ToolResponse } from '@/tools/types'
|
||||||
|
|
||||||
export const ParallelBlock: BlockConfig<ToolResponse> = {
|
export const ParallelBlock: BlockConfig<ToolResponse> = {
|
||||||
type: 'parallel_ai',
|
type: 'parallel_ai',
|
||||||
name: 'Parallel AI',
|
name: 'Parallel AI',
|
||||||
description: 'Search with 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',
|
docsLink: 'https://docs.parallel.ai/search-api/search-quickstart',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#E0E0E0',
|
bgColor: '#E0E0E0',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PerplexityIcon } from '@/components/icons'
|
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'
|
import type { PerplexityChatResponse } from '@/tools/perplexity/types'
|
||||||
|
|
||||||
export const PerplexityBlock: BlockConfig<PerplexityChatResponse> = {
|
export const PerplexityBlock: BlockConfig<PerplexityChatResponse> = {
|
||||||
@@ -7,7 +7,8 @@ export const PerplexityBlock: BlockConfig<PerplexityChatResponse> = {
|
|||||||
name: 'Perplexity',
|
name: 'Perplexity',
|
||||||
description: 'Use Perplexity AI chat models',
|
description: 'Use Perplexity AI chat models',
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/perplexity',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#20808D', // Perplexity turquoise color
|
bgColor: '#20808D', // Perplexity turquoise color
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { PineconeIcon } from '@/components/icons'
|
import { PineconeIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { PineconeResponse } from '@/tools/pinecone/types'
|
import type { PineconeResponse } from '@/tools/pinecone/types'
|
||||||
|
|
||||||
export const PineconeBlock: BlockConfig<PineconeResponse> = {
|
export const PineconeBlock: BlockConfig<PineconeResponse> = {
|
||||||
type: 'pinecone',
|
type: 'pinecone',
|
||||||
name: 'Pinecone',
|
name: 'Pinecone',
|
||||||
description: 'Use Pinecone vector database',
|
description: 'Use Pinecone vector database',
|
||||||
|
authMode: AuthMode.ApiKey,
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/pinecone',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#0D1117',
|
bgColor: '#0D1117',
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { QdrantIcon } from '@/components/icons'
|
import { QdrantIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { QdrantResponse } from '@/tools/qdrant/types'
|
import type { QdrantResponse } from '@/tools/qdrant/types'
|
||||||
|
|
||||||
export const QdrantBlock: BlockConfig<QdrantResponse> = {
|
export const QdrantBlock: BlockConfig<QdrantResponse> = {
|
||||||
type: 'qdrant',
|
type: 'qdrant',
|
||||||
name: 'Qdrant',
|
name: 'Qdrant',
|
||||||
description: 'Use Qdrant vector database',
|
description: 'Use Qdrant vector database',
|
||||||
longDescription:
|
authMode: AuthMode.ApiKey,
|
||||||
'Integrate Qdrant into the workflow. Can upsert, search, and fetch points. Requires API Key.',
|
longDescription: 'Integrate Qdrant into the workflow. Can upsert, search, and fetch points.',
|
||||||
docsLink: 'https://qdrant.tech/documentation/',
|
docsLink: 'https://qdrant.tech/documentation/',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#1A223F',
|
bgColor: '#1A223F',
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { RedditIcon } from '@/components/icons'
|
import { RedditIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { RedditResponse } from '@/tools/reddit/types'
|
import type { RedditResponse } from '@/tools/reddit/types'
|
||||||
|
|
||||||
export const RedditBlock: BlockConfig<RedditResponse> = {
|
export const RedditBlock: BlockConfig<RedditResponse> = {
|
||||||
type: 'reddit',
|
type: 'reddit',
|
||||||
name: 'Reddit',
|
name: 'Reddit',
|
||||||
description: 'Access Reddit data and content',
|
description: 'Access Reddit data and content',
|
||||||
|
authMode: AuthMode.OAuth,
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/reddit',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#FF5700',
|
bgColor: '#FF5700',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ConnectIcon } from '@/components/icons'
|
import { ConnectIcon } from '@/components/icons'
|
||||||
import { isHosted } from '@/lib/environment'
|
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 type { ProviderId } from '@/providers/types'
|
||||||
import {
|
import {
|
||||||
getAllModelProviders,
|
getAllModelProviders,
|
||||||
@@ -108,6 +108,7 @@ export const RouterBlock: BlockConfig<RouterResponse> = {
|
|||||||
type: 'router',
|
type: 'router',
|
||||||
name: 'Router',
|
name: 'Router',
|
||||||
description: 'Route workflow',
|
description: 'Route workflow',
|
||||||
|
authMode: AuthMode.ApiKey,
|
||||||
longDescription:
|
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.',
|
'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',
|
category: 'blocks',
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { S3Icon } from '@/components/icons'
|
import { S3Icon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { S3Response } from '@/tools/s3/types'
|
import type { S3Response } from '@/tools/s3/types'
|
||||||
|
|
||||||
export const S3Block: BlockConfig<S3Response> = {
|
export const S3Block: BlockConfig<S3Response> = {
|
||||||
type: 's3',
|
type: 's3',
|
||||||
name: 'S3',
|
name: 'S3',
|
||||||
description: 'View S3 files',
|
description: 'View S3 files',
|
||||||
|
authMode: AuthMode.ApiKey,
|
||||||
longDescription:
|
longDescription:
|
||||||
'Integrate S3 into the workflow. Can get presigned URLs for S3 objects. Requires access key and secret access key.',
|
'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',
|
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'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
|
||||||
|
const ScheduleIcon = (props: SVGProps<SVGSVGElement>) => createElement(Clock, props)
|
||||||
|
|
||||||
export const ScheduleBlock: BlockConfig = {
|
export const ScheduleBlock: BlockConfig = {
|
||||||
type: 'schedule',
|
type: 'schedule',
|
||||||
name: 'Schedule',
|
name: 'Schedule',
|
||||||
@@ -8,7 +12,7 @@ export const ScheduleBlock: BlockConfig = {
|
|||||||
longDescription:
|
longDescription:
|
||||||
'Integrate Schedule into the workflow. Can trigger a workflow on a schedule configuration.',
|
'Integrate Schedule into the workflow. Can trigger a workflow on a schedule configuration.',
|
||||||
category: 'triggers',
|
category: 'triggers',
|
||||||
bgColor: '#7B68EE',
|
bgColor: '#6366F1',
|
||||||
icon: ScheduleIcon,
|
icon: ScheduleIcon,
|
||||||
|
|
||||||
subBlocks: [
|
subBlocks: [
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { SerperIcon } from '@/components/icons'
|
import { SerperIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { SearchResponse } from '@/tools/serper/types'
|
import type { SearchResponse } from '@/tools/serper/types'
|
||||||
|
|
||||||
export const SerperBlock: BlockConfig<SearchResponse> = {
|
export const SerperBlock: BlockConfig<SearchResponse> = {
|
||||||
type: 'serper',
|
type: 'serper',
|
||||||
name: 'Serper',
|
name: 'Serper',
|
||||||
description: 'Search the web using 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',
|
docsLink: 'https://docs.sim.ai/tools/serper',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#2B3543',
|
bgColor: '#2B3543',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { MicrosoftSharepointIcon } from '@/components/icons'
|
import { MicrosoftSharepointIcon } from '@/components/icons'
|
||||||
import { createLogger } from '@/lib/logs/console/logger'
|
import { createLogger } from '@/lib/logs/console/logger'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { SharepointResponse } from '@/tools/sharepoint/types'
|
import type { SharepointResponse } from '@/tools/sharepoint/types'
|
||||||
|
|
||||||
const logger = createLogger('SharepointBlock')
|
const logger = createLogger('SharepointBlock')
|
||||||
@@ -9,6 +10,7 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
|||||||
type: 'sharepoint',
|
type: 'sharepoint',
|
||||||
name: 'Sharepoint',
|
name: 'Sharepoint',
|
||||||
description: 'Work with pages and lists',
|
description: 'Work with pages and lists',
|
||||||
|
authMode: AuthMode.OAuth,
|
||||||
longDescription:
|
longDescription:
|
||||||
'Integrate SharePoint into the workflow. Read/create pages, list sites, and work with lists (read, create, update items). Requires OAuth.',
|
'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',
|
docsLink: 'https://docs.sim.ai/tools/sharepoint',
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { SlackIcon } from '@/components/icons'
|
import { SlackIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { SlackResponse } from '@/tools/slack/types'
|
import type { SlackResponse } from '@/tools/slack/types'
|
||||||
|
|
||||||
export const SlackBlock: BlockConfig<SlackResponse> = {
|
export const SlackBlock: BlockConfig<SlackResponse> = {
|
||||||
type: 'slack',
|
type: 'slack',
|
||||||
name: 'Slack',
|
name: 'Slack',
|
||||||
description: 'Send messages to Slack or trigger workflows from Slack events',
|
description: 'Send messages to Slack or trigger workflows from Slack events',
|
||||||
|
authMode: AuthMode.OAuth,
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/slack',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#611f69',
|
bgColor: '#611f69',
|
||||||
icon: SlackIcon,
|
icon: SlackIcon,
|
||||||
|
triggerAllowed: true,
|
||||||
subBlocks: [
|
subBlocks: [
|
||||||
{
|
{
|
||||||
id: 'operation',
|
id: 'operation',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { StagehandIcon } from '@/components/icons'
|
import { StagehandIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||||
import type { ToolResponse } from '@/tools/types'
|
import type { ToolResponse } from '@/tools/types'
|
||||||
|
|
||||||
export interface StagehandExtractResponse extends ToolResponse {
|
export interface StagehandExtractResponse extends ToolResponse {
|
||||||
@@ -12,8 +12,9 @@ export const StagehandBlock: BlockConfig<StagehandExtractResponse> = {
|
|||||||
type: 'stagehand',
|
type: 'stagehand',
|
||||||
name: 'Stagehand Extract',
|
name: 'Stagehand Extract',
|
||||||
description: 'Extract data from websites',
|
description: 'Extract data from websites',
|
||||||
|
authMode: AuthMode.ApiKey,
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/stagehand',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#FFC83C',
|
bgColor: '#FFC83C',
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { StagehandIcon } from '@/components/icons'
|
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'
|
import type { StagehandAgentResponse } from '@/tools/stagehand/types'
|
||||||
|
|
||||||
export const StagehandAgentBlock: BlockConfig<StagehandAgentResponse> = {
|
export const StagehandAgentBlock: BlockConfig<StagehandAgentResponse> = {
|
||||||
type: 'stagehand_agent',
|
type: 'stagehand_agent',
|
||||||
name: 'Stagehand Agent',
|
name: 'Stagehand Agent',
|
||||||
description: 'Autonomous web browsing agent',
|
description: 'Autonomous web browsing agent',
|
||||||
|
authMode: AuthMode.ApiKey,
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/stagehand_agent',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#FFC83C',
|
bgColor: '#FFC83C',
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const StarterBlock: BlockConfig = {
|
|||||||
category: 'blocks',
|
category: 'blocks',
|
||||||
bgColor: '#2FB3FF',
|
bgColor: '#2FB3FF',
|
||||||
icon: StartIcon,
|
icon: StartIcon,
|
||||||
|
hideFromToolbar: true,
|
||||||
subBlocks: [
|
subBlocks: [
|
||||||
// Main trigger selector
|
// Main trigger selector
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { SupabaseIcon } from '@/components/icons'
|
import { SupabaseIcon } from '@/components/icons'
|
||||||
import { createLogger } from '@/lib/logs/console/logger'
|
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'
|
import type { SupabaseResponse } from '@/tools/supabase/types'
|
||||||
|
|
||||||
const logger = createLogger('SupabaseBlock')
|
const logger = createLogger('SupabaseBlock')
|
||||||
@@ -9,6 +9,7 @@ export const SupabaseBlock: BlockConfig<SupabaseResponse> = {
|
|||||||
type: 'supabase',
|
type: 'supabase',
|
||||||
name: 'Supabase',
|
name: 'Supabase',
|
||||||
description: 'Use Supabase database',
|
description: 'Use Supabase database',
|
||||||
|
authMode: AuthMode.ApiKey,
|
||||||
longDescription:
|
longDescription:
|
||||||
'Integrate Supabase into the workflow. Can get many rows, get, create, update, delete, and upsert a row.',
|
'Integrate Supabase into the workflow. Can get many rows, get, create, update, delete, and upsert a row.',
|
||||||
docsLink: 'https://docs.sim.ai/tools/supabase',
|
docsLink: 'https://docs.sim.ai/tools/supabase',
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { TavilyIcon } from '@/components/icons'
|
import { TavilyIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { TavilyResponse } from '@/tools/tavily/types'
|
import type { TavilyResponse } from '@/tools/tavily/types'
|
||||||
|
|
||||||
export const TavilyBlock: BlockConfig<TavilyResponse> = {
|
export const TavilyBlock: BlockConfig<TavilyResponse> = {
|
||||||
type: 'tavily',
|
type: 'tavily',
|
||||||
name: 'Tavily',
|
name: 'Tavily',
|
||||||
description: 'Search and extract information',
|
description: 'Search and extract information',
|
||||||
|
authMode: AuthMode.ApiKey,
|
||||||
longDescription:
|
longDescription:
|
||||||
'Integrate Tavily into the workflow. Can search the web and extract content from specific URLs. Requires API Key.',
|
'Integrate Tavily into the workflow. Can search the web and extract content from specific URLs. Requires API Key.',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { TelegramIcon } from '@/components/icons'
|
import { TelegramIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { TelegramMessageResponse } from '@/tools/telegram/types'
|
import type { TelegramMessageResponse } from '@/tools/telegram/types'
|
||||||
|
|
||||||
export const TelegramBlock: BlockConfig<TelegramMessageResponse> = {
|
export const TelegramBlock: BlockConfig<TelegramMessageResponse> = {
|
||||||
type: 'telegram',
|
type: 'telegram',
|
||||||
name: 'Telegram',
|
name: 'Telegram',
|
||||||
description: 'Send messages through Telegram or trigger workflows from Telegram events',
|
description: 'Send messages through Telegram or trigger workflows from Telegram events',
|
||||||
|
authMode: AuthMode.BotToken,
|
||||||
longDescription:
|
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.',
|
'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',
|
docsLink: 'https://docs.sim.ai/tools/telegram',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#E0E0E0',
|
bgColor: '#E0E0E0',
|
||||||
icon: TelegramIcon,
|
icon: TelegramIcon,
|
||||||
|
triggerAllowed: true,
|
||||||
subBlocks: [
|
subBlocks: [
|
||||||
{
|
{
|
||||||
id: 'botToken',
|
id: 'botToken',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { TranslateIcon } from '@/components/icons'
|
import { TranslateIcon } from '@/components/icons'
|
||||||
import { isHosted } from '@/lib/environment'
|
import { isHosted } from '@/lib/environment'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||||
import {
|
import {
|
||||||
getAllModelProviders,
|
getAllModelProviders,
|
||||||
getBaseModelProviders,
|
getBaseModelProviders,
|
||||||
@@ -29,6 +29,7 @@ export const TranslateBlock: BlockConfig = {
|
|||||||
type: 'translate',
|
type: 'translate',
|
||||||
name: 'Translate',
|
name: 'Translate',
|
||||||
description: 'Translate text to any language',
|
description: 'Translate text to any language',
|
||||||
|
authMode: AuthMode.ApiKey,
|
||||||
longDescription: 'Integrate Translate into the workflow. Can translate text to any language.',
|
longDescription: 'Integrate Translate into the workflow. Can translate text to any language.',
|
||||||
docsLink: 'https://docs.sim.ai/tools/translate',
|
docsLink: 'https://docs.sim.ai/tools/translate',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { TwilioIcon } from '@/components/icons'
|
import { TwilioIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { TwilioSMSBlockOutput } from '@/tools/twilio/types'
|
import type { TwilioSMSBlockOutput } from '@/tools/twilio/types'
|
||||||
|
|
||||||
export const TwilioSMSBlock: BlockConfig<TwilioSMSBlockOutput> = {
|
export const TwilioSMSBlock: BlockConfig<TwilioSMSBlockOutput> = {
|
||||||
type: 'twilio_sms',
|
type: 'twilio_sms',
|
||||||
name: 'Twilio SMS',
|
name: 'Twilio SMS',
|
||||||
description: 'Send SMS messages',
|
description: 'Send SMS messages',
|
||||||
|
authMode: AuthMode.ApiKey,
|
||||||
longDescription: 'Integrate Twilio into the workflow. Can send SMS messages.',
|
longDescription: 'Integrate Twilio into the workflow. Can send SMS messages.',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#F22F46', // Twilio brand color
|
bgColor: '#F22F46', // Twilio brand color
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { TypeformIcon } from '@/components/icons'
|
import { TypeformIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { TypeformResponse } from '@/tools/typeform/types'
|
import type { TypeformResponse } from '@/tools/typeform/types'
|
||||||
|
|
||||||
export const TypeformBlock: BlockConfig<TypeformResponse> = {
|
export const TypeformBlock: BlockConfig<TypeformResponse> = {
|
||||||
type: 'typeform',
|
type: 'typeform',
|
||||||
name: 'Typeform',
|
name: 'Typeform',
|
||||||
description: 'Interact with Typeform',
|
description: 'Interact with Typeform',
|
||||||
|
authMode: AuthMode.ApiKey,
|
||||||
longDescription:
|
longDescription:
|
||||||
'Integrate Typeform into the workflow. Can retrieve responses, download files, and get form insights. Requires API Key.',
|
'Integrate Typeform into the workflow. Can retrieve responses, download files, and get form insights. Requires API Key.',
|
||||||
docsLink: 'https://docs.sim.ai/tools/typeform',
|
docsLink: 'https://docs.sim.ai/tools/typeform',
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { EyeIcon } from '@/components/icons'
|
import { EyeIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { VisionResponse } from '@/tools/vision/types'
|
import type { VisionResponse } from '@/tools/vision/types'
|
||||||
|
|
||||||
export const VisionBlock: BlockConfig<VisionResponse> = {
|
export const VisionBlock: BlockConfig<VisionResponse> = {
|
||||||
type: 'vision',
|
type: 'vision',
|
||||||
name: 'Vision',
|
name: 'Vision',
|
||||||
description: 'Analyze images with vision models',
|
description: 'Analyze images with vision models',
|
||||||
longDescription:
|
authMode: AuthMode.ApiKey,
|
||||||
'Integrate Vision into the workflow. Can analyze images with vision models. Requires API Key.',
|
longDescription: 'Integrate Vision into the workflow. Can analyze images with vision models.',
|
||||||
docsLink: 'https://docs.sim.ai/tools/vision',
|
docsLink: 'https://docs.sim.ai/tools/vision',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#4D5FFF',
|
bgColor: '#4D5FFF',
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { WealthboxIcon } from '@/components/icons'
|
import { WealthboxIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { WealthboxResponse } from '@/tools/wealthbox/types'
|
import type { WealthboxResponse } from '@/tools/wealthbox/types'
|
||||||
|
|
||||||
export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
|
export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
|
||||||
type: 'wealthbox',
|
type: 'wealthbox',
|
||||||
name: 'Wealthbox',
|
name: 'Wealthbox',
|
||||||
description: 'Interact with Wealthbox',
|
description: 'Interact with Wealthbox',
|
||||||
|
authMode: AuthMode.OAuth,
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/wealthbox',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#E0E0E0',
|
bgColor: '#E0E0E0',
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
WhatsAppIcon,
|
WhatsAppIcon,
|
||||||
} from '@/components/icons'
|
} from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
|
|
||||||
const getWebhookProviderIcon = (provider: string) => {
|
const getWebhookProviderIcon = (provider: string) => {
|
||||||
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||||
@@ -36,9 +37,11 @@ export const WebhookBlock: BlockConfig = {
|
|||||||
type: 'webhook',
|
type: 'webhook',
|
||||||
name: 'Webhook',
|
name: 'Webhook',
|
||||||
description: 'Trigger workflow execution from external webhooks',
|
description: 'Trigger workflow execution from external webhooks',
|
||||||
|
authMode: AuthMode.OAuth,
|
||||||
category: 'triggers',
|
category: 'triggers',
|
||||||
icon: WebhookIcon,
|
icon: WebhookIcon,
|
||||||
bgColor: '#10B981', // Green color for triggers
|
bgColor: '#10B981', // Green color for triggers
|
||||||
|
triggerAllowed: true,
|
||||||
hideFromToolbar: true, // Hidden for backwards compatibility - use generic webhook trigger instead
|
hideFromToolbar: true, // Hidden for backwards compatibility - use generic webhook trigger instead
|
||||||
|
|
||||||
subBlocks: [
|
subBlocks: [
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import { WhatsAppIcon } from '@/components/icons'
|
import { WhatsAppIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { WhatsAppResponse } from '@/tools/whatsapp/types'
|
import type { WhatsAppResponse } from '@/tools/whatsapp/types'
|
||||||
|
|
||||||
export const WhatsAppBlock: BlockConfig<WhatsAppResponse> = {
|
export const WhatsAppBlock: BlockConfig<WhatsAppResponse> = {
|
||||||
type: 'whatsapp',
|
type: 'whatsapp',
|
||||||
name: 'WhatsApp',
|
name: 'WhatsApp',
|
||||||
description: 'Send WhatsApp messages',
|
description: 'Send WhatsApp messages',
|
||||||
|
authMode: AuthMode.ApiKey,
|
||||||
longDescription: 'Integrate WhatsApp into the workflow. Can send messages.',
|
longDescription: 'Integrate WhatsApp into the workflow. Can send messages.',
|
||||||
docsLink: 'https://docs.sim.ai/tools/whatsapp',
|
docsLink: 'https://docs.sim.ai/tools/whatsapp',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#25D366',
|
bgColor: '#25D366',
|
||||||
icon: WhatsAppIcon,
|
icon: WhatsAppIcon,
|
||||||
|
triggerAllowed: true,
|
||||||
subBlocks: [
|
subBlocks: [
|
||||||
{
|
{
|
||||||
id: 'phoneNumber',
|
id: 'phoneNumber',
|
||||||
|
|||||||
@@ -80,4 +80,5 @@ export const WorkflowBlock: BlockConfig = {
|
|||||||
result: { type: 'json', description: 'Workflow execution result' },
|
result: { type: 'json', description: 'Workflow execution result' },
|
||||||
error: { type: 'string', description: 'Error message' },
|
error: { type: 'string', description: 'Error message' },
|
||||||
},
|
},
|
||||||
|
hideFromToolbar: true,
|
||||||
}
|
}
|
||||||
|
|||||||
58
apps/sim/blocks/blocks/workflow_input.ts
Normal file
58
apps/sim/blocks/blocks/workflow_input.ts
Normal file
@@ -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 { xIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
|
import { AuthMode } from '@/blocks/types'
|
||||||
import type { XResponse } from '@/tools/x/types'
|
import type { XResponse } from '@/tools/x/types'
|
||||||
|
|
||||||
export const XBlock: BlockConfig<XResponse> = {
|
export const XBlock: BlockConfig<XResponse> = {
|
||||||
type: 'x',
|
type: 'x',
|
||||||
name: 'X',
|
name: 'X',
|
||||||
description: 'Interact with X',
|
description: 'Interact with X',
|
||||||
|
authMode: AuthMode.OAuth,
|
||||||
longDescription:
|
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',
|
docsLink: 'https://docs.sim.ai/tools/x',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#000000', // X's black color
|
bgColor: '#000000', // X's black color
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import { YouTubeIcon } from '@/components/icons'
|
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'
|
import type { YouTubeSearchResponse } from '@/tools/youtube/types'
|
||||||
|
|
||||||
|
const YouTubeBlockIcon: BlockIcon = (props) => YouTubeIcon(props as any)
|
||||||
|
|
||||||
export const YouTubeBlock: BlockConfig<YouTubeSearchResponse> = {
|
export const YouTubeBlock: BlockConfig<YouTubeSearchResponse> = {
|
||||||
type: 'youtube',
|
type: 'youtube',
|
||||||
name: 'YouTube',
|
name: 'YouTube',
|
||||||
description: 'Search for videos on 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',
|
docsLink: 'https://docs.sim.ai/tools/youtube',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#FF0000',
|
bgColor: '#FF0000',
|
||||||
icon: YouTubeIcon,
|
icon: YouTubeBlockIcon,
|
||||||
subBlocks: [
|
subBlocks: [
|
||||||
{
|
{
|
||||||
id: 'query',
|
id: 'query',
|
||||||
|
|||||||
@@ -6,8 +6,10 @@
|
|||||||
import { AgentBlock } from '@/blocks/blocks/agent'
|
import { AgentBlock } from '@/blocks/blocks/agent'
|
||||||
import { AirtableBlock } from '@/blocks/blocks/airtable'
|
import { AirtableBlock } from '@/blocks/blocks/airtable'
|
||||||
import { ApiBlock } from '@/blocks/blocks/api'
|
import { ApiBlock } from '@/blocks/blocks/api'
|
||||||
|
import { ApiTriggerBlock } from '@/blocks/blocks/api_trigger'
|
||||||
import { ArxivBlock } from '@/blocks/blocks/arxiv'
|
import { ArxivBlock } from '@/blocks/blocks/arxiv'
|
||||||
import { BrowserUseBlock } from '@/blocks/blocks/browser_use'
|
import { BrowserUseBlock } from '@/blocks/blocks/browser_use'
|
||||||
|
import { ChatTriggerBlock } from '@/blocks/blocks/chat_trigger'
|
||||||
import { ClayBlock } from '@/blocks/blocks/clay'
|
import { ClayBlock } from '@/blocks/blocks/clay'
|
||||||
import { ConditionBlock } from '@/blocks/blocks/condition'
|
import { ConditionBlock } from '@/blocks/blocks/condition'
|
||||||
import { ConfluenceBlock } from '@/blocks/blocks/confluence'
|
import { ConfluenceBlock } from '@/blocks/blocks/confluence'
|
||||||
@@ -30,6 +32,7 @@ import { GoogleSheetsBlock } from '@/blocks/blocks/google_sheets'
|
|||||||
import { HuggingFaceBlock } from '@/blocks/blocks/huggingface'
|
import { HuggingFaceBlock } from '@/blocks/blocks/huggingface'
|
||||||
import { HunterBlock } from '@/blocks/blocks/hunter'
|
import { HunterBlock } from '@/blocks/blocks/hunter'
|
||||||
import { ImageGeneratorBlock } from '@/blocks/blocks/image_generator'
|
import { ImageGeneratorBlock } from '@/blocks/blocks/image_generator'
|
||||||
|
import { InputTriggerBlock } from '@/blocks/blocks/input_trigger'
|
||||||
import { JinaBlock } from '@/blocks/blocks/jina'
|
import { JinaBlock } from '@/blocks/blocks/jina'
|
||||||
import { JiraBlock } from '@/blocks/blocks/jira'
|
import { JiraBlock } from '@/blocks/blocks/jira'
|
||||||
import { KnowledgeBlock } from '@/blocks/blocks/knowledge'
|
import { KnowledgeBlock } from '@/blocks/blocks/knowledge'
|
||||||
@@ -78,6 +81,7 @@ import { WebhookBlock } from '@/blocks/blocks/webhook'
|
|||||||
import { WhatsAppBlock } from '@/blocks/blocks/whatsapp'
|
import { WhatsAppBlock } from '@/blocks/blocks/whatsapp'
|
||||||
import { WikipediaBlock } from '@/blocks/blocks/wikipedia'
|
import { WikipediaBlock } from '@/blocks/blocks/wikipedia'
|
||||||
import { WorkflowBlock } from '@/blocks/blocks/workflow'
|
import { WorkflowBlock } from '@/blocks/blocks/workflow'
|
||||||
|
import { WorkflowInputBlock } from '@/blocks/blocks/workflow_input'
|
||||||
import { XBlock } from '@/blocks/blocks/x'
|
import { XBlock } from '@/blocks/blocks/x'
|
||||||
import { YouTubeBlock } from '@/blocks/blocks/youtube'
|
import { YouTubeBlock } from '@/blocks/blocks/youtube'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
@@ -147,6 +151,9 @@ export const registry: Record<string, BlockConfig> = {
|
|||||||
stagehand_agent: StagehandAgentBlock,
|
stagehand_agent: StagehandAgentBlock,
|
||||||
slack: SlackBlock,
|
slack: SlackBlock,
|
||||||
starter: StarterBlock,
|
starter: StarterBlock,
|
||||||
|
input_trigger: InputTriggerBlock,
|
||||||
|
chat_trigger: ChatTriggerBlock,
|
||||||
|
api_trigger: ApiTriggerBlock,
|
||||||
supabase: SupabaseBlock,
|
supabase: SupabaseBlock,
|
||||||
tavily: TavilyBlock,
|
tavily: TavilyBlock,
|
||||||
telegram: TelegramBlock,
|
telegram: TelegramBlock,
|
||||||
@@ -160,6 +167,7 @@ export const registry: Record<string, BlockConfig> = {
|
|||||||
whatsapp: WhatsAppBlock,
|
whatsapp: WhatsAppBlock,
|
||||||
wikipedia: WikipediaBlock,
|
wikipedia: WikipediaBlock,
|
||||||
workflow: WorkflowBlock,
|
workflow: WorkflowBlock,
|
||||||
|
workflow_input: WorkflowInputBlock,
|
||||||
x: XBlock,
|
x: XBlock,
|
||||||
youtube: YouTubeBlock,
|
youtube: YouTubeBlock,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ export type PrimitiveValueType = 'string' | 'number' | 'boolean' | 'json' | 'arr
|
|||||||
|
|
||||||
export type BlockCategory = 'blocks' | 'tools' | 'triggers'
|
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 =
|
export type GenerationType =
|
||||||
| 'javascript-function-body'
|
| 'javascript-function-body'
|
||||||
| 'typescript-function-body'
|
| 'typescript-function-body'
|
||||||
@@ -54,6 +61,7 @@ export type SubBlockType =
|
|||||||
| 'input-format' // Input structure format
|
| 'input-format' // Input structure format
|
||||||
| 'response-format' // Response structure format
|
| 'response-format' // Response structure format
|
||||||
| 'file-upload' // File uploader
|
| 'file-upload' // File uploader
|
||||||
|
| 'input-mapping' // Map parent variables to child workflow input schema
|
||||||
|
|
||||||
export type SubBlockLayout = 'full' | 'half'
|
export type SubBlockLayout = 'full' | 'half'
|
||||||
|
|
||||||
@@ -186,6 +194,8 @@ export interface BlockConfig<T extends ToolResponse = ToolResponse> {
|
|||||||
bgColor: string
|
bgColor: string
|
||||||
icon: BlockIcon
|
icon: BlockIcon
|
||||||
subBlocks: SubBlockConfig[]
|
subBlocks: SubBlockConfig[]
|
||||||
|
triggerAllowed?: boolean
|
||||||
|
authMode?: AuthMode
|
||||||
tools: {
|
tools: {
|
||||||
access: string[]
|
access: string[]
|
||||||
config?: {
|
config?: {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ChevronRight } from 'lucide-react'
|
|||||||
import { BlockPathCalculator } from '@/lib/block-path-calculator'
|
import { BlockPathCalculator } from '@/lib/block-path-calculator'
|
||||||
import { extractFieldsFromSchema, parseResponseFormatSafely } from '@/lib/response-format'
|
import { extractFieldsFromSchema, parseResponseFormatSafely } from '@/lib/response-format'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { getBlockOutputPaths, getBlockOutputType } from '@/lib/workflows/block-outputs'
|
||||||
import { getBlock } from '@/blocks'
|
import { getBlock } from '@/blocks'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
import { Serializer } from '@/serializer'
|
import { Serializer } from '@/serializer'
|
||||||
@@ -101,7 +102,8 @@ const getOutputTypeForPath = (
|
|||||||
block: BlockState,
|
block: BlockState,
|
||||||
blockConfig: BlockConfig | null,
|
blockConfig: BlockConfig | null,
|
||||||
blockId: string,
|
blockId: string,
|
||||||
outputPath: string
|
outputPath: string,
|
||||||
|
mergedSubBlocksOverride?: Record<string, any>
|
||||||
): string => {
|
): string => {
|
||||||
if (block?.triggerMode && blockConfig?.triggers?.enabled) {
|
if (block?.triggerMode && blockConfig?.triggers?.enabled) {
|
||||||
const triggerId = blockConfig?.triggers?.available?.[0]
|
const triggerId = blockConfig?.triggers?.available?.[0]
|
||||||
@@ -125,7 +127,8 @@ const getOutputTypeForPath = (
|
|||||||
}
|
}
|
||||||
} else if (block?.type === 'starter') {
|
} else if (block?.type === 'starter') {
|
||||||
// Handle starter block specific outputs
|
// Handle starter block specific outputs
|
||||||
const startWorkflowValue = getSubBlockValue(blockId, 'startWorkflow')
|
const startWorkflowValue =
|
||||||
|
mergedSubBlocksOverride?.startWorkflow?.value ?? getSubBlockValue(blockId, 'startWorkflow')
|
||||||
|
|
||||||
if (startWorkflowValue === 'chat') {
|
if (startWorkflowValue === 'chat') {
|
||||||
// Define types for chat mode outputs
|
// Define types for chat mode outputs
|
||||||
@@ -137,7 +140,8 @@ const getOutputTypeForPath = (
|
|||||||
return chatModeTypes[outputPath] || 'any'
|
return chatModeTypes[outputPath] || 'any'
|
||||||
}
|
}
|
||||||
// For API mode, check inputFormat for custom field types
|
// 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)) {
|
if (inputFormatValue && Array.isArray(inputFormatValue)) {
|
||||||
const field = inputFormatValue.find(
|
const field = inputFormatValue.find(
|
||||||
(f: { name?: string; type?: string }) => f.name === outputPath
|
(f: { name?: string; type?: string }) => f.name === outputPath
|
||||||
@@ -146,6 +150,11 @@ const getOutputTypeForPath = (
|
|||||||
return field.type
|
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 {
|
} else {
|
||||||
const operationValue = getSubBlockValue(blockId, 'operation')
|
const operationValue = getSubBlockValue(blockId, 'operation')
|
||||||
if (blockConfig && operationValue) {
|
if (blockConfig && operationValue) {
|
||||||
@@ -297,6 +306,24 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
|||||||
const edges = useWorkflowStore((state) => state.edges)
|
const edges = useWorkflowStore((state) => state.edges)
|
||||||
const workflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
|
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 getVariablesByWorkflowId = useVariablesStore((state) => state.getVariablesByWorkflowId)
|
||||||
const variables = useVariablesStore((state) => state.variables)
|
const variables = useVariablesStore((state) => state.variables)
|
||||||
const workflowVariables = workflowId ? getVariablesByWorkflowId(workflowId) : []
|
const workflowVariables = workflowId ? getVariablesByWorkflowId(workflowId) : []
|
||||||
@@ -355,7 +382,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
|||||||
const blockName = sourceBlock.name || sourceBlock.type
|
const blockName = sourceBlock.name || sourceBlock.type
|
||||||
const normalizedBlockName = normalizeBlockName(blockName)
|
const normalizedBlockName = normalizeBlockName(blockName)
|
||||||
|
|
||||||
const responseFormatValue = getSubBlockValue(activeSourceBlockId, 'responseFormat')
|
const mergedSubBlocks = getMergedSubBlocks(activeSourceBlockId)
|
||||||
|
const responseFormatValue = mergedSubBlocks?.responseFormat?.value
|
||||||
const responseFormat = parseResponseFormatSafely(responseFormatValue, activeSourceBlockId)
|
const responseFormat = parseResponseFormatSafely(responseFormatValue, activeSourceBlockId)
|
||||||
|
|
||||||
let blockTags: string[]
|
let blockTags: string[]
|
||||||
@@ -382,7 +410,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
|||||||
}
|
}
|
||||||
} else if (!blockConfig.outputs || Object.keys(blockConfig.outputs).length === 0) {
|
} else if (!blockConfig.outputs || Object.keys(blockConfig.outputs).length === 0) {
|
||||||
if (sourceBlock.type === 'starter') {
|
if (sourceBlock.type === 'starter') {
|
||||||
const startWorkflowValue = getSubBlockValue(activeSourceBlockId, 'startWorkflow')
|
const startWorkflowValue = mergedSubBlocks?.startWorkflow?.value
|
||||||
|
|
||||||
if (startWorkflowValue === 'chat') {
|
if (startWorkflowValue === 'chat') {
|
||||||
// For chat mode, provide input, conversationId, and files
|
// For chat mode, provide input, conversationId, and files
|
||||||
@@ -392,7 +420,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
|||||||
`${normalizedBlockName}.files`,
|
`${normalizedBlockName}.files`,
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
const inputFormatValue = getSubBlockValue(activeSourceBlockId, 'inputFormat')
|
const inputFormatValue = mergedSubBlocks?.inputFormat?.value
|
||||||
|
|
||||||
if (
|
if (
|
||||||
inputFormatValue &&
|
inputFormatValue &&
|
||||||
@@ -410,7 +438,17 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
|||||||
blockTags = [normalizedBlockName]
|
blockTags = [normalizedBlockName]
|
||||||
}
|
}
|
||||||
} else {
|
} 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 triggerId = blockConfig?.triggers?.available?.[0]
|
||||||
const firstTrigger = triggerId
|
const firstTrigger = triggerId
|
||||||
? getTrigger(triggerId)
|
? getTrigger(triggerId)
|
||||||
@@ -426,7 +464,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Check for tool-specific outputs first
|
// Check for tool-specific outputs first
|
||||||
const operationValue = getSubBlockValue(activeSourceBlockId, 'operation')
|
const operationValue =
|
||||||
|
mergedSubBlocks?.operation?.value ?? getSubBlockValue(activeSourceBlockId, 'operation')
|
||||||
const toolOutputPaths = operationValue
|
const toolOutputPaths = operationValue
|
||||||
? generateToolOutputPaths(blockConfig, operationValue)
|
? generateToolOutputPaths(blockConfig, operationValue)
|
||||||
: []
|
: []
|
||||||
@@ -625,12 +664,34 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
|||||||
const blockName = accessibleBlock.name || accessibleBlock.type
|
const blockName = accessibleBlock.name || accessibleBlock.type
|
||||||
const normalizedBlockName = normalizeBlockName(blockName)
|
const normalizedBlockName = normalizeBlockName(blockName)
|
||||||
|
|
||||||
const responseFormatValue = getSubBlockValue(accessibleBlockId, 'responseFormat')
|
const mergedSubBlocks = getMergedSubBlocks(accessibleBlockId)
|
||||||
|
const responseFormatValue = mergedSubBlocks?.responseFormat?.value
|
||||||
const responseFormat = parseResponseFormatSafely(responseFormatValue, accessibleBlockId)
|
const responseFormat = parseResponseFormatSafely(responseFormatValue, accessibleBlockId)
|
||||||
|
|
||||||
let blockTags: string[]
|
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')
|
const metricsValue = getSubBlockValue(accessibleBlockId, 'metrics')
|
||||||
|
|
||||||
if (metricsValue && Array.isArray(metricsValue) && metricsValue.length > 0) {
|
if (metricsValue && Array.isArray(metricsValue) && metricsValue.length > 0) {
|
||||||
@@ -651,34 +712,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
|||||||
blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
||||||
}
|
}
|
||||||
} else if (!blockConfig.outputs || Object.keys(blockConfig.outputs).length === 0) {
|
} else if (!blockConfig.outputs || Object.keys(blockConfig.outputs).length === 0) {
|
||||||
if (accessibleBlock.type === 'starter') {
|
blockTags = [normalizedBlockName]
|
||||||
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]
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const blockState = blocks[accessibleBlockId]
|
const blockState = blocks[accessibleBlockId]
|
||||||
if (blockState?.triggerMode && blockConfig.triggers?.enabled) {
|
if (blockState?.triggerMode && blockConfig.triggers?.enabled) {
|
||||||
@@ -697,7 +731,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Check for tool-specific outputs first
|
// Check for tool-specific outputs first
|
||||||
const operationValue = getSubBlockValue(accessibleBlockId, 'operation')
|
const operationValue =
|
||||||
|
mergedSubBlocks?.operation?.value ?? getSubBlockValue(accessibleBlockId, 'operation')
|
||||||
const toolOutputPaths = operationValue
|
const toolOutputPaths = operationValue
|
||||||
? generateToolOutputPaths(blockConfig, operationValue)
|
? generateToolOutputPaths(blockConfig, operationValue)
|
||||||
: []
|
: []
|
||||||
@@ -746,7 +781,17 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
|||||||
variableInfoMap,
|
variableInfoMap,
|
||||||
blockTagGroups: finalBlockTagGroups,
|
blockTagGroups: finalBlockTagGroups,
|
||||||
}
|
}
|
||||||
}, [blocks, edges, loops, parallels, blockId, activeSourceBlockId, workflowVariables])
|
}, [
|
||||||
|
blocks,
|
||||||
|
edges,
|
||||||
|
loops,
|
||||||
|
parallels,
|
||||||
|
blockId,
|
||||||
|
activeSourceBlockId,
|
||||||
|
workflowVariables,
|
||||||
|
workflowSubBlockValues,
|
||||||
|
getMergedSubBlocks,
|
||||||
|
])
|
||||||
|
|
||||||
const filteredTags = useMemo(() => {
|
const filteredTags = useMemo(() => {
|
||||||
if (!searchTerm) return tags
|
if (!searchTerm) return tags
|
||||||
@@ -1328,12 +1373,14 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
|||||||
)
|
)
|
||||||
if (block) {
|
if (block) {
|
||||||
const blockConfig = getBlock(block.type)
|
const blockConfig = getBlock(block.type)
|
||||||
|
const mergedSubBlocks = getMergedSubBlocks(group.blockId)
|
||||||
|
|
||||||
tagDescription = getOutputTypeForPath(
|
tagDescription = getOutputTypeForPath(
|
||||||
block,
|
block,
|
||||||
blockConfig || null,
|
blockConfig || null,
|
||||||
group.blockId,
|
group.blockId,
|
||||||
outputPath
|
outputPath,
|
||||||
|
mergedSubBlocks
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1468,12 +1515,14 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
|||||||
)
|
)
|
||||||
if (block) {
|
if (block) {
|
||||||
const blockConfig = getBlock(block.type)
|
const blockConfig = getBlock(block.type)
|
||||||
|
const mergedSubBlocks = getMergedSubBlocks(group.blockId)
|
||||||
|
|
||||||
childType = getOutputTypeForPath(
|
childType = getOutputTypeForPath(
|
||||||
block,
|
block,
|
||||||
blockConfig || null,
|
blockConfig || null,
|
||||||
group.blockId,
|
group.blockId,
|
||||||
childOutputPath
|
childOutputPath,
|
||||||
|
mergedSubBlocks
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,20 @@ const MAX_WORKFLOW_DEPTH = 10
|
|||||||
export class WorkflowBlockHandler implements BlockHandler {
|
export class WorkflowBlockHandler implements BlockHandler {
|
||||||
private serializer = new Serializer()
|
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 {
|
canHandle(block: SerializedBlock): boolean {
|
||||||
return block.metadata?.id === BlockType.WORKFLOW
|
const id = block.metadata?.id
|
||||||
|
return id === BlockType.WORKFLOW || id === 'workflow_input'
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(
|
async execute(
|
||||||
@@ -63,13 +75,22 @@ export class WorkflowBlockHandler implements BlockHandler {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Prepare the input for the child workflow
|
// Prepare the input for the child workflow
|
||||||
// The input from this block should be passed as start.input to the child workflow
|
// Prefer structured mapping if provided; otherwise fall back to legacy 'input' passthrough
|
||||||
let childWorkflowInput = {}
|
let childWorkflowInput: Record<string, any> = {}
|
||||||
|
|
||||||
if (inputs.input !== undefined) {
|
if (inputs.inputMapping !== undefined && inputs.inputMapping !== null) {
|
||||||
// If input is provided, use it directly
|
// 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
|
childWorkflowInput = inputs.input
|
||||||
logger.info(`Passing input to child workflow: ${JSON.stringify(childWorkflowInput)}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the workflowId from the input to avoid confusion
|
// Remove the workflowId from the input to avoid confusion
|
||||||
@@ -308,10 +329,11 @@ export class WorkflowBlockHandler implements BlockHandler {
|
|||||||
}
|
}
|
||||||
return failure as Record<string, any>
|
return failure as Record<string, any>
|
||||||
}
|
}
|
||||||
let result = childResult
|
|
||||||
if (childResult?.output) {
|
// childResult is an ExecutionResult with structure { success, output, metadata, logs }
|
||||||
result = childResult.output
|
// We want the actual output from the execution
|
||||||
}
|
const result = childResult.output || {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
childWorkflowName,
|
childWorkflowName,
|
||||||
|
|||||||
@@ -630,17 +630,31 @@ export class Executor {
|
|||||||
*/
|
*/
|
||||||
private validateWorkflow(startBlockId?: string): void {
|
private validateWorkflow(startBlockId?: string): void {
|
||||||
if (startBlockId) {
|
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)
|
const startBlock = this.actualWorkflow.blocks.find((block) => block.id === startBlockId)
|
||||||
if (!startBlock || !startBlock.enabled) {
|
if (!startBlock || !startBlock.enabled) {
|
||||||
throw new Error(`Start block ${startBlockId} not found or disabled`)
|
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 {
|
} else {
|
||||||
// Default validation for starter block
|
// Legacy workflows: require a valid starter block and basic connection checks
|
||||||
const starterBlock = this.actualWorkflow.blocks.find(
|
|
||||||
(block) => block.metadata?.id === BlockType.STARTER
|
|
||||||
)
|
|
||||||
if (!starterBlock || !starterBlock.enabled) {
|
if (!starterBlock || !starterBlock.enabled) {
|
||||||
throw new Error('Workflow must have an enabled starter block')
|
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')
|
throw new Error('Starter block cannot have incoming connections')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there are any trigger blocks on the canvas
|
const outgoingFromStarter = this.actualWorkflow.connections.filter(
|
||||||
const hasTriggerBlocks = this.actualWorkflow.blocks.some((block) => {
|
(conn) => conn.source === starterBlock.id
|
||||||
return block.metadata?.category === 'triggers' || block.config?.params?.triggerMode === true
|
)
|
||||||
})
|
if (outgoingFromStarter.length === 0) {
|
||||||
|
throw new Error('Starter block must have at least one outgoing connection')
|
||||||
// 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')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// General graph validations
|
||||||
const blockIds = new Set(this.actualWorkflow.blocks.map((block) => block.id))
|
const blockIds = new Set(this.actualWorkflow.blocks.map((block) => block.id))
|
||||||
for (const conn of this.actualWorkflow.connections) {
|
for (const conn of this.actualWorkflow.connections) {
|
||||||
if (!blockIds.has(conn.source)) {
|
if (!blockIds.has(conn.source)) {
|
||||||
@@ -762,20 +769,54 @@ export class Executor {
|
|||||||
// Determine which block to initialize as the starting point
|
// Determine which block to initialize as the starting point
|
||||||
let initBlock: SerializedBlock | undefined
|
let initBlock: SerializedBlock | undefined
|
||||||
if (startBlockId) {
|
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)
|
initBlock = this.actualWorkflow.blocks.find((block) => block.id === startBlockId)
|
||||||
} else {
|
} else {
|
||||||
// Default to starter block
|
// Default to starter block (legacy) or find any trigger block
|
||||||
initBlock = this.actualWorkflow.blocks.find(
|
initBlock = this.actualWorkflow.blocks.find(
|
||||||
(block) => block.metadata?.id === BlockType.STARTER
|
(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) {
|
if (initBlock) {
|
||||||
// Initialize the starting block with the workflow input
|
// Initialize the starting block with the workflow input
|
||||||
try {
|
try {
|
||||||
|
// Get inputFormat from either old location (config.params) or new location (metadata.subBlocks)
|
||||||
const blockParams = initBlock.config.params
|
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 input format is defined, structure the input according to the schema
|
||||||
if (inputFormat && Array.isArray(inputFormat) && inputFormat.length > 0) {
|
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
|
// Use the structured input if we processed fields, otherwise use raw input
|
||||||
const finalInput = hasProcessedFields ? structuredInput : rawInputData
|
const finalInput = hasProcessedFields ? structuredInput : rawInputData
|
||||||
|
|
||||||
// Initialize the starting block with structured input (flattened)
|
// Initialize the starting block with structured input
|
||||||
const blockOutput = {
|
let blockOutput: any
|
||||||
input: finalInput,
|
|
||||||
conversationId: this.workflowInput?.conversationId, // Add conversationId to root
|
// For API/Input triggers, normalize primitives and mirror objects under input
|
||||||
...finalInput, // Add input fields directly at top level
|
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)
|
// 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 ensures files are captured in trace spans and execution logs
|
||||||
this.createStartedBlockWithFilesLog(initBlock, blockOutput, context)
|
this.createStartedBlockWithFilesLog(initBlock, blockOutput, context)
|
||||||
} else {
|
} else {
|
||||||
// Handle structured input (like API calls or chat messages)
|
// Handle triggers without inputFormat
|
||||||
if (this.workflowInput && typeof this.workflowInput === 'object') {
|
let starterOutput: any
|
||||||
// Check if this is a chat workflow input (has both input and conversationId)
|
|
||||||
if (
|
// Handle different trigger types
|
||||||
Object.hasOwn(this.workflowInput, 'input') &&
|
if (initBlock.metadata?.id === 'chat_trigger') {
|
||||||
Object.hasOwn(this.workflowInput, 'conversationId')
|
// Chat trigger: extract input, conversationId, and files
|
||||||
) {
|
starterOutput = {
|
||||||
// Chat workflow: extract input, conversationId, and files to root level
|
input: this.workflowInput?.input || '',
|
||||||
const starterOutput: any = {
|
conversationId: this.workflowInput?.conversationId || '',
|
||||||
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 {
|
} else {
|
||||||
// API workflow: spread the raw data directly (no wrapping)
|
starterOutput = { input: rawCandidate }
|
||||||
const starterOutput = { ...this.workflowInput }
|
|
||||||
|
|
||||||
context.blockStates.set(initBlock.id, {
|
|
||||||
output: starterOutput,
|
|
||||||
executed: true,
|
|
||||||
executionTime: 0,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback for primitive input values
|
// Legacy starter block handling
|
||||||
const starterOutput = {
|
if (this.workflowInput && typeof this.workflowInput === 'object') {
|
||||||
input: this.workflowInput,
|
// 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, {
|
// Add files if present
|
||||||
output: starterOutput,
|
if (this.workflowInput.files && Array.isArray(this.workflowInput.files)) {
|
||||||
executed: true,
|
starterOutput.files = this.workflowInput.files
|
||||||
executionTime: 0,
|
}
|
||||||
})
|
} 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) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { BlockPathCalculator } from '@/lib/block-path-calculator'
|
import { BlockPathCalculator } from '@/lib/block-path-calculator'
|
||||||
import { createLogger } from '@/lib/logs/console/logger'
|
import { createLogger } from '@/lib/logs/console/logger'
|
||||||
import { VariableManager } from '@/lib/variables/variable-manager'
|
import { VariableManager } from '@/lib/variables/variable-manager'
|
||||||
|
import { TRIGGER_REFERENCE_ALIAS_MAP } from '@/lib/workflows/triggers'
|
||||||
import { getBlock } from '@/blocks/index'
|
import { getBlock } from '@/blocks/index'
|
||||||
import type { LoopManager } from '@/executor/loops/loops'
|
import type { LoopManager } from '@/executor/loops/loops'
|
||||||
import type { ExecutionContext } from '@/executor/types'
|
import type { ExecutionContext } from '@/executor/types'
|
||||||
@@ -520,15 +521,19 @@ export class InputResolver {
|
|||||||
// System references and regular block references are both processed
|
// System references and regular block references are both processed
|
||||||
// Accessibility validation happens later in validateBlockReference
|
// Accessibility validation happens later in validateBlockReference
|
||||||
|
|
||||||
// Special case for "start" references
|
// Special case for trigger block references (start, api, chat, manual)
|
||||||
if (blockRef.toLowerCase() === 'start') {
|
const blockRefLower = blockRef.toLowerCase()
|
||||||
// Find the starter block
|
const triggerType =
|
||||||
const starterBlock = this.workflow.blocks.find((block) => block.metadata?.id === 'starter')
|
TRIGGER_REFERENCE_ALIAS_MAP[blockRefLower as keyof typeof TRIGGER_REFERENCE_ALIAS_MAP]
|
||||||
if (starterBlock) {
|
if (triggerType) {
|
||||||
const blockState = context.blockStates.get(starterBlock.id)
|
const triggerBlock = this.workflow.blocks.find(
|
||||||
|
(block) => block.metadata?.id === triggerType
|
||||||
|
)
|
||||||
|
if (triggerBlock) {
|
||||||
|
const blockState = context.blockStates.get(triggerBlock.id)
|
||||||
if (blockState) {
|
if (blockState) {
|
||||||
// For starter block, start directly with the flattened output
|
// For trigger blocks, start directly with the flattened output
|
||||||
// This enables direct access to <start.input> and <start.conversationId>
|
// This enables direct access to <start.input>, <api.fieldName>, <chat.input> etc
|
||||||
let replacementValue: any = blockState.output
|
let replacementValue: any = blockState.output
|
||||||
|
|
||||||
for (const part of pathParts) {
|
for (const part of pathParts) {
|
||||||
@@ -537,7 +542,7 @@ export class InputResolver {
|
|||||||
`[resolveBlockReferences] Invalid path "${part}" - replacementValue is not an object:`,
|
`[resolveBlockReferences] Invalid path "${part}" - replacementValue is not an object:`,
|
||||||
replacementValue
|
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]"
|
// Handle array indexing syntax like "files[0]" or "items[1]"
|
||||||
@@ -550,14 +555,14 @@ export class InputResolver {
|
|||||||
const arrayValue = replacementValue[arrayName]
|
const arrayValue = replacementValue[arrayName]
|
||||||
if (!Array.isArray(arrayValue)) {
|
if (!Array.isArray(arrayValue)) {
|
||||||
throw new Error(
|
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
|
// Then access the array element
|
||||||
if (index < 0 || index >= arrayValue.length) {
|
if (index < 0 || index >= arrayValue.length) {
|
||||||
throw new Error(
|
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) {
|
if (replacementValue === undefined) {
|
||||||
logger.warn(
|
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
|
// Format the value based on block type and path
|
||||||
let formattedValue: string
|
let formattedValue: string
|
||||||
|
|
||||||
// Special handling for all blocks referencing starter input
|
// Special handling for all blocks referencing trigger input
|
||||||
if (blockRef.toLowerCase() === 'start' && pathParts.join('.').includes('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
|
const blockType = currentBlock.metadata?.id
|
||||||
|
|
||||||
// Format based on which block is consuming this value
|
// Format based on which block is consuming this value
|
||||||
|
|||||||
@@ -662,7 +662,8 @@ export function useCollaborativeWorkflow() {
|
|||||||
data?: Record<string, any>,
|
data?: Record<string, any>,
|
||||||
parentId?: string,
|
parentId?: string,
|
||||||
extent?: 'parent',
|
extent?: 'parent',
|
||||||
autoConnectEdge?: Edge
|
autoConnectEdge?: Edge,
|
||||||
|
triggerMode?: boolean
|
||||||
) => {
|
) => {
|
||||||
// Skip socket operations when in diff mode
|
// Skip socket operations when in diff mode
|
||||||
if (isShowingDiff) {
|
if (isShowingDiff) {
|
||||||
@@ -695,6 +696,7 @@ export function useCollaborativeWorkflow() {
|
|||||||
horizontalHandles: true,
|
horizontalHandles: true,
|
||||||
isWide: false,
|
isWide: false,
|
||||||
advancedMode: false,
|
advancedMode: false,
|
||||||
|
triggerMode: triggerMode || false,
|
||||||
height: 0,
|
height: 0,
|
||||||
parentId,
|
parentId,
|
||||||
extent,
|
extent,
|
||||||
@@ -704,7 +706,7 @@ export function useCollaborativeWorkflow() {
|
|||||||
// Skip if applying remote changes
|
// Skip if applying remote changes
|
||||||
if (isApplyingRemoteChange.current) {
|
if (isApplyingRemoteChange.current) {
|
||||||
workflowStore.addBlock(id, type, name, position, data, parentId, extent, {
|
workflowStore.addBlock(id, type, name, position, data, parentId, extent, {
|
||||||
triggerMode: false,
|
triggerMode: triggerMode || false,
|
||||||
})
|
})
|
||||||
if (autoConnectEdge) {
|
if (autoConnectEdge) {
|
||||||
workflowStore.addEdge(autoConnectEdge)
|
workflowStore.addEdge(autoConnectEdge)
|
||||||
@@ -729,7 +731,7 @@ export function useCollaborativeWorkflow() {
|
|||||||
|
|
||||||
// Apply locally first (immediate UI feedback)
|
// Apply locally first (immediate UI feedback)
|
||||||
workflowStore.addBlock(id, type, name, position, data, parentId, extent, {
|
workflowStore.addBlock(id, type, name, position, data, parentId, extent, {
|
||||||
triggerMode: false,
|
triggerMode: triggerMode || false,
|
||||||
})
|
})
|
||||||
if (autoConnectEdge) {
|
if (autoConnectEdge) {
|
||||||
workflowStore.addEdge(autoConnectEdge)
|
workflowStore.addEdge(autoConnectEdge)
|
||||||
@@ -774,7 +776,7 @@ export function useCollaborativeWorkflow() {
|
|||||||
horizontalHandles: true,
|
horizontalHandles: true,
|
||||||
isWide: false,
|
isWide: false,
|
||||||
advancedMode: false,
|
advancedMode: false,
|
||||||
triggerMode: false,
|
triggerMode: triggerMode || false,
|
||||||
height: 0, // Default height, will be set by the UI
|
height: 0, // Default height, will be set by the UI
|
||||||
parentId,
|
parentId,
|
||||||
extent,
|
extent,
|
||||||
@@ -801,7 +803,7 @@ export function useCollaborativeWorkflow() {
|
|||||||
|
|
||||||
// Apply locally
|
// Apply locally
|
||||||
workflowStore.addBlock(id, type, name, position, data, parentId, extent, {
|
workflowStore.addBlock(id, type, name, position, data, parentId, extent, {
|
||||||
triggerMode: false,
|
triggerMode: triggerMode || false,
|
||||||
})
|
})
|
||||||
if (autoConnectEdge) {
|
if (autoConnectEdge) {
|
||||||
workflowStore.addEdge(autoConnectEdge)
|
workflowStore.addEdge(autoConnectEdge)
|
||||||
|
|||||||
@@ -57,7 +57,16 @@ export interface SendMessageRequest {
|
|||||||
chatId?: string
|
chatId?: string
|
||||||
workflowId?: string
|
workflowId?: string
|
||||||
mode?: 'ask' | 'agent'
|
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
|
prefetch?: boolean
|
||||||
createNewChat?: boolean
|
createNewChat?: boolean
|
||||||
stream?: 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