mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-08 22:48:14 -05:00
Added envvar store, resolves values in short-input, long-input, code sub-blocks. Resolves value at execution time
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSubBlockValue } from '../hooks/use-sub-block-value'
|
||||
import { formatDisplayText } from '@/components/ui/formatted-text'
|
||||
|
||||
interface CodeLine {
|
||||
id: string
|
||||
@@ -714,6 +715,9 @@ export function Code({ blockId, subBlockId, isConnecting }: CodeProps) {
|
||||
wrap="off"
|
||||
onPaste={handlePaste}
|
||||
/>
|
||||
<div className="absolute inset-0 pointer-events-none px-3 py-1 whitespace-pre text-sm">
|
||||
{formatDisplayText(line.content)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useSubBlockValue } from '../hooks/use-sub-block-value'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { SubBlockConfig } from '@/blocks/types'
|
||||
import { formatDisplayText } from '@/components/ui/formatted-text'
|
||||
|
||||
interface LongInputProps {
|
||||
placeholder?: string
|
||||
@@ -63,25 +64,6 @@ export function LongInput({
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
const formatDisplayText = (text: string) => {
|
||||
if (!text) return null
|
||||
|
||||
// Split the text by tag pattern <something.something>
|
||||
const parts = text.split(/(<[^>]+>)/g)
|
||||
|
||||
return parts.map((part, index) => {
|
||||
// Check if the part matches tag pattern
|
||||
if (part.match(/^<[^>]+>$/)) {
|
||||
return (
|
||||
<span key={index} className="text-blue-500">
|
||||
{part}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return <span key={index}>{part}</span>
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<Textarea
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useState, useRef, useEffect } from 'react'
|
||||
import { useSubBlockValue } from '../hooks/use-sub-block-value'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { SubBlockConfig } from '@/blocks/types'
|
||||
import { formatDisplayText } from '@/components/ui/formatted-text'
|
||||
|
||||
interface ShortInputProps {
|
||||
placeholder?: string
|
||||
@@ -84,25 +85,6 @@ export function ShortInput({
|
||||
? '•'.repeat(value?.toString().length ?? 0)
|
||||
: value?.toString() ?? ''
|
||||
|
||||
const formatDisplayText = (text: string) => {
|
||||
if (!text) return null
|
||||
|
||||
// Split the text by tag pattern <something.something>
|
||||
const parts = text.split(/(<[^>]+>)/g)
|
||||
|
||||
return parts.map((part, index) => {
|
||||
// Check if the part matches tag pattern
|
||||
if (part.match(/^<[^>]+>$/)) {
|
||||
return (
|
||||
<span key={index} className="text-blue-500">
|
||||
{part}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return <span key={index}>{part}</span>
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
|
||||
@@ -6,14 +6,26 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useState, useRef, useEffect, useMemo } from 'react'
|
||||
import { useEnvironmentStore } from '@/stores/environment/store'
|
||||
import { EnvironmentVariable as StoreEnvironmentVariable } from '@/stores/environment/types'
|
||||
|
||||
interface EnvVar {
|
||||
key: string
|
||||
value: string
|
||||
// Extend the store type with our UI-specific fields
|
||||
interface UIEnvironmentVariable extends StoreEnvironmentVariable {
|
||||
id?: number
|
||||
}
|
||||
|
||||
interface SettingsModalProps {
|
||||
@@ -22,52 +34,95 @@ interface SettingsModalProps {
|
||||
}
|
||||
|
||||
const GRID_COLS = 'grid grid-cols-[minmax(0,1fr),minmax(0,1fr),40px] gap-4'
|
||||
const INITIAL_ENV_VAR: EnvVar = { key: '', value: '' }
|
||||
const INITIAL_ENV_VAR: UIEnvironmentVariable = { key: '', value: '' }
|
||||
|
||||
export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
||||
const [envVars, setEnvVars] = useState<EnvVar[]>([INITIAL_ENV_VAR])
|
||||
const [focusedValueIndex, setFocusedValueIndex] = useState<number | null>(
|
||||
null
|
||||
)
|
||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([])
|
||||
const { variables, setVariable, removeVariable } = useEnvironmentStore()
|
||||
const [envVars, setEnvVars] = useState<UIEnvironmentVariable[]>([])
|
||||
const [focusedValueIndex, setFocusedValueIndex] = useState<number | null>(null)
|
||||
const [showUnsavedChanges, setShowUnsavedChanges] = useState(false)
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
const pendingClose = useRef(false)
|
||||
const initialVarsRef = useRef<UIEnvironmentVariable[]>([])
|
||||
|
||||
// Check if there are unsaved changes by comparing with initial state
|
||||
const hasChanges = useMemo(() => {
|
||||
// Filter out empty rows from both initial and current state
|
||||
const initialVars = initialVarsRef.current.filter(v => v.key || v.value)
|
||||
const currentVars = envVars.filter(v => v.key || v.value)
|
||||
|
||||
// Create maps for easier comparison
|
||||
const initialMap = new Map(initialVars.map(v => [v.key, v.value]))
|
||||
const currentMap = new Map(currentVars.map(v => [v.key, v.value]))
|
||||
|
||||
// Different number of non-empty variables
|
||||
if (initialMap.size !== currentMap.size) return true
|
||||
|
||||
// Check for any differences in keys or values
|
||||
for (const [key, value] of currentMap) {
|
||||
const initialValue = initialMap.get(key)
|
||||
// If key doesn't exist in initial or value is different
|
||||
if (initialValue !== value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any initial keys are missing from current
|
||||
for (const key of initialMap.keys()) {
|
||||
if (!currentMap.has(key)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}, [envVars])
|
||||
|
||||
// Reset state when modal is opened
|
||||
useEffect(() => {
|
||||
inputRefs.current = inputRefs.current.slice(0, envVars.length)
|
||||
}, [envVars.length])
|
||||
if (open) {
|
||||
const existingVars = Object.values(variables)
|
||||
const initialVars = existingVars.length ? existingVars : [INITIAL_ENV_VAR]
|
||||
// Create deep copy of initial vars to prevent reference issues
|
||||
initialVarsRef.current = JSON.parse(JSON.stringify(initialVars))
|
||||
setEnvVars(JSON.parse(JSON.stringify(initialVars)))
|
||||
pendingClose.current = false
|
||||
}
|
||||
}, [open, variables])
|
||||
|
||||
const handleClose = () => {
|
||||
if (hasChanges) {
|
||||
setShowUnsavedChanges(true)
|
||||
pendingClose.current = true
|
||||
} else {
|
||||
onOpenChange(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
// Reset to initial state when cancelling (without saving)
|
||||
setEnvVars(JSON.parse(JSON.stringify(initialVarsRef.current)))
|
||||
setShowUnsavedChanges(false)
|
||||
if (pendingClose.current) {
|
||||
onOpenChange(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollTop =
|
||||
scrollContainerRef.current.scrollHeight
|
||||
scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight
|
||||
}
|
||||
}, [envVars])
|
||||
|
||||
const setInputRef = (el: HTMLInputElement | null, index: number) => {
|
||||
inputRefs.current[index] = el
|
||||
}
|
||||
|
||||
const handleValueFocus = (index: number) => {
|
||||
const handleValueFocus = (index: number, e: React.FocusEvent<HTMLInputElement>) => {
|
||||
setFocusedValueIndex(index)
|
||||
setTimeout(() => {
|
||||
const input = inputRefs.current[index]
|
||||
if (input) {
|
||||
input.setSelectionRange(0, 0)
|
||||
input.scrollLeft = 0
|
||||
}
|
||||
}, 0)
|
||||
// Always scroll to the start of the input
|
||||
e.target.scrollLeft = 0
|
||||
}
|
||||
|
||||
const handleValueClick = (
|
||||
e: React.MouseEvent<HTMLInputElement>,
|
||||
index: number
|
||||
) => {
|
||||
const handleValueClick = (e: React.MouseEvent<HTMLInputElement>) => {
|
||||
e.preventDefault()
|
||||
const input = inputRefs.current[index]
|
||||
if (input) {
|
||||
input.setSelectionRange(0, 0)
|
||||
input.scrollLeft = 0
|
||||
}
|
||||
// Always scroll to the start of the input
|
||||
e.currentTarget.scrollLeft = 0
|
||||
}
|
||||
|
||||
const handleValueKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
@@ -110,9 +165,13 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const addEnvVar = () => setEnvVars([...envVars, INITIAL_ENV_VAR])
|
||||
const addEnvVar = () => {
|
||||
// Create a fresh empty variable with a unique id
|
||||
const newVar = { key: '', value: '', id: Date.now() }
|
||||
setEnvVars([...envVars, newVar])
|
||||
}
|
||||
|
||||
const updateEnvVar = (index: number, field: keyof EnvVar, value: string) => {
|
||||
const updateEnvVar = (index: number, field: 'key' | 'value', value: string) => {
|
||||
const newEnvVars = [...envVars]
|
||||
newEnvVars[index][field] = value
|
||||
setEnvVars(newEnvVars)
|
||||
@@ -123,8 +182,26 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
||||
setEnvVars(newEnvVars.length ? newEnvVars : [INITIAL_ENV_VAR])
|
||||
}
|
||||
|
||||
const renderEnvVarRow = (envVar: EnvVar, index: number) => (
|
||||
<div key={index} className={`${GRID_COLS} items-center`}>
|
||||
const handleSave = () => {
|
||||
// Save all valid environment variables to the store
|
||||
const validVars = envVars.filter(v => v.key && v.value)
|
||||
validVars.forEach(v => setVariable(v.key, v.value))
|
||||
|
||||
// Remove any variables that were deleted
|
||||
const currentKeys = new Set(validVars.map(v => v.key))
|
||||
Object.keys(variables).forEach(key => {
|
||||
if (!currentKeys.has(key)) {
|
||||
removeVariable(key)
|
||||
}
|
||||
})
|
||||
|
||||
if (pendingClose.current) {
|
||||
onOpenChange(false)
|
||||
}
|
||||
}
|
||||
|
||||
const renderEnvVarRow = (envVar: UIEnvironmentVariable, index: number) => (
|
||||
<div key={envVar.id || index} className={`${GRID_COLS} items-center`}>
|
||||
<Input
|
||||
value={envVar.key}
|
||||
onChange={(e) => updateEnvVar(index, 'key', e.target.value)}
|
||||
@@ -132,15 +209,15 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
||||
placeholder="e.g. API_KEY"
|
||||
/>
|
||||
<Input
|
||||
ref={(el) => setInputRef(el, index)}
|
||||
value={envVar.value}
|
||||
onChange={(e) => updateEnvVar(index, 'value', e.target.value)}
|
||||
type={focusedValueIndex === index ? 'text' : 'password'}
|
||||
onFocus={() => handleValueFocus(index)}
|
||||
onClick={(e) => handleValueClick(e, index)}
|
||||
onFocus={(e) => handleValueFocus(index, e)}
|
||||
onClick={handleValueClick}
|
||||
onBlur={() => setFocusedValueIndex(null)}
|
||||
onKeyDown={handleValueKeyDown}
|
||||
onPaste={(e) => handlePaste(e, index)}
|
||||
placeholder="Enter value"
|
||||
className="allow-scroll"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -154,44 +231,70 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[600px] max-h-[85vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Environment Variables</DialogTitle>
|
||||
</DialogHeader>
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[600px] max-h-[85vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Environment Variables</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col flex-1 min-h-0">
|
||||
<div className="space-y-1.5">
|
||||
<div className={`${GRID_COLS} px-0.5`}>
|
||||
<Label>Key</Label>
|
||||
<Label>Value</Label>
|
||||
<div />
|
||||
<div className="flex flex-col flex-1 min-h-0">
|
||||
<div className="space-y-1.5">
|
||||
<div className={`${GRID_COLS} px-0.5`}>
|
||||
<Label>Key</Label>
|
||||
<Label>Value</Label>
|
||||
<div />
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="overflow-y-auto max-h-[40vh] space-y-2 scrollbar-thin scrollbar-thumb-muted-foreground/20 hover:scrollbar-thumb-muted-foreground/25 scrollbar-track-transparent pr-6 -mr-6 pb-2 pt-2 px-2 -mx-2"
|
||||
>
|
||||
{envVars.map(renderEnvVarRow)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="overflow-y-auto max-h-[40vh] space-y-2 scrollbar-thin scrollbar-thumb-muted-foreground/20 hover:scrollbar-thumb-muted-foreground/25 scrollbar-track-transparent pr-6 -mr-6 pb-2 pt-2 px-2 -mx-2"
|
||||
>
|
||||
{envVars.map(renderEnvVarRow)}
|
||||
<div className="flex flex-col gap-4 pt-4 border-t mt-4">
|
||||
<Button variant="outline" size="sm" onClick={addEnvVar}>
|
||||
Add Variable
|
||||
</Button>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<div className="flex flex-col gap-4 pt-4 border-t mt-4">
|
||||
<Button variant="outline" size="sm" onClick={addEnvVar}>
|
||||
Add Variable
|
||||
</Button>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => onOpenChange(false)}>Save Changes</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<AlertDialog open={showUnsavedChanges} onOpenChange={setShowUnsavedChanges}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Unsaved Changes</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
You have unsaved changes. Do you want to save them before closing?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={handleCancel}>
|
||||
Discard Changes
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleSave}>
|
||||
Save Changes
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ExecutionResult } from '@/executor/types'
|
||||
import { useNotificationStore } from '@/stores/notifications/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflow/registry'
|
||||
import { useConsoleStore } from '@/stores/console/store'
|
||||
import { useEnvironmentStore } from '@/stores/environment/store'
|
||||
|
||||
export function useWorkflowExecution() {
|
||||
const [isExecuting, setIsExecuting] = useState(false)
|
||||
@@ -14,6 +15,7 @@ export function useWorkflowExecution() {
|
||||
const { activeWorkflowId } = useWorkflowRegistry()
|
||||
const { addNotification } = useNotificationStore()
|
||||
const { addConsole, toggleConsole, isOpen } = useConsoleStore()
|
||||
const { getAllVariables } = useEnvironmentStore()
|
||||
|
||||
const handleRunWorkflow = useCallback(async () => {
|
||||
if (!activeWorkflowId) return
|
||||
@@ -34,9 +36,16 @@ export function useWorkflowExecution() {
|
||||
return acc
|
||||
}, {} as Record<string, any>)
|
||||
|
||||
// Get environment variables
|
||||
const envVars = getAllVariables()
|
||||
const envVarValues = Object.entries(envVars).reduce((acc, [key, variable]) => {
|
||||
acc[key] = variable.value
|
||||
return acc
|
||||
}, {} as Record<string, string>)
|
||||
|
||||
// Execute workflow
|
||||
const workflow = new Serializer().serializeWorkflow(blocks, edges)
|
||||
const executor = new Executor(workflow, currentBlockStates)
|
||||
const executor = new Executor(workflow, currentBlockStates, envVarValues)
|
||||
|
||||
const result = await executor.execute('my-run-id')
|
||||
setExecutionResult(result)
|
||||
@@ -85,7 +94,8 @@ export function useWorkflowExecution() {
|
||||
setExecutionResult({
|
||||
success: false,
|
||||
output: { response: {} },
|
||||
error: errorMessage
|
||||
error: errorMessage,
|
||||
logs: []
|
||||
})
|
||||
|
||||
// Add error entry to console
|
||||
@@ -104,7 +114,7 @@ export function useWorkflowExecution() {
|
||||
} finally {
|
||||
setIsExecuting(false)
|
||||
}
|
||||
}, [activeWorkflowId, blocks, edges, addNotification, addConsole, isOpen, toggleConsole])
|
||||
}, [activeWorkflowId, blocks, edges, addNotification, addConsole, isOpen, toggleConsole, getAllVariables])
|
||||
|
||||
return { isExecuting, executionResult, handleRunWorkflow }
|
||||
}
|
||||
34
components/ui/formatted-text.tsx
Normal file
34
components/ui/formatted-text.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
'use client'
|
||||
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
/**
|
||||
* Formats text by highlighting block references (<...>) and environment variables ({...})
|
||||
* Used in code editor, long inputs, and short inputs for consistent syntax highlighting
|
||||
*/
|
||||
export function formatDisplayText(text: string | null): ReactNode {
|
||||
if (!text) return null
|
||||
|
||||
// Split the text by both tag patterns <something.something> and {ENV_VAR}
|
||||
const parts = text.split(/(<[^>]+>|\{[^}]+\})/g)
|
||||
|
||||
return parts.map((part, index) => {
|
||||
// Check if the part matches connection tag pattern
|
||||
if (part.match(/^<[^>]+>$/)) {
|
||||
return (
|
||||
<span key={index} className="text-blue-500">
|
||||
{part}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
// Check if the part matches environment variable pattern
|
||||
if (part.match(/^\{[^}]+\}$/)) {
|
||||
return (
|
||||
<span key={index} className="text-blue-500">
|
||||
{part}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return <span key={index}>{part}</span>
|
||||
})
|
||||
}
|
||||
@@ -23,7 +23,8 @@ export class Executor {
|
||||
constructor(
|
||||
private workflow: SerializedWorkflow,
|
||||
// Initial block states can be passed in if you need to resume workflows or pre-populate data.
|
||||
private initialBlockStates: Record<string, BlockOutput> = {}
|
||||
private initialBlockStates: Record<string, BlockOutput> = {},
|
||||
private environmentVariables: Record<string, string> = {}
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -40,6 +41,7 @@ export class Executor {
|
||||
metadata: {
|
||||
startTime: startTime.toISOString()
|
||||
},
|
||||
environmentVariables: this.environmentVariables
|
||||
}
|
||||
|
||||
// Pre-populate block states if initialBlockStates exist
|
||||
@@ -314,10 +316,12 @@ export class Executor {
|
||||
const resolvedInputs = Object.entries(inputs).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
const matches = value.match(/<([^>]+)>/g)
|
||||
if (matches) {
|
||||
let resolvedValue = value
|
||||
for (const match of matches) {
|
||||
let resolvedValue = value
|
||||
|
||||
// Handle block references with <> syntax
|
||||
const blockMatches = value.match(/<([^>]+)>/g)
|
||||
if (blockMatches) {
|
||||
for (const match of blockMatches) {
|
||||
// e.g. "<someBlockId.response>"
|
||||
const path = match.slice(1, -1) // remove < and >
|
||||
const [blockRef, ...pathParts] = path.split('.')
|
||||
@@ -365,20 +369,32 @@ export class Executor {
|
||||
throw new Error(`No value found at path "${path}" in block "${sourceBlock.metadata?.title}".`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt JSON parse if it looks like JSON
|
||||
try {
|
||||
if (resolvedValue.startsWith('{') || resolvedValue.startsWith('[')) {
|
||||
acc[key] = JSON.parse(resolvedValue)
|
||||
} else {
|
||||
acc[key] = resolvedValue
|
||||
// Handle environment variables with {} syntax
|
||||
const envMatches = resolvedValue.match(/\{([^}]+)\}/g)
|
||||
if (envMatches) {
|
||||
for (const match of envMatches) {
|
||||
const envKey = match.slice(1, -1) // remove { and }
|
||||
const envValue = context.environmentVariables?.[envKey]
|
||||
|
||||
if (envValue === undefined) {
|
||||
throw new Error(`Environment variable "${envKey}" was not found.`)
|
||||
}
|
||||
} catch {
|
||||
|
||||
resolvedValue = resolvedValue.replace(match, envValue)
|
||||
}
|
||||
}
|
||||
|
||||
// After all replacements are done, attempt JSON parse if it looks like JSON
|
||||
try {
|
||||
if (resolvedValue.startsWith('{') || resolvedValue.startsWith('[')) {
|
||||
acc[key] = JSON.parse(resolvedValue)
|
||||
} else {
|
||||
acc[key] = resolvedValue
|
||||
}
|
||||
} else {
|
||||
// No placeholders
|
||||
acc[key] = value
|
||||
} catch {
|
||||
acc[key] = resolvedValue
|
||||
}
|
||||
} else {
|
||||
// Not a string param
|
||||
|
||||
@@ -6,31 +6,30 @@ import { BlockOutput } from '@/blocks/types'
|
||||
export interface BlockLog {
|
||||
blockId: string
|
||||
blockTitle?: string
|
||||
success: boolean
|
||||
error?: string
|
||||
blockType?: string
|
||||
startedAt: string
|
||||
endedAt: string
|
||||
durationMs: number
|
||||
success: boolean
|
||||
output?: any
|
||||
blockType?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the runtime context for executing a workflow,
|
||||
* including all block outputs (blockStates), metadata for timing, and block logs.
|
||||
*/
|
||||
export interface ExecutionMetadata {
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
}
|
||||
|
||||
export interface ExecutionContext {
|
||||
workflowId: string
|
||||
blockStates: Map<string, BlockOutput>
|
||||
// Make metadata non-optional so we can assign .startTime or .endTime without TS warnings
|
||||
metadata: {
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
// You can keep an index signature if you want to store extra fields
|
||||
[key: string]: any
|
||||
}
|
||||
// We store logs in an array so the final result includes a step-by-step record
|
||||
blockLogs: BlockLog[]
|
||||
metadata: ExecutionMetadata
|
||||
environmentVariables?: Record<string, string>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,13 +40,12 @@ export interface ExecutionResult {
|
||||
success: boolean
|
||||
output: BlockOutput
|
||||
error?: string
|
||||
logs?: BlockLog[]
|
||||
metadata?: {
|
||||
duration: number
|
||||
startTime: string
|
||||
endTime: string
|
||||
}
|
||||
// Detailed logs of what happened in each block
|
||||
logs?: BlockLog[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
42
stores/environment/store.ts
Normal file
42
stores/environment/store.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { EnvironmentStore, EnvironmentVariable } from './types'
|
||||
|
||||
export const useEnvironmentStore = create<EnvironmentStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
variables: {},
|
||||
|
||||
setVariable: (key: string, value: string) => {
|
||||
set((state: EnvironmentStore) => ({
|
||||
variables: {
|
||||
...state.variables,
|
||||
[key]: { key, value }
|
||||
}
|
||||
}))
|
||||
},
|
||||
|
||||
removeVariable: (key: string) => {
|
||||
set((state: EnvironmentStore) => {
|
||||
const { [key]: _, ...rest } = state.variables
|
||||
return { variables: rest }
|
||||
})
|
||||
},
|
||||
|
||||
clearVariables: () => {
|
||||
set({ variables: {} })
|
||||
},
|
||||
|
||||
getVariable: (key: string) => {
|
||||
return get().variables[key]?.value
|
||||
},
|
||||
|
||||
getAllVariables: () => {
|
||||
return get().variables
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: 'environment-store'
|
||||
}
|
||||
)
|
||||
)
|
||||
16
stores/environment/types.ts
Normal file
16
stores/environment/types.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface EnvironmentVariable {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface EnvironmentState {
|
||||
variables: Record<string, EnvironmentVariable>
|
||||
}
|
||||
|
||||
export interface EnvironmentStore extends EnvironmentState {
|
||||
setVariable: (key: string, value: string) => void
|
||||
removeVariable: (key: string) => void
|
||||
clearVariables: () => void
|
||||
getVariable: (key: string) => string | undefined
|
||||
getAllVariables: () => Record<string, EnvironmentVariable>
|
||||
}
|
||||
Reference in New Issue
Block a user