chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import type { VariablesModalStore, VariablesPosition } from '@/stores/variables/types'
/**
* Floating variables modal default dimensions.
* Slightly larger than the chat modal for more comfortable editing.
*/
const DEFAULT_WIDTH = 320
const DEFAULT_HEIGHT = 320
/**
* Minimum and maximum modal dimensions.
*/
export const MIN_VARIABLES_WIDTH = DEFAULT_WIDTH
export const MIN_VARIABLES_HEIGHT = DEFAULT_HEIGHT
export const MAX_VARIABLES_WIDTH = 500
export const MAX_VARIABLES_HEIGHT = 600
/** Inset gap between the viewport edge and the content window */
const CONTENT_WINDOW_GAP = 8
/**
* Compute a center-biased default position, factoring in current layout chrome
* (sidebar, right panel, terminal) and content window inset.
*/
const calculateDefaultPosition = (): VariablesPosition => {
if (typeof window === 'undefined') {
return { x: 100, y: 100 }
}
const sidebarWidth = Number.parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0'
)
const panelWidth = Number.parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--panel-width') || '0'
)
const terminalHeight = Number.parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0'
)
const availableWidth = window.innerWidth - sidebarWidth - CONTENT_WINDOW_GAP - panelWidth
const availableHeight = window.innerHeight - CONTENT_WINDOW_GAP * 2 - terminalHeight
const x = sidebarWidth + (availableWidth - DEFAULT_WIDTH) / 2
const y = CONTENT_WINDOW_GAP + (availableHeight - DEFAULT_HEIGHT) / 2
return { x, y }
}
/**
* Constrain a position to the visible canvas, considering layout chrome.
*/
const constrainPosition = (
position: VariablesPosition,
width: number = DEFAULT_WIDTH,
height: number = DEFAULT_HEIGHT
): VariablesPosition => {
if (typeof window === 'undefined') return position
const sidebarWidth = Number.parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0'
)
const panelWidth = Number.parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--panel-width') || '0'
)
const terminalHeight = Number.parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0'
)
const minX = sidebarWidth
const maxX = window.innerWidth - CONTENT_WINDOW_GAP - panelWidth - width
const minY = CONTENT_WINDOW_GAP
const maxY = window.innerHeight - CONTENT_WINDOW_GAP - terminalHeight - height
return {
x: Math.max(minX, Math.min(maxX, position.x)),
y: Math.max(minY, Math.min(maxY, position.y)),
}
}
/**
* Return a valid, constrained position. If the stored one is off-bounds due to
* layout changes, prefer a fresh default center position.
*/
export const getVariablesPosition = (
stored: VariablesPosition | null,
width: number = DEFAULT_WIDTH,
height: number = DEFAULT_HEIGHT
): VariablesPosition => {
if (!stored) return calculateDefaultPosition()
const constrained = constrainPosition(stored, width, height)
const deltaX = Math.abs(constrained.x - stored.x)
const deltaY = Math.abs(constrained.y - stored.y)
if (deltaX > 100 || deltaY > 100) return calculateDefaultPosition()
return constrained
}
/**
* UI-only store for the floating variables modal.
* Variable data lives in the variables data store (`@/stores/variables/store`).
*/
export const useVariablesModalStore = create<VariablesModalStore>()(
devtools(
persist(
(set) => ({
isOpen: false,
position: null,
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
setIsOpen: (open) => set({ isOpen: open }),
setPosition: (position) => set({ position }),
setDimensions: (dimensions) =>
set({
width: Math.max(MIN_VARIABLES_WIDTH, Math.min(MAX_VARIABLES_WIDTH, dimensions.width)),
height: Math.max(
MIN_VARIABLES_HEIGHT,
Math.min(MAX_VARIABLES_HEIGHT, dimensions.height)
),
}),
resetPosition: () => set({ position: null }),
}),
{
name: 'variables-modal-store',
partialize: (state) => ({
position: state.position,
width: state.width,
height: state.height,
}),
}
),
{ name: 'variables-modal-store' }
)
)
+250
View File
@@ -0,0 +1,250 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import JSON5 from 'json5'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { normalizeName } from '@/executor/constants'
import { useOperationQueueStore } from '@/stores/operation-queue/store'
import type { Variable, VariablesStore } from '@/stores/variables/types'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
const logger = createLogger('VariablesStore')
function validateVariable(variable: Variable): string | undefined {
try {
switch (variable.type) {
case 'number':
if (Number.isNaN(Number(variable.value))) {
return 'Not a valid number'
}
break
case 'boolean':
if (!/^(true|false)$/i.test(String(variable.value).trim())) {
return 'Expected "true" or "false"'
}
break
case 'object':
try {
const valueToEvaluate = String(variable.value).trim()
if (!valueToEvaluate.startsWith('{') || !valueToEvaluate.endsWith('}')) {
return 'Not a valid object format'
}
const parsed = JSON5.parse(valueToEvaluate)
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
return 'Not a valid object'
}
return undefined
} catch (e) {
logger.error('Object parsing error:', e)
return 'Invalid object syntax'
}
case 'array':
try {
const parsed = JSON5.parse(String(variable.value))
if (!Array.isArray(parsed)) {
return 'Not a valid array'
}
} catch {
return 'Invalid array syntax'
}
break
}
return undefined
} catch (e) {
return getErrorMessage(e, 'Invalid format')
}
}
export const useVariablesStore = create<VariablesStore>()(
devtools((set, get) => ({
variables: {},
isLoading: false,
error: null,
isEditing: null,
addVariable: (variable, providedId?: string) => {
const id = providedId || generateId()
const workflowVariables = get().getVariablesByWorkflowId(variable.workflowId)
if (!variable.name || /^variable\d+$/.test(variable.name)) {
const existingNumbers = workflowVariables
.map((v) => {
const match = v.name.match(/^variable(\d+)$/)
return match ? Number.parseInt(match[1]) : 0
})
.filter((n) => !Number.isNaN(n))
const nextNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1
variable.name = `variable${nextNumber}`
}
let uniqueName = variable.name
let nameIndex = 1
while (workflowVariables.some((v) => v.name === uniqueName)) {
uniqueName = `${variable.name} (${nameIndex})`
nameIndex++
}
if (variable.type === 'string') {
variable.type = 'plain'
}
const newVariable: Variable = {
id,
workflowId: variable.workflowId,
name: uniqueName,
type: variable.type,
value: variable.value || '',
validationError: undefined,
}
const validationError = validateVariable(newVariable)
if (validationError) {
newVariable.validationError = validationError
}
set((state) => ({
variables: {
...state.variables,
[id]: newVariable,
},
}))
return id
},
updateVariable: (id, update) => {
set((state) => {
if (!state.variables[id]) return state
if (update.name !== undefined) {
const oldVariable = state.variables[id]
const oldVariableName = oldVariable.name
const newName = update.name.trim()
if (!newName) {
update = { ...update }
update.name = undefined
} else if (newName !== oldVariableName) {
const subBlockStore = useSubBlockStore.getState()
const targetWorkflowId = oldVariable.workflowId
if (targetWorkflowId) {
const workflowValues = subBlockStore.workflowValues[targetWorkflowId] || {}
const updatedWorkflowValues = { ...workflowValues }
const changedSubBlocks: Array<{ blockId: string; subBlockId: string; value: any }> =
[]
const oldVarName = normalizeName(oldVariableName)
const newVarName = normalizeName(newName)
const regex = new RegExp(`<variable\\.${oldVarName}>`, 'gi')
const updateReferences = (value: any, pattern: RegExp, replacement: string): any => {
if (typeof value === 'string') {
return pattern.test(value) ? value.replace(pattern, replacement) : value
}
if (Array.isArray(value)) {
return value.map((item) => updateReferences(item, pattern, replacement))
}
if (value !== null && typeof value === 'object') {
const result = { ...value }
for (const key in result) {
result[key] = updateReferences(result[key], pattern, replacement)
}
return result
}
return value
}
Object.entries(workflowValues).forEach(([blockId, blockValues]) => {
Object.entries(blockValues as Record<string, any>).forEach(
([subBlockId, value]) => {
const updatedValue = updateReferences(value, regex, `<variable.${newVarName}>`)
if (JSON.stringify(updatedValue) !== JSON.stringify(value)) {
if (!updatedWorkflowValues[blockId]) {
updatedWorkflowValues[blockId] = { ...workflowValues[blockId] }
}
updatedWorkflowValues[blockId][subBlockId] = updatedValue
changedSubBlocks.push({ blockId, subBlockId, value: updatedValue })
}
}
)
})
// Update local state
useSubBlockStore.setState({
workflowValues: {
...subBlockStore.workflowValues,
[targetWorkflowId]: updatedWorkflowValues,
},
})
// Queue operations for persistence via socket
const operationQueue = useOperationQueueStore.getState()
for (const { blockId, subBlockId, value } of changedSubBlocks) {
operationQueue.addToQueue({
id: generateId(),
operation: {
operation: 'subblock-update',
target: 'subblock',
payload: { blockId, subblockId: subBlockId, value },
},
workflowId: targetWorkflowId,
userId: 'system',
})
}
}
}
}
if (update.type === 'string') {
update = { ...update, type: 'plain' }
}
const updatedVariable: Variable = {
...state.variables[id],
...update,
validationError: undefined,
}
if (update.type || update.value !== undefined) {
updatedVariable.validationError = validateVariable(updatedVariable)
}
const updated = {
...state.variables,
[id]: updatedVariable,
}
return { variables: updated }
})
},
deleteVariable: (id) => {
set((state) => {
if (!state.variables[id]) return state
const { [id]: _, ...rest } = state.variables
return { variables: rest }
})
},
getVariablesByWorkflowId: (workflowId) => {
return Object.values(get().variables).filter((variable) => variable.workflowId === workflowId)
},
}))
)
+76
View File
@@ -0,0 +1,76 @@
/**
* Variable types supported in the application
* Note: 'string' is deprecated - use 'plain' for text values instead
*/
export type VariableType = 'plain' | 'number' | 'boolean' | 'object' | 'array' | 'string'
/**
* Represents a workflow variable with workflow-specific naming
* Variable names must be unique within each workflow
*/
export interface Variable {
id: string
workflowId: string
name: string // Must be unique per workflow
type: VariableType
value: unknown
validationError?: string // Tracks format validation errors
}
export interface VariablesStore {
variables: Record<string, Variable>
isLoading: boolean
error: string | null
isEditing: string | null
/**
* Adds a new variable with automatic name uniqueness validation
* If a variable with the same name exists, it will be suffixed with a number
* Optionally accepts a predetermined ID for collaborative operations
*/
addVariable: (variable: Omit<Variable, 'id'>, providedId?: string) => string
/**
* Updates a variable, ensuring name remains unique within the workflow
* If an updated name conflicts with existing ones, a numbered suffix is added
*/
updateVariable: (id: string, update: Partial<Omit<Variable, 'id' | 'workflowId'>>) => void
deleteVariable: (id: string) => void
/**
* Returns all variables for a specific workflow
*/
getVariablesByWorkflowId: (workflowId: string) => Variable[]
}
/**
* 2D position used by the floating variables modal.
*/
export interface VariablesPosition {
x: number
y: number
}
/**
* Dimensions for the floating variables modal.
*/
interface VariablesDimensions {
width: number
height: number
}
/**
* UI-only store interface for the floating variables modal.
* Variable data lives in the variables data store (`@/stores/variables/store`).
*/
export interface VariablesModalStore {
isOpen: boolean
position: VariablesPosition | null
width: number
height: number
setIsOpen: (open: boolean) => void
setPosition: (position: VariablesPosition) => void
setDimensions: (dimensions: VariablesDimensions) => void
resetPosition: () => void
}