From a5883171f922abacec4824c98f5ef05bbcbd60af Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Tue, 8 Jul 2025 22:07:48 -0700 Subject: [PATCH] Get user workflow tool --- .../app/api/tools/get-user-workflow/route.ts | 132 +++++++++ .../app/api/workflows/current/yaml/route.ts | 56 ---- .../app/api/workflows/yaml/convert/route.ts | 44 +++ apps/sim/lib/copilot/service.ts | 1 + apps/sim/lib/copilot/tools.ts | 41 +-- apps/sim/lib/workflows/yaml-generator.ts | 192 +++++++++++++ apps/sim/stores/workflows/yaml/store.ts | 256 ++++-------------- apps/sim/tools/workflow/get-yaml.ts | 4 +- 8 files changed, 445 insertions(+), 281 deletions(-) create mode 100644 apps/sim/app/api/tools/get-user-workflow/route.ts delete mode 100644 apps/sim/app/api/workflows/current/yaml/route.ts create mode 100644 apps/sim/app/api/workflows/yaml/convert/route.ts create mode 100644 apps/sim/lib/workflows/yaml-generator.ts diff --git a/apps/sim/app/api/tools/get-user-workflow/route.ts b/apps/sim/app/api/tools/get-user-workflow/route.ts new file mode 100644 index 0000000000..ae67b1882f --- /dev/null +++ b/apps/sim/app/api/tools/get-user-workflow/route.ts @@ -0,0 +1,132 @@ +import { NextRequest, NextResponse } from 'next/server' +import { createLogger } from '@/lib/logs/console-logger' +import { db } from '@/db' +import { workflow as workflowTable } from '@/db/schema' +import { eq } from 'drizzle-orm' +import { generateWorkflowYaml } from '@/lib/workflows/yaml-generator' +import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers' + +const logger = createLogger('GetUserWorkflowAPI') + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { workflowId, includeMetadata = false } = body + + if (!workflowId) { + return NextResponse.json( + { success: false, error: 'Workflow ID is required' }, + { status: 400 } + ) + } + + logger.info('Fetching workflow for YAML generation', { workflowId }) + + // Fetch workflow from database + const [workflowRecord] = await db + .select() + .from(workflowTable) + .where(eq(workflowTable.id, workflowId)) + .limit(1) + + if (!workflowRecord) { + return NextResponse.json( + { success: false, error: `Workflow ${workflowId} not found` }, + { status: 404 } + ) + } + + // Try to load from normalized tables first, fallback to JSON blob + let workflowState: any = null + let subBlockValues: Record> = {} + + const normalizedData = await loadWorkflowFromNormalizedTables(workflowId) + if (normalizedData) { + workflowState = { + blocks: normalizedData.blocks, + edges: normalizedData.edges, + loops: normalizedData.loops, + parallels: normalizedData.parallels, + } + + // Extract subblock values from normalized data + Object.entries(normalizedData.blocks).forEach(([blockId, block]) => { + subBlockValues[blockId] = {} + Object.entries((block as any).subBlocks || {}).forEach(([subBlockId, subBlock]) => { + if ((subBlock as any).value !== undefined) { + subBlockValues[blockId][subBlockId] = (subBlock as any).value + } + }) + }) + } else if (workflowRecord.state) { + // Fallback to JSON blob + workflowState = workflowRecord.state as any + // For JSON blob, subblock values are embedded in the block state + Object.entries((workflowState.blocks as any) || {}).forEach(([blockId, block]) => { + subBlockValues[blockId] = {} + Object.entries(((block as any).subBlocks || {})).forEach(([subBlockId, subBlock]) => { + if ((subBlock as any).value !== undefined) { + subBlockValues[blockId][subBlockId] = (subBlock as any).value + } + }) + }) + } + + if (!workflowState || !workflowState.blocks) { + return NextResponse.json( + { success: false, error: 'Workflow state is empty or invalid' }, + { status: 400 } + ) + } + + // Generate YAML using server-side function + const yaml = generateWorkflowYaml(workflowState, subBlockValues) + + if (!yaml || yaml.trim() === '') { + return NextResponse.json( + { success: false, error: 'Generated YAML is empty' }, + { status: 400 } + ) + } + + // Prepare response + const response: any = { + yaml, + format: 'yaml', + blockCount: Object.keys(workflowState.blocks).length, + edgeCount: (workflowState.edges || []).length, + } + + // Add metadata if requested + if (includeMetadata) { + response.metadata = { + workflowId: workflowRecord.id, + name: workflowRecord.name, + description: workflowRecord.description, + workspaceId: workflowRecord.workspaceId, + createdAt: workflowRecord.createdAt, + updatedAt: workflowRecord.updatedAt, + } + } + + logger.info('Successfully generated workflow YAML', { + workflowId, + blockCount: response.blockCount, + yamlLength: yaml.length, + }) + + return NextResponse.json({ + success: true, + output: response, + }) + } catch (error) { + logger.error('Failed to get workflow YAML:', error) + return NextResponse.json( + { + success: false, + error: `Failed to get workflow YAML: ${error instanceof Error ? error.message : 'Unknown error'}`, + }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/apps/sim/app/api/workflows/current/yaml/route.ts b/apps/sim/app/api/workflows/current/yaml/route.ts deleted file mode 100644 index f1996267aa..0000000000 --- a/apps/sim/app/api/workflows/current/yaml/route.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { type NextRequest, NextResponse } from 'next/server' -import { createLogger } from '@/lib/logs/console-logger' -import { useWorkflowYamlStore } from '@/stores/workflows/yaml/store' - -const logger = createLogger('GetWorkflowYamlAPI') - -export async function POST(request: NextRequest) { - try { - const { includeMetadata = false } = await request.json() - - logger.info('Executing get user workflow', { includeMetadata }) - - // Get the workflow YAML using the same store as the UI - const yamlStore = useWorkflowYamlStore.getState() - const yamlContent = yamlStore.getYaml() - - if (!yamlContent) { - return NextResponse.json( - { success: false, error: 'No workflow content available' }, - { status: 404 } - ) - } - - let metadata - if (includeMetadata) { - // Get additional workflow metadata if requested - const workflowStore = yamlStore as any // Access internal state - metadata = { - name: workflowStore.workflow?.name || 'Unnamed Workflow', - description: workflowStore.workflow?.description || '', - createdAt: workflowStore.workflow?.createdAt, - updatedAt: workflowStore.workflow?.updatedAt, - } - } - - logger.info('Successfully generated workflow YAML', { - includeMetadata, - yamlLength: yamlContent.length, - }) - - return NextResponse.json({ - success: true, - yaml: yamlContent, - metadata: metadata, - }) - } catch (error) { - logger.error('Get user workflow API failed', error) - return NextResponse.json( - { - success: false, - error: `Failed to get user workflow: ${error instanceof Error ? error.message : 'Unknown error'}`, - }, - { status: 500 } - ) - } -} diff --git a/apps/sim/app/api/workflows/yaml/convert/route.ts b/apps/sim/app/api/workflows/yaml/convert/route.ts new file mode 100644 index 0000000000..7bb70b7de8 --- /dev/null +++ b/apps/sim/app/api/workflows/yaml/convert/route.ts @@ -0,0 +1,44 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { createLogger } from '@/lib/logs/console-logger' +import { generateWorkflowYaml } from '@/lib/workflows/yaml-generator' + +const logger = createLogger('WorkflowYamlAPI') + +export async function POST(request: NextRequest) { + const requestId = crypto.randomUUID().slice(0, 8) + + try { + logger.info(`[${requestId}] Converting workflow JSON to YAML`) + + const body = await request.json() + const { workflowState, subBlockValues, includeMetadata = false } = body + + if (!workflowState) { + return NextResponse.json( + { success: false, error: 'workflowState is required' }, + { status: 400 } + ) + } + + // Generate YAML using the shared utility + const yamlContent = generateWorkflowYaml(workflowState, subBlockValues) + + logger.info(`[${requestId}] Successfully generated YAML`, { + yamlLength: yamlContent.length, + }) + + return NextResponse.json({ + success: true, + yaml: yamlContent, + }) + } catch (error) { + logger.error(`[${requestId}] YAML generation failed`, error) + return NextResponse.json( + { + success: false, + error: `Failed to generate YAML: ${error instanceof Error ? error.message : 'Unknown error'}`, + }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/apps/sim/lib/copilot/service.ts b/apps/sim/lib/copilot/service.ts index 55576f32ba..04fb121001 100644 --- a/apps/sim/lib/copilot/service.ts +++ b/apps/sim/lib/copilot/service.ts @@ -460,6 +460,7 @@ export async function generateChatResponse( maxTokens: config.chat.maxTokens, apiKey, stream, + workflowId: options.workflowId, }) // Handle StreamingExecution (from providers with tool calls) diff --git a/apps/sim/lib/copilot/tools.ts b/apps/sim/lib/copilot/tools.ts index f7a87db4cb..12552e6d56 100644 --- a/apps/sim/lib/copilot/tools.ts +++ b/apps/sim/lib/copilot/tools.ts @@ -80,43 +80,32 @@ const getUserWorkflowTool: CopilotTool = { 'Get the current user workflow as YAML format. This shows all blocks, their configurations, inputs, and connections in the workflow.', parameters: { type: 'object', - properties: { - includeMetadata: { - type: 'boolean', - description: 'Whether to include additional metadata about the workflow (default: false)', - default: false, - }, - }, + properties: {}, required: [], }, execute: async (args: Record): Promise => { try { - const { includeMetadata = false } = args - - logger.info('Executing get user workflow', { includeMetadata }) + logger.info('Executing get user workflow') // Import the workflow YAML store dynamically to avoid import issues const { useWorkflowYamlStore } = await import('@/stores/workflows/yaml/store') const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store') - // Get the current workflow YAML + // Get the current workflow YAML using the same logic as export const yamlContent = useWorkflowYamlStore.getState().getYaml() - // Get additional metadata if requested - let metadata = {} - if (includeMetadata) { - const registry = useWorkflowRegistry.getState() - const activeWorkflowId = registry.activeWorkflowId - const activeWorkflow = activeWorkflowId ? registry.workflows[activeWorkflowId] : null + // Get workflow metadata + const registry = useWorkflowRegistry.getState() + const activeWorkflowId = registry.activeWorkflowId + const activeWorkflow = activeWorkflowId ? registry.workflows[activeWorkflowId] : null - if (activeWorkflow) { - metadata = { - workflowId: activeWorkflowId, - name: activeWorkflow.name, - description: activeWorkflow.description, - lastModified: activeWorkflow.lastModified, - workspaceId: activeWorkflow.workspaceId, - } + let metadata = undefined + if (activeWorkflow) { + metadata = { + workflowId: activeWorkflowId, + name: activeWorkflow.name, + description: activeWorkflow.description, + workspaceId: activeWorkflow.workspaceId, } } @@ -126,7 +115,7 @@ const getUserWorkflowTool: CopilotTool = { success: true, data: { yaml: yamlContent, - metadata: includeMetadata ? metadata : undefined, + metadata: metadata, }, } } catch (error) { diff --git a/apps/sim/lib/workflows/yaml-generator.ts b/apps/sim/lib/workflows/yaml-generator.ts new file mode 100644 index 0000000000..f216d4d064 --- /dev/null +++ b/apps/sim/lib/workflows/yaml-generator.ts @@ -0,0 +1,192 @@ +import { dump as yamlDump } from 'js-yaml' +import { createLogger } from '@/lib/logs/console-logger' +import { getBlock } from '@/blocks' +import type { SubBlockConfig } from '@/blocks/types' +import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types' + +const logger = createLogger('WorkflowYamlGenerator') + +interface YamlBlock { + type: string + name: string + inputs?: Record + preceding?: string[] + following?: string[] +} + +interface YamlWorkflow { + version: string + blocks: Record +} + +/** + * Extract input values from a block's subBlocks based on its configuration + * This version works without client-side stores by using the provided subblock values + */ +function extractBlockInputs( + blockState: BlockState, + blockId: string, + subBlockValues?: Record> +): Record { + const blockConfig = getBlock(blockState.type) + const inputs: Record = {} + + // Get subblock values for this block (if provided) + const blockSubBlockValues = subBlockValues?.[blockId] || {} + + if (!blockConfig) { + // For custom blocks like loops/parallels, extract available subBlock values + Object.entries(blockState.subBlocks || {}).forEach(([subBlockId, subBlockState]) => { + const value = blockSubBlockValues[subBlockId] ?? subBlockState.value + if (value !== undefined && value !== null && value !== '') { + inputs[subBlockId] = value + } + }) + return inputs + } + + // Process each subBlock configuration + blockConfig.subBlocks.forEach((subBlockConfig: SubBlockConfig) => { + const subBlockId = subBlockConfig.id + + // Skip hidden or conditional fields that aren't active + if (subBlockConfig.hidden) return + + // Get value from provided values or fallback to block state + const value = blockSubBlockValues[subBlockId] ?? blockState.subBlocks[subBlockId]?.value + + // Include value if it exists and isn't empty + if (value !== undefined && value !== null && value !== '') { + // Handle different input types appropriately + switch (subBlockConfig.type) { + case 'table': + // Tables are arrays of objects + if (Array.isArray(value) && value.length > 0) { + inputs[subBlockId] = value + } + break + + case 'checkbox-list': + // Checkbox lists return arrays + if (Array.isArray(value) && value.length > 0) { + inputs[subBlockId] = value + } + break + + case 'code': + // Code blocks should preserve formatting + if (typeof value === 'string' && value.trim()) { + inputs[subBlockId] = value + } else if (typeof value === 'object') { + inputs[subBlockId] = value + } + break + + case 'switch': + // Boolean values + inputs[subBlockId] = Boolean(value) + break + + case 'slider': + // Numeric values + if ( + typeof value === 'number' || + (typeof value === 'string' && !Number.isNaN(Number(value))) + ) { + inputs[subBlockId] = Number(value) + } + break + + default: + // Text inputs, dropdowns, etc. + if (typeof value === 'string' && value.trim()) { + inputs[subBlockId] = value.trim() + } else if ( + typeof value === 'object' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + inputs[subBlockId] = value + } + break + } + } + }) + + return inputs +} + +/** + * Find preceding blocks for a given block ID + */ +function findPrecedingBlocks(blockId: string, edges: any[]): string[] { + return edges + .filter((edge) => edge.target === blockId) + .map((edge) => edge.source) + .filter((source, index, arr) => arr.indexOf(source) === index) // Remove duplicates +} + +/** + * Find following blocks for a given block ID + */ +function findFollowingBlocks(blockId: string, edges: any[]): string[] { + return edges + .filter((edge) => edge.source === blockId) + .map((edge) => edge.target) + .filter((target, index, arr) => arr.indexOf(target) === index) // Remove duplicates +} + +/** + * Generate YAML representation of the workflow + * This is the core function extracted from the client store, made server-compatible + */ +export function generateWorkflowYaml( + workflowState: WorkflowState, + subBlockValues?: Record> +): string { + try { + const yamlWorkflow: YamlWorkflow = { + version: '1.0', + blocks: {}, + } + + // Process each block + Object.entries(workflowState.blocks).forEach(([blockId, blockState]) => { + const inputs = extractBlockInputs(blockState, blockId, subBlockValues) + const preceding = findPrecedingBlocks(blockId, workflowState.edges) + const following = findFollowingBlocks(blockId, workflowState.edges) + + const yamlBlock: YamlBlock = { + type: blockState.type, + name: blockState.name, + } + + // Only include inputs if they exist + if (Object.keys(inputs).length > 0) { + yamlBlock.inputs = inputs + } + + // Only include connections if they exist + if (preceding.length > 0) { + yamlBlock.preceding = preceding + } + + if (following.length > 0) { + yamlBlock.following = following + } + + yamlWorkflow.blocks[blockId] = yamlBlock + }) + + // Convert to YAML with clean formatting + return yamlDump(yamlWorkflow, { + indent: 2, + lineWidth: -1, // Disable line wrapping + noRefs: true, + sortKeys: false, + }) + } catch (error) { + logger.error('Failed to generate workflow YAML:', error) + return `# Error generating YAML: ${error instanceof Error ? error.message : 'Unknown error'}` + } +} \ No newline at end of file diff --git a/apps/sim/stores/workflows/yaml/store.ts b/apps/sim/stores/workflows/yaml/store.ts index 8baea4649c..5e02099879 100644 --- a/apps/sim/stores/workflows/yaml/store.ts +++ b/apps/sim/stores/workflows/yaml/store.ts @@ -1,28 +1,12 @@ -import { dump as yamlDump } from 'js-yaml' import { create } from 'zustand' import { devtools } from 'zustand/middleware' import { createLogger } from '@/lib/logs/console-logger' -import { getBlock } from '@/blocks' -import type { SubBlockConfig } from '@/blocks/types' +import { generateWorkflowYaml } from '@/lib/workflows/yaml-generator' import { useSubBlockStore } from '../subblock/store' import { useWorkflowStore } from '../workflow/store' -import type { BlockState, WorkflowState } from '../workflow/types' const logger = createLogger('WorkflowYamlStore') -interface YamlBlock { - type: string - name: string - inputs?: Record - preceding?: string[] - following?: string[] -} - -interface YamlWorkflow { - version: string - blocks: Record -} - interface WorkflowYamlState { yaml: string lastGenerated?: number @@ -37,165 +21,72 @@ interface WorkflowYamlActions { type WorkflowYamlStore = WorkflowYamlState & WorkflowYamlActions /** - * Extract input values from a block's subBlocks based on its configuration + * Get subblock values organized by block for the shared utility */ -function extractBlockInputs(blockState: BlockState, blockId: string): Record { - const blockConfig = getBlock(blockState.type) +function getSubBlockValues() { + const workflowState = useWorkflowStore.getState() const subBlockStore = useSubBlockStore.getState() - const inputs: Record = {} - - if (!blockConfig) { - // For custom blocks like loops/parallels, extract available subBlock values - Object.entries(blockState.subBlocks || {}).forEach(([subBlockId, subBlockState]) => { - const value = subBlockStore.getValue(blockId, subBlockId) ?? subBlockState.value - if (value !== undefined && value !== null && value !== '') { - inputs[subBlockId] = value + + const subBlockValues: Record> = {} + Object.entries(workflowState.blocks).forEach(([blockId]) => { + subBlockValues[blockId] = {} + // Get all subblock values for this block + Object.keys(workflowState.blocks[blockId].subBlocks || {}).forEach((subBlockId) => { + const value = subBlockStore.getValue(blockId, subBlockId) + if (value !== undefined) { + subBlockValues[blockId][subBlockId] = value } }) - return inputs - } + }) + + return subBlockValues +} - // Process each subBlock configuration - blockConfig.subBlocks.forEach((subBlockConfig: SubBlockConfig) => { - const subBlockId = subBlockConfig.id +// Track if subscriptions have been initialized +let subscriptionsInitialized = false - // Skip hidden or conditional fields that aren't active - if (subBlockConfig.hidden) return +// Initialize subscriptions lazily +function initializeSubscriptions() { + if (subscriptionsInitialized) return + subscriptionsInitialized = true - // Get value from subblock store or fallback to block state - const value = - subBlockStore.getValue(blockId, subBlockId) ?? blockState.subBlocks[subBlockId]?.value + // Auto-refresh YAML when workflow state changes + let lastWorkflowState: { blockCount: number; edgeCount: number } | null = null - // Include value if it exists and isn't empty - if (value !== undefined && value !== null && value !== '') { - // Handle different input types appropriately - switch (subBlockConfig.type) { - case 'table': - // Tables are arrays of objects - if (Array.isArray(value) && value.length > 0) { - inputs[subBlockId] = value - } - break + useWorkflowStore.subscribe((state) => { + const currentState = { + blockCount: Object.keys(state.blocks).length, + edgeCount: state.edges.length, + } - case 'checkbox-list': - // Checkbox lists return arrays - if (Array.isArray(value) && value.length > 0) { - inputs[subBlockId] = value - } - break + // Only refresh if the structure has changed + if ( + !lastWorkflowState || + lastWorkflowState.blockCount !== currentState.blockCount || + lastWorkflowState.edgeCount !== currentState.edgeCount + ) { + lastWorkflowState = currentState - case 'code': - // Code blocks should preserve formatting - if (typeof value === 'string' && value.trim()) { - inputs[subBlockId] = value - } else if (typeof value === 'object') { - inputs[subBlockId] = value - } - break - - case 'switch': - // Boolean values - inputs[subBlockId] = Boolean(value) - break - - case 'slider': - // Numeric values - if ( - typeof value === 'number' || - (typeof value === 'string' && !Number.isNaN(Number(value))) - ) { - inputs[subBlockId] = Number(value) - } - break - - default: - // Text inputs, dropdowns, etc. - if (typeof value === 'string' && value.trim()) { - inputs[subBlockId] = value.trim() - } else if ( - typeof value === 'object' || - typeof value === 'number' || - typeof value === 'boolean' - ) { - inputs[subBlockId] = value - } - break - } + // Debounce the refresh to avoid excessive updates + const refreshYaml = useWorkflowYamlStore.getState().refreshYaml + setTimeout(refreshYaml, 100) } }) - return inputs -} + // Subscribe to subblock store changes + let lastSubBlockChangeTime = 0 -/** - * Find preceding blocks for a given block ID - */ -function findPrecedingBlocks(blockId: string, edges: any[]): string[] { - return edges - .filter((edge) => edge.target === blockId) - .map((edge) => edge.source) - .filter((source, index, arr) => arr.indexOf(source) === index) // Remove duplicates -} + useSubBlockStore.subscribe((state) => { + const currentTime = Date.now() -/** - * Find following blocks for a given block ID - */ -function findFollowingBlocks(blockId: string, edges: any[]): string[] { - return edges - .filter((edge) => edge.source === blockId) - .map((edge) => edge.target) - .filter((target, index, arr) => arr.indexOf(target) === index) // Remove duplicates -} + // Debounce rapid changes + if (currentTime - lastSubBlockChangeTime > 100) { + lastSubBlockChangeTime = currentTime -/** - * Generate YAML representation of the workflow - */ -function generateWorkflowYaml(workflowState: WorkflowState): string { - try { - const yamlWorkflow: YamlWorkflow = { - version: '1.0', - blocks: {}, + const refreshYaml = useWorkflowYamlStore.getState().refreshYaml + setTimeout(refreshYaml, 100) } - - // Process each block - Object.entries(workflowState.blocks).forEach(([blockId, blockState]) => { - const inputs = extractBlockInputs(blockState, blockId) - const preceding = findPrecedingBlocks(blockId, workflowState.edges) - const following = findFollowingBlocks(blockId, workflowState.edges) - - const yamlBlock: YamlBlock = { - type: blockState.type, - name: blockState.name, - } - - // Only include inputs if they exist - if (Object.keys(inputs).length > 0) { - yamlBlock.inputs = inputs - } - - // Only include connections if they exist - if (preceding.length > 0) { - yamlBlock.preceding = preceding - } - - if (following.length > 0) { - yamlBlock.following = following - } - - yamlWorkflow.blocks[blockId] = yamlBlock - }) - - // Convert to YAML with clean formatting - return yamlDump(yamlWorkflow, { - indent: 2, - lineWidth: -1, // Disable line wrapping - noRefs: true, - sortKeys: false, - }) - } catch (error) { - logger.error('Failed to generate workflow YAML:', error) - return `# Error generating YAML: ${error instanceof Error ? error.message : 'Unknown error'}` - } + }) } export const useWorkflowYamlStore = create()( @@ -205,8 +96,12 @@ export const useWorkflowYamlStore = create()( lastGenerated: undefined, generateYaml: () => { + // Initialize subscriptions on first use + initializeSubscriptions() + const workflowState = useWorkflowStore.getState() - const yaml = generateWorkflowYaml(workflowState) + const subBlockValues = getSubBlockValues() + const yaml = generateWorkflowYaml(workflowState, subBlockValues) set({ yaml, @@ -215,6 +110,9 @@ export const useWorkflowYamlStore = create()( }, getYaml: () => { + // Initialize subscriptions on first use + initializeSubscriptions() + const currentTime = Date.now() const { yaml, lastGenerated } = get() @@ -236,41 +134,3 @@ export const useWorkflowYamlStore = create()( } ) ) - -// Auto-refresh YAML when workflow state changes -let lastWorkflowState: { blockCount: number; edgeCount: number } | null = null - -useWorkflowStore.subscribe((state) => { - const currentState = { - blockCount: Object.keys(state.blocks).length, - edgeCount: state.edges.length, - } - - // Only refresh if the structure has changed - if ( - !lastWorkflowState || - lastWorkflowState.blockCount !== currentState.blockCount || - lastWorkflowState.edgeCount !== currentState.edgeCount - ) { - lastWorkflowState = currentState - - // Debounce the refresh to avoid excessive updates - const refreshYaml = useWorkflowYamlStore.getState().refreshYaml - setTimeout(refreshYaml, 100) - } -}) - -// Subscribe to subblock store changes -let lastSubBlockChangeTime = 0 - -useSubBlockStore.subscribe((state) => { - const currentTime = Date.now() - - // Debounce rapid changes - if (currentTime - lastSubBlockChangeTime > 100) { - lastSubBlockChangeTime = currentTime - - const refreshYaml = useWorkflowYamlStore.getState().refreshYaml - setTimeout(refreshYaml, 100) - } -}) diff --git a/apps/sim/tools/workflow/get-yaml.ts b/apps/sim/tools/workflow/get-yaml.ts index 2b48b686f7..2374d6cdb2 100644 --- a/apps/sim/tools/workflow/get-yaml.ts +++ b/apps/sim/tools/workflow/get-yaml.ts @@ -15,13 +15,15 @@ export const getUserWorkflowTool: ToolConfig = { }, }, + // Use API endpoint to avoid Node.js module import issues in browser request: { - url: '/api/workflows/current/yaml', + url: '/api/tools/get-user-workflow', method: 'POST', headers: () => ({ 'Content-Type': 'application/json', }), body: (params) => ({ + workflowId: params._context?.workflowId, includeMetadata: params.includeMetadata || false, }), isInternalRoute: true,