mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-12 16:38:15 -05:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3960ad77a |
@@ -4,6 +4,7 @@ import { createLogger } from '@/lib/logs/console-logger'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const runtime = 'nodejs'
|
||||
export const maxDuration = 60
|
||||
|
||||
const logger = createLogger('FunctionExecuteAPI')
|
||||
|
||||
@@ -14,45 +15,14 @@ const logger = createLogger('FunctionExecuteAPI')
|
||||
* @param envVars - Environment variables from the workflow
|
||||
* @returns Resolved code
|
||||
*/
|
||||
/**
|
||||
* Safely serialize a value to JSON string with proper escaping
|
||||
* This prevents JavaScript syntax errors when the serialized data is injected into code
|
||||
*/
|
||||
function safeJSONStringify(value: any): string {
|
||||
try {
|
||||
// Use JSON.stringify with proper escaping
|
||||
// The key is to let JSON.stringify handle the escaping properly
|
||||
return JSON.stringify(value)
|
||||
} catch (error) {
|
||||
// If JSON.stringify fails (e.g., circular references), return a safe fallback
|
||||
try {
|
||||
// Try to create a safe representation by removing circular references
|
||||
const seen = new WeakSet()
|
||||
const cleanValue = JSON.parse(
|
||||
JSON.stringify(value, (key, val) => {
|
||||
if (typeof val === 'object' && val !== null) {
|
||||
if (seen.has(val)) {
|
||||
return '[Circular Reference]'
|
||||
}
|
||||
seen.add(val)
|
||||
}
|
||||
return val
|
||||
})
|
||||
)
|
||||
return JSON.stringify(cleanValue)
|
||||
} catch {
|
||||
// If that also fails, return a safe string representation
|
||||
return JSON.stringify(String(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveCodeVariables(
|
||||
code: string,
|
||||
params: Record<string, any>,
|
||||
envVars: Record<string, string> = {}
|
||||
): string {
|
||||
): { resolvedCode: string; contextVariables: Record<string, any> } {
|
||||
let resolvedCode = code
|
||||
const contextVariables: Record<string, any> = {}
|
||||
|
||||
// Resolve environment variables with {{var_name}} syntax
|
||||
const envVarMatches = resolvedCode.match(/\{\{([^}]+)\}\}/g) || []
|
||||
@@ -60,11 +30,13 @@ function resolveCodeVariables(
|
||||
const varName = match.slice(2, -2).trim()
|
||||
// Priority: 1. Environment variables from workflow, 2. Params
|
||||
const varValue = envVars[varName] || params[varName] || ''
|
||||
// Use safe JSON stringify to prevent syntax errors
|
||||
resolvedCode = resolvedCode.replace(
|
||||
new RegExp(escapeRegExp(match), 'g'),
|
||||
safeJSONStringify(varValue)
|
||||
)
|
||||
|
||||
// Instead of injecting large JSON directly, create a variable reference
|
||||
const safeVarName = `__var_${varName.replace(/[^a-zA-Z0-9_]/g, '_')}`
|
||||
contextVariables[safeVarName] = varValue
|
||||
|
||||
// Replace the template with a variable reference
|
||||
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
|
||||
}
|
||||
|
||||
// Resolve tags with <tag_name> syntax
|
||||
@@ -72,13 +44,16 @@ function resolveCodeVariables(
|
||||
for (const match of tagMatches) {
|
||||
const tagName = match.slice(1, -1).trim()
|
||||
const tagValue = params[tagName] || ''
|
||||
resolvedCode = resolvedCode.replace(
|
||||
new RegExp(escapeRegExp(match), 'g'),
|
||||
safeJSONStringify(tagValue)
|
||||
)
|
||||
|
||||
// Instead of injecting large JSON directly, create a variable reference
|
||||
const safeVarName = `__tag_${tagName.replace(/[^a-zA-Z0-9_]/g, '_')}`
|
||||
contextVariables[safeVarName] = tagValue
|
||||
|
||||
// Replace the template with a variable reference
|
||||
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
|
||||
}
|
||||
|
||||
return resolvedCode
|
||||
return { resolvedCode, contextVariables }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,7 +93,7 @@ export async function POST(req: NextRequest) {
|
||||
})
|
||||
|
||||
// Resolve variables in the code with workflow environment variables
|
||||
const resolvedCode = resolveCodeVariables(code, executionParams, envVars)
|
||||
const { resolvedCode, contextVariables } = resolveCodeVariables(code, executionParams, envVars)
|
||||
|
||||
const executionMethod = 'vm' // Default execution method
|
||||
|
||||
@@ -280,6 +255,7 @@ export async function POST(req: NextRequest) {
|
||||
const context = createContext({
|
||||
params: executionParams,
|
||||
environmentVariables: envVars,
|
||||
...contextVariables, // Add resolved variables directly to context
|
||||
fetch: globalThis.fetch || require('node-fetch').default,
|
||||
console: {
|
||||
log: (...args: any[]) => {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface ConnectionStatusProps {
|
||||
isConnected: boolean
|
||||
}
|
||||
|
||||
export function ConnectionStatus({ isConnected }: ConnectionStatusProps) {
|
||||
const [showOfflineNotice, setShowOfflineNotice] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout
|
||||
|
||||
if (!isConnected) {
|
||||
// Show offline notice after 6 seconds of being disconnected
|
||||
timeoutId = setTimeout(() => {
|
||||
setShowOfflineNotice(true)
|
||||
}, 6000) // 6 seconds
|
||||
} else {
|
||||
// Hide notice immediately when reconnected
|
||||
setShowOfflineNotice(false)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
}, [isConnected])
|
||||
|
||||
// Don't render anything if connected or if we haven't been disconnected long enough
|
||||
if (!showOfflineNotice) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<div className='flex items-center gap-1.5 text-red-600'>
|
||||
<div className='relative flex items-center justify-center'>
|
||||
<div className='absolute h-3 w-3 animate-ping rounded-full bg-red-500/20' />
|
||||
<div className='relative h-2 w-2 rounded-full bg-red-500' />
|
||||
</div>
|
||||
<div className='flex flex-col'>
|
||||
<span className='font-medium text-xs leading-tight'>Connection lost</span>
|
||||
<span className='text-xs leading-tight opacity-90'>
|
||||
Changes not saved - please refresh
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { usePresence } from '../../../../hooks/use-presence'
|
||||
import { ConnectionStatus } from './components/connection-status/connection-status'
|
||||
import { UserAvatar } from './components/user-avatar/user-avatar'
|
||||
|
||||
interface User {
|
||||
@@ -25,7 +26,7 @@ export function UserAvatarStack({
|
||||
className = '',
|
||||
}: UserAvatarStackProps) {
|
||||
// Use presence data if no users are provided via props
|
||||
const { users: presenceUsers } = usePresence()
|
||||
const { users: presenceUsers, isConnected } = usePresence()
|
||||
const users = propUsers || presenceUsers
|
||||
|
||||
// Memoize the processed users to avoid unnecessary re-renders
|
||||
@@ -43,10 +44,14 @@ export function UserAvatarStack({
|
||||
}
|
||||
}, [users, maxVisible])
|
||||
|
||||
// Show connection status component regardless of user count
|
||||
// This will handle the offline notice when disconnected for 15 seconds
|
||||
const connectionStatusElement = <ConnectionStatus isConnected={isConnected} />
|
||||
|
||||
// Only show presence when there are multiple users (>1)
|
||||
// Don't render anything if there are no users or only 1 user
|
||||
// But always show connection status
|
||||
if (users.length <= 1) {
|
||||
return null
|
||||
return connectionStatusElement
|
||||
}
|
||||
|
||||
// Determine spacing based on size
|
||||
@@ -58,6 +63,9 @@ export function UserAvatarStack({
|
||||
|
||||
return (
|
||||
<div className={`flex items-center ${spacingClass} ${className}`}>
|
||||
{/* Connection status - always present */}
|
||||
{connectionStatusElement}
|
||||
|
||||
{/* Render visible user avatars */}
|
||||
{visibleUsers.map((user, index) => (
|
||||
<UserAvatar
|
||||
|
||||
@@ -683,7 +683,6 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<UserAvatarStack className='ml-3' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1275,8 +1274,10 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
|
||||
{/* Left Section - Workflow Info */}
|
||||
<div className='pl-4'>{renderWorkflowName()}</div>
|
||||
|
||||
{/* Middle Section - Reserved for future use */}
|
||||
<div className='flex-1' />
|
||||
{/* Middle Section - Connection Status */}
|
||||
<div className='flex flex-1 justify-center'>
|
||||
<UserAvatarStack />
|
||||
</div>
|
||||
|
||||
{/* Right Section - Actions */}
|
||||
<div className='flex items-center gap-1 pr-4'>
|
||||
|
||||
@@ -44,9 +44,7 @@ export class LoopBlockHandler implements BlockHandler {
|
||||
}
|
||||
|
||||
const currentIteration = context.loopIterations.get(block.id) || 0
|
||||
let maxIterations = loop.iterations || DEFAULT_MAX_ITERATIONS
|
||||
|
||||
// For forEach loops, we need to check the actual items length
|
||||
let maxIterations: number
|
||||
let forEachItems: any[] | Record<string, any> | null = null
|
||||
if (loop.loopType === 'forEach') {
|
||||
if (
|
||||
@@ -71,14 +69,19 @@ export class LoopBlockHandler implements BlockHandler {
|
||||
)
|
||||
}
|
||||
|
||||
// Adjust max iterations based on actual items
|
||||
// For forEach, max iterations = items length
|
||||
const itemsLength = Array.isArray(forEachItems)
|
||||
? forEachItems.length
|
||||
: Object.keys(forEachItems).length
|
||||
maxIterations = Math.min(maxIterations, itemsLength)
|
||||
|
||||
maxIterations = itemsLength
|
||||
|
||||
logger.info(
|
||||
`Loop ${block.id} max iterations set to ${maxIterations} based on ${itemsLength} items`
|
||||
`forEach loop ${block.id} - Items: ${itemsLength}, Max iterations: ${maxIterations}`
|
||||
)
|
||||
} else {
|
||||
maxIterations = loop.iterations || DEFAULT_MAX_ITERATIONS
|
||||
logger.info(`For loop ${block.id} - Max iterations: ${maxIterations}`)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
|
||||
@@ -81,7 +81,7 @@ export class LoopManager {
|
||||
// Determine the maximum iterations
|
||||
let maxIterations = loop.iterations || this.defaultIterations
|
||||
|
||||
// For forEach loops, check the actual items length
|
||||
// For forEach loops, use the actual items length
|
||||
if (loop.loopType === 'forEach' && loop.forEachItems) {
|
||||
// First check if the items have already been evaluated and stored by the loop handler
|
||||
const storedItems = context.loopItems.get(`${loopId}_items`)
|
||||
@@ -89,15 +89,18 @@ export class LoopManager {
|
||||
const itemsLength = Array.isArray(storedItems)
|
||||
? storedItems.length
|
||||
: Object.keys(storedItems).length
|
||||
maxIterations = Math.min(maxIterations, itemsLength)
|
||||
|
||||
maxIterations = itemsLength
|
||||
logger.info(
|
||||
`Loop ${loopId} using stored items length: ${itemsLength} (max iterations: ${maxIterations})`
|
||||
`forEach loop ${loopId} - Items: ${itemsLength}, Max iterations: ${maxIterations}`
|
||||
)
|
||||
} else {
|
||||
// Fallback to parsing the forEachItems string if it's not a reference
|
||||
const itemsLength = this.getItemsLength(loop.forEachItems)
|
||||
if (itemsLength > 0) {
|
||||
maxIterations = Math.min(maxIterations, itemsLength)
|
||||
maxIterations = itemsLength
|
||||
logger.info(
|
||||
`forEach loop ${loopId} - Parsed items: ${itemsLength}, Max iterations: ${maxIterations}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user