mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-08 14:43:54 -05:00
v0.2.7: fix + feat (#615)
* feat(logging): add additional logs for proxy routes
* fix(blocks): workflow handler not working outside gui (#609)
* fix: key to call api internally for workflow block
* feat: use jwt for internal auth to avoid a static key
* chore: formatter
* fix(sidebar): added loop & parallel subblcoks to sidebar search
* merged improvement/connection into staging (#604)
* merged improvement/connection into staging
* fix: merge conflicts and improved block path calculation
* fix: removed migration
* fix: removed duplicate call
* fix: resolver and merge conflicts
* fix: knowledge base folder
* fix: settings modal
* fix: typeform block
* fix: parallel handler
* fix: stores index
* fix: tests
* fix: tag-dropdown
* improvement: start block input and tag dropdown
* fix block id resolution + missing bracket
* fix lint
* fix test
* works
* fix
* fix lint
* Revert "fix lint"
This reverts commit 433e2f9cfc.
---------
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-MacBook-Air.local>
* fix(autopan): migration missing (#614)
* add autopan migration
* fix lint
* fix linter
* fix tests
---------
Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@vikhyaths-air.lan>
---------
Co-authored-by: Waleed Latif <walif6@gmail.com>
Co-authored-by: Aditya Tripathi <aditya@climactic.co>
Co-authored-by: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-MacBook-Air.local>
Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@vikhyaths-air.lan>
This commit is contained in:
committed by
GitHub
parent
016cd6750c
commit
78b5ae7b3d
@@ -66,17 +66,17 @@ Define the data to pass to the child workflow:
|
||||
|
||||
- **Single Variable Input**: Select a variable or block output to pass to the child workflow
|
||||
- **Variable References**: Use `<variable.name>` to reference workflow variables
|
||||
- **Block References**: Use `<blockName.response.field>` to reference outputs from previous blocks
|
||||
- **Automatic Mapping**: The selected data is automatically available as `start.response.input` in the child workflow
|
||||
- **Block References**: Use `<blockName.field>` to reference outputs from previous blocks
|
||||
- **Automatic Mapping**: The selected data is automatically available as `start.input` in the child workflow
|
||||
- **Optional**: The input field is optional - child workflows can run without input data
|
||||
- **Type Preservation**: Variable types (strings, numbers, objects, etc.) are preserved when passed to the child workflow
|
||||
|
||||
### Examples of Input References
|
||||
|
||||
- `<variable.customerData>` - Pass a workflow variable
|
||||
- `<dataProcessor.response.result>` - Pass the result from a previous block
|
||||
- `<start.response.input>` - Pass the original workflow input
|
||||
- `<apiCall.response.data.user>` - Pass a specific field from an API response
|
||||
- `<dataProcessor.result>` - Pass the result from a previous block
|
||||
- `<start.input>` - Pass the original workflow input
|
||||
- `<apiCall.data.user>` - Pass a specific field from an API response
|
||||
|
||||
### Execution Context
|
||||
|
||||
@@ -109,7 +109,7 @@ To prevent infinite recursion and ensure system stability, the Workflow block in
|
||||
<strong>Workflow ID</strong>: The identifier of the workflow to execute
|
||||
</li>
|
||||
<li>
|
||||
<strong>Input Variable</strong>: Variable or block reference to pass to the child workflow (e.g., `<variable.name>` or `<block.response.field>`)
|
||||
<strong>Input Variable</strong>: Variable or block reference to pass to the child workflow (e.g., `<variable.name>` or `<block.field>`)
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
@@ -150,23 +150,23 @@ blocks:
|
||||
- type: workflow
|
||||
name: "Setup Customer Account"
|
||||
workflowId: "account-setup-workflow"
|
||||
input: "<Validate Customer Data.response.result>"
|
||||
input: "<Validate Customer Data.result>"
|
||||
|
||||
- type: workflow
|
||||
name: "Send Welcome Email"
|
||||
workflowId: "welcome-email-workflow"
|
||||
input: "<Setup Customer Account.response.result.accountDetails>"
|
||||
input: "<Setup Customer Account.result.accountDetails>"
|
||||
```
|
||||
|
||||
### Child Workflow: Customer Validation
|
||||
```yaml
|
||||
# Reusable customer validation workflow
|
||||
# Access the input data using: start.response.input
|
||||
# Access the input data using: start.input
|
||||
blocks:
|
||||
- type: function
|
||||
name: "Validate Email"
|
||||
code: |
|
||||
const customerData = start.response.input;
|
||||
const customerData = start.input;
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(customerData.email);
|
||||
|
||||
@@ -174,7 +174,7 @@ blocks:
|
||||
name: "Check Credit Score"
|
||||
url: "https://api.creditcheck.com/score"
|
||||
method: "POST"
|
||||
body: "<start.response.input>"
|
||||
body: "<start.input>"
|
||||
```
|
||||
|
||||
### Variable Reference Examples
|
||||
@@ -184,13 +184,13 @@ blocks:
|
||||
input: "<variable.customerInfo>"
|
||||
|
||||
# Using block outputs
|
||||
input: "<dataProcessor.response.cleanedData>"
|
||||
input: "<dataProcessor.cleanedData>"
|
||||
|
||||
# Using nested object properties
|
||||
input: "<apiCall.response.data.user.profile>"
|
||||
input: "<apiCall.data.user.profile>"
|
||||
|
||||
# Using array elements (if supported by the resolver)
|
||||
input: "<listProcessor.response.items[0]>"
|
||||
input: "<listProcessor.items[0]>"
|
||||
```
|
||||
|
||||
## Access Control and Permissions
|
||||
|
||||
@@ -93,7 +93,7 @@ export const sampleWorkflowState = {
|
||||
webhookPath: { id: 'webhookPath', type: 'short-input', value: '' },
|
||||
},
|
||||
outputs: {
|
||||
response: { type: { input: 'any' } },
|
||||
input: 'any',
|
||||
},
|
||||
enabled: true,
|
||||
horizontalHandles: true,
|
||||
@@ -111,7 +111,7 @@ export const sampleWorkflowState = {
|
||||
type: 'long-input',
|
||||
value: 'You are a helpful assistant',
|
||||
},
|
||||
context: { id: 'context', type: 'short-input', value: '<start.response.input>' },
|
||||
context: { id: 'context', type: 'short-input', value: '<start.input>' },
|
||||
model: { id: 'model', type: 'dropdown', value: 'gpt-4o' },
|
||||
apiKey: { id: 'apiKey', type: 'short-input', value: '{{OPENAI_API_KEY}}' },
|
||||
},
|
||||
@@ -138,6 +138,7 @@ export const sampleWorkflowState = {
|
||||
},
|
||||
],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
lastSaved: Date.now(),
|
||||
isDeployed: false,
|
||||
}
|
||||
|
||||
@@ -241,7 +241,7 @@ describe('Chat Subdomain API Route', () => {
|
||||
})
|
||||
|
||||
describe('POST endpoint', () => {
|
||||
it('should handle authentication requests without messages', async () => {
|
||||
it('should handle authentication requests without input', async () => {
|
||||
const req = createMockRequest('POST', { password: 'test-password' })
|
||||
const params = Promise.resolve({ subdomain: 'password-protected-chat' })
|
||||
|
||||
@@ -257,7 +257,7 @@ describe('Chat Subdomain API Route', () => {
|
||||
expect(mockSetChatAuthCookie).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return 400 for requests without message', async () => {
|
||||
it('should return 400 for requests without input', async () => {
|
||||
const req = createMockRequest('POST', {})
|
||||
const params = Promise.resolve({ subdomain: 'test-chat' })
|
||||
|
||||
@@ -269,7 +269,7 @@ describe('Chat Subdomain API Route', () => {
|
||||
|
||||
const data = await response.json()
|
||||
expect(data).toHaveProperty('error')
|
||||
expect(data).toHaveProperty('message', 'No message provided')
|
||||
expect(data).toHaveProperty('message', 'No input provided')
|
||||
})
|
||||
|
||||
it('should return 401 for unauthorized access', async () => {
|
||||
@@ -279,7 +279,7 @@ describe('Chat Subdomain API Route', () => {
|
||||
error: 'Authentication required',
|
||||
}))
|
||||
|
||||
const req = createMockRequest('POST', { message: 'Hello' })
|
||||
const req = createMockRequest('POST', { input: 'Hello' })
|
||||
const params = Promise.resolve({ subdomain: 'protected-chat' })
|
||||
|
||||
const { POST } = await import('./route')
|
||||
@@ -342,7 +342,7 @@ describe('Chat Subdomain API Route', () => {
|
||||
}
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', { message: 'Hello' })
|
||||
const req = createMockRequest('POST', { input: 'Hello' })
|
||||
const params = Promise.resolve({ subdomain: 'test-chat' })
|
||||
|
||||
const { POST } = await import('./route')
|
||||
@@ -357,7 +357,7 @@ describe('Chat Subdomain API Route', () => {
|
||||
})
|
||||
|
||||
it('should return streaming response for valid chat messages', async () => {
|
||||
const req = createMockRequest('POST', { message: 'Hello world', conversationId: 'conv-123' })
|
||||
const req = createMockRequest('POST', { input: 'Hello world', conversationId: 'conv-123' })
|
||||
const params = Promise.resolve({ subdomain: 'test-chat' })
|
||||
|
||||
const { POST } = await import('./route')
|
||||
@@ -374,7 +374,7 @@ describe('Chat Subdomain API Route', () => {
|
||||
})
|
||||
|
||||
it('should handle streaming response body correctly', async () => {
|
||||
const req = createMockRequest('POST', { message: 'Hello world' })
|
||||
const req = createMockRequest('POST', { input: 'Hello world' })
|
||||
const params = Promise.resolve({ subdomain: 'test-chat' })
|
||||
|
||||
const { POST } = await import('./route')
|
||||
@@ -404,7 +404,7 @@ describe('Chat Subdomain API Route', () => {
|
||||
throw new Error('Execution failed')
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', { message: 'Trigger error' })
|
||||
const req = createMockRequest('POST', { input: 'Trigger error' })
|
||||
const params = Promise.resolve({ subdomain: 'test-chat' })
|
||||
|
||||
const { POST } = await import('./route')
|
||||
@@ -444,7 +444,7 @@ describe('Chat Subdomain API Route', () => {
|
||||
|
||||
it('should pass conversationId to executeWorkflowForChat when provided', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
message: 'Hello world',
|
||||
input: 'Hello world',
|
||||
conversationId: 'test-conversation-123',
|
||||
})
|
||||
const params = Promise.resolve({ subdomain: 'test-chat' })
|
||||
@@ -461,7 +461,7 @@ describe('Chat Subdomain API Route', () => {
|
||||
})
|
||||
|
||||
it('should handle missing conversationId gracefully', async () => {
|
||||
const req = createMockRequest('POST', { message: 'Hello world' })
|
||||
const req = createMockRequest('POST', { input: 'Hello world' })
|
||||
const params = Promise.resolve({ subdomain: 'test-chat' })
|
||||
|
||||
const { POST } = await import('./route')
|
||||
|
||||
@@ -72,11 +72,11 @@ export async function POST(
|
||||
}
|
||||
|
||||
// Use the already parsed body
|
||||
const { message, password, email, conversationId } = parsedBody
|
||||
const { input, password, email, conversationId } = parsedBody
|
||||
|
||||
// If this is an authentication request (has password or email but no message),
|
||||
// If this is an authentication request (has password or email but no input),
|
||||
// set auth cookie and return success
|
||||
if ((password || email) && !message) {
|
||||
if ((password || email) && !input) {
|
||||
const response = addCorsHeaders(createSuccessResponse({ authenticated: true }), request)
|
||||
|
||||
// Set authentication cookie
|
||||
@@ -86,8 +86,8 @@ export async function POST(
|
||||
}
|
||||
|
||||
// For chat messages, create regular response
|
||||
if (!message) {
|
||||
return addCorsHeaders(createErrorResponse('No message provided', 400), request)
|
||||
if (!input) {
|
||||
return addCorsHeaders(createErrorResponse('No input provided', 400), request)
|
||||
}
|
||||
|
||||
// Get the workflow for this chat
|
||||
@@ -105,8 +105,8 @@ export async function POST(
|
||||
}
|
||||
|
||||
try {
|
||||
// Execute workflow with structured input (message + conversationId for context)
|
||||
const result = await executeWorkflowForChat(deployment.id, message, conversationId)
|
||||
// Execute workflow with structured input (input + conversationId for context)
|
||||
const result = await executeWorkflowForChat(deployment.id, input, conversationId)
|
||||
|
||||
// The result is always a ReadableStream that we can pipe to the client
|
||||
const streamResponse = new NextResponse(result, {
|
||||
|
||||
@@ -128,10 +128,10 @@ export async function validateChatAuth(
|
||||
return { authorized: false, error: 'Password is required' }
|
||||
}
|
||||
|
||||
const { password, message } = parsedBody
|
||||
const { password, input } = parsedBody
|
||||
|
||||
// If this is a chat message, not an auth attempt
|
||||
if (message && !password) {
|
||||
if (input && !password) {
|
||||
return { authorized: false, error: 'auth_required_password' }
|
||||
}
|
||||
|
||||
@@ -170,10 +170,10 @@ export async function validateChatAuth(
|
||||
return { authorized: false, error: 'Email is required' }
|
||||
}
|
||||
|
||||
const { email, message } = parsedBody
|
||||
const { email, input } = parsedBody
|
||||
|
||||
// If this is a chat message, not an auth attempt
|
||||
if (message && !email) {
|
||||
if (input && !email) {
|
||||
return { authorized: false, error: 'auth_required_email' }
|
||||
}
|
||||
|
||||
@@ -211,17 +211,17 @@ export async function validateChatAuth(
|
||||
/**
|
||||
* Executes a workflow for a chat request and returns the formatted output.
|
||||
*
|
||||
* When workflows reference <start.response.input>, they receive a structured JSON
|
||||
* containing both the message and conversationId for maintaining chat context.
|
||||
* When workflows reference <start.input>, they receive the input directly.
|
||||
* The conversationId is available at <start.conversationId> for maintaining chat context.
|
||||
*
|
||||
* @param chatId - Chat deployment identifier
|
||||
* @param message - User's chat message
|
||||
* @param input - User's chat input
|
||||
* @param conversationId - Optional ID for maintaining conversation context
|
||||
* @returns Workflow execution result formatted for the chat interface
|
||||
*/
|
||||
export async function executeWorkflowForChat(
|
||||
chatId: string,
|
||||
message: string,
|
||||
input: string,
|
||||
conversationId?: string
|
||||
): Promise<any> {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
@@ -445,7 +445,7 @@ export async function executeWorkflowForChat(
|
||||
workflow: serializedWorkflow,
|
||||
currentBlockStates: processedBlockStates,
|
||||
envVarValues: decryptedEnvVars,
|
||||
workflowInput: { input: message, conversationId },
|
||||
workflowInput: { input: input, conversationId },
|
||||
workflowVariables,
|
||||
contextExtensions: {
|
||||
stream: true,
|
||||
@@ -463,8 +463,8 @@ export async function executeWorkflowForChat(
|
||||
if (result && 'success' in result) {
|
||||
result.logs?.forEach((log: BlockLog) => {
|
||||
if (streamedContent.has(log.blockId)) {
|
||||
if (log.output?.response) {
|
||||
log.output.response.content = streamedContent.get(log.blockId)
|
||||
if (log.output) {
|
||||
log.output.content = streamedContent.get(log.blockId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -239,7 +239,7 @@ Example Scenario:
|
||||
User Prompt: "Fetch user data from an API. Use the User ID passed in as 'userId' and an API Key stored as the 'SERVICE_API_KEY' environment variable."
|
||||
|
||||
Generated Code:
|
||||
const userId = <block.response.content>; // Correct: Accessing input parameter without quotes
|
||||
const userId = <block.content>; // Correct: Accessing input parameter without quotes
|
||||
const apiKey = {{SERVICE_API_KEY}}; // Correct: Accessing environment variable without quotes
|
||||
const url = \`https://api.example.com/users/\${userId}\`;
|
||||
|
||||
@@ -273,7 +273,7 @@ Do not include import/require statements unless absolutely necessary and they ar
|
||||
Do not include markdown formatting or explanations.
|
||||
Output only the raw TypeScript code. Use modern TypeScript features where appropriate. Do not use semicolons.
|
||||
Example:
|
||||
const userId = <block.response.content> as string
|
||||
const userId = <block.content> as string
|
||||
const apiKey = {{SERVICE_API_KEY}}
|
||||
const response = await fetch(\`https://api.example.com/users/\${userId}\`, { headers: { Authorization: \`Bearer \${apiKey}\` } })
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -137,24 +137,22 @@ export async function POST(request: NextRequest) {
|
||||
const safeExecutionData = {
|
||||
success: executionData.success,
|
||||
output: {
|
||||
response: {
|
||||
// Sanitize content to remove non-ASCII characters that would cause ByteString errors
|
||||
content: executionData.output?.response?.content
|
||||
? String(executionData.output.response.content).replace(/[\u0080-\uFFFF]/g, '')
|
||||
: '',
|
||||
model: executionData.output?.response?.model,
|
||||
tokens: executionData.output?.response?.tokens || {
|
||||
prompt: 0,
|
||||
completion: 0,
|
||||
total: 0,
|
||||
},
|
||||
// Sanitize any potential Unicode characters in tool calls
|
||||
toolCalls: executionData.output?.response?.toolCalls
|
||||
? sanitizeToolCalls(executionData.output.response.toolCalls)
|
||||
: undefined,
|
||||
providerTiming: executionData.output?.response?.providerTiming,
|
||||
cost: executionData.output?.response?.cost,
|
||||
// Sanitize content to remove non-ASCII characters that would cause ByteString errors
|
||||
content: executionData.output?.content
|
||||
? String(executionData.output.content).replace(/[\u0080-\uFFFF]/g, '')
|
||||
: '',
|
||||
model: executionData.output?.model,
|
||||
tokens: executionData.output?.tokens || {
|
||||
prompt: 0,
|
||||
completion: 0,
|
||||
total: 0,
|
||||
},
|
||||
// Sanitize any potential Unicode characters in tool calls
|
||||
toolCalls: executionData.output?.toolCalls
|
||||
? sanitizeToolCalls(executionData.output.toolCalls)
|
||||
: undefined,
|
||||
providerTiming: executionData.output?.providerTiming,
|
||||
cost: executionData.output?.cost,
|
||||
},
|
||||
error: executionData.error,
|
||||
logs: [], // Strip logs from header to avoid encoding issues
|
||||
|
||||
@@ -46,11 +46,19 @@ const formatResponse = (responseData: any, status = 200) => {
|
||||
*/
|
||||
const createErrorResponse = (error: any, status = 500, additionalData = {}) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
const errorStack = error instanceof Error ? error.stack : undefined
|
||||
|
||||
logger.error('Creating error response', {
|
||||
errorMessage,
|
||||
status,
|
||||
stack: process.env.NODE_ENV === 'development' ? errorStack : undefined,
|
||||
})
|
||||
|
||||
return formatResponse(
|
||||
{
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
stack: process.env.NODE_ENV === 'development' ? errorStack : undefined,
|
||||
...additionalData,
|
||||
},
|
||||
status
|
||||
@@ -67,6 +75,7 @@ export async function GET(request: Request) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
if (!targetUrl) {
|
||||
logger.error(`[${requestId}] Missing 'url' parameter`)
|
||||
return createErrorResponse("Missing 'url' parameter", 400)
|
||||
}
|
||||
|
||||
@@ -126,6 +135,10 @@ export async function GET(request: Request) {
|
||||
: response.statusText || `HTTP error ${response.status}`
|
||||
: undefined
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error(`[${requestId}] External API error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
// Return the proxied response
|
||||
return formatResponse({
|
||||
success: response.ok,
|
||||
@@ -139,6 +152,7 @@ export async function GET(request: Request) {
|
||||
logger.error(`[${requestId}] Proxy GET request failed`, {
|
||||
url: targetUrl,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return createErrorResponse(error)
|
||||
@@ -151,22 +165,40 @@ export async function POST(request: Request) {
|
||||
const startTimeISO = startTime.toISOString()
|
||||
|
||||
try {
|
||||
const { toolId, params } = await request.json()
|
||||
// Parse request body
|
||||
let requestBody
|
||||
try {
|
||||
requestBody = await request.json()
|
||||
} catch (parseError) {
|
||||
logger.error(`[${requestId}] Failed to parse request body`, {
|
||||
error: parseError instanceof Error ? parseError.message : String(parseError),
|
||||
})
|
||||
throw new Error('Invalid JSON in request body')
|
||||
}
|
||||
|
||||
logger.debug(`[${requestId}] Proxy request for tool`, {
|
||||
toolId,
|
||||
hasParams: !!params && Object.keys(params).length > 0,
|
||||
})
|
||||
const { toolId, params } = requestBody
|
||||
|
||||
if (!toolId) {
|
||||
logger.error(`[${requestId}] Missing toolId in request`)
|
||||
throw new Error('Missing toolId in request')
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Processing tool: ${toolId}`)
|
||||
|
||||
// Get tool
|
||||
const tool = getTool(toolId)
|
||||
|
||||
if (!tool) {
|
||||
logger.error(`[${requestId}] Tool not found: ${toolId}`)
|
||||
throw new Error(`Tool not found: ${toolId}`)
|
||||
}
|
||||
|
||||
// Validate the tool and its parameters
|
||||
try {
|
||||
validateToolRequest(toolId, tool, params)
|
||||
} catch (error) {
|
||||
logger.warn(`[${requestId}] Tool validation failed`, {
|
||||
toolId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
} catch (validationError) {
|
||||
logger.warn(`[${requestId}] Tool validation failed for ${toolId}`, {
|
||||
error: validationError instanceof Error ? validationError.message : String(validationError),
|
||||
})
|
||||
|
||||
// Add timing information even to error responses
|
||||
@@ -174,23 +206,18 @@ export async function POST(request: Request) {
|
||||
const endTimeISO = endTime.toISOString()
|
||||
const duration = endTime.getTime() - startTime.getTime()
|
||||
|
||||
return createErrorResponse(error, 400, {
|
||||
return createErrorResponse(validationError, 400, {
|
||||
startTime: startTimeISO,
|
||||
endTime: endTimeISO,
|
||||
duration,
|
||||
})
|
||||
}
|
||||
if (!tool) {
|
||||
logger.error(`[${requestId}] Tool not found`, { toolId })
|
||||
throw new Error(`Tool not found: ${toolId}`)
|
||||
}
|
||||
|
||||
// Use executeTool with skipProxy=true to prevent recursive proxy calls, and skipPostProcess=true to prevent duplicate post-processing
|
||||
// Execute tool
|
||||
const result = await executeTool(toolId, params, true, true)
|
||||
|
||||
if (!result.success) {
|
||||
logger.warn(`[${requestId}] Tool execution failed`, {
|
||||
toolId,
|
||||
logger.warn(`[${requestId}] Tool execution failed for ${toolId}`, {
|
||||
error: result.error || 'Unknown error',
|
||||
})
|
||||
|
||||
@@ -217,9 +244,13 @@ export async function POST(request: Request) {
|
||||
}
|
||||
// Fallback
|
||||
throw new Error('Tool returned an error')
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
throw e
|
||||
} catch (transformError) {
|
||||
logger.error(`[${requestId}] Error transformation failed for ${toolId}`, {
|
||||
error:
|
||||
transformError instanceof Error ? transformError.message : String(transformError),
|
||||
})
|
||||
if (transformError instanceof Error) {
|
||||
throw transformError
|
||||
}
|
||||
throw new Error('Tool returned an error')
|
||||
}
|
||||
@@ -246,12 +277,7 @@ export async function POST(request: Request) {
|
||||
},
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Tool executed successfully`, {
|
||||
toolId,
|
||||
duration,
|
||||
startTime: startTimeISO,
|
||||
endTime: endTimeISO,
|
||||
})
|
||||
logger.info(`[${requestId}] Tool executed successfully: ${toolId} (${duration}ms)`)
|
||||
|
||||
// Return the response with CORS headers
|
||||
return formatResponse(responseWithTimingData)
|
||||
@@ -259,6 +285,7 @@ export async function POST(request: Request) {
|
||||
logger.error(`[${requestId}] Proxy request failed`, {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
name: error instanceof Error ? error.name : undefined,
|
||||
})
|
||||
|
||||
// Add timing information even to error responses
|
||||
|
||||
@@ -14,6 +14,7 @@ const SettingsSchema = z.object({
|
||||
debugMode: z.boolean().optional(),
|
||||
autoConnect: z.boolean().optional(),
|
||||
autoFillEnvVars: z.boolean().optional(),
|
||||
autoPan: z.boolean().optional(),
|
||||
telemetryEnabled: z.boolean().optional(),
|
||||
telemetryNotifiedUser: z.boolean().optional(),
|
||||
emailPreferences: z
|
||||
@@ -32,6 +33,7 @@ const defaultSettings = {
|
||||
debugMode: false,
|
||||
autoConnect: true,
|
||||
autoFillEnvVars: true,
|
||||
autoPan: true,
|
||||
telemetryEnabled: true,
|
||||
telemetryNotifiedUser: false,
|
||||
emailPreferences: {},
|
||||
@@ -65,6 +67,7 @@ export async function GET() {
|
||||
debugMode: userSettings.debugMode,
|
||||
autoConnect: userSettings.autoConnect,
|
||||
autoFillEnvVars: userSettings.autoFillEnvVars,
|
||||
autoPan: userSettings.autoPan,
|
||||
telemetryEnabled: userSettings.telemetryEnabled,
|
||||
telemetryNotifiedUser: userSettings.telemetryNotifiedUser,
|
||||
emailPreferences: userSettings.emailPreferences ?? {},
|
||||
|
||||
@@ -31,6 +31,27 @@ describe('Workflow Deployment API Route', () => {
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock serializer
|
||||
vi.doMock('@/serializer', () => ({
|
||||
serializeWorkflow: vi.fn().mockReturnValue({
|
||||
version: '1.0',
|
||||
blocks: [
|
||||
{
|
||||
id: 'block-1',
|
||||
metadata: { id: 'starter', name: 'Start' },
|
||||
position: { x: 100, y: 100 },
|
||||
config: { tool: 'starter', params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
connections: [],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/workflows/db-helpers', () => ({
|
||||
loadWorkflowFromNormalizedTables: vi.fn().mockResolvedValue({
|
||||
blocks: {
|
||||
@@ -75,6 +96,80 @@ describe('Workflow Deployment API Route', () => {
|
||||
})
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock the database schema module
|
||||
vi.doMock('@/db/schema', () => ({
|
||||
workflow: {},
|
||||
apiKey: {},
|
||||
workflowBlocks: {},
|
||||
workflowEdges: {},
|
||||
workflowSubflows: {},
|
||||
}))
|
||||
|
||||
// Mock drizzle-orm operators
|
||||
vi.doMock('drizzle-orm', () => ({
|
||||
eq: vi.fn((field, value) => ({ field, value, type: 'eq' })),
|
||||
and: vi.fn((...conditions) => ({ conditions, type: 'and' })),
|
||||
}))
|
||||
|
||||
// Mock the database module with proper chainable query builder
|
||||
let selectCallCount = 0
|
||||
vi.doMock('@/db', () => ({
|
||||
db: {
|
||||
select: vi.fn().mockImplementation(() => {
|
||||
selectCallCount++
|
||||
return {
|
||||
from: vi.fn().mockImplementation(() => ({
|
||||
where: vi.fn().mockImplementation(() => ({
|
||||
limit: vi.fn().mockImplementation(() => {
|
||||
// First call: workflow lookup (should return workflow)
|
||||
if (selectCallCount === 1) {
|
||||
return Promise.resolve([{ userId: 'user-id', id: 'workflow-id' }])
|
||||
}
|
||||
// Second call: blocks lookup
|
||||
if (selectCallCount === 2) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
id: 'block-1',
|
||||
type: 'starter',
|
||||
name: 'Start',
|
||||
positionX: '100',
|
||||
positionY: '100',
|
||||
enabled: true,
|
||||
subBlocks: {},
|
||||
data: {},
|
||||
},
|
||||
])
|
||||
}
|
||||
// Third call: edges lookup
|
||||
if (selectCallCount === 3) {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
// Fourth call: subflows lookup
|
||||
if (selectCallCount === 4) {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
// Fifth call: API key lookup (should return empty for new key test)
|
||||
if (selectCallCount === 5) {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
// Default: empty array
|
||||
return Promise.resolve([])
|
||||
}),
|
||||
})),
|
||||
})),
|
||||
}
|
||||
}),
|
||||
insert: vi.fn().mockImplementation(() => ({
|
||||
values: vi.fn().mockResolvedValue([{ id: 'mock-api-key-id' }]),
|
||||
})),
|
||||
update: vi.fn().mockImplementation(() => ({
|
||||
set: vi.fn().mockImplementation(() => ({
|
||||
where: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
})),
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -126,16 +221,7 @@ describe('Workflow Deployment API Route', () => {
|
||||
* This should generate a new API key
|
||||
*/
|
||||
it('should create new API key when deploying workflow for user with no API key', async () => {
|
||||
const mockInsert = vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockReturnValue(undefined),
|
||||
})
|
||||
|
||||
const mockUpdate = vi.fn().mockReturnValue({
|
||||
set: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockResolvedValue([{ id: 'workflow-id' }]),
|
||||
}),
|
||||
})
|
||||
|
||||
// Override the global mock for this specific test
|
||||
vi.doMock('@/db', () => ({
|
||||
db: {
|
||||
select: vi
|
||||
@@ -143,11 +229,7 @@ describe('Workflow Deployment API Route', () => {
|
||||
.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([
|
||||
{
|
||||
userId: 'user-id',
|
||||
},
|
||||
]),
|
||||
limit: vi.fn().mockResolvedValue([{ userId: 'user-id', id: 'workflow-id' }]),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
@@ -184,8 +266,14 @@ describe('Workflow Deployment API Route', () => {
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
insert: mockInsert,
|
||||
update: mockUpdate,
|
||||
insert: vi.fn().mockImplementation(() => ({
|
||||
values: vi.fn().mockResolvedValue([{ id: 'mock-api-key-id' }]),
|
||||
})),
|
||||
update: vi.fn().mockImplementation(() => ({
|
||||
set: vi.fn().mockImplementation(() => ({
|
||||
where: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
})),
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -204,9 +292,6 @@ describe('Workflow Deployment API Route', () => {
|
||||
expect(data).toHaveProperty('apiKey', 'sim_testkeygenerated12345')
|
||||
expect(data).toHaveProperty('isDeployed', true)
|
||||
expect(data).toHaveProperty('deployedAt')
|
||||
|
||||
expect(mockInsert).toHaveBeenCalled()
|
||||
expect(mockUpdate).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -214,14 +299,7 @@ describe('Workflow Deployment API Route', () => {
|
||||
* This should use the existing API key
|
||||
*/
|
||||
it('should use existing API key when deploying workflow', async () => {
|
||||
const mockInsert = vi.fn()
|
||||
|
||||
const mockUpdate = vi.fn().mockReturnValue({
|
||||
set: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockResolvedValue([{ id: 'workflow-id' }]),
|
||||
}),
|
||||
})
|
||||
|
||||
// Override the global mock for this specific test
|
||||
vi.doMock('@/db', () => ({
|
||||
db: {
|
||||
select: vi
|
||||
@@ -229,11 +307,7 @@ describe('Workflow Deployment API Route', () => {
|
||||
.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([
|
||||
{
|
||||
userId: 'user-id',
|
||||
},
|
||||
]),
|
||||
limit: vi.fn().mockResolvedValue([{ userId: 'user-id', id: 'workflow-id' }]),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
@@ -266,16 +340,18 @@ describe('Workflow Deployment API Route', () => {
|
||||
.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([
|
||||
{
|
||||
key: 'sim_existingtestapikey12345',
|
||||
},
|
||||
]), // Existing API key
|
||||
limit: vi.fn().mockResolvedValue([{ key: 'sim_existingtestapikey12345' }]), // Existing API key
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
insert: mockInsert,
|
||||
update: mockUpdate,
|
||||
insert: vi.fn().mockImplementation(() => ({
|
||||
values: vi.fn().mockResolvedValue([{ id: 'mock-api-key-id' }]),
|
||||
})),
|
||||
update: vi.fn().mockImplementation(() => ({
|
||||
set: vi.fn().mockImplementation(() => ({
|
||||
where: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
})),
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -293,9 +369,6 @@ describe('Workflow Deployment API Route', () => {
|
||||
|
||||
expect(data).toHaveProperty('apiKey', 'sim_existingtestapikey12345')
|
||||
expect(data).toHaveProperty('isDeployed', true)
|
||||
|
||||
expect(mockInsert).not.toHaveBeenCalled()
|
||||
expect(mockUpdate).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -139,7 +139,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
return createErrorResponse(validation.error.message, validation.error.status)
|
||||
}
|
||||
|
||||
// Get the workflow to find the user (removed deprecated state column)
|
||||
// Get the workflow to find the user
|
||||
const workflowData = await db
|
||||
.select({
|
||||
userId: workflow.userId,
|
||||
|
||||
@@ -246,10 +246,7 @@ describe('Workflow Execution API Route', () => {
|
||||
expect.anything(), // serializedWorkflow
|
||||
expect.anything(), // processedBlockStates
|
||||
expect.anything(), // decryptedEnvVars
|
||||
expect.objectContaining({
|
||||
// processedInput
|
||||
input: requestBody,
|
||||
}),
|
||||
requestBody, // processedInput (direct input, not wrapped)
|
||||
expect.anything() // workflowVariables
|
||||
)
|
||||
})
|
||||
@@ -285,10 +282,7 @@ describe('Workflow Execution API Route', () => {
|
||||
expect.anything(), // serializedWorkflow
|
||||
expect.anything(), // processedBlockStates
|
||||
expect.anything(), // decryptedEnvVars
|
||||
expect.objectContaining({
|
||||
// processedInput
|
||||
input: structuredInput,
|
||||
}),
|
||||
structuredInput, // processedInput (direct input, not wrapped)
|
||||
expect.anything() // workflowVariables
|
||||
)
|
||||
})
|
||||
|
||||
@@ -77,19 +77,12 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any) {
|
||||
input ? JSON.stringify(input, null, 2) : 'No input provided'
|
||||
)
|
||||
|
||||
// Validate and structure input for maximum compatibility
|
||||
let processedInput = input
|
||||
if (input && typeof input === 'object') {
|
||||
// Ensure input is properly structured for the starter block
|
||||
if (input.input === undefined) {
|
||||
// If input is not already nested, structure it properly
|
||||
processedInput = { input: input }
|
||||
logger.info(
|
||||
`[${requestId}] Restructured input for workflow:`,
|
||||
JSON.stringify(processedInput, null, 2)
|
||||
)
|
||||
}
|
||||
}
|
||||
// Use input directly for API workflows
|
||||
const processedInput = input
|
||||
logger.info(
|
||||
`[${requestId}] Using input directly for workflow:`,
|
||||
JSON.stringify(processedInput, null, 2)
|
||||
)
|
||||
|
||||
try {
|
||||
runningExecutions.add(executionKey)
|
||||
@@ -381,13 +374,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
logger.info(`[${requestId}] No request body provided`)
|
||||
}
|
||||
|
||||
// Don't double-nest the input if it's already structured
|
||||
// Pass the raw body directly as input for API workflows
|
||||
const hasContent = Object.keys(body).length > 0
|
||||
const input = hasContent ? { input: body } : {}
|
||||
const input = hasContent ? body : {}
|
||||
|
||||
logger.info(`[${requestId}] Input passed to workflow:`, JSON.stringify(input, null, 2))
|
||||
|
||||
// Execute workflow with the structured input
|
||||
// Execute workflow with the raw input
|
||||
const result = await executeWorkflow(validation.workflow, requestId, input)
|
||||
|
||||
// Check if the workflow execution contains a response block output
|
||||
|
||||
@@ -2,6 +2,7 @@ import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { verifyInternalToken } from '@/lib/auth/internal'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { getUserEntityPermissions, hasAdminPermission } from '@/lib/permissions/utils'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
|
||||
@@ -28,14 +29,29 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
const { id: workflowId } = await params
|
||||
|
||||
try {
|
||||
// Get the session
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized access attempt for workflow ${workflowId}`)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
// Check for internal JWT token for server-side calls
|
||||
const authHeader = request.headers.get('authorization')
|
||||
let isInternalCall = false
|
||||
|
||||
if (authHeader?.startsWith('Bearer ')) {
|
||||
const token = authHeader.split(' ')[1]
|
||||
isInternalCall = await verifyInternalToken(token)
|
||||
}
|
||||
|
||||
const userId = session.user.id
|
||||
let userId: string | null = null
|
||||
|
||||
if (isInternalCall) {
|
||||
// For internal calls, we'll skip user-specific access checks
|
||||
logger.info(`[${requestId}] Internal API call for workflow ${workflowId}`)
|
||||
} else {
|
||||
// Get the session for regular user calls
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized access attempt for workflow ${workflowId}`)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
userId = session.user.id
|
||||
}
|
||||
|
||||
// Fetch the workflow
|
||||
const workflowData = await db
|
||||
@@ -52,26 +68,31 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
// Check if user has access to this workflow
|
||||
let hasAccess = false
|
||||
|
||||
// Case 1: User owns the workflow
|
||||
if (workflowData.userId === userId) {
|
||||
if (isInternalCall) {
|
||||
// Internal calls have full access
|
||||
hasAccess = true
|
||||
}
|
||||
|
||||
// Case 2: Workflow belongs to a workspace the user has permissions for
|
||||
if (!hasAccess && workflowData.workspaceId) {
|
||||
const userPermission = await getUserEntityPermissions(
|
||||
userId,
|
||||
'workspace',
|
||||
workflowData.workspaceId
|
||||
)
|
||||
if (userPermission !== null) {
|
||||
} else {
|
||||
// Case 1: User owns the workflow
|
||||
if (workflowData.userId === userId) {
|
||||
hasAccess = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAccess) {
|
||||
logger.warn(`[${requestId}] User ${userId} denied access to workflow ${workflowId}`)
|
||||
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
||||
// Case 2: Workflow belongs to a workspace the user has permissions for
|
||||
if (!hasAccess && workflowData.workspaceId && userId) {
|
||||
const userPermission = await getUserEntityPermissions(
|
||||
userId,
|
||||
'workspace',
|
||||
workflowData.workspaceId
|
||||
)
|
||||
if (userPermission !== null) {
|
||||
hasAccess = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAccess) {
|
||||
logger.warn(`[${requestId}] User ${userId} denied access to workflow ${workflowId}`)
|
||||
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
||||
}
|
||||
}
|
||||
|
||||
// Try to load from normalized tables first
|
||||
|
||||
@@ -297,7 +297,7 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
|
||||
try {
|
||||
// Send structured payload to maintain chat context
|
||||
const payload = {
|
||||
message:
|
||||
input:
|
||||
typeof userMessage.content === 'string'
|
||||
? userMessage.content
|
||||
: JSON.stringify(userMessage.content),
|
||||
|
||||
@@ -140,12 +140,20 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
|
||||
result.logs?.filter((log) => !messageIdMap.has(log.blockId)) || []
|
||||
|
||||
if (nonStreamingLogs.length > 0) {
|
||||
const outputsToRender = selectedOutputs.filter((outputId) =>
|
||||
nonStreamingLogs.some((log) => log.blockId === outputId.split('.')[0])
|
||||
)
|
||||
const outputsToRender = selectedOutputs.filter((outputId) => {
|
||||
// Extract block ID correctly - handle both formats:
|
||||
// - "blockId" (direct block ID)
|
||||
// - "blockId_response.result" (block ID with path)
|
||||
const blockIdForOutput = outputId.includes('_')
|
||||
? outputId.split('_')[0]
|
||||
: outputId.split('.')[0]
|
||||
return nonStreamingLogs.some((log) => log.blockId === blockIdForOutput)
|
||||
})
|
||||
|
||||
for (const outputId of outputsToRender) {
|
||||
const blockIdForOutput = outputId.split('.')[0]
|
||||
const blockIdForOutput = outputId.includes('_')
|
||||
? outputId.split('_')[0]
|
||||
: outputId.split('.')[0]
|
||||
const path = outputId.substring(blockIdForOutput.length + 1)
|
||||
const log = nonStreamingLogs.find((l) => l.blockId === blockIdForOutput)
|
||||
|
||||
|
||||
@@ -53,13 +53,41 @@ export function OutputSelect({
|
||||
const addOutput = (path: string, outputObj: any, prefix = '') => {
|
||||
const fullPath = prefix ? `${prefix}.${path}` : path
|
||||
|
||||
if (typeof outputObj === 'object' && outputObj !== null) {
|
||||
// For objects, recursively add each property
|
||||
// If not an object or is null, treat as leaf node
|
||||
if (typeof outputObj !== 'object' || outputObj === null) {
|
||||
const output = {
|
||||
id: `${block.id}_${fullPath}`,
|
||||
label: `${blockName}.${fullPath}`,
|
||||
blockId: block.id,
|
||||
blockName: block.name || `Block ${block.id}`,
|
||||
blockType: block.type,
|
||||
path: fullPath,
|
||||
}
|
||||
outputs.push(output)
|
||||
return
|
||||
}
|
||||
|
||||
// If has 'type' property, treat as schema definition (leaf node)
|
||||
if ('type' in outputObj && typeof outputObj.type === 'string') {
|
||||
const output = {
|
||||
id: `${block.id}_${fullPath}`,
|
||||
label: `${blockName}.${fullPath}`,
|
||||
blockId: block.id,
|
||||
blockName: block.name || `Block ${block.id}`,
|
||||
blockType: block.type,
|
||||
path: fullPath,
|
||||
}
|
||||
outputs.push(output)
|
||||
return
|
||||
}
|
||||
|
||||
// For objects without type, recursively add each property
|
||||
if (!Array.isArray(outputObj)) {
|
||||
Object.entries(outputObj).forEach(([key, value]) => {
|
||||
addOutput(key, value, fullPath)
|
||||
})
|
||||
} else {
|
||||
// Add leaf node as output option
|
||||
// For arrays, treat as leaf node
|
||||
outputs.push({
|
||||
id: `${block.id}_${fullPath}`,
|
||||
label: `${blockName}.${fullPath}`,
|
||||
@@ -71,10 +99,10 @@ export function OutputSelect({
|
||||
}
|
||||
}
|
||||
|
||||
// Start with the response object
|
||||
if (block.outputs.response) {
|
||||
addOutput('response', block.outputs.response)
|
||||
}
|
||||
// Process all output properties directly (flattened structure)
|
||||
Object.entries(block.outputs).forEach(([key, value]) => {
|
||||
addOutput(key, value)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -145,11 +145,13 @@ export const Toolbar = React.memo(() => {
|
||||
{blocks.map((block) => (
|
||||
<ToolbarBlock key={block.type} config={block} disabled={!userPermissions.canEdit} />
|
||||
))}
|
||||
{activeTab === 'blocks' && !searchQuery && (
|
||||
<>
|
||||
<LoopToolbarItem disabled={!userPermissions.canEdit} />
|
||||
<ParallelToolbarItem disabled={!userPermissions.canEdit} />
|
||||
</>
|
||||
{((activeTab === 'blocks' && !searchQuery) ||
|
||||
(searchQuery && 'loop'.includes(searchQuery.toLowerCase()))) && (
|
||||
<LoopToolbarItem disabled={!userPermissions.canEdit} />
|
||||
)}
|
||||
{((activeTab === 'blocks' && !searchQuery) ||
|
||||
(searchQuery && 'parallel'.includes(searchQuery.toLowerCase()))) && (
|
||||
<ParallelToolbarItem disabled={!userPermissions.canEdit} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,10 +4,11 @@ import {
|
||||
type ConnectedBlock,
|
||||
useBlockConnections,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-connections'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { getBlock } from '@/blocks'
|
||||
|
||||
interface ConnectionBlocksProps {
|
||||
blockId: string
|
||||
horizontalHandles: boolean
|
||||
setIsConnecting: (isConnecting: boolean) => void
|
||||
isDisabled?: boolean
|
||||
}
|
||||
@@ -20,6 +21,7 @@ interface ResponseField {
|
||||
|
||||
export function ConnectionBlocks({
|
||||
blockId,
|
||||
horizontalHandles,
|
||||
setIsConnecting,
|
||||
isDisabled = false,
|
||||
}: ConnectionBlocksProps) {
|
||||
@@ -39,6 +41,10 @@ export function ConnectionBlocks({
|
||||
|
||||
e.stopPropagation() // Prevent parent drag handlers from firing
|
||||
setIsConnecting(true)
|
||||
|
||||
// If no specific field is provided, use all available output types
|
||||
const outputType = field ? field.name : connection.outputType
|
||||
|
||||
e.dataTransfer.setData(
|
||||
'application/json',
|
||||
JSON.stringify({
|
||||
@@ -46,9 +52,13 @@ export function ConnectionBlocks({
|
||||
connectionData: {
|
||||
id: connection.id,
|
||||
name: connection.name,
|
||||
outputType: field ? field.name : connection.outputType,
|
||||
outputType: outputType,
|
||||
sourceBlockId: connection.id,
|
||||
fieldType: field?.type,
|
||||
// Include all available output types for reference
|
||||
allOutputTypes: Array.isArray(connection.outputType)
|
||||
? connection.outputType
|
||||
: [connection.outputType],
|
||||
},
|
||||
})
|
||||
)
|
||||
@@ -59,147 +69,59 @@ export function ConnectionBlocks({
|
||||
setIsConnecting(false)
|
||||
}
|
||||
|
||||
// Helper function to extract fields from JSON Schema
|
||||
const extractFieldsFromSchema = (connection: ConnectedBlock): ResponseField[] => {
|
||||
// Handle legacy format with fields array
|
||||
if (connection.responseFormat?.fields) {
|
||||
return connection.responseFormat.fields
|
||||
}
|
||||
|
||||
// Handle new JSON Schema format
|
||||
const schema = connection.responseFormat?.schema || connection.responseFormat
|
||||
// Safely check if schema and properties exist
|
||||
if (
|
||||
!schema ||
|
||||
typeof schema !== 'object' ||
|
||||
!('properties' in schema) ||
|
||||
typeof schema.properties !== 'object'
|
||||
) {
|
||||
return []
|
||||
}
|
||||
return Object.entries(schema.properties).map(([name, prop]: [string, any]) => ({
|
||||
name,
|
||||
type: Array.isArray(prop) ? 'array' : prop.type || 'string',
|
||||
description: prop.description,
|
||||
}))
|
||||
}
|
||||
|
||||
// Extract fields from starter block input format
|
||||
const extractFieldsFromStarterInput = (connection: ConnectedBlock): ResponseField[] => {
|
||||
// Only process for starter blocks
|
||||
if (connection.type !== 'starter') return []
|
||||
|
||||
try {
|
||||
// Get input format from subblock store
|
||||
const inputFormat = useSubBlockStore.getState().getValue(connection.id, 'inputFormat')
|
||||
|
||||
// Make sure we have a valid input format
|
||||
if (!inputFormat || !Array.isArray(inputFormat) || inputFormat.length === 0) {
|
||||
return [{ name: 'input', type: 'any' }]
|
||||
}
|
||||
|
||||
// Check if any fields have been configured with names
|
||||
const hasConfiguredFields = inputFormat.some(
|
||||
(field: any) => field.name && field.name.trim() !== ''
|
||||
)
|
||||
|
||||
// If no fields have been configured, return the default input field
|
||||
if (!hasConfiguredFields) {
|
||||
return [{ name: 'input', type: 'any' }]
|
||||
}
|
||||
|
||||
// Map input fields to response fields
|
||||
return inputFormat.map((field: any) => ({
|
||||
name: `input.${field.name}`,
|
||||
type: field.type || 'string',
|
||||
description: field.description,
|
||||
}))
|
||||
} catch (e) {
|
||||
console.error('Error extracting fields from starter input format:', e)
|
||||
return [{ name: 'input', type: 'any' }]
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate connections by ID
|
||||
const connectionMap = incomingConnections.reduce(
|
||||
(acc, connection) => {
|
||||
acc[connection.id] = connection
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, ConnectedBlock>
|
||||
)
|
||||
|
||||
// Sort connections by name
|
||||
const sortedConnections = Object.values(connectionMap).sort((a, b) =>
|
||||
a.name.localeCompare(b.name)
|
||||
)
|
||||
// Use connections in distance order (already sorted and deduplicated by the hook)
|
||||
const sortedConnections = incomingConnections
|
||||
|
||||
// Helper function to render a connection card
|
||||
const renderConnectionCard = (connection: ConnectedBlock, field?: ResponseField) => {
|
||||
const displayName = connection.name.replace(/\s+/g, '').toLowerCase()
|
||||
const renderConnectionCard = (connection: ConnectedBlock) => {
|
||||
// Get block configuration for icon and color
|
||||
const blockConfig = getBlock(connection.type)
|
||||
const displayName = connection.name // Use the actual block name instead of transforming it
|
||||
const Icon = blockConfig?.icon
|
||||
const bgColor = blockConfig?.bgColor || '#6B7280' // Fallback to gray
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={`${field ? field.name : connection.id}`}
|
||||
key={`${connection.id}-${connection.name}`}
|
||||
draggable={!isDisabled}
|
||||
onDragStart={(e) => handleDragStart(e, connection, field)}
|
||||
onDragStart={(e) => handleDragStart(e, connection)}
|
||||
onDragEnd={handleDragEnd}
|
||||
className={cn(
|
||||
'group flex w-max items-center rounded-lg border bg-card p-2 shadow-sm transition-colors',
|
||||
'group flex w-max items-center gap-2 rounded-lg border bg-card p-2 shadow-sm transition-colors',
|
||||
!isDisabled
|
||||
? 'cursor-grab hover:bg-accent/50 active:cursor-grabbing'
|
||||
: 'cursor-not-allowed opacity-60'
|
||||
)}
|
||||
>
|
||||
{/* Block icon with color */}
|
||||
{Icon && (
|
||||
<div
|
||||
className='flex h-5 w-5 flex-shrink-0 items-center justify-center rounded'
|
||||
style={{ backgroundColor: bgColor }}
|
||||
>
|
||||
<Icon className='h-3 w-3 text-white' />
|
||||
</div>
|
||||
)}
|
||||
<div className='text-sm'>
|
||||
<span className='font-medium leading-none'>{displayName}</span>
|
||||
<span className='text-muted-foreground'>
|
||||
{field
|
||||
? `.${field.name}`
|
||||
: typeof connection.outputType === 'string'
|
||||
? `.${connection.outputType}`
|
||||
: ''}
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='absolute top-0 right-full flex max-h-[400px] flex-col items-end space-y-2 overflow-y-auto pr-5'>
|
||||
{sortedConnections.map((connection, index) => {
|
||||
// Special handling for starter blocks with input format
|
||||
if (connection.type === 'starter') {
|
||||
const starterFields = extractFieldsFromStarterInput(connection)
|
||||
// Generate all connection cards - one per block, not per output field
|
||||
const connectionCards: React.ReactNode[] = []
|
||||
|
||||
if (starterFields.length > 0) {
|
||||
return (
|
||||
<div key={connection.id} className='space-y-2'>
|
||||
{starterFields.map((field) => renderConnectionCard(connection, field))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
sortedConnections.forEach((connection) => {
|
||||
connectionCards.push(renderConnectionCard(connection))
|
||||
})
|
||||
|
||||
// Regular connection handling
|
||||
return (
|
||||
<div key={`${connection.id}-${index}`} className='space-y-2'>
|
||||
{Array.isArray(connection.outputType)
|
||||
? // Handle array of field names
|
||||
connection.outputType.map((fieldName) => {
|
||||
// Try to find field in response format
|
||||
const fields = extractFieldsFromSchema(connection)
|
||||
const field = fields.find((f) => f.name === fieldName) || {
|
||||
name: fieldName,
|
||||
type: 'string',
|
||||
}
|
||||
// Position and layout based on handle orientation - reverse of ports
|
||||
// When ports are horizontal: connection blocks on top, aligned to left, closest blocks on bottom row
|
||||
// When ports are vertical (default): connection blocks on left, stack vertically, aligned to right
|
||||
const containerClasses = horizontalHandles
|
||||
? 'absolute bottom-full left-0 flex max-w-[600px] flex-wrap-reverse gap-2 pb-3'
|
||||
: 'absolute top-0 right-full flex max-h-[400px] max-w-[200px] flex-col items-end gap-2 overflow-y-auto pr-3'
|
||||
|
||||
return renderConnectionCard(connection, field)
|
||||
})
|
||||
: renderConnectionCard(connection)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
return <div className={containerClasses}>{connectionCards}</div>
|
||||
}
|
||||
|
||||
@@ -451,6 +451,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
blockId={id}
|
||||
setIsConnecting={setIsConnecting}
|
||||
isDisabled={!userPermissions.canEdit}
|
||||
horizontalHandles={horizontalHandles}
|
||||
/>
|
||||
|
||||
{/* Input Handle - Don't show for starter blocks */}
|
||||
@@ -698,7 +699,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
{Object.entries(config.outputs).map(([key, value]) => (
|
||||
<div key={key} className='mb-1'>
|
||||
<span className='text-muted-foreground'>{key}</span>{' '}
|
||||
{typeof value.type === 'object' ? (
|
||||
{typeof value === 'object' ? (
|
||||
<div className='mt-1 pl-3'>
|
||||
{Object.entries(value.type).map(([typeKey, typeValue]) => (
|
||||
<div key={typeKey} className='flex items-start'>
|
||||
@@ -712,7 +713,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className='text-green-500'>{value.type as string}</span>
|
||||
<span className='text-green-500'>{value as string}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { shallow } from 'zustand/shallow'
|
||||
import { BlockPathCalculator } from '@/lib/block-path-calculator'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
@@ -53,63 +54,6 @@ function extractFieldsFromSchema(schema: any): Field[] {
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all blocks along paths leading to the target block
|
||||
* This is a reverse traversal from the target node to find all ancestors
|
||||
* along connected paths
|
||||
* @param edges - List of all edges in the graph
|
||||
* @param targetNodeId - ID of the target block we're finding connections for
|
||||
* @returns Array of unique ancestor node IDs
|
||||
*/
|
||||
function findAllPathNodes(edges: any[], targetNodeId: string): string[] {
|
||||
// We'll use a reverse topological sort approach by tracking "distance" from target
|
||||
const nodeDistances = new Map<string, number>()
|
||||
const visited = new Set<string>()
|
||||
const queue: [string, number][] = [[targetNodeId, 0]] // [nodeId, distance]
|
||||
const pathNodes = new Set<string>()
|
||||
|
||||
// Build a reverse adjacency list for faster traversal
|
||||
const reverseAdjList: Record<string, string[]> = {}
|
||||
for (const edge of edges) {
|
||||
if (!reverseAdjList[edge.target]) {
|
||||
reverseAdjList[edge.target] = []
|
||||
}
|
||||
reverseAdjList[edge.target].push(edge.source)
|
||||
}
|
||||
|
||||
// BFS to find all ancestors and their shortest distance from target
|
||||
while (queue.length > 0) {
|
||||
const [currentNodeId, distance] = queue.shift()!
|
||||
|
||||
if (visited.has(currentNodeId)) {
|
||||
// If we've seen this node before, update its distance if this path is shorter
|
||||
const currentDistance = nodeDistances.get(currentNodeId) || Number.POSITIVE_INFINITY
|
||||
if (distance < currentDistance) {
|
||||
nodeDistances.set(currentNodeId, distance)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
visited.add(currentNodeId)
|
||||
nodeDistances.set(currentNodeId, distance)
|
||||
|
||||
// Don't add the target node itself to the results
|
||||
if (currentNodeId !== targetNodeId) {
|
||||
pathNodes.add(currentNodeId)
|
||||
}
|
||||
|
||||
// Get all incoming edges from the reverse adjacency list
|
||||
const incomingNodeIds = reverseAdjList[currentNodeId] || []
|
||||
|
||||
// Add all source nodes to the queue with incremented distance
|
||||
for (const sourceId of incomingNodeIds) {
|
||||
queue.push([sourceId, distance + 1])
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(pathNodes)
|
||||
}
|
||||
|
||||
export function useBlockConnections(blockId: string) {
|
||||
const { edges, blocks } = useWorkflowStore(
|
||||
(state) => ({
|
||||
@@ -120,7 +64,7 @@ export function useBlockConnections(blockId: string) {
|
||||
)
|
||||
|
||||
// Find all blocks along paths leading to this block
|
||||
const allPathNodeIds = findAllPathNodes(edges, blockId)
|
||||
const allPathNodeIds = BlockPathCalculator.findAllPathNodes(edges, blockId)
|
||||
|
||||
// Map each path node to a ConnectedBlock structure
|
||||
const allPathConnections = allPathNodeIds
|
||||
|
||||
@@ -82,9 +82,9 @@ export function useWorkflowExecution() {
|
||||
}
|
||||
|
||||
// If this was a streaming response and we have the final content, update it
|
||||
if (streamContent && result.output?.response && typeof streamContent === 'string') {
|
||||
if (streamContent && result.output && typeof streamContent === 'string') {
|
||||
// Update the content with the final streaming content
|
||||
enrichedResult.output.response.content = streamContent
|
||||
enrichedResult.output.content = streamContent
|
||||
|
||||
// Also update any block logs to include the content where appropriate
|
||||
if (enrichedResult.logs) {
|
||||
@@ -97,10 +97,9 @@ export function useWorkflowExecution() {
|
||||
if (
|
||||
isStreamingBlock &&
|
||||
(log.blockType === 'agent' || log.blockType === 'router') &&
|
||||
log.output?.response
|
||||
) {
|
||||
log.output.response.content = streamContent
|
||||
}
|
||||
log.output
|
||||
)
|
||||
log.output.content = streamContent
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,7 +121,7 @@ export function useWorkflowExecution() {
|
||||
|
||||
return executionId
|
||||
} catch (error) {
|
||||
logger.error('Error persisting logs:', { error })
|
||||
logger.error('Error persisting logs:', error)
|
||||
return executionId
|
||||
}
|
||||
}
|
||||
@@ -215,8 +214,8 @@ export function useWorkflowExecution() {
|
||||
result.logs?.forEach((log: BlockLog) => {
|
||||
if (streamedContent.has(log.blockId)) {
|
||||
const content = streamedContent.get(log.blockId) || ''
|
||||
if (log.output?.response) {
|
||||
log.output.response.content = content
|
||||
if (log.output) {
|
||||
log.output.content = content
|
||||
}
|
||||
useConsoleStore.getState().updateConsole(log.blockId, content)
|
||||
}
|
||||
@@ -225,9 +224,9 @@ export function useWorkflowExecution() {
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify({ event: 'final', data: result })}\n\n`)
|
||||
)
|
||||
persistLogs(executionId, result).catch((err) => {
|
||||
logger.error('Error persisting logs:', { error: err })
|
||||
})
|
||||
persistLogs(executionId, result).catch((err) =>
|
||||
logger.error('Error persisting logs:', err)
|
||||
)
|
||||
}
|
||||
} catch (error: any) {
|
||||
controller.error(error)
|
||||
@@ -437,7 +436,7 @@ export function useWorkflowExecution() {
|
||||
|
||||
const errorResult: ExecutionResult = {
|
||||
success: false,
|
||||
output: { response: {} },
|
||||
output: {},
|
||||
error: errorMessage,
|
||||
logs: [],
|
||||
}
|
||||
@@ -560,7 +559,7 @@ export function useWorkflowExecution() {
|
||||
// Create error result
|
||||
const errorResult = {
|
||||
success: false,
|
||||
output: { response: {} },
|
||||
output: {},
|
||||
error: errorMessage,
|
||||
logs: debugContext.blockLogs,
|
||||
}
|
||||
@@ -647,7 +646,7 @@ export function useWorkflowExecution() {
|
||||
|
||||
let currentResult: ExecutionResult = {
|
||||
success: true,
|
||||
output: { response: {} },
|
||||
output: {},
|
||||
logs: debugContext.blockLogs,
|
||||
}
|
||||
|
||||
@@ -743,7 +742,7 @@ export function useWorkflowExecution() {
|
||||
// Create error result
|
||||
const errorResult = {
|
||||
success: false,
|
||||
output: { response: {} },
|
||||
output: {},
|
||||
error: errorMessage,
|
||||
logs: debugContext.blockLogs,
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ const TOOLTIPS = {
|
||||
debugMode: 'Enable visual debugging information during execution.',
|
||||
autoConnect: 'Automatically connect nodes.',
|
||||
autoFillEnvVars: 'Automatically fill API keys.',
|
||||
autoPan: 'Automatically pan to active blocks during workflow execution.',
|
||||
}
|
||||
|
||||
export function General() {
|
||||
@@ -30,11 +31,13 @@ export function General() {
|
||||
const isAutoConnectEnabled = useGeneralStore((state) => state.isAutoConnectEnabled)
|
||||
const isDebugModeEnabled = useGeneralStore((state) => state.isDebugModeEnabled)
|
||||
const isAutoFillEnvVarsEnabled = useGeneralStore((state) => state.isAutoFillEnvVarsEnabled)
|
||||
const isAutoPanEnabled = useGeneralStore((state) => state.isAutoPanEnabled)
|
||||
|
||||
const setTheme = useGeneralStore((state) => state.setTheme)
|
||||
const toggleAutoConnect = useGeneralStore((state) => state.toggleAutoConnect)
|
||||
const toggleDebugMode = useGeneralStore((state) => state.toggleDebugMode)
|
||||
const toggleAutoFillEnvVars = useGeneralStore((state) => state.toggleAutoFillEnvVars)
|
||||
const toggleAutoPan = useGeneralStore((state) => state.toggleAutoPan)
|
||||
const loadSettings = useGeneralStore((state) => state.loadSettings)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -66,6 +69,12 @@ export function General() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleAutoPanChange = (checked: boolean) => {
|
||||
if (checked !== isAutoPanEnabled) {
|
||||
toggleAutoPan()
|
||||
}
|
||||
}
|
||||
|
||||
const handleRetry = () => {
|
||||
setRetryCount((prev) => prev + 1)
|
||||
}
|
||||
@@ -200,6 +209,35 @@ export function General() {
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center justify-between py-1'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Label htmlFor='auto-pan' className='font-medium'>
|
||||
Auto-pan during execution
|
||||
</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='h-7 p-1 text-gray-500'
|
||||
aria-label='Learn more about auto-pan feature'
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Info className='h-5 w-5' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='top' className='max-w-[300px] p-3'>
|
||||
<p className='text-sm'>{TOOLTIPS.autoPan}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Switch
|
||||
id='auto-pan'
|
||||
checked={isAutoPanEnabled}
|
||||
onCheckedChange={handleAutoPanChange}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -332,25 +332,9 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
|
||||
tools: { type: 'json', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
content: 'string',
|
||||
model: 'string',
|
||||
tokens: 'any',
|
||||
toolCalls: 'any',
|
||||
},
|
||||
dependsOn: {
|
||||
subBlockId: 'responseFormat',
|
||||
condition: {
|
||||
whenEmpty: {
|
||||
content: 'string',
|
||||
model: 'string',
|
||||
tokens: 'any',
|
||||
toolCalls: 'any',
|
||||
},
|
||||
whenFilled: 'json',
|
||||
},
|
||||
},
|
||||
},
|
||||
content: 'string',
|
||||
model: 'string',
|
||||
tokens: 'any',
|
||||
toolCalls: 'any',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -179,12 +179,8 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
|
||||
},
|
||||
// Output structure depends on the operation, covered by AirtableResponse union type
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
records: 'json', // Optional: for list, create, updateMultiple
|
||||
record: 'json', // Optional: for get, update single
|
||||
metadata: 'json', // Required: present in all responses
|
||||
},
|
||||
},
|
||||
records: 'json', // Optional: for list, create, updateMultiple
|
||||
record: 'json', // Optional: for get, update single
|
||||
metadata: 'json', // Required: present in all responses
|
||||
},
|
||||
}
|
||||
|
||||
@@ -62,12 +62,8 @@ export const ApiBlock: BlockConfig<RequestResponse> = {
|
||||
params: { type: 'json', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
data: 'any',
|
||||
status: 'number',
|
||||
headers: 'json',
|
||||
},
|
||||
},
|
||||
data: 'any',
|
||||
status: 'number',
|
||||
headers: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -112,13 +112,9 @@ export const AutoblocksBlock: BlockConfig<AutoblocksResponse> = {
|
||||
environment: { type: 'string', required: true },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
promptId: 'string',
|
||||
version: 'string',
|
||||
renderedPrompt: 'string',
|
||||
templates: 'json',
|
||||
},
|
||||
},
|
||||
promptId: 'string',
|
||||
version: 'string',
|
||||
renderedPrompt: 'string',
|
||||
templates: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -76,13 +76,9 @@ export const BrowserUseBlock: BlockConfig<BrowserUseResponse> = {
|
||||
save_browser_data: { type: 'boolean', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
id: 'string',
|
||||
success: 'boolean',
|
||||
output: 'any',
|
||||
steps: 'json',
|
||||
},
|
||||
},
|
||||
id: 'string',
|
||||
success: 'boolean',
|
||||
output: 'any',
|
||||
steps: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -50,10 +50,6 @@ Plain Text: Best for populating a table in free-form style.
|
||||
data: { type: 'json', required: true },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
data: 'any',
|
||||
},
|
||||
},
|
||||
data: 'any',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -37,13 +37,9 @@ export const ConditionBlock: BlockConfig<ConditionBlockOutput> = {
|
||||
},
|
||||
inputs: {},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
content: 'string',
|
||||
conditionResult: 'boolean',
|
||||
selectedPath: 'json',
|
||||
selectedConditionId: 'string',
|
||||
},
|
||||
},
|
||||
content: 'string',
|
||||
conditionResult: 'boolean',
|
||||
selectedPath: 'json',
|
||||
selectedConditionId: 'string',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -109,14 +109,10 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
|
||||
content: { type: 'string', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
ts: 'string',
|
||||
pageId: 'string',
|
||||
content: 'string',
|
||||
title: 'string',
|
||||
success: 'boolean',
|
||||
},
|
||||
},
|
||||
ts: 'string',
|
||||
pageId: 'string',
|
||||
content: 'string',
|
||||
title: 'string',
|
||||
success: 'boolean',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -149,11 +149,7 @@ export const DiscordBlock: BlockConfig<DiscordResponse> = {
|
||||
userId: { type: 'string', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
message: 'string',
|
||||
data: 'any',
|
||||
},
|
||||
},
|
||||
message: 'string',
|
||||
data: 'any',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -39,11 +39,7 @@ export const ElevenLabsBlock: BlockConfig<ElevenLabsBlockResponse> = {
|
||||
},
|
||||
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
audioUrl: 'string',
|
||||
},
|
||||
},
|
||||
audioUrl: 'string',
|
||||
},
|
||||
|
||||
subBlocks: [
|
||||
|
||||
@@ -307,25 +307,9 @@ export const EvaluatorBlock: BlockConfig<EvaluatorResponse> = {
|
||||
content: { type: 'string' as ParamType, required: true },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
content: 'string',
|
||||
model: 'string',
|
||||
tokens: 'any',
|
||||
cost: 'any',
|
||||
},
|
||||
dependsOn: {
|
||||
subBlockId: 'metrics',
|
||||
condition: {
|
||||
whenEmpty: {
|
||||
content: 'string',
|
||||
model: 'string',
|
||||
tokens: 'any',
|
||||
cost: 'any',
|
||||
},
|
||||
whenFilled: 'json',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
content: 'string',
|
||||
model: 'string',
|
||||
tokens: 'any',
|
||||
cost: 'any',
|
||||
} as any,
|
||||
}
|
||||
|
||||
@@ -190,16 +190,12 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
|
||||
url: { type: 'string', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
// Search output
|
||||
results: 'json',
|
||||
// Find Similar Links output
|
||||
similarLinks: 'json',
|
||||
// Answer output
|
||||
answer: 'string',
|
||||
citations: 'json',
|
||||
},
|
||||
},
|
||||
// Search output
|
||||
results: 'json',
|
||||
// Find Similar Links output
|
||||
similarLinks: 'json',
|
||||
// Answer output
|
||||
answer: 'string',
|
||||
citations: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -130,11 +130,7 @@ export const FileBlock: BlockConfig<FileParserOutput> = {
|
||||
file: { type: 'json', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
files: 'json',
|
||||
combinedContent: 'string',
|
||||
},
|
||||
},
|
||||
files: 'json',
|
||||
combinedContent: 'string',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -90,16 +90,12 @@ export const FirecrawlBlock: BlockConfig<FirecrawlResponse> = {
|
||||
scrapeOptions: { type: 'json', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
// Scrape output
|
||||
markdown: 'string',
|
||||
html: 'any',
|
||||
metadata: 'json',
|
||||
// Search output
|
||||
data: 'json',
|
||||
warning: 'any',
|
||||
},
|
||||
},
|
||||
// Scrape output
|
||||
markdown: 'string',
|
||||
html: 'any',
|
||||
metadata: 'json',
|
||||
// Search output
|
||||
data: 'json',
|
||||
warning: 'any',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -27,11 +27,7 @@ export const FunctionBlock: BlockConfig<CodeExecutionOutput> = {
|
||||
timeout: { type: 'number', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
result: 'any',
|
||||
stdout: 'string',
|
||||
},
|
||||
},
|
||||
result: 'any',
|
||||
stdout: 'string',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -167,11 +167,7 @@ export const GitHubBlock: BlockConfig<GitHubResponse> = {
|
||||
branch: { type: 'string', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
content: 'string',
|
||||
metadata: 'json',
|
||||
},
|
||||
},
|
||||
content: 'string',
|
||||
metadata: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -179,11 +179,7 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
|
||||
maxResults: { type: 'number', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
content: 'string',
|
||||
metadata: 'json',
|
||||
},
|
||||
},
|
||||
content: 'string',
|
||||
metadata: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -87,11 +87,7 @@ export const GoogleSearchBlock: BlockConfig<GoogleSearchResponse> = {
|
||||
},
|
||||
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
items: 'json',
|
||||
searchInformation: 'json',
|
||||
} as any,
|
||||
},
|
||||
items: 'json',
|
||||
searchInformation: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -284,11 +284,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
sendUpdates: { type: 'string', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
content: 'string',
|
||||
metadata: 'json',
|
||||
},
|
||||
},
|
||||
content: 'string',
|
||||
metadata: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -181,12 +181,8 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
|
||||
content: { type: 'string', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
content: 'string',
|
||||
metadata: 'json',
|
||||
updatedContent: 'boolean',
|
||||
},
|
||||
},
|
||||
content: 'string',
|
||||
metadata: 'json',
|
||||
updatedContent: 'boolean',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -265,11 +265,7 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
|
||||
pageSize: { type: 'number', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
file: 'json',
|
||||
files: 'json',
|
||||
},
|
||||
},
|
||||
file: 'json',
|
||||
files: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -211,16 +211,12 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
|
||||
insertDataOption: { type: 'string', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
data: 'json',
|
||||
metadata: 'json',
|
||||
updatedRange: 'string',
|
||||
updatedRows: 'number',
|
||||
updatedColumns: 'number',
|
||||
updatedCells: 'number',
|
||||
tableRange: 'string',
|
||||
},
|
||||
},
|
||||
data: 'json',
|
||||
metadata: 'json',
|
||||
updatedRange: 'string',
|
||||
updatedRows: 'number',
|
||||
updatedColumns: 'number',
|
||||
updatedCells: 'number',
|
||||
tableRange: 'string',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -82,17 +82,13 @@ export const GuestyBlock: BlockConfig<GuestyReservationResponse | GuestyGuestRes
|
||||
phoneNumber: { type: 'string', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
id: 'string',
|
||||
guest: 'json',
|
||||
checkIn: 'string',
|
||||
checkOut: 'string',
|
||||
status: 'string',
|
||||
listing: 'json',
|
||||
money: 'json',
|
||||
guests: 'json',
|
||||
},
|
||||
},
|
||||
id: 'string',
|
||||
guest: 'json',
|
||||
checkIn: 'string',
|
||||
checkOut: 'string',
|
||||
status: 'string',
|
||||
listing: 'json',
|
||||
money: 'json',
|
||||
guests: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -114,12 +114,8 @@ export const HuggingFaceBlock: BlockConfig<HuggingFaceChatResponse> = {
|
||||
apiKey: { type: 'string', required: true },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
content: 'string',
|
||||
model: 'string',
|
||||
usage: 'json',
|
||||
},
|
||||
},
|
||||
content: 'string',
|
||||
model: 'string',
|
||||
usage: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -153,12 +153,8 @@ export const ImageGeneratorBlock: BlockConfig<DalleResponse> = {
|
||||
apiKey: { type: 'string', required: true },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
content: 'string',
|
||||
image: 'string',
|
||||
metadata: 'json',
|
||||
},
|
||||
},
|
||||
content: 'string',
|
||||
image: 'string',
|
||||
metadata: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@ export const JinaBlock: BlockConfig<ReadUrlResponse> = {
|
||||
apiKey: { type: 'string', required: true },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
content: 'string',
|
||||
},
|
||||
},
|
||||
content: 'string',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -187,17 +187,13 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
|
||||
issueType: { type: 'string', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
ts: 'string',
|
||||
issueKey: 'string',
|
||||
summary: 'string',
|
||||
description: 'string',
|
||||
created: 'string',
|
||||
updated: 'string',
|
||||
success: 'boolean',
|
||||
url: 'string',
|
||||
},
|
||||
},
|
||||
ts: 'string',
|
||||
issueKey: 'string',
|
||||
summary: 'string',
|
||||
description: 'string',
|
||||
created: 'string',
|
||||
updated: 'string',
|
||||
success: 'boolean',
|
||||
url: 'string',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -38,13 +38,9 @@ export const KnowledgeBlock: BlockConfig = {
|
||||
content: { type: 'string', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
results: 'json',
|
||||
query: 'string',
|
||||
totalResults: 'number',
|
||||
},
|
||||
},
|
||||
results: 'json',
|
||||
query: 'string',
|
||||
totalResults: 'number',
|
||||
},
|
||||
subBlocks: [
|
||||
{
|
||||
|
||||
@@ -99,11 +99,7 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
|
||||
description: { type: 'string', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
issues: 'json',
|
||||
issue: 'json',
|
||||
},
|
||||
},
|
||||
issues: 'json',
|
||||
issue: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -63,11 +63,7 @@ export const LinkupBlock: BlockConfig<LinkupSearchToolResponse> = {
|
||||
},
|
||||
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
answer: 'string',
|
||||
sources: 'json',
|
||||
},
|
||||
},
|
||||
answer: 'string',
|
||||
sources: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -290,12 +290,8 @@ export const Mem0Block: BlockConfig<Mem0Response> = {
|
||||
limit: { type: 'number', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
ids: 'any',
|
||||
memories: 'any',
|
||||
searchResults: 'any',
|
||||
},
|
||||
},
|
||||
ids: 'any',
|
||||
memories: 'any',
|
||||
searchResults: 'any',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -105,12 +105,8 @@ export const MemoryBlock: BlockConfig = {
|
||||
content: { type: 'string', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
memories: 'any',
|
||||
id: 'string',
|
||||
},
|
||||
},
|
||||
memories: 'any',
|
||||
id: 'string',
|
||||
},
|
||||
subBlocks: [
|
||||
{
|
||||
|
||||
@@ -199,17 +199,13 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
|
||||
valueInputOption: { type: 'string', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
data: 'json',
|
||||
metadata: 'json',
|
||||
updatedRange: 'string',
|
||||
updatedRows: 'number',
|
||||
updatedColumns: 'number',
|
||||
updatedCells: 'number',
|
||||
index: 'number',
|
||||
values: 'json',
|
||||
},
|
||||
},
|
||||
data: 'json',
|
||||
metadata: 'json',
|
||||
updatedRange: 'string',
|
||||
updatedRows: 'number',
|
||||
updatedColumns: 'number',
|
||||
updatedCells: 'number',
|
||||
index: 'number',
|
||||
values: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -169,12 +169,8 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
|
||||
content: { type: 'string', required: true },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
content: 'string',
|
||||
metadata: 'json',
|
||||
updatedContent: 'boolean',
|
||||
},
|
||||
},
|
||||
content: 'string',
|
||||
metadata: 'json',
|
||||
updatedContent: 'boolean',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -202,11 +202,7 @@ export const MistralParseBlock: BlockConfig<MistralParserOutput> = {
|
||||
// imageMinSize: { type: 'string', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
content: 'string',
|
||||
metadata: 'json',
|
||||
},
|
||||
},
|
||||
content: 'string',
|
||||
metadata: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -174,11 +174,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
properties: { type: 'string', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
content: 'string',
|
||||
metadata: 'any',
|
||||
},
|
||||
},
|
||||
content: 'string',
|
||||
metadata: 'any',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -49,12 +49,8 @@ export const OpenAIBlock: BlockConfig = {
|
||||
apiKey: { type: 'string', required: true },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
embeddings: 'json',
|
||||
model: 'string',
|
||||
usage: 'json',
|
||||
},
|
||||
},
|
||||
embeddings: 'json',
|
||||
model: 'string',
|
||||
usage: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -140,11 +140,7 @@ export const OutlookBlock: BlockConfig<
|
||||
maxResults: { type: 'number', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
message: 'string',
|
||||
results: 'json',
|
||||
},
|
||||
},
|
||||
message: 'string',
|
||||
results: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -106,12 +106,8 @@ export const PerplexityBlock: BlockConfig<PerplexityChatResponse> = {
|
||||
apiKey: { type: 'string', required: true },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
content: 'string',
|
||||
model: 'string',
|
||||
usage: 'json',
|
||||
},
|
||||
},
|
||||
content: 'string',
|
||||
model: 'string',
|
||||
usage: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -268,15 +268,11 @@ export const PineconeBlock: BlockConfig<PineconeResponse> = {
|
||||
},
|
||||
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
matches: 'any',
|
||||
upsertedCount: 'any',
|
||||
data: 'any',
|
||||
model: 'any',
|
||||
vector_type: 'any',
|
||||
usage: 'any',
|
||||
},
|
||||
},
|
||||
matches: 'any',
|
||||
upsertedCount: 'any',
|
||||
data: 'any',
|
||||
model: 'any',
|
||||
vector_type: 'any',
|
||||
usage: 'any',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -181,13 +181,9 @@ export const RedditBlock: BlockConfig<
|
||||
commentLimit: { type: 'number', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
subreddit: 'string',
|
||||
posts: 'json',
|
||||
post: 'json',
|
||||
comments: 'json',
|
||||
},
|
||||
},
|
||||
subreddit: 'string',
|
||||
posts: 'json',
|
||||
post: 'json',
|
||||
comments: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -92,12 +92,8 @@ export const ResponseBlock: BlockConfig<ResponseBlockOutput> = {
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
data: 'json',
|
||||
status: 'number',
|
||||
headers: 'json',
|
||||
},
|
||||
},
|
||||
data: 'json',
|
||||
status: 'number',
|
||||
headers: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -180,14 +180,10 @@ export const RouterBlock: BlockConfig<RouterResponse> = {
|
||||
apiKey: { type: 'string', required: true },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
content: 'string',
|
||||
model: 'string',
|
||||
tokens: 'any',
|
||||
cost: 'any',
|
||||
selectedPath: 'json',
|
||||
},
|
||||
},
|
||||
content: 'string',
|
||||
model: 'string',
|
||||
tokens: 'any',
|
||||
cost: 'any',
|
||||
selectedPath: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -96,11 +96,7 @@ export const S3Block: BlockConfig<S3Response> = {
|
||||
s3Uri: { type: 'string', required: true },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
url: 'string',
|
||||
metadata: 'json',
|
||||
},
|
||||
},
|
||||
url: 'string',
|
||||
metadata: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -69,10 +69,6 @@ export const SerperBlock: BlockConfig<SearchResponse> = {
|
||||
type: { type: 'string', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
searchResults: 'json',
|
||||
},
|
||||
},
|
||||
searchResults: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -138,11 +138,7 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
|
||||
text: { type: 'string', required: true },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
ts: 'string',
|
||||
channel: 'string',
|
||||
},
|
||||
},
|
||||
ts: 'string',
|
||||
channel: 'string',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -64,10 +64,6 @@ export const StagehandBlock: BlockConfig<StagehandExtractResponse> = {
|
||||
apiKey: { type: 'string', required: true },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
data: 'json',
|
||||
},
|
||||
},
|
||||
data: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -83,11 +83,7 @@ export const StagehandAgentBlock: BlockConfig<StagehandAgentResponse> = {
|
||||
outputSchema: { type: 'json', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
agentResult: 'json',
|
||||
structuredOutput: 'any',
|
||||
},
|
||||
},
|
||||
agentResult: 'json',
|
||||
structuredOutput: 'any',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import { StartIcon } from '@/components/icons'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
import type { BlockConfig } from '../types'
|
||||
|
||||
interface StarterBlockOutput extends ToolResponse {
|
||||
output: {
|
||||
input: any
|
||||
}
|
||||
}
|
||||
|
||||
export const StarterBlock: BlockConfig<StarterBlockOutput> = {
|
||||
export const StarterBlock: BlockConfig = {
|
||||
type: 'starter',
|
||||
name: 'Starter',
|
||||
description: 'Start workflow',
|
||||
@@ -189,11 +182,5 @@ export const StarterBlock: BlockConfig<StarterBlockOutput> = {
|
||||
inputs: {
|
||||
input: { type: 'json', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
input: 'any',
|
||||
},
|
||||
},
|
||||
},
|
||||
outputs: {},
|
||||
}
|
||||
|
||||
@@ -109,11 +109,7 @@ export const SupabaseBlock: BlockConfig<SupabaseResponse> = {
|
||||
data: { type: 'string', required: false, requiredForToolCall: true },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
message: 'string',
|
||||
results: 'json',
|
||||
},
|
||||
},
|
||||
message: 'string',
|
||||
results: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -98,15 +98,11 @@ export const TavilyBlock: BlockConfig<TavilyResponse> = {
|
||||
extract_depth: { type: 'string', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
results: 'json',
|
||||
answer: 'any',
|
||||
query: 'string',
|
||||
content: 'string',
|
||||
title: 'string',
|
||||
url: 'string',
|
||||
},
|
||||
},
|
||||
results: 'json',
|
||||
answer: 'any',
|
||||
query: 'string',
|
||||
content: 'string',
|
||||
title: 'string',
|
||||
url: 'string',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -55,11 +55,7 @@ export const TelegramBlock: BlockConfig<TelegramMessageResponse> = {
|
||||
text: { type: 'string', required: true },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
ok: 'boolean',
|
||||
result: 'json',
|
||||
},
|
||||
},
|
||||
ok: 'boolean',
|
||||
result: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -36,11 +36,7 @@ export const ThinkingBlock: BlockConfig<ThinkingToolResponse> = {
|
||||
},
|
||||
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
acknowledgedThought: 'string',
|
||||
},
|
||||
},
|
||||
acknowledgedThought: 'string',
|
||||
},
|
||||
|
||||
tools: {
|
||||
|
||||
@@ -93,12 +93,8 @@ export const TranslateBlock: BlockConfig = {
|
||||
systemPrompt: { type: 'string', required: true },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
content: 'string',
|
||||
model: 'string',
|
||||
tokens: 'any',
|
||||
},
|
||||
},
|
||||
content: 'string',
|
||||
model: 'string',
|
||||
tokens: 'any',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -62,13 +62,9 @@ export const TwilioSMSBlock: BlockConfig<TwilioSMSBlockOutput> = {
|
||||
fromNumber: { type: 'string', required: true },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
success: 'boolean',
|
||||
messageId: 'any',
|
||||
status: 'any',
|
||||
error: 'any',
|
||||
},
|
||||
},
|
||||
success: 'boolean',
|
||||
messageId: 'any',
|
||||
status: 'any',
|
||||
error: 'any',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -215,23 +215,8 @@ export const TypeformBlock: BlockConfig<TypeformResponse> = {
|
||||
inline: { type: 'boolean', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
total_items: 'number',
|
||||
page_count: 'number',
|
||||
items: 'json',
|
||||
},
|
||||
dependsOn: {
|
||||
subBlockId: 'operation',
|
||||
condition: {
|
||||
whenEmpty: {
|
||||
total_items: 'number',
|
||||
page_count: 'number',
|
||||
items: 'json',
|
||||
},
|
||||
whenFilled: 'json',
|
||||
},
|
||||
},
|
||||
},
|
||||
total_items: 'number',
|
||||
page_count: 'number',
|
||||
items: 'json',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -53,12 +53,8 @@ export const VisionBlock: BlockConfig<VisionResponse> = {
|
||||
prompt: { type: 'string', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
content: 'string',
|
||||
model: 'any',
|
||||
tokens: 'any',
|
||||
},
|
||||
},
|
||||
content: 'string',
|
||||
model: 'any',
|
||||
tokens: 'any',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -64,12 +64,8 @@ export const WhatsAppBlock: BlockConfig<WhatsAppBlockOutput> = {
|
||||
accessToken: { type: 'string', required: true },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
success: 'boolean',
|
||||
messageId: 'any',
|
||||
error: 'any',
|
||||
},
|
||||
},
|
||||
success: 'boolean',
|
||||
messageId: 'any',
|
||||
error: 'any',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export const WorkflowBlock: BlockConfig = {
|
||||
title: 'Input Variable (Optional)',
|
||||
type: 'short-input',
|
||||
placeholder: 'Select a variable to pass to the child workflow',
|
||||
description: 'This variable will be available as start.response.input in the child workflow',
|
||||
description: 'This variable will be available as start.input in the child workflow',
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
@@ -74,13 +74,9 @@ export const WorkflowBlock: BlockConfig = {
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
success: 'boolean',
|
||||
childWorkflowName: 'string',
|
||||
result: 'json',
|
||||
error: 'string',
|
||||
},
|
||||
},
|
||||
success: 'boolean',
|
||||
childWorkflowName: 'string',
|
||||
result: 'json',
|
||||
error: 'string',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -211,17 +211,13 @@ export const XBlock: BlockConfig<XResponse> = {
|
||||
includeRecentTweets: { type: 'boolean', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
tweet: 'json',
|
||||
replies: 'any',
|
||||
context: 'any',
|
||||
tweets: 'json',
|
||||
includes: 'any',
|
||||
meta: 'json',
|
||||
user: 'json',
|
||||
recentTweets: 'any',
|
||||
},
|
||||
},
|
||||
tweet: 'json',
|
||||
replies: 'any',
|
||||
context: 'any',
|
||||
tweets: 'json',
|
||||
includes: 'any',
|
||||
meta: 'json',
|
||||
user: 'json',
|
||||
recentTweets: 'any',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -46,11 +46,7 @@ export const YouTubeBlock: BlockConfig<YouTubeSearchResponse> = {
|
||||
maxResults: { type: 'number', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
items: 'json',
|
||||
totalResults: 'number',
|
||||
},
|
||||
},
|
||||
items: 'json',
|
||||
totalResults: 'number',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -157,20 +157,10 @@ export interface BlockConfig<T extends ToolResponse = ToolResponse> {
|
||||
}
|
||||
}
|
||||
inputs: Record<string, ParamConfig>
|
||||
outputs: {
|
||||
response: {
|
||||
type: ToolOutputToValueType<ExtractToolOutput<T>>
|
||||
dependsOn?: {
|
||||
subBlockId: string
|
||||
condition: {
|
||||
whenEmpty: ToolOutputToValueType<ExtractToolOutput<T>>
|
||||
whenFilled: 'json'
|
||||
}
|
||||
}
|
||||
visualization?: {
|
||||
type: 'image'
|
||||
url: string
|
||||
}
|
||||
outputs: ToolOutputToValueType<ExtractToolOutput<T>> & {
|
||||
visualization?: {
|
||||
type: 'image'
|
||||
url: string
|
||||
}
|
||||
}
|
||||
hideFromToolbar?: boolean
|
||||
@@ -179,11 +169,4 @@ export interface BlockConfig<T extends ToolResponse = ToolResponse> {
|
||||
// Output configuration rules
|
||||
export interface OutputConfig {
|
||||
type: BlockOutput
|
||||
dependsOn?: {
|
||||
subBlockId: string
|
||||
condition: {
|
||||
whenEmpty: BlockOutput
|
||||
whenFilled: BlockOutput
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,13 @@
|
||||
import type { BlockOutput, OutputConfig } from '@/blocks/types'
|
||||
import type { SubBlockState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
interface CodeLine {
|
||||
id: string
|
||||
content: string
|
||||
}
|
||||
|
||||
function isEmptyValue(value: SubBlockState['value']): boolean {
|
||||
if (value === null || value === undefined) return true
|
||||
if (typeof value === 'string') return value.trim() === ''
|
||||
if (typeof value === 'number') return false
|
||||
if (Array.isArray(value)) {
|
||||
// Handle code editor's array of lines format
|
||||
if (value.length === 0) return true
|
||||
if (isCodeEditorValue(value)) {
|
||||
return value.every((line: any) => !line.content.trim())
|
||||
}
|
||||
return value.length === 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function isCodeEditorValue(value: any[]): value is CodeLine[] {
|
||||
return value.length > 0 && 'id' in value[0] && 'content' in value[0]
|
||||
}
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
|
||||
export function resolveOutputType(
|
||||
outputs: Record<string, OutputConfig>,
|
||||
subBlocks: Record<string, SubBlockState>
|
||||
outputs: Record<string, string | BlockOutput>
|
||||
): Record<string, BlockOutput> {
|
||||
const resolvedOutputs: Record<string, BlockOutput> = {}
|
||||
|
||||
for (const [key, outputConfig] of Object.entries(outputs)) {
|
||||
// If no dependencies, use the type directly
|
||||
if (!outputConfig.dependsOn) {
|
||||
resolvedOutputs[key] = outputConfig.type
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle dependent output types
|
||||
const subBlock = subBlocks[outputConfig.dependsOn.subBlockId]
|
||||
resolvedOutputs[key] = isEmptyValue(subBlock?.value)
|
||||
? outputConfig.dependsOn.condition.whenEmpty
|
||||
: outputConfig.dependsOn.condition.whenFilled
|
||||
for (const [key, outputType] of Object.entries(outputs)) {
|
||||
// Since dependsOn has been removed, just use the type directly
|
||||
resolvedOutputs[key] = outputType as BlockOutput
|
||||
}
|
||||
|
||||
return resolvedOutputs
|
||||
|
||||
@@ -274,7 +274,7 @@ describe('TagDropdown Search and Filtering', () => {
|
||||
'loop.index',
|
||||
'loop.currentItem',
|
||||
'parallel.index',
|
||||
'block.response.data',
|
||||
'block.data',
|
||||
]
|
||||
|
||||
const searchTerm = 'user'
|
||||
@@ -288,7 +288,7 @@ describe('TagDropdown Search and Filtering', () => {
|
||||
'variable.userName',
|
||||
'loop.index',
|
||||
'parallel.currentItem',
|
||||
'block.response.data',
|
||||
'block.data',
|
||||
'variable.userAge',
|
||||
'loop.currentItem',
|
||||
]
|
||||
@@ -313,7 +313,7 @@ describe('TagDropdown Search and Filtering', () => {
|
||||
expect(variableTags).toEqual(['variable.userName', 'variable.userAge'])
|
||||
expect(loopTags).toEqual(['loop.index', 'loop.currentItem'])
|
||||
expect(parallelTags).toEqual(['parallel.currentItem'])
|
||||
expect(blockTags).toEqual(['block.response.data'])
|
||||
expect(blockTags).toEqual(['block.data'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -358,22 +358,6 @@ describe('checkTagTrigger helper function', () => {
|
||||
})
|
||||
|
||||
describe('extractFieldsFromSchema helper function logic', () => {
|
||||
test('should extract fields from legacy format with fields array', () => {
|
||||
const responseFormat = {
|
||||
fields: [
|
||||
{ name: 'name', type: 'string', description: 'User name' },
|
||||
{ name: 'age', type: 'number', description: 'User age' },
|
||||
],
|
||||
}
|
||||
|
||||
const fields = extractFieldsFromSchema(responseFormat)
|
||||
|
||||
expect(fields).toEqual([
|
||||
{ name: 'name', type: 'string', description: 'User name' },
|
||||
{ name: 'age', type: 'number', description: 'User age' },
|
||||
])
|
||||
})
|
||||
|
||||
test('should extract fields from JSON Schema format', () => {
|
||||
const responseFormat = {
|
||||
schema: {
|
||||
@@ -450,6 +434,26 @@ describe('extractFieldsFromSchema helper function logic', () => {
|
||||
{ name: 'age', type: 'number', description: undefined },
|
||||
])
|
||||
})
|
||||
|
||||
test('should handle flattened response format (new format)', () => {
|
||||
const responseFormat = {
|
||||
schema: {
|
||||
properties: {
|
||||
name: { type: 'string', description: 'User name' },
|
||||
age: { type: 'number', description: 'User age' },
|
||||
status: { type: 'boolean', description: 'Active status' },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const fields = extractFieldsFromSchema(responseFormat)
|
||||
|
||||
expect(fields).toEqual([
|
||||
{ name: 'name', type: 'string', description: 'User name' },
|
||||
{ name: 'age', type: 'number', description: 'User age' },
|
||||
{ name: 'status', type: 'boolean', description: 'Active status' },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('TagDropdown Tag Ordering', () => {
|
||||
@@ -457,7 +461,7 @@ describe('TagDropdown Tag Ordering', () => {
|
||||
const variableTags = ['variable.userName', 'variable.userAge']
|
||||
const loopTags = ['loop.index', 'loop.currentItem']
|
||||
const parallelTags = ['parallel.index']
|
||||
const blockTags = ['block.response.data']
|
||||
const blockTags = ['block.data']
|
||||
|
||||
const orderedTags = [...variableTags, ...loopTags, ...parallelTags, ...blockTags]
|
||||
|
||||
@@ -467,12 +471,12 @@ describe('TagDropdown Tag Ordering', () => {
|
||||
'loop.index',
|
||||
'loop.currentItem',
|
||||
'parallel.index',
|
||||
'block.response.data',
|
||||
'block.data',
|
||||
])
|
||||
})
|
||||
|
||||
test('should create tag index map correctly', () => {
|
||||
const orderedTags = ['variable.userName', 'loop.index', 'block.response.data']
|
||||
const orderedTags = ['variable.userName', 'loop.index', 'block.data']
|
||||
|
||||
const tagIndexMap = new Map<string, number>()
|
||||
orderedTags.forEach((tag, index) => {
|
||||
@@ -481,7 +485,7 @@ describe('TagDropdown Tag Ordering', () => {
|
||||
|
||||
expect(tagIndexMap.get('variable.userName')).toBe(0)
|
||||
expect(tagIndexMap.get('loop.index')).toBe(1)
|
||||
expect(tagIndexMap.get('block.response.data')).toBe(2)
|
||||
expect(tagIndexMap.get('block.data')).toBe(2)
|
||||
expect(tagIndexMap.get('nonexistent')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -491,39 +495,39 @@ describe('TagDropdown Tag Selection Logic', () => {
|
||||
const testCases = [
|
||||
{
|
||||
description: 'should remove existing closing bracket from incomplete tag',
|
||||
inputValue: 'Hello <start.response.>',
|
||||
cursorPosition: 21, // cursor after the dot
|
||||
tag: 'start.response.input',
|
||||
expectedResult: 'Hello <start.response.input>',
|
||||
inputValue: 'Hello <start.>',
|
||||
cursorPosition: 13, // cursor after the dot
|
||||
tag: 'start.input',
|
||||
expectedResult: 'Hello <start.input>',
|
||||
},
|
||||
{
|
||||
description: 'should remove existing closing bracket when replacing tag content',
|
||||
inputValue: 'Hello <start.response.input>',
|
||||
cursorPosition: 22, // cursor after 'response.'
|
||||
tag: 'start.response.data',
|
||||
expectedResult: 'Hello <start.response.data>',
|
||||
inputValue: 'Hello <start.input>',
|
||||
cursorPosition: 12, // cursor after 'start.'
|
||||
tag: 'start.data',
|
||||
expectedResult: 'Hello <start.data>',
|
||||
},
|
||||
{
|
||||
description: 'should preserve content after closing bracket',
|
||||
inputValue: 'Hello <start.response.> world',
|
||||
cursorPosition: 21,
|
||||
tag: 'start.response.input',
|
||||
expectedResult: 'Hello <start.response.input> world',
|
||||
inputValue: 'Hello <start.> world',
|
||||
cursorPosition: 13,
|
||||
tag: 'start.input',
|
||||
expectedResult: 'Hello <start.input> world',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'should not affect closing bracket if text between contains invalid characters',
|
||||
inputValue: 'Hello <start.response.input> and <other>',
|
||||
cursorPosition: 22,
|
||||
tag: 'start.response.data',
|
||||
expectedResult: 'Hello <start.response.data> and <other>',
|
||||
inputValue: 'Hello <start.input> and <other>',
|
||||
cursorPosition: 12,
|
||||
tag: 'start.data',
|
||||
expectedResult: 'Hello <start.data> and <other>',
|
||||
},
|
||||
{
|
||||
description: 'should handle case with no existing closing bracket',
|
||||
inputValue: 'Hello <start.response',
|
||||
cursorPosition: 21,
|
||||
tag: 'start.response.input',
|
||||
expectedResult: 'Hello <start.response.input>',
|
||||
inputValue: 'Hello <start',
|
||||
cursorPosition: 12,
|
||||
tag: 'start.input',
|
||||
expectedResult: 'Hello <start.input>',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -556,25 +560,25 @@ describe('TagDropdown Tag Selection Logic', () => {
|
||||
// Valid tag-like text
|
||||
expect(regex.test('')).toBe(true) // empty string
|
||||
expect(regex.test('input')).toBe(true)
|
||||
expect(regex.test('response.data')).toBe(true)
|
||||
expect(regex.test('content.data')).toBe(true)
|
||||
expect(regex.test('user_name')).toBe(true)
|
||||
expect(regex.test('item123')).toBe(true)
|
||||
expect(regex.test('response.data.item_1')).toBe(true)
|
||||
expect(regex.test('content.data.item_1')).toBe(true)
|
||||
|
||||
// Invalid tag-like text (should not remove closing bracket)
|
||||
expect(regex.test('input> and more')).toBe(false)
|
||||
expect(regex.test('response data')).toBe(false) // space
|
||||
expect(regex.test('content data')).toBe(false) // space
|
||||
expect(regex.test('user-name')).toBe(false) // hyphen
|
||||
expect(regex.test('data[')).toBe(false) // bracket
|
||||
expect(regex.test('response.data!')).toBe(false) // exclamation
|
||||
expect(regex.test('content.data!')).toBe(false) // exclamation
|
||||
})
|
||||
|
||||
test('should find correct position of last open bracket', () => {
|
||||
const testCases = [
|
||||
{ input: 'Hello <start.response', expected: 6 },
|
||||
{ input: 'Hello <var> and <start.response', expected: 16 },
|
||||
{ input: 'Hello <start', expected: 6 },
|
||||
{ input: 'Hello <var> and <start', expected: 16 },
|
||||
{ input: 'No brackets here', expected: -1 },
|
||||
{ input: '<start.response', expected: 0 },
|
||||
{ input: '<start', expected: 0 },
|
||||
{ input: 'Multiple < < < <last', expected: 15 },
|
||||
]
|
||||
|
||||
@@ -587,7 +591,7 @@ describe('TagDropdown Tag Selection Logic', () => {
|
||||
test('should find correct position of next closing bracket', () => {
|
||||
const testCases = [
|
||||
{ input: 'input>', expected: 5 },
|
||||
{ input: 'response.data> more text', expected: 13 },
|
||||
{ input: 'content.data> more text', expected: 12 },
|
||||
{ input: 'no closing bracket', expected: -1 },
|
||||
{ input: '>', expected: 0 },
|
||||
{ input: 'multiple > > > >last', expected: 9 },
|
||||
|
||||
@@ -1,33 +1,67 @@
|
||||
import type React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { BlockPathCalculator } from '@/lib/block-path-calculator'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
type ConnectedBlock,
|
||||
useBlockConnections,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-connections'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { Serializer } from '@/serializer'
|
||||
import { useVariablesStore } from '@/stores/panel/variables/store'
|
||||
import type { Variable } from '@/stores/panel/variables/types'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
|
||||
const logger = createLogger('TagDropdown')
|
||||
|
||||
// Type definitions for component data structures
|
||||
interface BlockTagGroup {
|
||||
blockName: string
|
||||
blockId: string
|
||||
blockType: string
|
||||
tags: string[]
|
||||
distance: number
|
||||
}
|
||||
|
||||
interface Field {
|
||||
name: string
|
||||
type: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
interface Metric {
|
||||
name: string
|
||||
description: string
|
||||
range: {
|
||||
min: number
|
||||
max: number
|
||||
// Helper function to extract fields from JSON Schema
|
||||
export function extractFieldsFromSchema(schema: any): Field[] {
|
||||
if (!schema || typeof schema !== 'object') {
|
||||
return []
|
||||
}
|
||||
|
||||
// Handle legacy format with fields array
|
||||
if (Array.isArray(schema.fields)) {
|
||||
return schema.fields
|
||||
}
|
||||
|
||||
// Handle new JSON Schema format
|
||||
const schemaObj = schema.schema || schema
|
||||
if (!schemaObj || !schemaObj.properties || typeof schemaObj.properties !== 'object') {
|
||||
return []
|
||||
}
|
||||
|
||||
// Extract fields from schema properties
|
||||
return Object.entries(schemaObj.properties).map(([name, prop]: [string, any]) => {
|
||||
// Handle array format like ['string', 'array']
|
||||
if (Array.isArray(prop)) {
|
||||
return {
|
||||
name,
|
||||
type: prop.includes('array') ? 'array' : prop[0] || 'string',
|
||||
description: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle object format like { type: 'string', description: '...' }
|
||||
return {
|
||||
name,
|
||||
type: prop.type || 'string',
|
||||
description: prop.description,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
interface TagDropdownProps {
|
||||
@@ -42,32 +76,42 @@ interface TagDropdownProps {
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
// Add a helper function to extract fields from JSON Schema
|
||||
export const extractFieldsFromSchema = (responseFormat: any): Field[] => {
|
||||
if (!responseFormat) return []
|
||||
// Check if tag trigger '<' should show dropdown
|
||||
export const checkTagTrigger = (text: string, cursorPosition: number): { show: boolean } => {
|
||||
if (cursorPosition >= 1) {
|
||||
const textBeforeCursor = text.slice(0, cursorPosition)
|
||||
const lastOpenBracket = textBeforeCursor.lastIndexOf('<')
|
||||
const lastCloseBracket = textBeforeCursor.lastIndexOf('>')
|
||||
|
||||
// Handle legacy format with fields array
|
||||
if (Array.isArray(responseFormat.fields)) {
|
||||
return responseFormat.fields
|
||||
// Show if we have an unclosed '<' that's not part of a completed tag
|
||||
if (lastOpenBracket !== -1 && (lastCloseBracket === -1 || lastCloseBracket < lastOpenBracket)) {
|
||||
return { show: true }
|
||||
}
|
||||
}
|
||||
return { show: false }
|
||||
}
|
||||
|
||||
// Generate output paths from block configuration outputs
|
||||
const generateOutputPaths = (outputs: Record<string, any>, prefix = ''): string[] => {
|
||||
const paths: string[] = []
|
||||
|
||||
for (const [key, value] of Object.entries(outputs)) {
|
||||
const currentPath = prefix ? `${prefix}.${key}` : key
|
||||
|
||||
if (typeof value === 'string') {
|
||||
// Simple type like 'string', 'number', 'json', 'any'
|
||||
paths.push(currentPath)
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
// Nested object - recurse
|
||||
const subPaths = generateOutputPaths(value, currentPath)
|
||||
paths.push(...subPaths)
|
||||
} else {
|
||||
// Fallback - add the path
|
||||
paths.push(currentPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle new JSON Schema format
|
||||
const schema = responseFormat.schema || responseFormat
|
||||
if (
|
||||
!schema ||
|
||||
typeof schema !== 'object' ||
|
||||
!('properties' in schema) ||
|
||||
typeof schema.properties !== 'object' ||
|
||||
schema.properties === null
|
||||
) {
|
||||
return []
|
||||
}
|
||||
|
||||
return Object.entries(schema.properties).map(([name, prop]: [string, any]) => ({
|
||||
name,
|
||||
type: Array.isArray(prop) ? 'array' : prop.type || 'string',
|
||||
description: prop.description,
|
||||
}))
|
||||
return paths
|
||||
}
|
||||
|
||||
export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
@@ -81,90 +125,129 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
onClose,
|
||||
style,
|
||||
}) => {
|
||||
// Component state
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
|
||||
// Get available tags from workflow state
|
||||
// Store hooks for workflow data
|
||||
const blocks = useWorkflowStore((state) => state.blocks)
|
||||
const loops = useWorkflowStore((state) => state.loops)
|
||||
const parallels = useWorkflowStore((state) => state.parallels)
|
||||
const _edges = useWorkflowStore((state) => state.edges)
|
||||
const edges = useWorkflowStore((state) => state.edges)
|
||||
const workflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
|
||||
|
||||
// Get variables from variables store
|
||||
// Store hooks for variables
|
||||
const getVariablesByWorkflowId = useVariablesStore((state) => state.getVariablesByWorkflowId)
|
||||
const loadVariables = useVariablesStore((state) => state.loadVariables)
|
||||
const variables = useVariablesStore((state) => state.variables)
|
||||
const workflowVariables = workflowId ? getVariablesByWorkflowId(workflowId) : []
|
||||
|
||||
// Get all connected blocks using useBlockConnections
|
||||
const { incomingConnections } = useBlockConnections(blockId)
|
||||
|
||||
// Load variables when workflowId changes
|
||||
// Load variables when workflow changes
|
||||
useEffect(() => {
|
||||
if (workflowId) {
|
||||
loadVariables(workflowId)
|
||||
}
|
||||
}, [workflowId, loadVariables])
|
||||
|
||||
// Extract search term from input
|
||||
// Extract current search term from input
|
||||
const searchTerm = useMemo(() => {
|
||||
const textBeforeCursor = inputValue.slice(0, cursorPosition)
|
||||
const match = textBeforeCursor.match(/<([^>]*)$/)
|
||||
return match ? match[1].toLowerCase() : ''
|
||||
}, [inputValue, cursorPosition])
|
||||
|
||||
// Get source block and compute tags
|
||||
const { tags, variableInfoMap = {} } = useMemo(() => {
|
||||
// Helper function to get output paths
|
||||
const getOutputPaths = (obj: any, prefix = '', isStarterBlock = false): string[] => {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return prefix ? [prefix] : []
|
||||
// Generate all available tags using BlockPathCalculator and clean block outputs
|
||||
const {
|
||||
tags,
|
||||
variableInfoMap = {},
|
||||
blockTagGroups = [],
|
||||
} = useMemo(() => {
|
||||
// Handle active source block (drag & drop from specific block)
|
||||
if (activeSourceBlockId) {
|
||||
const sourceBlock = blocks[activeSourceBlockId]
|
||||
if (!sourceBlock) {
|
||||
return { tags: [], variableInfoMap: {}, blockTagGroups: [] }
|
||||
}
|
||||
|
||||
// Special handling for starter block with input format
|
||||
if (isStarterBlock && prefix === 'response') {
|
||||
try {
|
||||
// Check if there's an input format defined
|
||||
const inputFormatValue = useSubBlockStore
|
||||
.getState()
|
||||
.getValue(activeSourceBlockId || blockId, 'inputFormat')
|
||||
if (inputFormatValue && Array.isArray(inputFormatValue) && inputFormatValue.length > 0) {
|
||||
// Check if any fields have been configured with names
|
||||
const hasConfiguredFields = inputFormatValue.some(
|
||||
(field: any) => field.name && field.name.trim() !== ''
|
||||
)
|
||||
|
||||
// If no fields have been configured, return the default input path
|
||||
if (!hasConfiguredFields) {
|
||||
return ['response.input']
|
||||
}
|
||||
|
||||
// Return fields from input format
|
||||
return inputFormatValue.map((field: any) => `response.input.${field.name}`)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Error parsing input format:', { e })
|
||||
}
|
||||
|
||||
return ['response.input']
|
||||
const blockConfig = getBlock(sourceBlock.type)
|
||||
if (!blockConfig) {
|
||||
return { tags: [], variableInfoMap: {}, blockTagGroups: [] }
|
||||
}
|
||||
|
||||
if ('type' in obj && typeof obj.type === 'string') {
|
||||
return [prefix]
|
||||
const blockName = sourceBlock.name || sourceBlock.type
|
||||
const normalizedBlockName = blockName.replace(/\s+/g, '').toLowerCase()
|
||||
|
||||
// Handle blocks with no outputs (like starter) - show as just <blockname>
|
||||
let blockTags: string[]
|
||||
if (Object.keys(blockConfig.outputs).length === 0) {
|
||||
blockTags = [normalizedBlockName]
|
||||
} else {
|
||||
const outputPaths = generateOutputPaths(blockConfig.outputs)
|
||||
blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
||||
}
|
||||
|
||||
return Object.entries(obj).flatMap(([key, value]) => {
|
||||
const newPrefix = prefix ? `${prefix}.${key}` : key
|
||||
return getOutputPaths(value, newPrefix, isStarterBlock)
|
||||
})
|
||||
const blockTagGroups: BlockTagGroup[] = [
|
||||
{
|
||||
blockName,
|
||||
blockId: activeSourceBlockId,
|
||||
blockType: sourceBlock.type,
|
||||
tags: blockTags,
|
||||
distance: 0,
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
tags: blockTags,
|
||||
variableInfoMap: {},
|
||||
blockTagGroups,
|
||||
}
|
||||
}
|
||||
|
||||
// Variables as tags - format as variable.{variableName}
|
||||
// Create serialized workflow for BlockPathCalculator
|
||||
const serializer = new Serializer()
|
||||
const serializedWorkflow = serializer.serializeWorkflow(blocks, edges, loops, parallels)
|
||||
|
||||
// Find accessible blocks using BlockPathCalculator
|
||||
const accessibleBlockIds = BlockPathCalculator.findAllPathNodes(
|
||||
serializedWorkflow.connections,
|
||||
blockId
|
||||
)
|
||||
|
||||
// Always include starter block
|
||||
const starterBlock = Object.values(blocks).find((block) => block.type === 'starter')
|
||||
if (starterBlock && !accessibleBlockIds.includes(starterBlock.id)) {
|
||||
accessibleBlockIds.push(starterBlock.id)
|
||||
}
|
||||
|
||||
// Calculate distances from starter block for ordering
|
||||
const blockDistances: Record<string, number> = {}
|
||||
if (starterBlock) {
|
||||
const adjList: Record<string, string[]> = {}
|
||||
for (const edge of edges) {
|
||||
if (!adjList[edge.source]) adjList[edge.source] = []
|
||||
adjList[edge.source].push(edge.target)
|
||||
}
|
||||
|
||||
const visited = new Set<string>()
|
||||
const queue: [string, number][] = [[starterBlock.id, 0]]
|
||||
|
||||
while (queue.length > 0) {
|
||||
const [currentNodeId, distance] = queue.shift()!
|
||||
if (visited.has(currentNodeId)) continue
|
||||
visited.add(currentNodeId)
|
||||
blockDistances[currentNodeId] = distance
|
||||
|
||||
const outgoingNodeIds = adjList[currentNodeId] || []
|
||||
for (const targetId of outgoingNodeIds) {
|
||||
queue.push([targetId, distance + 1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create variable tags
|
||||
const variableTags = workflowVariables.map(
|
||||
(variable: Variable) => `variable.${variable.name.replace(/\s+/g, '')}`
|
||||
)
|
||||
|
||||
// Create a map of variable tags to their type information
|
||||
const variableInfoMap = workflowVariables.reduce(
|
||||
(acc, variable) => {
|
||||
const tagName = `variable.${variable.name.replace(/\s+/g, '')}`
|
||||
@@ -177,225 +260,73 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
{} as Record<string, { type: string; id: string }>
|
||||
)
|
||||
|
||||
// Loop tags - Add if this block is in a loop
|
||||
// Generate loop tags if current block is in a loop
|
||||
const loopTags: string[] = []
|
||||
|
||||
// Check if the current block is part of a loop
|
||||
const containingLoop = Object.entries(loops).find(([_, loop]) => loop.nodes.includes(blockId))
|
||||
|
||||
if (containingLoop) {
|
||||
const [_loopId, loop] = containingLoop
|
||||
const loopType = loop.loopType || 'for'
|
||||
|
||||
// Add loop.index for all loop types
|
||||
loopTags.push('loop.index')
|
||||
|
||||
// Add forEach specific properties
|
||||
if (loopType === 'forEach') {
|
||||
// Add loop.currentItem and loop.items
|
||||
loopTags.push('loop.currentItem')
|
||||
loopTags.push('loop.items')
|
||||
}
|
||||
}
|
||||
|
||||
// Parallel tags - Add if this block is in a parallel
|
||||
// Generate parallel tags if current block is in parallel
|
||||
const parallelTags: string[] = []
|
||||
|
||||
// Check if the current block is part of a parallel
|
||||
const containingParallel = Object.entries(parallels || {}).find(([_, parallel]) =>
|
||||
parallel.nodes.includes(blockId)
|
||||
)
|
||||
|
||||
if (containingParallel) {
|
||||
// Add parallel.index for all parallel blocks
|
||||
parallelTags.push('parallel.index')
|
||||
|
||||
// Add parallel.currentItem and parallel.items
|
||||
parallelTags.push('parallel.currentItem')
|
||||
parallelTags.push('parallel.items')
|
||||
}
|
||||
|
||||
// If we have an active source block ID from a drop, use that specific block only
|
||||
if (activeSourceBlockId) {
|
||||
const sourceBlock = blocks[activeSourceBlockId]
|
||||
if (!sourceBlock) return { tags: [...variableTags] }
|
||||
// Create block tag groups from accessible blocks
|
||||
const blockTagGroups: BlockTagGroup[] = []
|
||||
const allBlockTags: string[] = []
|
||||
|
||||
const blockName = sourceBlock.name || sourceBlock.type
|
||||
for (const accessibleBlockId of accessibleBlockIds) {
|
||||
const accessibleBlock = blocks[accessibleBlockId]
|
||||
if (!accessibleBlock) continue
|
||||
|
||||
const blockConfig = getBlock(accessibleBlock.type)
|
||||
if (!blockConfig) continue
|
||||
|
||||
const blockName = accessibleBlock.name || accessibleBlock.type
|
||||
const normalizedBlockName = blockName.replace(/\s+/g, '').toLowerCase()
|
||||
|
||||
// First check for evaluator metrics
|
||||
if (sourceBlock.type === 'evaluator') {
|
||||
try {
|
||||
const metricsValue = useSubBlockStore
|
||||
.getState()
|
||||
.getValue(activeSourceBlockId, 'metrics') as unknown as Metric[]
|
||||
if (Array.isArray(metricsValue)) {
|
||||
return {
|
||||
tags: [
|
||||
...variableTags,
|
||||
...metricsValue.map(
|
||||
(metric) => `${normalizedBlockName}.response.${metric.name.toLowerCase()}`
|
||||
),
|
||||
],
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Error parsing metrics:', { e })
|
||||
}
|
||||
// Handle blocks with no outputs (like starter) - show as just <blockname>
|
||||
let blockTags: string[]
|
||||
if (Object.keys(blockConfig.outputs).length === 0) {
|
||||
blockTags = [normalizedBlockName]
|
||||
} else {
|
||||
const outputPaths = generateOutputPaths(blockConfig.outputs)
|
||||
blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
||||
}
|
||||
|
||||
// Then check for response format
|
||||
try {
|
||||
const responseFormatValue = useSubBlockStore
|
||||
.getState()
|
||||
.getValue(activeSourceBlockId, 'responseFormat')
|
||||
if (responseFormatValue) {
|
||||
const responseFormat =
|
||||
typeof responseFormatValue === 'string'
|
||||
? JSON.parse(responseFormatValue)
|
||||
: responseFormatValue
|
||||
blockTagGroups.push({
|
||||
blockName,
|
||||
blockId: accessibleBlockId,
|
||||
blockType: accessibleBlock.type,
|
||||
tags: blockTags,
|
||||
distance: blockDistances[accessibleBlockId] || 0,
|
||||
})
|
||||
|
||||
if (responseFormat) {
|
||||
const fields = extractFieldsFromSchema(responseFormat)
|
||||
if (fields.length > 0) {
|
||||
return {
|
||||
tags: [
|
||||
...variableTags,
|
||||
...fields.map((field: Field) => `${normalizedBlockName}.response.${field.name}`),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Error parsing response format:', { e })
|
||||
}
|
||||
|
||||
// Fall back to default outputs if no response format
|
||||
const outputPaths = getOutputPaths(sourceBlock.outputs, '', sourceBlock.type === 'starter')
|
||||
return {
|
||||
tags: [...variableTags, ...outputPaths.map((path) => `${normalizedBlockName}.${path}`)],
|
||||
}
|
||||
allBlockTags.push(...blockTags)
|
||||
}
|
||||
|
||||
// Find parallel and loop blocks connected via end-source handles
|
||||
const endSourceConnections: ConnectedBlock[] = []
|
||||
// Sort block groups by distance (closest first)
|
||||
blockTagGroups.sort((a, b) => a.distance - b.distance)
|
||||
|
||||
// Get all edges that connect to this block
|
||||
const incomingEdges = useWorkflowStore
|
||||
.getState()
|
||||
.edges.filter((edge) => edge.target === blockId)
|
||||
|
||||
for (const edge of incomingEdges) {
|
||||
const sourceBlock = blocks[edge.source]
|
||||
if (!sourceBlock) continue
|
||||
|
||||
// Check if this is a parallel-end-source or loop-end-source connection
|
||||
if (edge.sourceHandle === 'parallel-end-source' && sourceBlock.type === 'parallel') {
|
||||
const blockName = sourceBlock.name || sourceBlock.type
|
||||
const normalizedBlockName = blockName.replace(/\s+/g, '').toLowerCase()
|
||||
|
||||
// Add the parallel block as a referenceable block with its aggregated results
|
||||
endSourceConnections.push({
|
||||
id: sourceBlock.id,
|
||||
type: sourceBlock.type,
|
||||
outputType: ['response'],
|
||||
name: blockName,
|
||||
responseFormat: {
|
||||
fields: [
|
||||
{
|
||||
name: 'completed',
|
||||
type: 'boolean',
|
||||
description: 'Whether all executions completed',
|
||||
},
|
||||
{
|
||||
name: 'results',
|
||||
type: 'array',
|
||||
description: 'Aggregated results from all parallel executions',
|
||||
},
|
||||
{ name: 'message', type: 'string', description: 'Status message' },
|
||||
],
|
||||
},
|
||||
})
|
||||
} else if (edge.sourceHandle === 'loop-end-source' && sourceBlock.type === 'loop') {
|
||||
const blockName = sourceBlock.name || sourceBlock.type
|
||||
const normalizedBlockName = blockName.replace(/\s+/g, '').toLowerCase()
|
||||
|
||||
// Add the loop block as a referenceable block with its aggregated results
|
||||
endSourceConnections.push({
|
||||
id: sourceBlock.id,
|
||||
type: sourceBlock.type,
|
||||
outputType: ['response'],
|
||||
name: blockName,
|
||||
responseFormat: {
|
||||
fields: [
|
||||
{
|
||||
name: 'completed',
|
||||
type: 'boolean',
|
||||
description: 'Whether all iterations completed',
|
||||
},
|
||||
{
|
||||
name: 'results',
|
||||
type: 'array',
|
||||
description: 'Aggregated results from all loop iterations',
|
||||
},
|
||||
{ name: 'message', type: 'string', description: 'Status message' },
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
return {
|
||||
tags: [...variableTags, ...loopTags, ...parallelTags, ...allBlockTags],
|
||||
variableInfoMap,
|
||||
blockTagGroups,
|
||||
}
|
||||
|
||||
// Use all incoming connections plus end-source connections
|
||||
const allConnections = [...incomingConnections, ...endSourceConnections]
|
||||
|
||||
const sourceTags = allConnections.flatMap((connection: ConnectedBlock) => {
|
||||
const blockName = connection.name || connection.type
|
||||
const normalizedBlockName = blockName.replace(/\s+/g, '').toLowerCase()
|
||||
|
||||
// Extract fields from response format
|
||||
if (connection.responseFormat) {
|
||||
const fields = extractFieldsFromSchema(connection.responseFormat)
|
||||
if (fields.length > 0) {
|
||||
return fields.map((field: Field) => `${normalizedBlockName}.response.${field.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
// For evaluator blocks, use metrics
|
||||
if (connection.type === 'evaluator') {
|
||||
try {
|
||||
const metricsValue = useSubBlockStore
|
||||
.getState()
|
||||
.getValue(connection.id, 'metrics') as unknown as Metric[]
|
||||
if (Array.isArray(metricsValue)) {
|
||||
return metricsValue.map(
|
||||
(metric) => `${normalizedBlockName}.response.${metric.name.toLowerCase()}`
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Error parsing metrics:', { e })
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to default outputs if no response format
|
||||
const sourceBlock = blocks[connection.id]
|
||||
if (!sourceBlock) return []
|
||||
|
||||
const outputPaths = getOutputPaths(sourceBlock.outputs, '', sourceBlock.type === 'starter')
|
||||
return outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
||||
})
|
||||
|
||||
return { tags: [...variableTags, ...loopTags, ...parallelTags, ...sourceTags], variableInfoMap }
|
||||
}, [
|
||||
blocks,
|
||||
incomingConnections,
|
||||
blockId,
|
||||
activeSourceBlockId,
|
||||
workflowVariables,
|
||||
loops,
|
||||
parallels,
|
||||
])
|
||||
}, [blocks, edges, loops, parallels, blockId, activeSourceBlockId, workflowVariables])
|
||||
|
||||
// Filter tags based on search term
|
||||
const filteredTags = useMemo(() => {
|
||||
@@ -403,12 +334,11 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
return tags.filter((tag: string) => tag.toLowerCase().includes(searchTerm))
|
||||
}, [tags, searchTerm])
|
||||
|
||||
// Group tags into variables, loops, and blocks
|
||||
const { variableTags, loopTags, parallelTags, blockTags } = useMemo(() => {
|
||||
// Group filtered tags by category
|
||||
const { variableTags, loopTags, parallelTags, filteredBlockTagGroups } = useMemo(() => {
|
||||
const varTags: string[] = []
|
||||
const loopTags: string[] = []
|
||||
const parTags: string[] = []
|
||||
const blkTags: string[] = []
|
||||
|
||||
filteredTags.forEach((tag) => {
|
||||
if (tag.startsWith('variable.')) {
|
||||
@@ -417,20 +347,32 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
loopTags.push(tag)
|
||||
} else if (tag.startsWith('parallel.')) {
|
||||
parTags.push(tag)
|
||||
} else {
|
||||
blkTags.push(tag)
|
||||
}
|
||||
})
|
||||
|
||||
return { variableTags: varTags, loopTags: loopTags, parallelTags: parTags, blockTags: blkTags }
|
||||
}, [filteredTags])
|
||||
// Filter block tag groups based on search term
|
||||
const filteredBlockTagGroups = blockTagGroups
|
||||
.map((group) => ({
|
||||
...group,
|
||||
tags: group.tags.filter((tag) => !searchTerm || tag.toLowerCase().includes(searchTerm)),
|
||||
}))
|
||||
.filter((group) => group.tags.length > 0)
|
||||
|
||||
// Create ordered tags array that matches the display order for keyboard navigation
|
||||
return {
|
||||
variableTags: varTags,
|
||||
loopTags: loopTags,
|
||||
parallelTags: parTags,
|
||||
filteredBlockTagGroups,
|
||||
}
|
||||
}, [filteredTags, blockTagGroups, searchTerm])
|
||||
|
||||
// Create ordered tags for keyboard navigation
|
||||
const orderedTags = useMemo(() => {
|
||||
return [...variableTags, ...loopTags, ...parallelTags, ...blockTags]
|
||||
}, [variableTags, loopTags, parallelTags, blockTags])
|
||||
const allBlockTags = filteredBlockTagGroups.flatMap((group) => group.tags)
|
||||
return [...variableTags, ...loopTags, ...parallelTags, ...allBlockTags]
|
||||
}, [variableTags, loopTags, parallelTags, filteredBlockTagGroups])
|
||||
|
||||
// Create a map for efficient tag index lookups
|
||||
// Create efficient tag index lookup map
|
||||
const tagIndexMap = useMemo(() => {
|
||||
const map = new Map<string, number>()
|
||||
orderedTags.forEach((tag, index) => {
|
||||
@@ -439,19 +381,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
return map
|
||||
}, [orderedTags])
|
||||
|
||||
// Reset selection when filtered results change
|
||||
useEffect(() => {
|
||||
setSelectedIndex(0)
|
||||
}, [searchTerm])
|
||||
|
||||
// Ensure selectedIndex stays within bounds when orderedTags changes
|
||||
useEffect(() => {
|
||||
if (selectedIndex >= orderedTags.length) {
|
||||
setSelectedIndex(Math.max(0, orderedTags.length - 1))
|
||||
}
|
||||
}, [orderedTags.length, selectedIndex])
|
||||
|
||||
// Handle tag selection
|
||||
// Handle tag selection and text replacement
|
||||
const handleTagSelect = useCallback(
|
||||
(tag: string) => {
|
||||
const textBeforeCursor = inputValue.slice(0, cursorPosition)
|
||||
@@ -461,34 +391,26 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
const lastOpenBracket = textBeforeCursor.lastIndexOf('<')
|
||||
if (lastOpenBracket === -1) return
|
||||
|
||||
// Process the tag if it's a variable tag
|
||||
// Process variable tags to maintain compatibility
|
||||
let processedTag = tag
|
||||
if (tag.startsWith('variable.')) {
|
||||
// Get the variable name from the tag (after 'variable.')
|
||||
const variableName = tag.substring('variable.'.length)
|
||||
|
||||
// Find the variable in the store by name
|
||||
const variableObj = Object.values(variables).find(
|
||||
(v) => v.name.replace(/\s+/g, '') === variableName
|
||||
)
|
||||
|
||||
// We still use the full tag format internally to maintain compatibility
|
||||
if (variableObj) {
|
||||
processedTag = tag
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there's a closing bracket in textAfterCursor that belongs to the current tag
|
||||
// Find the first '>' in textAfterCursor (if any)
|
||||
// Handle existing closing bracket
|
||||
const nextCloseBracket = textAfterCursor.indexOf('>')
|
||||
let remainingTextAfterCursor = textAfterCursor
|
||||
|
||||
// If there's a '>' right after the cursor or with only whitespace/tag content in between,
|
||||
// it's likely part of the existing tag being edited, so we should skip it
|
||||
if (nextCloseBracket !== -1) {
|
||||
const textBetween = textAfterCursor.slice(0, nextCloseBracket)
|
||||
// If the text between cursor and '>' contains only tag-like characters (letters, dots, numbers)
|
||||
// then it's likely part of the current tag being edited
|
||||
// If text between cursor and '>' contains only tag-like characters, skip it
|
||||
if (/^[a-zA-Z0-9._]*$/.test(textBetween)) {
|
||||
remainingTextAfterCursor = textAfterCursor.slice(nextCloseBracket + 1)
|
||||
}
|
||||
@@ -502,7 +424,17 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
[inputValue, cursorPosition, variables, onSelect, onClose]
|
||||
)
|
||||
|
||||
// Add and remove keyboard event listener
|
||||
// Reset selection when search results change
|
||||
useEffect(() => setSelectedIndex(0), [searchTerm])
|
||||
|
||||
// Keep selection within bounds when tags change
|
||||
useEffect(() => {
|
||||
if (selectedIndex >= orderedTags.length) {
|
||||
setSelectedIndex(Math.max(0, orderedTags.length - 1))
|
||||
}
|
||||
}, [orderedTags.length, selectedIndex])
|
||||
|
||||
// Handle keyboard navigation
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
const handleKeyboardEvent = (e: KeyboardEvent) => {
|
||||
@@ -539,7 +471,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
}
|
||||
}, [visible, selectedIndex, orderedTags, handleTagSelect, onClose])
|
||||
|
||||
// Don't render if not visible or no tags
|
||||
// Early return if dropdown should not be visible
|
||||
if (!visible || tags.length === 0 || orderedTags.length === 0) return null
|
||||
|
||||
return (
|
||||
@@ -555,6 +487,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
<div className='px-3 py-2 text-muted-foreground text-sm'>No matching tags found</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Variables section */}
|
||||
{variableTags.length > 0 && (
|
||||
<>
|
||||
<div className='px-2 pt-2.5 pb-0.5 font-medium text-muted-foreground text-xs'>
|
||||
@@ -578,8 +511,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
)}
|
||||
onMouseEnter={() => setSelectedIndex(tagIndex >= 0 ? tagIndex : 0)}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault() // Prevent input blur
|
||||
e.stopPropagation() // Prevent event bubbling
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handleTagSelect(tag)
|
||||
}}
|
||||
onClick={(e) => {
|
||||
@@ -609,6 +542,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Loop section */}
|
||||
{loopTags.length > 0 && (
|
||||
<>
|
||||
{variableTags.length > 0 && <div className='my-0' />}
|
||||
@@ -620,10 +554,10 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
const tagIndex = tagIndexMap.get(tag) ?? -1
|
||||
const loopProperty = tag.split('.')[1]
|
||||
|
||||
// Choose appropriate icon/label based on type
|
||||
// Choose appropriate icon and description based on loop property
|
||||
let tagIcon = 'L'
|
||||
let tagDescription = ''
|
||||
const bgColor = '#8857E6' // Purple for loop variables
|
||||
const bgColor = '#8857E6'
|
||||
|
||||
if (loopProperty === 'currentItem') {
|
||||
tagIcon = 'i'
|
||||
@@ -649,8 +583,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
)}
|
||||
onMouseEnter={() => setSelectedIndex(tagIndex >= 0 ? tagIndex : 0)}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault() // Prevent input blur
|
||||
e.stopPropagation() // Prevent event bubbling
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handleTagSelect(tag)
|
||||
}}
|
||||
onClick={(e) => {
|
||||
@@ -676,6 +610,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Parallel section */}
|
||||
{parallelTags.length > 0 && (
|
||||
<>
|
||||
{loopTags.length > 0 && <div className='my-0' />}
|
||||
@@ -687,10 +622,10 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
const tagIndex = tagIndexMap.get(tag) ?? -1
|
||||
const parallelProperty = tag.split('.')[1]
|
||||
|
||||
// Choose appropriate icon/label based on type
|
||||
// Choose appropriate icon and description based on parallel property
|
||||
let tagIcon = 'P'
|
||||
let tagDescription = ''
|
||||
const bgColor = '#FF5757' // Red for parallel variables
|
||||
const bgColor = '#FF5757'
|
||||
|
||||
if (parallelProperty === 'currentItem') {
|
||||
tagIcon = 'i'
|
||||
@@ -716,8 +651,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
)}
|
||||
onMouseEnter={() => setSelectedIndex(tagIndex >= 0 ? tagIndex : 0)}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault() // Prevent input blur
|
||||
e.stopPropagation() // Prevent event bubbling
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handleTagSelect(tag)
|
||||
}}
|
||||
onClick={(e) => {
|
||||
@@ -743,68 +678,72 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{blockTags.length > 0 && (
|
||||
{/* Block sections */}
|
||||
{filteredBlockTagGroups.length > 0 && (
|
||||
<>
|
||||
{(variableTags.length > 0 || loopTags.length > 0 || parallelTags.length > 0) && (
|
||||
<div className='my-0' />
|
||||
)}
|
||||
<div className='px-2 pt-2.5 pb-0.5 font-medium text-muted-foreground text-xs'>
|
||||
Blocks
|
||||
</div>
|
||||
<div className='-mx-1 -px-1'>
|
||||
{blockTags.map((tag: string) => {
|
||||
const tagIndex = tagIndexMap.get(tag) ?? -1
|
||||
{filteredBlockTagGroups.map((group) => {
|
||||
// Get block color from configuration
|
||||
const blockConfig = getBlock(group.blockType)
|
||||
const blockColor = blockConfig?.bgColor || '#2F55FF'
|
||||
|
||||
// Get block name from tag (first part before the dot)
|
||||
const blockName = tag.split('.')[0]
|
||||
return (
|
||||
<div key={group.blockId}>
|
||||
<div className='border-t px-2 pt-1.5 pb-0.5 font-medium text-muted-foreground text-xs first:border-t-0'>
|
||||
{group.blockName}
|
||||
</div>
|
||||
<div>
|
||||
{group.tags.map((tag: string) => {
|
||||
const tagIndex = tagIndexMap.get(tag) ?? -1
|
||||
// Extract path after block name (e.g., "field" from "blockname.field")
|
||||
// For root reference blocks, show the block name instead of empty path
|
||||
const tagParts = tag.split('.')
|
||||
const path = tagParts.slice(1).join('.')
|
||||
const displayText = path || group.blockName
|
||||
|
||||
// Get block type from blocks
|
||||
const blockType = Object.values(blocks).find(
|
||||
(block) =>
|
||||
(block.name || block.type || '').replace(/\s+/g, '').toLowerCase() ===
|
||||
blockName
|
||||
)?.type
|
||||
|
||||
// Get block color from block config
|
||||
const blockConfig = blockType ? getBlock(blockType) : null
|
||||
const blockColor = blockConfig?.bgColor || '#2F55FF' // Default to blue if not found
|
||||
|
||||
return (
|
||||
<button
|
||||
key={tag}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm',
|
||||
'hover:bg-accent hover:text-accent-foreground',
|
||||
'focus:bg-accent focus:text-accent-foreground focus:outline-none',
|
||||
tagIndex === selectedIndex &&
|
||||
tagIndex >= 0 &&
|
||||
'bg-accent text-accent-foreground'
|
||||
)}
|
||||
onMouseEnter={() => setSelectedIndex(tagIndex >= 0 ? tagIndex : 0)}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault() // Prevent input blur
|
||||
e.stopPropagation() // Prevent event bubbling
|
||||
handleTagSelect(tag)
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handleTagSelect(tag)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className='flex h-5 w-5 items-center justify-center rounded'
|
||||
style={{ backgroundColor: blockColor }}
|
||||
>
|
||||
<span className='h-3 w-3 font-bold text-white text-xs'>
|
||||
{blockName.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<span className='flex-1 truncate'>{tag}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
return (
|
||||
<button
|
||||
key={tag}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm',
|
||||
'hover:bg-accent hover:text-accent-foreground',
|
||||
'focus:bg-accent focus:text-accent-foreground focus:outline-none',
|
||||
tagIndex === selectedIndex &&
|
||||
tagIndex >= 0 &&
|
||||
'bg-accent text-accent-foreground'
|
||||
)}
|
||||
onMouseEnter={() => setSelectedIndex(tagIndex >= 0 ? tagIndex : 0)}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handleTagSelect(tag)
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handleTagSelect(tag)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className='flex h-5 w-5 flex-shrink-0 items-center justify-center rounded'
|
||||
style={{ backgroundColor: blockColor }}
|
||||
>
|
||||
<span className='h-3 w-3 font-bold text-white text-xs'>
|
||||
{group.blockName.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<span className='max-w-[calc(100%-32px)] truncate'>
|
||||
{displayText}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@@ -813,18 +752,3 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Helper function to check for '<' trigger
|
||||
export const checkTagTrigger = (text: string, cursorPosition: number): { show: boolean } => {
|
||||
if (cursorPosition >= 1) {
|
||||
const textBeforeCursor = text.slice(0, cursorPosition)
|
||||
const lastOpenBracket = textBeforeCursor.lastIndexOf('<')
|
||||
const lastCloseBracket = textBeforeCursor.lastIndexOf('>')
|
||||
|
||||
// Show if we have an unclosed '<' that's not part of a completed tag
|
||||
if (lastOpenBracket !== -1 && (lastCloseBracket === -1 || lastCloseBracket < lastOpenBracket)) {
|
||||
return { show: true }
|
||||
}
|
||||
}
|
||||
return { show: false }
|
||||
}
|
||||
|
||||
@@ -168,7 +168,6 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
|
||||
socketInstance.on('connect', () => {
|
||||
setIsConnected(true)
|
||||
setIsConnecting(false)
|
||||
|
||||
logger.info('Socket connected successfully', {
|
||||
socketId: socketInstance.id,
|
||||
connected: socketInstance.connected,
|
||||
|
||||
1
apps/sim/db/migrations/0050_big_mattie_franklin.sql
Normal file
1
apps/sim/db/migrations/0050_big_mattie_franklin.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "settings" ADD COLUMN "auto_pan" boolean DEFAULT true NOT NULL;
|
||||
4468
apps/sim/db/migrations/meta/0050_snapshot.json
Normal file
4468
apps/sim/db/migrations/meta/0050_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -344,6 +344,13 @@
|
||||
"when": 1751430703326,
|
||||
"tag": "0049_fancy_cardiac",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 50,
|
||||
"version": "7",
|
||||
"when": 1751659528896,
|
||||
"tag": "0050_big_mattie_franklin",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -394,6 +394,7 @@ export const settings = pgTable('settings', {
|
||||
debugMode: boolean('debug_mode').notNull().default(false),
|
||||
autoConnect: boolean('auto_connect').notNull().default(true),
|
||||
autoFillEnvVars: boolean('auto_fill_env_vars').notNull().default(true),
|
||||
autoPan: boolean('auto_pan').notNull().default(true),
|
||||
|
||||
// Privacy settings
|
||||
telemetryEnabled: boolean('telemetry_enabled').notNull().default(true),
|
||||
|
||||
@@ -15,7 +15,7 @@ export const createMockHandler = (
|
||||
block.metadata?.id === handlerName || handlerName === 'generic'
|
||||
|
||||
const defaultExecuteResult = {
|
||||
response: { result: `${handlerName} executed` },
|
||||
result: `${handlerName} executed`,
|
||||
}
|
||||
|
||||
return vi.fn().mockImplementation(() => ({
|
||||
@@ -614,12 +614,8 @@ export const createFunctionBlockHandler = vi.fn().mockImplementation(() => ({
|
||||
canHandle: (block: any) => block.metadata?.id === 'function',
|
||||
execute: vi.fn().mockImplementation(async (block, inputs) => {
|
||||
return {
|
||||
response: {
|
||||
result: inputs.code
|
||||
? new Function(inputs.code)()
|
||||
: { key: inputs.key, value: inputs.value },
|
||||
stdout: '',
|
||||
},
|
||||
result: inputs.code ? new Function(inputs.code)() : { key: inputs.key, value: inputs.value },
|
||||
stdout: '',
|
||||
}
|
||||
}),
|
||||
}))
|
||||
@@ -679,13 +675,11 @@ export const createParallelBlockHandler = vi.fn().mockImplementation(() => {
|
||||
}
|
||||
|
||||
return {
|
||||
response: {
|
||||
parallelId,
|
||||
parallelCount,
|
||||
distributionType: 'distributed',
|
||||
started: true,
|
||||
message: `Initialized ${parallelCount} parallel executions`,
|
||||
},
|
||||
parallelId,
|
||||
parallelCount,
|
||||
distributionType: 'distributed',
|
||||
started: true,
|
||||
message: `Initialized ${parallelCount} parallel executions`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,22 +708,18 @@ export const createParallelBlockHandler = vi.fn().mockImplementation(() => {
|
||||
}
|
||||
|
||||
return {
|
||||
response: {
|
||||
parallelId,
|
||||
parallelCount: parallelState.parallelCount,
|
||||
completed: true,
|
||||
message: `Completed all ${parallelState.parallelCount} executions`,
|
||||
},
|
||||
parallelId,
|
||||
parallelCount: parallelState.parallelCount,
|
||||
completed: true,
|
||||
message: `Completed all ${parallelState.parallelCount} executions`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
response: {
|
||||
parallelId,
|
||||
parallelCount: parallelState.parallelCount,
|
||||
waiting: true,
|
||||
message: 'Waiting for iterations to complete',
|
||||
},
|
||||
parallelId,
|
||||
parallelCount: parallelState.parallelCount,
|
||||
waiting: true,
|
||||
message: 'Waiting for iterations to complete',
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export class TestExecutor extends Executor {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
response: { result: 'Test execution completed' },
|
||||
result: 'Test execution completed',
|
||||
} as NormalizedBlockOutput,
|
||||
logs: [],
|
||||
metadata: {
|
||||
@@ -39,7 +39,7 @@ export class TestExecutor extends Executor {
|
||||
// If validation fails, return a failure result
|
||||
return {
|
||||
success: false,
|
||||
output: { response: {} } as NormalizedBlockOutput,
|
||||
output: {} as NormalizedBlockOutput,
|
||||
error: error.message,
|
||||
logs: [],
|
||||
}
|
||||
|
||||
@@ -210,14 +210,12 @@ describe('AgentBlockHandler', () => {
|
||||
mockGetProviderFromModel.mockReturnValue('openai')
|
||||
|
||||
const expectedOutput = {
|
||||
response: {
|
||||
content: 'Mocked response content',
|
||||
model: 'mock-model',
|
||||
tokens: { prompt: 10, completion: 20, total: 30 },
|
||||
toolCalls: { list: [], count: 0 },
|
||||
providerTiming: { total: 100 },
|
||||
cost: 0.001,
|
||||
},
|
||||
content: 'Mocked response content',
|
||||
model: 'mock-model',
|
||||
tokens: { prompt: 10, completion: 20, total: 30 },
|
||||
toolCalls: { list: [], count: 0 },
|
||||
providerTiming: { total: 100 },
|
||||
cost: 0.001,
|
||||
}
|
||||
|
||||
const result = await handler.execute(mockBlock, inputs, mockContext)
|
||||
@@ -587,14 +585,12 @@ describe('AgentBlockHandler', () => {
|
||||
mockGetProviderFromModel.mockReturnValue('openai')
|
||||
|
||||
const expectedOutput = {
|
||||
response: {
|
||||
content: 'Mocked response content',
|
||||
model: 'mock-model',
|
||||
tokens: { prompt: 10, completion: 20, total: 30 },
|
||||
toolCalls: { list: [], count: 0 }, // Assuming no tool calls in this mock response
|
||||
providerTiming: { total: 100 },
|
||||
cost: 0.001,
|
||||
},
|
||||
content: 'Mocked response content',
|
||||
model: 'mock-model',
|
||||
tokens: { prompt: 10, completion: 20, total: 30 },
|
||||
toolCalls: { list: [], count: 0 }, // Assuming no tool calls in this mock response
|
||||
providerTiming: { total: 100 },
|
||||
cost: 0.001,
|
||||
}
|
||||
|
||||
const result = await handler.execute(mockBlock, inputs, mockContext)
|
||||
@@ -691,14 +687,12 @@ describe('AgentBlockHandler', () => {
|
||||
const result = await handler.execute(mockBlock, inputs, mockContext)
|
||||
|
||||
expect(result).toEqual({
|
||||
response: {
|
||||
result: 'Success',
|
||||
score: 0.95,
|
||||
tokens: { prompt: 10, completion: 20, total: 30 },
|
||||
toolCalls: { list: [], count: 0 },
|
||||
providerTiming: { total: 100 },
|
||||
cost: undefined,
|
||||
},
|
||||
result: 'Success',
|
||||
score: 0.95,
|
||||
tokens: { prompt: 10, completion: 20, total: 30 },
|
||||
toolCalls: { list: [], count: 0 },
|
||||
providerTiming: { total: 100 },
|
||||
cost: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -733,13 +727,12 @@ describe('AgentBlockHandler', () => {
|
||||
const result = await handler.execute(mockBlock, inputs, mockContext)
|
||||
|
||||
expect(result).toEqual({
|
||||
response: {
|
||||
content: 'Regular text response',
|
||||
model: 'mock-model',
|
||||
tokens: { prompt: 10, completion: 20, total: 30 },
|
||||
toolCalls: { list: [], count: 0 },
|
||||
providerTiming: { total: 100 },
|
||||
},
|
||||
content: 'Regular text response',
|
||||
model: 'mock-model',
|
||||
tokens: { prompt: 10, completion: 20, total: 30 },
|
||||
toolCalls: { list: [], count: 0 },
|
||||
providerTiming: { total: 100 },
|
||||
cost: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -793,7 +786,7 @@ describe('AgentBlockHandler', () => {
|
||||
stream: mockStreamBody,
|
||||
execution: {
|
||||
success: true,
|
||||
output: { response: {} },
|
||||
output: {},
|
||||
logs: [],
|
||||
metadata: {
|
||||
duration: 0,
|
||||
@@ -821,7 +814,7 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
expect((result as StreamingExecution).execution).toHaveProperty('success', true)
|
||||
expect((result as StreamingExecution).execution).toHaveProperty('output')
|
||||
expect((result as StreamingExecution).execution.output).toHaveProperty('response')
|
||||
expect((result as StreamingExecution).execution.output).toBeDefined()
|
||||
expect((result as StreamingExecution).execution).toHaveProperty('logs')
|
||||
})
|
||||
|
||||
@@ -835,11 +828,9 @@ describe('AgentBlockHandler', () => {
|
||||
const mockExecutionData = {
|
||||
success: true,
|
||||
output: {
|
||||
response: {
|
||||
content: '',
|
||||
model: 'mock-model',
|
||||
tokens: { prompt: 10, completion: 20, total: 30 },
|
||||
},
|
||||
content: '',
|
||||
model: 'mock-model',
|
||||
tokens: { prompt: 10, completion: 20, total: 30 },
|
||||
},
|
||||
logs: [
|
||||
{
|
||||
@@ -891,7 +882,7 @@ describe('AgentBlockHandler', () => {
|
||||
expect(result).toHaveProperty('execution')
|
||||
|
||||
expect((result as StreamingExecution).execution.success).toBe(true)
|
||||
expect((result as StreamingExecution).execution.output.response.model).toBe('mock-model')
|
||||
expect((result as StreamingExecution).execution.output.model).toBe('mock-model')
|
||||
const logs = (result as StreamingExecution).execution.logs
|
||||
expect(logs?.length).toBe(1)
|
||||
if (logs && logs.length > 0 && logs[0]) {
|
||||
@@ -918,11 +909,9 @@ describe('AgentBlockHandler', () => {
|
||||
execution: {
|
||||
success: true,
|
||||
output: {
|
||||
response: {
|
||||
content: 'Test streaming content',
|
||||
model: 'gpt-4o',
|
||||
tokens: { prompt: 10, completion: 5, total: 15 },
|
||||
},
|
||||
content: 'Test streaming content',
|
||||
model: 'gpt-4o',
|
||||
tokens: { prompt: 10, completion: 5, total: 15 },
|
||||
},
|
||||
logs: [],
|
||||
metadata: {
|
||||
@@ -950,10 +939,8 @@ describe('AgentBlockHandler', () => {
|
||||
expect(result).toHaveProperty('execution')
|
||||
|
||||
expect((result as StreamingExecution).execution.success).toBe(true)
|
||||
expect((result as StreamingExecution).execution.output.response.content).toBe(
|
||||
'Test streaming content'
|
||||
)
|
||||
expect((result as StreamingExecution).execution.output.response.model).toBe('gpt-4o')
|
||||
expect((result as StreamingExecution).execution.output.content).toBe('Test streaming content')
|
||||
expect((result as StreamingExecution).execution.output.model).toBe('gpt-4o')
|
||||
})
|
||||
|
||||
it('should process memories in advanced mode with system prompt and user prompt', async () => {
|
||||
@@ -1006,18 +993,16 @@ describe('AgentBlockHandler', () => {
|
||||
systemPrompt: 'You are a helpful assistant.',
|
||||
userPrompt: 'Continue our conversation.',
|
||||
memories: {
|
||||
response: {
|
||||
memories: [
|
||||
{
|
||||
key: 'conversation-1',
|
||||
type: 'agent',
|
||||
data: [
|
||||
{ role: 'user', content: 'Hi there!' },
|
||||
{ role: 'assistant', content: 'Hello! How can I help you?' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
memories: [
|
||||
{
|
||||
key: 'conversation-1',
|
||||
type: 'agent',
|
||||
data: [
|
||||
{ role: 'user', content: 'Hi there!' },
|
||||
{ role: 'assistant', content: 'Hello! How can I help you?' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
apiKey: 'test-api-key',
|
||||
}
|
||||
|
||||
@@ -194,9 +194,7 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
if (!memories) return []
|
||||
|
||||
let memoryArray: any[] = []
|
||||
if (memories?.response?.memories && Array.isArray(memories.response.memories)) {
|
||||
memoryArray = memories.response.memories
|
||||
} else if (memories?.memories && Array.isArray(memories.memories)) {
|
||||
if (memories?.memories && Array.isArray(memories.memories)) {
|
||||
memoryArray = memories.memories
|
||||
} else if (Array.isArray(memories)) {
|
||||
memoryArray = memories
|
||||
@@ -473,7 +471,7 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
stream: response.body!,
|
||||
execution: {
|
||||
success: executionData.success,
|
||||
output: executionData.output || { response: {} },
|
||||
output: executionData.output || {},
|
||||
error: executionData.error,
|
||||
logs: [], // Logs are stripped from headers, will be populated by executor
|
||||
metadata: executionData.metadata || {
|
||||
@@ -621,7 +619,7 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
const streamingExec = response as StreamingExecution
|
||||
logger.info(`Received StreamingExecution for block ${block.id}`)
|
||||
|
||||
if (streamingExec.execution.output?.response) {
|
||||
if (streamingExec.execution.output) {
|
||||
const execution = streamingExec.execution as any
|
||||
if (block.metadata?.name) execution.blockName = block.metadata.name
|
||||
if (block.metadata?.id) execution.blockType = block.metadata.id
|
||||
@@ -637,7 +635,7 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
stream,
|
||||
execution: {
|
||||
success: true,
|
||||
output: { response: {} },
|
||||
output: {},
|
||||
logs: [],
|
||||
metadata: {
|
||||
duration: 0,
|
||||
@@ -667,10 +665,8 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
try {
|
||||
const parsedContent = JSON.parse(result.content)
|
||||
return {
|
||||
response: {
|
||||
...parsedContent,
|
||||
...this.createResponseMetadata(result),
|
||||
},
|
||||
...parsedContent,
|
||||
...this.createResponseMetadata(result),
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to parse response content:', { error })
|
||||
@@ -680,11 +676,9 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
|
||||
private processStandardResponse(result: any): BlockOutput {
|
||||
return {
|
||||
response: {
|
||||
content: result.content,
|
||||
model: result.model,
|
||||
...this.createResponseMetadata(result),
|
||||
},
|
||||
content: result.content,
|
||||
model: result.model,
|
||||
...this.createResponseMetadata(result),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ describe('ApiBlockHandler', () => {
|
||||
body: JSON.stringify({ key: 'value' }),
|
||||
}
|
||||
|
||||
const expectedOutput = { response: { data: 'Success' } }
|
||||
const expectedOutput = { data: 'Success' }
|
||||
|
||||
mockExecuteTool.mockResolvedValue({ success: true, output: { data: 'Success' } })
|
||||
|
||||
@@ -113,7 +113,7 @@ describe('ApiBlockHandler', () => {
|
||||
method: 'GET',
|
||||
}
|
||||
|
||||
const expectedOutput = { response: { content: '', success: true } }
|
||||
const expectedOutput = { data: null, status: 200, headers: {} }
|
||||
|
||||
const result = await handler.execute(mockBlock, inputs, mockContext)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import { executeTool } from '@/tools'
|
||||
import { getTool } from '@/tools/utils'
|
||||
@@ -19,7 +18,7 @@ export class ApiBlockHandler implements BlockHandler {
|
||||
block: SerializedBlock,
|
||||
inputs: Record<string, any>,
|
||||
context: ExecutionContext
|
||||
): Promise<BlockOutput> {
|
||||
): Promise<any> {
|
||||
const tool = getTool(block.config.tool)
|
||||
if (!tool) {
|
||||
throw new Error(`Tool not found: ${block.config.tool}`)
|
||||
@@ -27,7 +26,7 @@ export class ApiBlockHandler implements BlockHandler {
|
||||
|
||||
// Early return with empty success response if URL is not provided or empty
|
||||
if (tool.name?.includes('HTTP') && (!inputs.url || inputs.url.trim() === '')) {
|
||||
return { response: { content: '', success: true } }
|
||||
return { data: null, status: 200, headers: {} }
|
||||
}
|
||||
|
||||
// Pre-validate common HTTP request issues to provide better error messages
|
||||
@@ -154,7 +153,7 @@ export class ApiBlockHandler implements BlockHandler {
|
||||
throw error
|
||||
}
|
||||
|
||||
return { response: result.output }
|
||||
return result.output
|
||||
} catch (error: any) {
|
||||
// Ensure we have a meaningful error message
|
||||
if (!error.message || error.message === 'undefined (undefined)') {
|
||||
|
||||
@@ -96,7 +96,7 @@ describe('ConditionBlockHandler', () => {
|
||||
[
|
||||
mockSourceBlock.id,
|
||||
{
|
||||
output: { response: { value: 10, text: 'hello' } },
|
||||
output: { value: 10, text: 'hello' },
|
||||
executed: true,
|
||||
executionTime: 100,
|
||||
},
|
||||
@@ -129,32 +129,30 @@ describe('ConditionBlockHandler', () => {
|
||||
|
||||
it('should execute condition block correctly and select first path', async () => {
|
||||
const conditions = [
|
||||
{ id: 'cond1', title: 'if', value: 'context.response.value > 5' },
|
||||
{ id: 'cond1', title: 'if', value: 'context.value > 5' },
|
||||
{ id: 'else1', title: 'else', value: '' },
|
||||
]
|
||||
const inputs = { conditions: JSON.stringify(conditions) }
|
||||
|
||||
const expectedOutput = {
|
||||
response: {
|
||||
value: 10,
|
||||
text: 'hello',
|
||||
conditionResult: true,
|
||||
selectedPath: {
|
||||
blockId: mockTargetBlock1.id,
|
||||
blockType: 'target',
|
||||
blockTitle: 'Target Block 1',
|
||||
},
|
||||
selectedConditionId: 'cond1',
|
||||
value: 10,
|
||||
text: 'hello',
|
||||
conditionResult: true,
|
||||
selectedPath: {
|
||||
blockId: mockTargetBlock1.id,
|
||||
blockType: 'target',
|
||||
blockTitle: 'Target Block 1',
|
||||
},
|
||||
selectedConditionId: 'cond1',
|
||||
}
|
||||
|
||||
// Mock directly in the test
|
||||
mockResolver.resolveBlockReferences.mockReturnValue('context.response.value > 5')
|
||||
mockResolver.resolveBlockReferences.mockReturnValue('context.value > 5')
|
||||
|
||||
const result = (await handler.execute(mockBlock, inputs, mockContext)) as { response: any }
|
||||
const result = await handler.execute(mockBlock, inputs, mockContext)
|
||||
|
||||
expect(mockResolver.resolveBlockReferences).toHaveBeenCalledWith(
|
||||
'context.response.value > 5',
|
||||
'context.value > 5',
|
||||
mockContext,
|
||||
mockBlock
|
||||
)
|
||||
@@ -170,23 +168,21 @@ describe('ConditionBlockHandler', () => {
|
||||
const inputs = { conditions: JSON.stringify(conditions) }
|
||||
|
||||
const expectedOutput = {
|
||||
response: {
|
||||
value: 10,
|
||||
text: 'hello',
|
||||
conditionResult: true,
|
||||
selectedPath: {
|
||||
blockId: mockTargetBlock2.id,
|
||||
blockType: 'target',
|
||||
blockTitle: 'Target Block 2',
|
||||
},
|
||||
selectedConditionId: 'else1',
|
||||
value: 10,
|
||||
text: 'hello',
|
||||
conditionResult: true,
|
||||
selectedPath: {
|
||||
blockId: mockTargetBlock2.id,
|
||||
blockType: 'target',
|
||||
blockTitle: 'Target Block 2',
|
||||
},
|
||||
selectedConditionId: 'else1',
|
||||
}
|
||||
|
||||
// Mock directly in the test
|
||||
mockResolver.resolveBlockReferences.mockReturnValue('context.value < 0')
|
||||
|
||||
const result = (await handler.execute(mockBlock, inputs, mockContext)) as { response: any }
|
||||
const result = await handler.execute(mockBlock, inputs, mockContext)
|
||||
|
||||
expect(mockResolver.resolveBlockReferences).toHaveBeenCalledWith(
|
||||
'context.value < 0',
|
||||
@@ -207,7 +203,7 @@ describe('ConditionBlockHandler', () => {
|
||||
|
||||
it('should resolve references in conditions before evaluation', async () => {
|
||||
const conditions = [
|
||||
{ id: 'cond1', title: 'if', value: '{{source-block-1.response.value}} > 5' },
|
||||
{ id: 'cond1', title: 'if', value: '{{source-block-1.value}} > 5' },
|
||||
{ id: 'else1', title: 'else', value: '' },
|
||||
]
|
||||
const inputs = { conditions: JSON.stringify(conditions) }
|
||||
@@ -215,10 +211,10 @@ describe('ConditionBlockHandler', () => {
|
||||
// Mock directly in the test
|
||||
mockResolver.resolveBlockReferences.mockReturnValue('10 > 5')
|
||||
|
||||
const _result = (await handler.execute(mockBlock, inputs, mockContext)) as { response: any }
|
||||
const _result = await handler.execute(mockBlock, inputs, mockContext)
|
||||
|
||||
expect(mockResolver.resolveBlockReferences).toHaveBeenCalledWith(
|
||||
'{{source-block-1.response.value}} > 5',
|
||||
'{{source-block-1.value}} > 5',
|
||||
mockContext,
|
||||
mockBlock
|
||||
)
|
||||
@@ -320,9 +316,9 @@ describe('ConditionBlockHandler', () => {
|
||||
// Mock directly in the test
|
||||
mockResolver.resolveBlockReferences.mockReturnValue('context.item === "apple"')
|
||||
|
||||
const result = (await handler.execute(mockBlock, inputs, mockContext)) as { response: any }
|
||||
const result = await handler.execute(mockBlock, inputs, mockContext)
|
||||
|
||||
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('cond1')
|
||||
expect(result.response.selectedConditionId).toBe('cond1')
|
||||
expect((result as any).selectedConditionId).toBe('cond1')
|
||||
})
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user