mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
Get user workflow tool
This commit is contained in:
132
apps/sim/app/api/tools/get-user-workflow/route.ts
Normal file
132
apps/sim/app/api/tools/get-user-workflow/route.ts
Normal file
@@ -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<string, Record<string, any>> = {}
|
||||
|
||||
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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
44
apps/sim/app/api/workflows/yaml/convert/route.ts
Normal file
44
apps/sim/app/api/workflows/yaml/convert/route.ts
Normal file
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -460,6 +460,7 @@ export async function generateChatResponse(
|
||||
maxTokens: config.chat.maxTokens,
|
||||
apiKey,
|
||||
stream,
|
||||
workflowId: options.workflowId,
|
||||
})
|
||||
|
||||
// Handle StreamingExecution (from providers with tool calls)
|
||||
|
||||
@@ -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<string, any>): Promise<CopilotToolResult> => {
|
||||
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) {
|
||||
|
||||
192
apps/sim/lib/workflows/yaml-generator.ts
Normal file
192
apps/sim/lib/workflows/yaml-generator.ts
Normal file
@@ -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<string, any>
|
||||
preceding?: string[]
|
||||
following?: string[]
|
||||
}
|
||||
|
||||
interface YamlWorkflow {
|
||||
version: string
|
||||
blocks: Record<string, YamlBlock>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<string, Record<string, any>>
|
||||
): Record<string, any> {
|
||||
const blockConfig = getBlock(blockState.type)
|
||||
const inputs: Record<string, any> = {}
|
||||
|
||||
// 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, Record<string, any>>
|
||||
): 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'}`
|
||||
}
|
||||
}
|
||||
@@ -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<string, any>
|
||||
preceding?: string[]
|
||||
following?: string[]
|
||||
}
|
||||
|
||||
interface YamlWorkflow {
|
||||
version: string
|
||||
blocks: Record<string, YamlBlock>
|
||||
}
|
||||
|
||||
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<string, any> {
|
||||
const blockConfig = getBlock(blockState.type)
|
||||
function getSubBlockValues() {
|
||||
const workflowState = useWorkflowStore.getState()
|
||||
const subBlockStore = useSubBlockStore.getState()
|
||||
const inputs: Record<string, any> = {}
|
||||
|
||||
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<string, Record<string, any>> = {}
|
||||
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<WorkflowYamlStore>()(
|
||||
@@ -205,8 +96,12 @@ export const useWorkflowYamlStore = create<WorkflowYamlStore>()(
|
||||
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<WorkflowYamlStore>()(
|
||||
},
|
||||
|
||||
getYaml: () => {
|
||||
// Initialize subscriptions on first use
|
||||
initializeSubscriptions()
|
||||
|
||||
const currentTime = Date.now()
|
||||
const { yaml, lastGenerated } = get()
|
||||
|
||||
@@ -236,41 +134,3 @@ export const useWorkflowYamlStore = create<WorkflowYamlStore>()(
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user