cleaned up logic

This commit is contained in:
Adam Gough
2025-05-23 14:38:03 -07:00
parent 44c8f0556a
commit 7b50046fbf
31 changed files with 930 additions and 1104 deletions

View File

@@ -21,14 +21,12 @@ import { DeployStatus } from '@/app/w/[id]/components/control-bar/components/dep
import { ExampleCommand } from '@/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command/example-command'
import { useNotificationStore } from '@/stores/notifications/store'
import { DeployedWorkflowModal } from '../../../deployment-controls/components/deployed-workflow-modal'
import { createLogger } from '@/lib/logs/console-logger'
import { WorkflowState } from '@/stores/workflows/workflow/types'
const logger = createLogger('DeploymentInfo')
interface DeploymentInfoProps {
isLoading?: boolean
deploymentInfo: {
isDeployed: boolean
deployedAt?: string
apiKey: string
endpoint: string
@@ -40,7 +38,7 @@ interface DeploymentInfoProps {
isSubmitting: boolean
isUndeploying: boolean
workflowId: string | null
deployedState: any
deployedState: WorkflowState
isLoadingDeployedState: boolean
}
@@ -69,7 +67,6 @@ export function DeploymentInfo({
setIsViewingDeployed(true)
return
} else if (!isLoadingDeployedState) {
logger.debug(`No deployed state found`)
addNotification('error', 'Cannot view deployment: No deployed state available', workflowId)
}
}

View File

@@ -27,6 +27,7 @@ import { useNotificationStore } from '@/stores/notifications/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { WorkflowState } from '@/stores/workflows/workflow/types'
const logger = createLogger('DeployModal')
@@ -36,7 +37,7 @@ interface DeployModalProps {
workflowId: string | null
needsRedeployment: boolean
setNeedsRedeployment: (value: boolean) => void
deployedState: any
deployedState: WorkflowState
isLoadingDeployedState: boolean
refetchDeployedState: () => Promise<void>
}
@@ -313,7 +314,6 @@ export function DeployModal({
setDeploymentInfo(newDeploymentInfo)
// Fetch the updated deployed state after deployment
logger.info('Deployment successful, fetching initial deployed state')
await refetchDeployedState()
// No notification on successful deploy

View File

@@ -1,7 +1,6 @@
'use client'
import { useState, useEffect, useMemo } from 'react'
import { Button } from '@/components/ui/button'
import { useState, useMemo } from 'react'
import { Card, CardContent, CardHeader } from '@/components/ui/card'
import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label'
@@ -9,32 +8,13 @@ import { cn } from '@/lib/utils'
import { createLogger } from '@/lib/logs/console-logger'
import { WorkflowPreview } from '@/app/w/components/workflow-preview/workflow-preview'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { WorkflowState } from '@/stores/workflows/workflow/types'
const logger = createLogger('DeployedWorkflowCard')
interface DeployedWorkflowCardProps {
currentWorkflowState?: {
blocks: Record<string, any>
edges: Array<any>
loops: Record<string, any>
_metadata?: {
workflowId?: string
fetchTimestamp?: number
requestId?: number
[key: string]: any
}
}
deployedWorkflowState: {
blocks: Record<string, any>
edges: Array<any>
loops: Record<string, any>
_metadata?: {
workflowId?: string
fetchTimestamp?: number
requestId?: number
[key: string]: any
}
}
currentWorkflowState?: WorkflowState
deployedWorkflowState: WorkflowState
className?: string
}
@@ -48,44 +28,44 @@ export function DeployedWorkflowCard({
const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
// Create sanitized workflow state
const sanitizedWorkflowState = useMemo(() => {
if (!workflowToShow) return null;
// const sanitizedWorkflowState = useMemo(() => {
// if (!workflowToShow) return null;
// Verify the workflow ID matches if metadata exists
if (workflowToShow._metadata?.workflowId &&
workflowToShow._metadata.workflowId !== activeWorkflowId) {
logger.warn('Workflow ID mismatch detected in card', {
stateWorkflowId: workflowToShow._metadata.workflowId,
activeWorkflowId,
isDeployed: showingDeployed
});
}
// // Verify the workflow ID matches if metadata exists
// if (workflowToShow._metadata?.workflowId &&
// workflowToShow._metadata.workflowId !== activeWorkflowId) {
// logger.warn('Workflow ID mismatch detected in card', {
// stateWorkflowId: workflowToShow._metadata.workflowId,
// activeWorkflowId,
// isDeployed: showingDeployed
// });
// }
// Filter out invalid blocks and make deep clone to avoid reference issues
const result = {
blocks: Object.fromEntries(
Object.entries(workflowToShow.blocks || {})
.filter(([_, block]) => block && block.type) // Filter out invalid blocks
.map(([id, block]) => {
// Deep clone the block to avoid any reference sharing
const clonedBlock = structuredClone(block);
return [id, clonedBlock];
})
),
edges: workflowToShow.edges ? structuredClone(workflowToShow.edges) : [],
loops: workflowToShow.loops ? structuredClone(workflowToShow.loops) : {},
_metadata: {
...(workflowToShow._metadata || {}),
workflowId: activeWorkflowId,
viewType: showingDeployed ? 'deployed' : 'current',
sanitizedAt: Date.now()
}
};
// // Filter out invalid blocks and make deep clone to avoid reference issues
// const result = {
// blocks: Object.fromEntries(
// Object.entries(workflowToShow.blocks || {})
// .filter(([_, block]) => block && block.type) // Filter out invalid blocks
// .map(([id, block]) => {
// // Deep clone the block to avoid any reference sharing
// const clonedBlock = structuredClone(block);
// return [id, clonedBlock];
// })
// ),
// edges: workflowToShow.edges ? structuredClone(workflowToShow.edges) : [],
// loops: workflowToShow.loops ? structuredClone(workflowToShow.loops) : {},
// _metadata: {
// ...(workflowToShow._metadata || {}),
// workflowId: activeWorkflowId,
// viewType: showingDeployed ? 'deployed' : 'current',
// sanitizedAt: Date.now()
// }
// };
return result;
}, [workflowToShow, showingDeployed, activeWorkflowId]);
// return result;
// }, [workflowToShow, showingDeployed, activeWorkflowId]);
// Generate a unique key for the workflow preview
// // Generate a unique key for the workflow preview
const previewKey = useMemo(() => {
return `${showingDeployed ? 'deployed' : 'current'}-preview-${activeWorkflowId}}`;
}, [showingDeployed, activeWorkflowId]);
@@ -94,8 +74,7 @@ export function DeployedWorkflowCard({
<Card className={cn('relative overflow-hidden', className)}>
<CardHeader
className={cn(
'sticky top-0 z-10 space-y-4 p-4',
'backdrop-blur-xl',
'space-y-4 p-4 sticky top-0 z-10',
'bg-background/70 dark:bg-background/50',
'border-border/30 border-b dark:border-border/20',
'shadow-sm'
@@ -129,13 +108,13 @@ export function DeployedWorkflowCard({
<div className="h-px w-full bg-border shadow-sm"></div>
<CardContent className='p-0'>
<CardContent className="p-0">
{/* Workflow preview with fixed height */}
<div className="h-[500px] w-full">
{sanitizedWorkflowState ? (
{/* {sanitizedWorkflowState ? ( */}
<WorkflowPreview
key={previewKey}
workflowState={sanitizedWorkflowState}
workflowState={workflowToShow as WorkflowState}
showSubBlocks={true}
height='100%'
width='100%'
@@ -143,11 +122,6 @@ export function DeployedWorkflowCard({
defaultPosition={{ x: 0, y: 0 }}
defaultZoom={1}
/>
) : (
<div className='flex h-full items-center justify-center text-muted-foreground'>
No workflow data available
</div>
)}
</div>
</CardContent>
</Card>

View File

@@ -25,6 +25,7 @@ import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { mergeSubblockState } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { DeployedWorkflowCard } from './deployed-workflow-card'
import { WorkflowState } from '@/stores/workflows/workflow/types'
const logger = createLogger('DeployedWorkflowModal')
@@ -32,18 +33,7 @@ interface DeployedWorkflowModalProps {
isOpen: boolean
onClose: () => void
needsRedeployment: boolean
deployedWorkflowState: {
blocks: Record<string, any>
edges: Array<any>
loops: Record<string, any>
parallels: Record<string, any>
_metadata?: {
workflowId?: string
fetchTimestamp?: number
requestId?: number
[key: string]: any
}
}
deployedWorkflowState: WorkflowState
}
export function DeployedWorkflowModal({
@@ -74,94 +64,95 @@ export function DeployedWorkflowModal({
}))
// Sanitize states to ensure no invalid blocks are passed to components
const sanitizedCurrentState = useMemo(() => {
if (!currentWorkflowState) return undefined;
// const sanitizedCurrentState = useMemo(() => {
// if (!currentWorkflowState) return undefined;
const result = {
blocks: Object.fromEntries(
Object.entries(currentWorkflowState.blocks || {})
.filter(([_, block]) => block && block.type)
.map(([id, block]) => {
// Deep clone the block to avoid any reference sharing
return [id, structuredClone(block)];
})
),
edges: currentWorkflowState.edges ? [...currentWorkflowState.edges] : [],
loops: currentWorkflowState.loops ? {...currentWorkflowState.loops} : {},
_metadata: {
workflowId: activeWorkflowId || undefined,
type: 'current',
timestamp: Date.now()
}
};
// const result = {
// blocks: Object.fromEntries(
// Object.entries(currentWorkflowState.blocks || {})
// .filter(([_, block]) => block && block.type)
// .map(([id, block]) => {
// // Deep clone the block to avoid any reference sharing
// return [id, structuredClone(block)];
// })
// ),
// edges: currentWorkflowState.edges ? [...currentWorkflowState.edges] : [],
// loops: currentWorkflowState.loops ? {...currentWorkflowState.loops} : {},
// _metadata: {
// workflowId: activeWorkflowId || undefined,
// type: 'current',
// timestamp: Date.now()
// }
// };
return result;
}, [currentWorkflowState, activeWorkflowId]);
// return result;
// }, [currentWorkflowState, activeWorkflowId]);
const sanitizedDeployedState = useMemo(() => {
if (!deployedWorkflowState) return {
blocks: {},
edges: [],
loops: {},
_metadata: {
workflowId: activeWorkflowId || undefined,
type: 'deployed-empty',
timestamp: Date.now()
}
};
// const sanitizedDeployedState = useMemo(() => {
// if (!deployedWorkflowState) return {
// blocks: {},
// edges: [],
// loops: {},
// _metadata: {
// workflowId: activeWorkflowId || undefined,
// type: 'deployed-empty',
// timestamp: Date.now()
// }
// };
const stateWorkflowId = deployedWorkflowState?._metadata?.workflowId;
const stateMatch = stateWorkflowId === activeWorkflowId;
// const stateWorkflowId = deployedWorkflowState?._metadata?.workflowId;
// const stateMatch = stateWorkflowId === activeWorkflowId;
// Check if the deployed state belongs to the current workflow
// This is a critical safety check to prevent showing the wrong workflow state
if (stateWorkflowId && !stateMatch) {
logger.error('Attempted to use deployed state from wrong workflow', {
stateWorkflowId,
activeWorkflowId,
});
// // Check if the deployed state belongs to the current workflow
// // This is a critical safety check to prevent showing the wrong workflow state
// if (stateWorkflowId && !stateMatch) {
// logger.error('Attempted to use deployed state from wrong workflow', {
// stateWorkflowId,
// activeWorkflowId,
// });
// Return empty state to prevent showing wrong workflow data
return {
blocks: {},
edges: [],
loops: {},
_metadata: {
workflowId: activeWorkflowId || undefined,
type: 'deployed-empty-mismatch',
originalWorkflowId: stateWorkflowId,
timestamp: Date.now()
}
};
}
// // Return empty state to prevent showing wrong workflow data
// return {
// blocks: {},
// edges: [],
// loops: {},
// _metadata: {
// workflowId: activeWorkflowId || undefined,
// type: 'deployed-empty-mismatch',
// originalWorkflowId: stateWorkflowId,
// timestamp: Date.now()
// }
// };
// }
const result = {
blocks: Object.fromEntries(
Object.entries(deployedWorkflowState.blocks || {})
.filter(([_, block]) => block && block.type)
.map(([id, block]) => {
// Deep clone the block to avoid any reference sharing
return [id, structuredClone(block)];
})
),
edges: deployedWorkflowState.edges ? [...deployedWorkflowState.edges] : [],
loops: deployedWorkflowState.loops ? {...deployedWorkflowState.loops} : {},
_metadata: {
...(deployedWorkflowState._metadata || {}),
workflowId: deployedWorkflowState._metadata?.workflowId || activeWorkflowId || undefined,
type: 'deployed-sanitized',
sanitizedAt: Date.now()
}
};
// const result = {
// blocks: Object.fromEntries(
// Object.entries(deployedWorkflowState.blocks || {})
// .filter(([_, block]) => block && block.type)
// .map(([id, block]) => {
// // Deep clone the block to avoid any reference sharing
// return [id, structuredClone(block)];
// })
// ),
// edges: deployedWorkflowState.edges ? [...deployedWorkflowState.edges] : [],
// loops: deployedWorkflowState.loops ? {...deployedWorkflowState.loops} : {},
// _metadata: {
// ...(deployedWorkflowState._metadata || {}),
// workflowId: deployedWorkflowState._metadata?.workflowId || activeWorkflowId || undefined,
// type: 'deployed-sanitized',
// sanitizedAt: Date.now()
// }
// };
return result;
}, [deployedWorkflowState, activeWorkflowId]);
// return result;
// }, [deployedWorkflowState, activeWorkflowId]);
const handleRevert = () => {
// Revert to the deployed state
revertToDeployedState(deployedWorkflowState)
setShowRevertDialog(false)
onClose()
if (activeWorkflowId) {
revertToDeployedState(deployedWorkflowState)
setShowRevertDialog(false)
onClose()
}
}
return (
@@ -177,8 +168,8 @@ export function DeployedWorkflowModal({
</DialogHeader>
</div>
<DeployedWorkflowCard
currentWorkflowState={sanitizedCurrentState}
deployedWorkflowState={sanitizedDeployedState}
currentWorkflowState={currentWorkflowState}
deployedWorkflowState={deployedWorkflowState}
/>
<div className="flex justify-between mt-6">

View File

@@ -125,7 +125,7 @@ export function DeploymentControls({
workflowId={activeWorkflowId}
needsRedeployment={workflowNeedsRedeployment}
setNeedsRedeployment={setNeedsRedeployment}
deployedState={deployedState}
deployedState={deployedState as WorkflowState}
isLoadingDeployedState={isLoadingDeployedState}
refetchDeployedState={refetchWithErrorHandling}
/>

View File

@@ -46,6 +46,7 @@ import { usePanelStore } from '@/stores/panel/store'
import { useGeneralStore } from '@/stores/settings/general/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { WorkflowState } from '@/stores/workflows/workflow/types'
import {
getKeyboardShortcutText,
useKeyboardShortcuts,
@@ -56,7 +57,6 @@ import { DeploymentControls } from './components/deployment-controls/deployment-
import { HistoryDropdownItem } from './components/history-dropdown-item/history-dropdown-item'
import { MarketplaceModal } from './components/marketplace-modal/marketplace-modal'
import { NotificationDropdownItem } from './components/notification-dropdown-item/notification-dropdown-item'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
const logger = createLogger('ControlBar')
@@ -108,14 +108,9 @@ export function ControlBar() {
const [mounted, setMounted] = useState(false)
const [, forceUpdate] = useState({})
// Add deployedState management
const [deployedState, setDeployedState] = useState<any>(null)
// Deployed state management
const [deployedState, setDeployedState] = useState<WorkflowState | null>(null)
const [isLoadingDeployedState, setIsLoadingDeployedState] = useState<boolean>(false)
// Add refs to manage fetch state and prevent race conditions
const abortControllerRef = useRef<AbortController | null>(null)
const lastFetchedWorkflowIdRef = useRef<string | null>(null)
const lastDeployedStateRef = useRef<boolean>(false)
// Workflow name editing state
const [isEditing, setIsEditing] = useState(false)
@@ -160,11 +155,6 @@ export function ControlBar() {
isExecuting || isMultiRunning || isCancelling
)
// Get notifications for current workflow
// const workflowNotifications = activeWorkflowId
// ? getWorkflowNotifications(activeWorkflowId)
// : notifications // Show all if no workflow is active
// Get the marketplace data from the workflow registry if available
const getMarketplaceData = () => {
if (!activeWorkflowId || !workflows[activeWorkflowId]) return null
@@ -177,12 +167,6 @@ export function ControlBar() {
return !!marketplaceData
}
// // Check if the current user is the owner of the published workflow
// const isWorkflowOwner = () => {
// const marketplaceData = getMarketplaceData()
// return marketplaceData?.status === 'owner'
// }
// Get deployment status from registry
const deploymentStatus = useWorkflowRegistry((state) =>
state.getWorkflowDeploymentStatus(activeWorkflowId)
@@ -204,217 +188,109 @@ export function ControlBar() {
return () => clearInterval(interval)
}, [])
// Listen for workflow changes and check if redeployment is needed
useEffect(() => {
if (!activeWorkflowId || !isDeployed) return
// Create a debounced function to check for changes
let debounceTimer: NodeJS.Timeout | null = null
let lastCheckTime = 0
let pendingChanges = 0
const DEBOUNCE_DELAY = 1000
const THROTTLE_INTERVAL = 3000
// Function to check if redeployment is needed
const checkForChanges = async () => {
// Skip if we're already showing needsRedeployment
// Reset the pending changes counter
pendingChanges = 0;
lastCheckTime = Date.now();
try {
// Get the deployed state from the API
const response = await fetch(`/api/workflows/${activeWorkflowId}/status`)
if (response.ok) {
const data = await response.json()
// If the API says we need redeployment, update our state and the store
if (data.needsRedeployment) {
setNeedsRedeployment(true)
// Also update the store state so other components can access this flag
useWorkflowStore.getState().setNeedsRedeploymentFlag(true)
} else {
// Add this else branch to handle the case when changes are reverted
setNeedsRedeployment(false)
useWorkflowStore.getState().setNeedsRedeploymentFlag(false)
}
}
} catch (error) {
logger.error('Failed to check workflow change status:', { error })
}
}
// Debounced check function
const debouncedCheck = () => {
// Increment the pending changes counter
pendingChanges++
// Clear any existing timer
if (debounceTimer) {
clearTimeout(debounceTimer)
}
// If we recently checked, and it's within throttle interval, wait longer
const timeElapsed = Date.now() - lastCheckTime
if (timeElapsed < THROTTLE_INTERVAL && lastCheckTime > 0) {
// Wait until the throttle interval has passed
const adjustedDelay = Math.max(THROTTLE_INTERVAL - timeElapsed, DEBOUNCE_DELAY)
debounceTimer = setTimeout(() => {
// Only check if we have pending changes
if (pendingChanges > 0) {
checkForChanges()
}
}, adjustedDelay)
} else {
// Standard debounce delay if we haven't checked recently
debounceTimer = setTimeout(() => {
// Only check if we have pending changes
if (pendingChanges > 0) {
checkForChanges()
}
}, DEBOUNCE_DELAY)
}
}
// Subscribe to workflow store changes
const workflowUnsubscribe = useWorkflowStore.subscribe(debouncedCheck)
// Also subscribe to subblock store changes
const subBlockUnsubscribe = useSubBlockStore.subscribe((state) => {
// Only check for the active workflow
if (!activeWorkflowId || !isDeployed || needsRedeployment) return
// Only trigger when there is an update to the current workflow's subblocks
const workflowSubBlocks = state.workflowValues[activeWorkflowId]
if (workflowSubBlocks && Object.keys(workflowSubBlocks).length > 0) {
debouncedCheck()
}
})
return () => {
if (debounceTimer) {
clearTimeout(debounceTimer)
}
workflowUnsubscribe()
subBlockUnsubscribe()
}
}, [activeWorkflowId, isDeployed, needsRedeployment])
/**
* Fetches the deployed state of the workflow from the server
* This is the single source of truth for deployed workflow state
* @param options.forceRefetch Force a refetch even if conditions wouldn't normally trigger it
* @returns Promise that resolves when the deployed state is fetched
*/
const fetchDeployedState = async (options = { forceRefetch: false }) => {
// Cancel any in-flight requests
if (abortControllerRef.current) {
abortControllerRef.current.abort();
if (!activeWorkflowId) {
setDeployedState(null)
return
}
// Create new abort controller for this request
abortControllerRef.current = new AbortController();
const { signal } = abortControllerRef.current;
const requestId = Date.now();
const currentWorkflowId = activeWorkflowId;
// Skip fetching if we don't have an active workflow ID or it's not deployed
// unless we're explicitly forcing a refetch
if ((!currentWorkflowId || !isDeployed) && !options.forceRefetch) {
setDeployedState(null);
return;
// Skip fetching if not deployed unless forcing a refetch
if (!isDeployed && !options.forceRefetch) {
setDeployedState(null)
return
}
// Store the workflow ID at the start of the request to prevent race conditions
const requestWorkflowId = activeWorkflowId
try {
setIsLoadingDeployedState(true);
setIsLoadingDeployedState(true)
// Pass the abort signal to the fetch call
const response = await fetch(
`/api/workflows/${currentWorkflowId}/deployed`,
{ signal }
);
const response = await fetch(`/api/workflows/${requestWorkflowId}/deployed`)
// Check if the workflow ID changed during the request (user navigated away)
if (requestWorkflowId !== useWorkflowRegistry.getState().activeWorkflowId) {
logger.debug('Workflow changed during deployed state fetch, ignoring response')
return
}
if (!response.ok) {
throw new Error(`Failed to fetch deployed state: ${response.status}`);
if (response.status === 404) {
// No deployed state found
setDeployedState(null)
return
}
throw new Error(`Failed to fetch deployed state: ${response.statusText}`)
}
const data = await response.json();
const data = await response.json()
// Final workflow ID check before updating state
if (currentWorkflowId !== activeWorkflowId) {
return;
}
if (data.deployedState) {
// Create a single deep clone with metadata
const deployedStateWithMetadata = {
...JSON.parse(JSON.stringify(data.deployedState)),
_metadata: {
workflowId: currentWorkflowId,
fetchTimestamp: Date.now(),
requestId
}
};
setDeployedState(deployedStateWithMetadata);
// Final check to ensure we're still on the same workflow
if (requestWorkflowId === useWorkflowRegistry.getState().activeWorkflowId) {
setDeployedState(data.deployedState || null)
} else {
setDeployedState(null);
logger.debug('Workflow changed after deployed state response, ignoring result')
}
} catch (error: unknown) {
// Don't log AbortError as it's expected when cancelling requests
if (error instanceof Error && error.name === 'AbortError') {
// Silently ignore abort errors
} else {
logger.error(`Error fetching deployed state:`, { error });
setDeployedState(null);
} catch (error) {
logger.error('Error fetching deployed state:', { error })
// Only set error state if we're still on the same workflow
if (requestWorkflowId === useWorkflowRegistry.getState().activeWorkflowId) {
setDeployedState(null)
}
} finally {
setIsLoadingDeployedState(false);
// Only clear loading state if we're still on the same workflow
if (requestWorkflowId === useWorkflowRegistry.getState().activeWorkflowId) {
setIsLoadingDeployedState(false)
}
}
};
// Alias for clarity when explicitly triggering a refetch
const refetchDeployedState = () => fetchDeployedState({ forceRefetch: true });
}
// Fetch deployed state when the workflow ID changes or deployment status changes
useEffect(() => {
// Only fetch if the workflow ID or deployed status has actually changed
if (activeWorkflowId !== lastFetchedWorkflowIdRef.current ||
isDeployed !== lastDeployedStateRef.current) {
// Update refs to track what we're fetching for
lastFetchedWorkflowIdRef.current = activeWorkflowId;
lastDeployedStateRef.current = isDeployed;
fetchDeployedState();
// Immediately clear deployed state when workflow changes to prevent mixup
if (activeWorkflowId) {
setDeployedState(null)
setIsLoadingDeployedState(false)
}
}, [activeWorkflowId, isDeployed]);
// Then fetch the new deployed state
fetchDeployedState()
}, [activeWorkflowId, isDeployed])
// Listen for deployment status changes
useEffect(() => {
// When deployment status changes and isDeployed becomes true,
// that means a deployment just occurred, so reset the needsRedeployment flag
// Clear deployed state immediately when workflow changes
if (!activeWorkflowId) {
setDeployedState(null)
setIsLoadingDeployedState(false)
return
}
if (isDeployed) {
// When deployment status becomes true, reset the needsRedeployment flag
setNeedsRedeployment(false)
useWorkflowStore.getState().setNeedsRedeploymentFlag(false)
// Fetch the latest deployed state
fetchDeployedState()
} else {
// If workflow is undeployed, clear the deployed state
setDeployedState(null)
setIsLoadingDeployedState(false)
}
}, [isDeployed, activeWorkflowId])
// Add a listener for the needsRedeployment flag in the workflow store
useEffect(() => {
const unsubscribe = useWorkflowStore.subscribe((state) => {
// Update local state when the store flag changes
if (state.needsRedeployment !== undefined) {
setNeedsRedeployment(state.needsRedeployment)
}
})
return () => unsubscribe()
}, [])
@@ -427,14 +303,11 @@ export function ControlBar() {
)
if (apiNotification && apiNotification.options?.needsRedeployment !== needsRedeployment) {
// If there's an existing API notification and its state doesn't match, update it
if (apiNotification.isVisible) {
// Only update if it's currently showing to the user
removeNotification(apiNotification.id)
// The DeploymentControls component will handle showing the appropriate notification
}
}
}, [needsRedeployment, activeWorkflowId, notifications, removeNotification, addNotification])
}, [needsRedeployment, activeWorkflowId, notifications, removeNotification])
// Check usage limits when component mounts and when user executes a workflow
useEffect(() => {
@@ -536,23 +409,6 @@ export function ControlBar() {
removeWorkflow(activeWorkflowId)
}
// /**
// * Handle opening marketplace modal or showing published status
// */
// const handlePublishWorkflow = async () => {
// if (!activeWorkflowId) return
// // If already published, show marketplace modal with info instead of notifications
// const isPublished = isPublishedToMarketplace()
// if (isPublished) {
// setIsMarketplaceModalOpen(true)
// return
// }
// // If not published, open the modal to start the publishing process
// setIsMarketplaceModalOpen(true)
// }
/**
* Handle multiple workflow runs
*/
@@ -760,7 +616,7 @@ export function ControlBar() {
setNeedsRedeployment={setNeedsRedeployment}
deployedState={deployedState}
isLoadingDeployedState={isLoadingDeployedState}
refetchDeployedState={refetchDeployedState}
refetchDeployedState={fetchDeployedState}
/>
)

View File

@@ -11,7 +11,7 @@ interface CheckboxListProps {
options: { label: string; id: string }[]
layout?: 'full' | 'half'
isPreview?: boolean
value?: Record<string, boolean>
subBlockValues?: Record<string, any>
}
export function CheckboxList({
@@ -21,25 +21,36 @@ export function CheckboxList({
options,
layout,
isPreview = false,
value: propValues
subBlockValues
}: CheckboxListProps) {
return (
<div className={cn('grid gap-4', layout === 'half' ? 'grid-cols-2' : 'grid-cols-1', 'pt-1')}>
{options.map((option) => {
const [value, setValue] = useSubBlockValue(
const [storeValue, setStoreValue] = useSubBlockValue(
blockId,
option.id,
false,
isPreview,
propValues?.[option.id]
option.id
)
// Get preview value for this specific option
const previewValue = isPreview && subBlockValues ? subBlockValues[option.id]?.value : undefined
// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : storeValue
const handleChange = (checked: boolean) => {
// Only update store when not in preview mode
if (!isPreview) {
setStoreValue(checked)
}
}
return (
<div key={option.id} className='flex items-center space-x-2'>
<Checkbox
id={`${blockId}-${option.id}`}
checked={Boolean(value)}
onCheckedChange={(checked) => setValue(checked as boolean)}
onCheckedChange={handleChange}
disabled={isPreview}
/>
<Label
htmlFor={`${blockId}-${option.id}`}

View File

@@ -1,5 +1,5 @@
import { useEffect, useRef, useState, useMemo } from 'react'
import type { ReactElement } from 'react'
import { useEffect, useRef, useState } from 'react'
import { Wand2 } from 'lucide-react'
import { highlight, languages } from 'prismjs'
import 'prismjs/components/prism-javascript'
@@ -25,8 +25,9 @@ interface CodeProps {
placeholder?: string
language?: 'javascript' | 'json'
generationType?: 'javascript-function-body' | 'json-schema'
isPreview?: boolean
value?: string
isPreview?: boolean
previewValue?: string | null
}
if (typeof document !== 'undefined') {
@@ -54,17 +55,17 @@ export function Code({
placeholder = 'Write JavaScript...',
language = 'javascript',
generationType = 'javascript-function-body',
value: propValue,
isPreview = false,
value: propValue
previewValue
}: CodeProps) {
// Determine the AI prompt placeholder based on language
const aiPromptPlaceholder =
language === 'json'
? 'Describe the JSON schema to generate...'
: 'Describe the JavaScript code to generate...'
const aiPromptPlaceholder = useMemo(() => {
return language === 'json' ? 'Describe the JSON schema you need...' : 'Describe the function you need...'
}, [language])
// State management
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId, false, isPreview, propValue)
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
const [code, setCode] = useState<string>('')
const [_lineCount, setLineCount] = useState(1)
const [showTags, setShowTags] = useState(false)
@@ -87,6 +88,8 @@ export function Code({
const toggleCollapsed = () => {
setCollapsedValue(blockId, collapsedStateKey, !isCollapsed)
}
// Use preview value when in preview mode, otherwise use store value or prop value
const value = isPreview ? previewValue : (propValue !== undefined ? propValue : storeValue)
// AI Code Generation Hook
const handleStreamStart = () => {
@@ -97,14 +100,18 @@ export function Code({
const handleGeneratedContent = (generatedCode: string) => {
setCode(generatedCode)
setStoreValue(generatedCode)
if (!isPreview) {
setStoreValue(generatedCode)
}
}
// Handle streaming chunks directly into the editor
const handleStreamChunk = (chunk: string) => {
setCode((currentCode) => {
const newCode = currentCode + chunk
setStoreValue(newCode)
if (!isPreview) {
setStoreValue(newCode)
}
return newCode
})
}
@@ -130,11 +137,11 @@ export function Code({
// Effects
useEffect(() => {
const valueString = storeValue?.toString() ?? ''
const valueString = value?.toString() ?? ''
if (valueString !== code) {
setCode(valueString)
}
}, [storeValue])
}, [value])
useEffect(() => {
if (!editorRef.current) return
@@ -205,6 +212,7 @@ export function Code({
// Handlers
const handleDrop = (e: React.DragEvent) => {
if (isPreview) return
e.preventDefault()
try {
const data = JSON.parse(e.dataTransfer.getData('application/json'))
@@ -237,8 +245,10 @@ export function Code({
}
const handleTagSelect = (newValue: string) => {
setCode(newValue)
setStoreValue(newValue)
if (!isPreview) {
setCode(newValue)
setStoreValue(newValue)
}
setShowTags(false)
setActiveSourceBlockId(null)
@@ -248,8 +258,10 @@ export function Code({
}
const handleEnvVarSelect = (newValue: string) => {
setCode(newValue)
setStoreValue(newValue)
if (!isPreview) {
setCode(newValue)
setStoreValue(newValue)
}
setShowEnvVars(false)
setTimeout(() => {
@@ -313,8 +325,8 @@ export function Code({
onDragOver={(e) => e.preventDefault()}
onDrop={handleDrop}
>
<div className='absolute top-2 right-3 z-10 flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100'>
{!isCollapsed && !isAiStreaming && (
<div className="absolute right-3 top-2 z-10 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
{!isCollapsed && !isAiStreaming && !isPreview && (
<Button
variant='ghost'
size='icon'
@@ -327,7 +339,7 @@ export function Code({
</Button>
)}
{showCollapseButton && !isAiStreaming && (
{showCollapseButton && !isAiStreaming && !isPreview && (
<Button
variant='ghost'
size='sm'
@@ -364,7 +376,7 @@ export function Code({
<Editor
value={code}
onValueChange={(newCode) => {
if (!isCollapsed && !isAiStreaming) {
if (!isCollapsed && !isAiStreaming && !isPreview) {
setCode(newCode)
setStoreValue(newCode)

View File

@@ -34,7 +34,7 @@ interface ConditionInputProps {
subBlockId: string
isConnecting: boolean
isPreview?: boolean
value?: string
previewValue?: string | null
}
// Generate a stable ID based on the blockId and a suffix
@@ -42,8 +42,14 @@ const generateStableId = (blockId: string, suffix: string): string => {
return `${blockId}-${suffix}`
}
export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview = false, value: propValue }: ConditionInputProps) {
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId, false, isPreview, propValue)
export function ConditionInput({
blockId,
subBlockId,
isConnecting,
isPreview = false,
previewValue
}: ConditionInputProps) {
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
const editorRef = useRef<HTMLDivElement>(null)
const [visualLineHeights, setVisualLineHeights] = useState<{
[key: string]: number[]
@@ -126,15 +132,17 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
// Skip if syncing is already in progress
if (isSyncingFromStoreRef.current) return
// Convert storeValue to string if it's not null
const storeValueStr = storeValue !== null ? storeValue.toString() : null
// Use preview value when in preview mode, otherwise use store value
const effectiveValue = isPreview ? previewValue : storeValue
// Convert effectiveValue to string if it's not null
const effectiveValueStr = effectiveValue !== null ? effectiveValue?.toString() : null
// Set that we're syncing from store to prevent loops
isSyncingFromStoreRef.current = true
try {
// If store value is null, and we've already initialized, keep current state
if (storeValueStr === null) {
// If effective value is null, and we've already initialized, keep current state
if (effectiveValueStr === null) {
if (hasInitializedRef.current) {
// We already have blocks, just mark as ready if not already
if (!isReady) setIsReady(true)
@@ -150,18 +158,18 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
return
}
// Skip if the store value hasn't changed and we're already initialized
if (storeValueStr === prevStoreValueRef.current && hasInitializedRef.current) {
// Skip if the effective value hasn't changed and we're already initialized
if (effectiveValueStr === prevStoreValueRef.current && hasInitializedRef.current) {
if (!isReady) setIsReady(true)
isSyncingFromStoreRef.current = false
return
}
// Update the previous store value ref
prevStoreValueRef.current = storeValueStr
prevStoreValueRef.current = effectiveValueStr
// Parse the store value
const parsedBlocks = safeParseJSON(storeValueStr)
// Parse the effective value
const parsedBlocks = safeParseJSON(effectiveValueStr)
if (parsedBlocks) {
// Use the parsed blocks, but ensure titles are correct based on position
@@ -185,13 +193,13 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
isSyncingFromStoreRef.current = false
}, 0)
}
}, [storeValue, blockId, isReady])
}, [storeValue, previewValue, isPreview, blockId, isReady])
// Update store whenever conditional blocks change
useEffect(() => {
// Skip if we're currently syncing from store to prevent loops
// or if we're not ready yet (still initializing)
if (isSyncingFromStoreRef.current || !isReady || conditionalBlocks.length === 0) return
// or if we're not ready yet (still initializing) or in preview mode
if (isSyncingFromStoreRef.current || !isReady || conditionalBlocks.length === 0 || isPreview) return
const newValue = JSON.stringify(conditionalBlocks)
@@ -201,7 +209,7 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
setStoreValue(newValue)
updateNodeInternals(`${blockId}-${subBlockId}`)
}
}, [conditionalBlocks, blockId, subBlockId, setStoreValue, updateNodeInternals, isReady])
}, [conditionalBlocks, blockId, subBlockId, setStoreValue, updateNodeInternals, isReady, isPreview])
// Cleanup when component unmounts
useEffect(() => {
@@ -218,6 +226,8 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
newValue: string,
textarea: HTMLTextAreaElement | null
) => {
if (isPreview) return
try {
setConditionalBlocks((blocks) =>
blocks.map((block) => {
@@ -345,6 +355,7 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
// Handle drops from connection blocks - updated for individual blocks
const handleDrop = (blockId: string, e: React.DragEvent) => {
if (isPreview) return
e.preventDefault()
try {
const data = JSON.parse(e.dataTransfer.getData('application/json'))
@@ -386,6 +397,7 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
// Handle tag selection - updated for individual blocks
const handleTagSelect = (blockId: string, newValue: string) => {
if (isPreview) return
setConditionalBlocks((blocks) =>
blocks.map((block) =>
block.id === blockId
@@ -402,6 +414,7 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
// Handle environment variable selection - updated for individual blocks
const handleEnvVarSelect = (blockId: string, newValue: string) => {
if (isPreview) return
setConditionalBlocks((blocks) =>
blocks.map((block) =>
block.id === blockId
@@ -426,6 +439,8 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
// Update these functions to use updateBlockTitles and stable IDs
const addBlock = (afterId: string) => {
if (isPreview) return
const blockIndex = conditionalBlocks.findIndex((block) => block.id === afterId)
// Generate a stable ID using the blockId and a timestamp
@@ -458,6 +473,8 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
}
const removeBlock = (id: string) => {
if (isPreview) return
// Remove any associated edges before removing the block
edges.forEach((edge) => {
if (edge.sourceHandle?.startsWith(`condition-${id}`)) {
@@ -470,6 +487,8 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
}
const moveBlock = (id: string, direction: 'up' | 'down') => {
if (isPreview) return
const blockIndex = conditionalBlocks.findIndex((block) => block.id === id)
if (
(direction === 'up' && blockIndex === 0) ||
@@ -511,6 +530,9 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
})
}, [conditionalBlocks.length])
// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : storeValue
// Show loading or empty state if not ready or no blocks
if (!isReady || conditionalBlocks.length === 0) {
return (
@@ -574,7 +596,8 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
variant='ghost'
size='sm'
onClick={() => addBlock(block.id)}
className='h-8 w-8'
disabled={isPreview}
className="h-8 w-8"
>
<Plus className='h-4 w-4' />
<span className='sr-only'>Add Block</span>
@@ -590,8 +613,8 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
variant='ghost'
size='sm'
onClick={() => moveBlock(block.id, 'up')}
disabled={index === 0}
className='h-8 w-8'
disabled={isPreview || index === 0}
className="h-8 w-8"
>
<ChevronUp className='h-4 w-4' />
<span className='sr-only'>Move Up</span>
@@ -606,8 +629,8 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
variant='ghost'
size='sm'
onClick={() => moveBlock(block.id, 'down')}
disabled={index === conditionalBlocks.length - 1}
className='h-8 w-8'
disabled={isPreview || index === conditionalBlocks.length - 1}
className="h-8 w-8"
>
<ChevronDown className='h-4 w-4' />
<span className='sr-only'>Move Down</span>
@@ -623,8 +646,8 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
variant='ghost'
size='sm'
onClick={() => removeBlock(block.id)}
disabled={conditionalBlocks.length === 1}
className='h-8 w-8 text-destructive hover:text-destructive'
disabled={isPreview || conditionalBlocks.length === 1}
className="h-8 w-8 text-destructive hover:text-destructive"
>
<Trash className='h-4 w-4' />
<span className='sr-only'>Delete Block</span>
@@ -664,10 +687,12 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
<Editor
value={block.value}
onValueChange={(newCode) => {
const textarea = editorRef.current?.querySelector(
`[data-block-id="${block.id}"] textarea`
)
updateBlockValue(block.id, newCode, textarea as HTMLTextAreaElement | null)
if (!isPreview) {
const textarea = editorRef.current?.querySelector(
`[data-block-id="${block.id}"] textarea`
)
updateBlockValue(block.id, newCode, textarea as HTMLTextAreaElement | null)
}
}}
onKeyDown={(e) => {
if (e.key === 'Escape') {
@@ -685,8 +710,8 @@ export function ConditionInput({ blockId, subBlockId, isConnecting, isPreview =
minHeight: '46px',
lineHeight: '21px',
}}
className='focus:outline-none'
textareaClassName='focus:outline-none focus:ring-0 bg-transparent'
className={cn('focus:outline-none', isPreview && 'opacity-50 cursor-not-allowed')}
textareaClassName={cn('focus:outline-none focus:ring-0 bg-transparent', isPreview && 'pointer-events-none')}
/>
{block.showEnvVars && (

View File

@@ -16,11 +16,20 @@ interface DateInputProps {
subBlockId: string
placeholder?: string
isPreview?: boolean
value?: string
previewValue?: string | null
}
export function DateInput({ blockId, subBlockId, placeholder, isPreview = false, value: propValue }: DateInputProps) {
const [value, setValue] = useSubBlockValue<string>(blockId, subBlockId, true, isPreview, propValue)
export function DateInput({
blockId,
subBlockId,
placeholder,
isPreview = false,
previewValue
}: DateInputProps) {
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : storeValue
const addNotification = useNotificationStore((state) => state.addNotification)
const date = value ? new Date(value) : undefined
@@ -32,6 +41,8 @@ export function DateInput({ blockId, subBlockId, placeholder, isPreview = false,
}, [date])
const handleDateSelect = (selectedDate: Date | undefined) => {
if (isPreview) return
if (selectedDate) {
const today = new Date()
today.setHours(0, 0, 0, 0)
@@ -40,14 +51,15 @@ export function DateInput({ blockId, subBlockId, placeholder, isPreview = false,
addNotification('error', 'Cannot start at a date in the past', blockId)
}
}
setValue(selectedDate?.toISOString() || '')
setStoreValue(selectedDate?.toISOString() || '')
}
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant='outline'
variant="outline"
disabled={isPreview}
className={cn(
'w-full justify-start text-left font-normal',
!date && 'text-muted-foreground',

View File

@@ -9,8 +9,6 @@ import {
import { createLogger } from '@/lib/logs/console-logger'
import { useSubBlockValue } from '../hooks/use-sub-block-value'
const logger = createLogger('Dropdown')
interface DropdownProps {
options:
| Array<string | { label: string; id: string }>
@@ -18,8 +16,9 @@ interface DropdownProps {
defaultValue?: string
blockId: string
subBlockId: string
isPreview?: boolean
value?: string
isPreview?: boolean
previewValue?: string | null
}
export function Dropdown({
@@ -27,12 +26,16 @@ export function Dropdown({
defaultValue,
blockId,
subBlockId,
value: propValue,
isPreview = false,
value: propValue
previewValue
}: DropdownProps) {
const [value, setValue] = useSubBlockValue<string>(blockId, subBlockId, true, isPreview, propValue)
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlockId)
const [storeInitialized, setStoreInitialized] = useState(false)
// Use preview value when in preview mode, otherwise use store value or prop value
const value = isPreview ? previewValue : (propValue !== undefined ? propValue : storeValue)
// Evaluate options if it's a function
const evaluatedOptions = useMemo(() => {
return typeof options === 'function' ? options() : options
@@ -72,9 +75,9 @@ export function Dropdown({
(value === null || value === undefined) &&
defaultOptionValue !== undefined
) {
setValue(defaultOptionValue)
setStoreValue(defaultOptionValue)
}
}, [storeInitialized, value, defaultOptionValue, setValue])
}, [storeInitialized, value, defaultOptionValue, setStoreValue])
// Calculate the effective value to use in the dropdown
const effectiveValue = useMemo(() => {
@@ -101,7 +104,13 @@ export function Dropdown({
return (
<Select
value={isValueInOptions ? effectiveValue : undefined}
onValueChange={(newValue) => setValue(newValue)}
onValueChange={(newValue) => {
// Only update store when not in preview mode
if (!isPreview) {
setStoreValue(newValue)
}
}}
disabled={isPreview}
>
<SelectTrigger className='text-left'>
<SelectValue placeholder='Select an option' />

View File

@@ -20,7 +20,7 @@ interface EvalInputProps {
blockId: string
subBlockId: string
isPreview?: boolean
value?: EvalMetric[]
previewValue?: EvalMetric[] | null
}
// Default values
@@ -31,56 +31,52 @@ const DEFAULT_METRIC: EvalMetric = {
range: { min: 0, max: 1 },
}
export function EvalInput({ blockId, subBlockId, isPreview = false, value: propValue }: EvalInputProps) {
export function EvalInput({
blockId,
subBlockId,
isPreview = false,
previewValue
}: EvalInputProps) {
const [storeValue, setStoreValue] = useSubBlockValue<EvalMetric[]>(blockId, subBlockId)
// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : storeValue
// State hooks
const [value, setValue] = useSubBlockValue<EvalMetric[]>(blockId, subBlockId, false, isPreview, propValue)
const metrics = value || [DEFAULT_METRIC]
const metrics: EvalMetric[] = value || [DEFAULT_METRIC]
// Metric operations
const addMetric = () => {
if (isPreview) return
const newMetric: EvalMetric = {
...DEFAULT_METRIC,
id: crypto.randomUUID(),
}
setValue([...metrics, newMetric])
setStoreValue([...metrics, newMetric])
}
const removeMetric = (id: string) => {
if (metrics.length === 1) return
setValue(metrics.filter((metric) => metric.id !== id))
if (isPreview || metrics.length === 1) return
setStoreValue(metrics.filter((metric) => metric.id !== id))
}
// Update handlers
const updateMetric = (id: string, field: keyof EvalMetric, value: any) => {
setValue(metrics.map((metric) => (metric.id === id ? { ...metric, [field]: value } : metric)))
if (isPreview) return
setStoreValue(metrics.map((metric) => (metric.id === id ? { ...metric, [field]: value } : metric)))
}
const updateRange = (id: string, field: 'min' | 'max', value: string) => {
setValue(
metrics.map((metric) =>
metric.id === id
? {
...metric,
range: { ...metric.range, [field]: value },
}
: metric
)
)
}
// Validation handlers
const handleRangeBlur = (id: string, field: 'min' | 'max', value: string) => {
const sanitizedValue = value.replace(/[^\d.-]/g, '')
const numValue = Number.parseFloat(sanitizedValue)
setValue(
if (isPreview) return
setStoreValue(
metrics.map((metric) =>
metric.id === id
? {
...metric,
range: {
...metric.range,
[field]: !Number.isNaN(numValue) ? numValue : 0,
[field]: value === '' ? undefined : parseInt(value, 10),
},
}
: metric
@@ -88,6 +84,29 @@ export function EvalInput({ blockId, subBlockId, isPreview = false, value: propV
)
}
const updateThreshold = (id: string, value: string) => {
if (isPreview) return
// Allow empty values for clearing
const sanitizedValue = value.replace(/[^0-9.-]/g, '')
if (sanitizedValue === '') {
setStoreValue(
metrics.map((metric) =>
metric.id === id ? { ...metric, threshold: undefined } : metric
)
)
return
}
const numValue = parseFloat(sanitizedValue)
setStoreValue(
metrics.map((metric) =>
metric.id === id ? { ...metric, threshold: isNaN(numValue) ? undefined : numValue } : metric
)
)
}
// Metric header
const renderMetricHeader = (metric: EvalMetric, index: number) => (
<div className='flex h-10 items-center justify-between rounded-t-lg border-b bg-card px-3'>
@@ -95,9 +114,9 @@ export function EvalInput({ blockId, subBlockId, isPreview = false, value: propV
<div className='flex items-center gap-1'>
<Tooltip>
<TooltipTrigger asChild>
<Button variant='ghost' size='sm' onClick={addMetric} className='h-8 w-8'>
<Plus className='h-4 w-4' />
<span className='sr-only'>Add Metric</span>
<Button variant="ghost" size="sm" onClick={addMetric} disabled={isPreview} className="h-8 w-8">
<Plus className="h-4 w-4" />
<span className="sr-only">Add Metric</span>
</Button>
</TooltipTrigger>
<TooltipContent>Add Metric</TooltipContent>
@@ -109,8 +128,8 @@ export function EvalInput({ blockId, subBlockId, isPreview = false, value: propV
variant='ghost'
size='sm'
onClick={() => removeMetric(metric.id)}
disabled={metrics.length === 1}
className='h-8 w-8 text-destructive hover:text-destructive'
disabled={isPreview || metrics.length === 1}
className="h-8 w-8 text-destructive hover:text-destructive"
>
<Trash className='h-4 w-4' />
<span className='sr-only'>Delete Metric</span>
@@ -140,8 +159,9 @@ export function EvalInput({ blockId, subBlockId, isPreview = false, value: propV
name='name'
value={metric.name}
onChange={(e) => updateMetric(metric.id, 'name', e.target.value)}
placeholder='Accuracy'
className='placeholder:text-muted-foreground/50'
placeholder="Accuracy"
disabled={isPreview}
className="placeholder:text-muted-foreground/50"
/>
</div>
@@ -150,8 +170,9 @@ export function EvalInput({ blockId, subBlockId, isPreview = false, value: propV
<Input
value={metric.description}
onChange={(e) => updateMetric(metric.id, 'description', e.target.value)}
placeholder='How accurate is the response?'
className='placeholder:text-muted-foreground/50'
placeholder="How accurate is the response?"
disabled={isPreview}
className="placeholder:text-muted-foreground/50"
/>
</div>
@@ -162,8 +183,9 @@ export function EvalInput({ blockId, subBlockId, isPreview = false, value: propV
type='text'
value={metric.range.min}
onChange={(e) => updateRange(metric.id, 'min', e.target.value)}
onBlur={(e) => handleRangeBlur(metric.id, 'min', e.target.value)}
className='placeholder:text-muted-foreground/50'
onBlur={(e) => updateThreshold(metric.id, e.target.value)}
disabled={isPreview}
className="placeholder:text-muted-foreground/50"
/>
</div>
<div className='space-y-1'>
@@ -172,8 +194,9 @@ export function EvalInput({ blockId, subBlockId, isPreview = false, value: propV
type='text'
value={metric.range.max}
onChange={(e) => updateRange(metric.id, 'max', e.target.value)}
onBlur={(e) => handleRangeBlur(metric.id, 'max', e.target.value)}
className='placeholder:text-muted-foreground/50'
onBlur={(e) => updateThreshold(metric.id, e.target.value)}
disabled={isPreview}
className="placeholder:text-muted-foreground/50"
/>
</div>
</div>

View File

@@ -24,17 +24,17 @@ import { TeamsMessageSelector } from './components/teams-message-selector'
interface FileSelectorInputProps {
blockId: string
subBlock: SubBlockConfig
disabled?: boolean
disabled: boolean
isPreview?: boolean
value?: string
previewValue?: any | null
}
export function FileSelectorInput({
blockId,
subBlock,
disabled = false,
disabled,
isPreview = false,
value: propValue
previewValue
}: FileSelectorInputProps) {
const { getValue, setValue } = useSubBlockStore()
const { activeWorkflowId } = useWorkflowRegistry()
@@ -60,10 +60,13 @@ export function FileSelectorInput({
const botToken = isDiscord ? (getValue(blockId, 'botToken') as string) || '' : ''
const serverId = isDiscord ? (getValue(blockId, 'serverId') as string) || '' : ''
// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : getValue(blockId, subBlock.id)
// Get the current value from the store or prop value if in preview mode
useEffect(() => {
if (isPreview && propValue !== undefined) {
const value = propValue;
if (isPreview && previewValue !== undefined) {
const value = previewValue;
if (value && typeof value === 'string') {
if (isJira) {
setSelectedIssueId(value);
@@ -87,7 +90,7 @@ export function FileSelectorInput({
}
}
}
}, [blockId, subBlock.id, getValue, isJira, isDiscord, isMicrosoftTeams, isPreview, propValue]);
}, [blockId, subBlock.id, getValue, isJira, isDiscord, isPreview, previewValue]);
// Handle file selection
const handleFileChange = (fileId: string, info?: any) => {

View File

@@ -18,7 +18,7 @@ interface FileUploadProps {
acceptedTypes?: string // comma separated MIME types
multiple?: boolean // whether to allow multiple file uploads
isPreview?: boolean
value?: UploadedFile | UploadedFile[] | null
previewValue?: any | null
}
interface UploadedFile {
@@ -41,16 +41,10 @@ export function FileUpload({
acceptedTypes = '*',
multiple = false, // Default to single file for backward compatibility
isPreview = false,
value: propValue
previewValue
}: FileUploadProps) {
// State management - handle both single file and array of files
const [value, setValue] = useSubBlockValue<UploadedFile | UploadedFile[] | null>(
blockId,
subBlockId,
true,
isPreview,
propValue
)
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
const [uploadingFiles, setUploadingFiles] = useState<UploadingFile[]>([])
const [uploadProgress, setUploadProgress] = useState(0)
@@ -64,6 +58,9 @@ export function FileUpload({
const { addNotification } = useNotificationStore()
const { activeWorkflowId } = useWorkflowRegistry()
// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : storeValue
/**
* Opens file dialog
* Prevents event propagation to avoid ReactFlow capturing the event
@@ -91,6 +88,8 @@ export function FileUpload({
* Handles file upload when new file(s) are selected
*/
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
if (isPreview) return
e.stopPropagation()
const files = e.target.files
@@ -287,14 +286,14 @@ export function FileUpload({
// Convert map values back to array
const newFiles = Array.from(uniqueFiles.values())
setValue(newFiles)
setStoreValue(newFiles)
// Make sure to update the subblock store value for the workflow execution
useSubBlockStore.getState().setValue(blockId, subBlockId, newFiles)
useWorkflowStore.getState().triggerUpdate()
} else {
// For single file: Replace with last uploaded file
setValue(uploadedFiles[0] || null)
setStoreValue(uploadedFiles[0] || null)
// Make sure to update the subblock store value for the workflow execution
useSubBlockStore.getState().setValue(blockId, subBlockId, uploadedFiles[0] || null)
@@ -352,7 +351,7 @@ export function FileUpload({
// For multiple files: Remove the specific file
const filesArray = Array.isArray(value) ? value : value ? [value] : []
const updatedFiles = filesArray.filter((f) => f.path !== file.path)
setValue(updatedFiles.length > 0 ? updatedFiles : null)
setStoreValue(updatedFiles.length > 0 ? updatedFiles : null)
// Make sure to update the subblock store value for the workflow execution
useSubBlockStore
@@ -360,7 +359,7 @@ export function FileUpload({
.setValue(blockId, subBlockId, updatedFiles.length > 0 ? updatedFiles : null)
} else {
// For single file: Clear the value
setValue(null)
setStoreValue(null)
// Make sure to update the subblock store
useSubBlockStore.getState().setValue(blockId, subBlockId, null)
@@ -403,7 +402,7 @@ export function FileUpload({
setDeletingFiles(deletingStatus)
// Clear input state immediately for better UX
setValue(null)
setStoreValue(null)
useSubBlockStore.getState().setValue(blockId, subBlockId, null)
useWorkflowStore.getState().triggerUpdate()

View File

@@ -14,7 +14,7 @@ interface FolderSelectorInputProps {
subBlock: SubBlockConfig
disabled?: boolean
isPreview?: boolean
value?: string
previewValue?: any | null
}
export function FolderSelectorInput({
@@ -22,7 +22,7 @@ export function FolderSelectorInput({
subBlock,
disabled = false,
isPreview = false,
value: propValue
previewValue
}: FolderSelectorInputProps) {
const { getValue, setValue } = useSubBlockStore()
const [selectedFolderId, setSelectedFolderId] = useState<string>('')
@@ -30,8 +30,8 @@ export function FolderSelectorInput({
// Get the current value from the store or prop value if in preview mode
useEffect(() => {
if (isPreview && propValue !== undefined) {
setSelectedFolderId(propValue);
if (isPreview && previewValue !== undefined) {
setSelectedFolderId(previewValue);
} else {
const value = getValue(blockId, subBlock.id);
if (value && typeof value === 'string') {
@@ -44,7 +44,7 @@ export function FolderSelectorInput({
}
}
}
}, [blockId, subBlock.id, getValue, setValue, isPreview, propValue]);
}, [blockId, subBlock.id, getValue, setValue, isPreview, previewValue]);
// Handle folder selection
const handleFolderChange = (folderId: string, info?: FolderInfo) => {

View File

@@ -20,7 +20,9 @@ interface LongInputProps {
config: SubBlockConfig
rows?: number
isPreview?: boolean
previewValue?: string | null
value?: string
onChange?: (value: string) => void
}
// Constants
@@ -36,9 +38,11 @@ export function LongInput({
config,
rows,
isPreview = false,
previewValue,
value: propValue,
onChange,
}: LongInputProps) {
const [value, setValue] = useSubBlockValue(blockId, subBlockId, false, isPreview, propValue)
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
const [showEnvVars, setShowEnvVars] = useState(false)
const [showTags, setShowTags] = useState(false)
const [searchTerm, setSearchTerm] = useState('')
@@ -48,6 +52,9 @@ export function LongInput({
const [activeSourceBlockId, setActiveSourceBlockId] = useState<string | null>(null)
const containerRef = useRef<HTMLDivElement>(null)
// Use preview value when in preview mode, otherwise use store value or prop value
const value = isPreview ? previewValue : (propValue !== undefined ? propValue : storeValue)
// Calculate initial height based on rows prop with reasonable defaults
const getInitialHeight = () => {
// Use provided rows or default, then convert to pixels with a minimum
@@ -76,7 +83,14 @@ export function LongInput({
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value
const newCursorPosition = e.target.selectionStart ?? 0
setValue(newValue)
if (onChange) {
onChange(newValue)
} else if (!isPreview) {
// Only update store when not in preview mode
setStoreValue(newValue)
}
setCursorPosition(newCursorPosition)
// Check for environment variables trigger
@@ -171,7 +185,9 @@ export function LongInput({
// Update all state in a single batch
Promise.resolve().then(() => {
setValue(newValue)
if (!isPreview) {
setStoreValue(newValue)
}
setCursorPosition(dropPosition + 1)
setShowTags(true)
@@ -272,6 +288,7 @@ export function LongInput({
setShowTags(false)
setSearchTerm('')
}}
disabled={isPreview}
style={{
fontFamily: 'inherit',
lineHeight: 'inherit',
@@ -302,29 +319,43 @@ export function LongInput({
<ChevronsUpDown className='h-3 w-3 text-muted-foreground/70' />
</div>
<EnvVarDropdown
visible={showEnvVars}
onSelect={setValue}
searchTerm={searchTerm}
inputValue={value?.toString() ?? ''}
cursorPosition={cursorPosition}
onClose={() => {
setShowEnvVars(false)
setSearchTerm('')
}}
/>
<TagDropdown
visible={showTags}
onSelect={setValue}
blockId={blockId}
activeSourceBlockId={activeSourceBlockId}
inputValue={value?.toString() ?? ''}
cursorPosition={cursorPosition}
onClose={() => {
setShowTags(false)
setActiveSourceBlockId(null)
}}
/>
<div>
<EnvVarDropdown
visible={showEnvVars}
onSelect={(newValue) => {
if (onChange) {
onChange(newValue)
} else if (!isPreview) {
setStoreValue(newValue)
}
}}
searchTerm={searchTerm}
inputValue={value?.toString() ?? ''}
cursorPosition={cursorPosition}
onClose={() => {
setShowEnvVars(false)
setSearchTerm('')
}}
/>
<TagDropdown
visible={showTags}
onSelect={(newValue) => {
if (onChange) {
onChange(newValue)
} else if (!isPreview) {
setStoreValue(newValue)
}
}}
blockId={blockId}
activeSourceBlockId={activeSourceBlockId}
inputValue={value?.toString() ?? ''}
cursorPosition={cursorPosition}
onClose={() => {
setShowTags(false)
setActiveSourceBlockId(null)
}}
/>
</div>
</div>
)
}

View File

@@ -20,7 +20,7 @@ interface ProjectSelectorInputProps {
disabled?: boolean
onProjectSelect?: (projectId: string) => void
isPreview?: boolean
value?: string
previewValue?: any | null
}
export function ProjectSelectorInput({
@@ -29,7 +29,7 @@ export function ProjectSelectorInput({
disabled = false,
onProjectSelect,
isPreview = false,
value: propValue
previewValue
}: ProjectSelectorInputProps) {
const { getValue, setValue } = useSubBlockStore()
const [selectedProjectId, setSelectedProjectId] = useState<string>('')
@@ -46,15 +46,15 @@ export function ProjectSelectorInput({
// Get the current value from the store or prop value if in preview mode
useEffect(() => {
if (isPreview && propValue !== undefined) {
setSelectedProjectId(propValue);
if (isPreview && previewValue !== undefined) {
setSelectedProjectId(previewValue);
} else {
const value = getValue(blockId, subBlock.id);
if (value && typeof value === 'string') {
setSelectedProjectId(value);
}
}
}, [blockId, subBlock.id, getValue, isPreview, propValue]);
}, [blockId, subBlock.id, getValue, isPreview, previewValue]);
// Handle project selection
const handleProjectChange = (

View File

@@ -17,18 +17,18 @@ const logger = createLogger('ScheduleConfig')
interface ScheduleConfigProps {
blockId: string
subBlockId?: string
subBlockId: string
isConnecting: boolean
isPreview?: boolean
value?: any
previewValue?: any | null
}
export function ScheduleConfig({
blockId,
subBlockId,
isConnecting,
isConnecting,
isPreview = false,
value: propValue
previewValue
}: ScheduleConfigProps) {
const [error, setError] = useState<string | null>(null)
const [scheduleId, setScheduleId] = useState<string | null>(null)
@@ -50,13 +50,18 @@ export function ScheduleConfig({
const setScheduleStatus = useWorkflowStore((state) => state.setScheduleStatus)
// Get the schedule type from the block state
const [scheduleType] = useSubBlockValue(blockId, 'scheduleType', false, isPreview, propValue?.scheduleType)
const [scheduleType] = useSubBlockValue(blockId, 'scheduleType')
// Get the startWorkflow value to determine if scheduling is enabled
// and expose the setter so we can update it
const [startWorkflow, setStartWorkflow] = useSubBlockValue(blockId, 'startWorkflow', false, isPreview, propValue?.startWorkflow)
const [startWorkflow, setStartWorkflow] = useSubBlockValue(blockId, 'startWorkflow')
const isScheduleEnabled = startWorkflow === 'schedule'
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : storeValue
// Function to check if schedule exists in the database
const checkSchedule = async () => {
setIsLoading(true)
@@ -132,6 +137,7 @@ export function ScheduleConfig({
}
const handleOpenModal = () => {
if (isPreview) return
setIsModalOpen(true)
}
@@ -145,6 +151,8 @@ export function ScheduleConfig({
}
const handleSaveSchedule = async (): Promise<boolean> => {
if (isPreview) return false
setIsSaving(true)
setError(null)
@@ -247,7 +255,7 @@ export function ScheduleConfig({
}
const handleDeleteSchedule = async (): Promise<boolean> => {
if (!scheduleId) return false
if (isPreview || !scheduleId) return false
setIsDeleting(true)
try {
@@ -320,7 +328,7 @@ export function ScheduleConfig({
size='icon'
className='h-8 w-8 shrink-0'
onClick={handleOpenModal}
disabled={isDeleting || isConnecting}
disabled={isPreview || isDeleting || isConnecting}
>
{isDeleting ? (
<div className='h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
@@ -336,7 +344,7 @@ export function ScheduleConfig({
size='sm'
className='flex h-10 w-full items-center bg-background font-normal text-sm'
onClick={handleOpenModal}
disabled={isConnecting || isSaving || isDeleting}
disabled={isPreview || isConnecting || isSaving || isDeleting}
>
{isLoading ? (
<div className='mr-2 h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />

View File

@@ -21,6 +21,7 @@ interface ShortInputProps {
value?: string
onChange?: (value: string) => void
isPreview?: boolean
previewValue?: string | null
}
export function ShortInput({
@@ -30,9 +31,10 @@ export function ShortInput({
password,
isConnecting,
config,
value: propValue,
onChange,
value: propValue,
isPreview = false,
previewValue
}: ShortInputProps) {
const [isFocused, setIsFocused] = useState(false)
const [showEnvVars, setShowEnvVars] = useState(false)
@@ -48,10 +50,7 @@ export function ShortInput({
}
const [storeValue, setStoreValue] = useSubBlockValue(
blockId,
subBlockId,
false, // No workflow update needed
isPreview,
validatePropValue(propValue)
subBlockId
)
const [searchTerm, setSearchTerm] = useState('')
const [cursorPosition, setCursorPosition] = useState(0)
@@ -62,10 +61,8 @@ export function ShortInput({
// Get ReactFlow instance for zoom control
const reactFlowInstance = useReactFlow()
// Use either controlled or uncontrolled value, prioritizing the direct value if in preview mode
const value = isPreview && propValue !== undefined
? propValue
: (propValue !== undefined ? propValue : storeValue)
// Use preview value when in preview mode, otherwise use store value or prop value
const value = isPreview ? previewValue : (propValue !== undefined ? propValue : storeValue)
// Check if this input is API key related
const isApiKeyField = useMemo(() => {
@@ -103,7 +100,8 @@ export function ShortInput({
if (onChange) {
onChange(newValue)
} else {
} else if (!isPreview) {
// Only update store when not in preview mode
setStoreValue(newValue)
}
@@ -284,7 +282,8 @@ export function ShortInput({
if (onChange) {
onChange(newValue)
} else {
} else if (!isPreview) {
// Only update store when not in preview mode
setStoreValue(newValue)
}
}
@@ -332,6 +331,7 @@ export function ShortInput({
onKeyDown={handleKeyDown}
autoComplete='off'
style={{ overflowX: 'auto' }}
disabled={isPreview}
/>
<div
ref={overlayRef}

View File

@@ -4,76 +4,69 @@ import { useSubBlockValue } from '../hooks/use-sub-block-value'
interface SliderInputProps {
min?: number
max?: number
defaultValue: number
blockId: string
subBlockId: string
min?: number
max?: number
defaultValue?: number
step?: number
integer?: boolean
isPreview?: boolean
value?: number
previewValue?: number | null
}
export function SliderInput({
min = 0,
max = 100,
defaultValue,
blockId,
subBlockId,
step = 0.1,
min = 0,
max = 100,
defaultValue = 50,
step = 1,
integer = false,
isPreview = false,
value: propValue
previewValue
}: SliderInputProps) {
const [value, setValue] = useSubBlockValue<number>(blockId, subBlockId, false, isPreview, propValue)
const [storeValue, setStoreValue] = useSubBlockValue<number>(blockId, subBlockId)
// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : storeValue
// Clamp the value within bounds while preserving relative position when possible
const normalizedValue = useMemo(() => {
if (value === null) return defaultValue
const normalizedValue = value !== null && value !== undefined
? Math.max(min, Math.min(max, value))
: defaultValue
// If value exceeds max, scale it down proportionally
if (value > max) {
const prevMax = Math.max(max * 2, value) // Assume previous max was at least the current value
const scaledValue = (value / prevMax) * max
return integer ? Math.round(scaledValue) : scaledValue
}
const displayValue = normalizedValue ?? defaultValue
// Otherwise just clamp it
const clampedValue = Math.min(Math.max(value, min), max)
return integer ? Math.round(clampedValue) : clampedValue
}, [value, min, max, defaultValue, integer])
// Update the value if it needs normalization
// Ensure the normalized value is set if it differs from the current value
useEffect(() => {
if (value !== null && value !== normalizedValue) {
setValue(normalizedValue)
if (!isPreview && value !== null && value !== undefined && value !== normalizedValue) {
setStoreValue(normalizedValue)
}
}, [normalizedValue, value, setValue])
}, [normalizedValue, value, setStoreValue, isPreview])
const handleValueChange = (newValue: number[]) => {
if (!isPreview) {
const processedValue = integer ? Math.round(newValue[0]) : newValue[0]
setStoreValue(processedValue)
}
}
return (
<div className='relative pt-2 pb-6'>
<Slider
value={[normalizedValue]}
min={min}
max={max}
step={integer ? 1 : step}
onValueChange={(value) => setValue(integer ? Math.round(value[0]) : value[0])}
className='[&_[class*=SliderTrack]]:h-1 [&_[role=slider]]:h-4 [&_[role=slider]]:w-4'
/>
<div
className='absolute text-muted-foreground text-sm'
style={{
left: `clamp(0%, ${((normalizedValue - min) / (max - min)) * 100}%, 100%)`,
transform: `translateX(-${(() => {
const percentage = ((normalizedValue - min) / (max - min)) * 100
const bias = -25 * Math.sin((percentage * Math.PI) / 50)
return percentage === 0 ? 0 : percentage === 100 ? 100 : 50 + bias
})()}%)`,
top: '24px',
}}
>
{integer ? Math.round(normalizedValue).toString() : Number(normalizedValue).toFixed(1)}
<div className="flex items-center space-x-4">
<div className="flex-1">
<Slider
value={[displayValue]}
min={min}
max={max}
step={integer ? 1 : step}
onValueChange={handleValueChange}
disabled={isPreview}
className="[&_[role=slider]]:h-4 [&_[role=slider]]:w-4 [&_[class*=SliderTrack]]:h-1"
/>
</div>
<div className="text-sm font-medium min-w-[3rem] text-right">
{displayValue}
</div>
</div>
)

View File

@@ -24,7 +24,7 @@ interface InputFormatProps {
blockId: string
subBlockId: string
isPreview?: boolean
value?: InputField[]
previewValue?: InputField[] | null
}
// Default values
@@ -35,32 +35,43 @@ const DEFAULT_FIELD: InputField = {
collapsed: true,
}
export function InputFormat({ blockId, subBlockId, isPreview = false, value: propValue }: InputFormatProps) {
// State hooks
const [value, setValue] = useSubBlockValue<InputField[]>(blockId, subBlockId, false, isPreview, propValue)
const fields = value || [DEFAULT_FIELD]
export function InputFormat({
blockId,
subBlockId,
isPreview = false,
previewValue
}: InputFormatProps) {
const [storeValue, setStoreValue] = useSubBlockValue<InputField[]>(blockId, subBlockId)
// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : storeValue
const fields: InputField[] = value || [DEFAULT_FIELD]
// Field operations
const addField = () => {
if (isPreview) return
const newField: InputField = {
...DEFAULT_FIELD,
id: crypto.randomUUID(),
}
setValue([...fields, newField])
setStoreValue([...fields, newField])
}
const removeField = (id: string) => {
if (fields.length === 1) return
setValue(fields.filter((field) => field.id !== id))
if (isPreview || fields.length === 1) return
setStoreValue(fields.filter((field: InputField) => field.id !== id))
}
// Update handlers
const updateField = (id: string, field: keyof InputField, value: any) => {
setValue(fields.map((f) => (f.id === id ? { ...f, [field]: value } : f)))
if (isPreview) return
setStoreValue(fields.map((f: InputField) => (f.id === id ? { ...f, [field]: value } : f)))
}
const toggleCollapse = (id: string) => {
setValue(fields.map((f) => (f.id === id ? { ...f, collapsed: !f.collapsed } : f)))
if (isPreview) return
setStoreValue(fields.map((f: InputField) => (f.id === id ? { ...f, collapsed: !f.collapsed } : f)))
}
// Field header
@@ -87,18 +98,18 @@ export function InputFormat({ blockId, subBlockId, isPreview = false, value: pro
</Badge>
)}
</div>
<div className='flex items-center gap-1' onClick={(e) => e.stopPropagation()}>
<Button variant='ghost' size='icon' onClick={addField} className='h-6 w-6 rounded-full'>
<Plus className='h-3.5 w-3.5' />
<span className='sr-only'>Add Field</span>
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
<Button variant="ghost" size="icon" onClick={addField} disabled={isPreview} className="h-6 w-6 rounded-full">
<Plus className="h-3.5 w-3.5" />
<span className="sr-only">Add Field</span>
</Button>
<Button
variant='ghost'
size='icon'
onClick={() => removeField(field.id)}
disabled={fields.length === 1}
className='h-6 w-6 rounded-full text-destructive hover:text-destructive'
disabled={isPreview || fields.length === 1}
className="h-6 w-6 rounded-full text-destructive hover:text-destructive"
>
<Trash className='h-3.5 w-3.5' />
<span className='sr-only'>Delete Field</span>
@@ -137,8 +148,9 @@ export function InputFormat({ blockId, subBlockId, isPreview = false, value: pro
name='name'
value={field.name}
onChange={(e) => updateField(field.id, 'name', e.target.value)}
placeholder='firstName'
className='h-9 placeholder:text-muted-foreground/50'
placeholder="firstName"
disabled={isPreview}
className="h-9 placeholder:text-muted-foreground/50"
/>
</div>
@@ -146,8 +158,8 @@ export function InputFormat({ blockId, subBlockId, isPreview = false, value: pro
<Label className='text-xs'>Type</Label>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='outline' className='h-9 w-full justify-between font-normal'>
<div className='flex items-center'>
<Button variant="outline" disabled={isPreview} className="w-full justify-between h-9 font-normal">
<div className="flex items-center">
<span>{field.type}</span>
</div>
<ChevronDown className='h-4 w-4 opacity-50' />

View File

@@ -3,30 +3,49 @@ import { Label } from '@/components/ui/label'
import { Switch as UISwitch } from '@/components/ui/switch'
import { useSubBlockValue } from '../hooks/use-sub-block-value'
interface SwitchProps {
blockId: string
subBlockId: string
title: string
isPreview?: boolean
value?: boolean
isPreview?: boolean
previewValue?: boolean | null
}
export function Switch({
blockId,
subBlockId,
title,
value: propValue,
isPreview = false,
value: propValue
previewValue
}: SwitchProps) {
const [value, setValue] = useSubBlockValue(blockId, subBlockId, false, isPreview, propValue)
const [storeValue, setStoreValue] = useSubBlockValue<boolean>(blockId, subBlockId)
// Use preview value when in preview mode, otherwise use store value or prop value
const value = isPreview ? previewValue : (propValue !== undefined ? propValue : storeValue)
const handleChange = (checked: boolean) => {
// Only update store when not in preview mode
if (!isPreview) {
setStoreValue(checked)
}
}
return (
<div className='flex flex-col gap-2'>
<Label className='font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'>
<div className="flex items-center space-x-3">
<UISwitch
id={`${blockId}-${subBlockId}`}
checked={Boolean(value)}
onCheckedChange={handleChange}
disabled={isPreview}
/>
<Label
htmlFor={`${blockId}-${subBlockId}`}
className="text-sm font-normal leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
>
{title}
</Label>
<UISwitch checked={Boolean(value)} onCheckedChange={(checked) => setValue(checked)} />
</div>
)
}

View File

@@ -10,11 +10,11 @@ import { useSubBlockValue } from '../hooks/use-sub-block-value'
interface TableProps {
columns: string[]
blockId: string
subBlockId: string
columns: string[]
isPreview?: boolean
value?: TableRow[]
previewValue?: TableRow[] | null
}
interface TableRow {
@@ -22,8 +22,17 @@ interface TableRow {
cells: Record<string, string>
}
export function Table({ columns, blockId, subBlockId, isPreview = false, value: propValue }: TableProps) {
const [value, setValue] = useSubBlockValue(blockId, subBlockId, false, isPreview, propValue)
export function Table({
blockId,
subBlockId,
columns,
isPreview = false,
previewValue
}: TableProps) {
const [storeValue, setStoreValue] = useSubBlockValue<TableRow[]>(blockId, subBlockId)
// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : storeValue
// Create refs for input elements
const inputRefs = useRef<Map<string, HTMLInputElement>>(new Map())
@@ -74,6 +83,8 @@ export function Table({ columns, blockId, subBlockId, isPreview = false, value:
}, [activeCell])
const handleCellChange = (rowIndex: number, column: string, value: string) => {
if (isPreview) return
const updatedRows = [...rows].map((row, idx) =>
idx === rowIndex
? {
@@ -90,12 +101,12 @@ export function Table({ columns, blockId, subBlockId, isPreview = false, value:
})
}
setValue(updatedRows)
setStoreValue(updatedRows)
}
const handleDeleteRow = (rowIndex: number) => {
if (rows.length === 1) return
setValue(rows.filter((_, index) => index !== rowIndex))
if (isPreview || rows.length === 1) return
setStoreValue(rows.filter((_, index) => index !== rowIndex))
}
const renderHeader = () => (
@@ -175,7 +186,8 @@ export function Table({ columns, blockId, subBlockId, isPreview = false, value:
setActiveCell(null)
}
}}
className='w-full border-0 text-transparent caret-foreground placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0'
disabled={isPreview}
className="border-0 focus-visible:ring-0 focus-visible:ring-offset-0 text-transparent caret-foreground placeholder:text-muted-foreground/50 w-full"
/>
<div
data-overlay={cellKey}
@@ -189,8 +201,8 @@ export function Table({ columns, blockId, subBlockId, isPreview = false, value:
}
const renderDeleteButton = (rowIndex: number) =>
rows.length > 1 && (
<td className='w-0 p-0'>
rows.length > 1 && !isPreview && (
<td className="w-0 p-0">
<Button
variant='ghost'
size='icon'

View File

@@ -13,20 +13,23 @@ interface TimeInputProps {
blockId: string
subBlockId: string
placeholder?: string
className?: string
isPreview?: boolean
value?: string
previewValue?: string | null
className?: string
}
export function TimeInput({
blockId,
subBlockId,
placeholder,
className,
placeholder,
isPreview = false,
value: propValue
previewValue,
className,
}: TimeInputProps) {
const [value, setValue] = useSubBlockValue<string>(blockId, subBlockId, true, isPreview, propValue)
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlockId)
// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : storeValue
const [isOpen, setIsOpen] = React.useState(false)
// Convert 24h time string to display format (12h with AM/PM)
@@ -51,10 +54,11 @@ export function TimeInput({
// Update the time when any component changes
const updateTime = (newHour?: string, newMinute?: string, newAmpm?: 'AM' | 'PM') => {
const h = Number.parseInt(newHour ?? hour) || 12
const m = Number.parseInt(newMinute ?? minute) || 0
if (isPreview) return
const h = parseInt(newHour ?? hour) || 12
const m = parseInt(newMinute ?? minute) || 0
const p = newAmpm ?? ampm
setValue(formatStorageTime(h, m, p))
setStoreValue(formatStorageTime(h, m, p))
}
// Initialize from existing value
@@ -87,7 +91,8 @@ export function TimeInput({
>
<PopoverTrigger asChild>
<Button
variant='outline'
variant="outline"
disabled={isPreview}
className={cn(
'w-full justify-start text-left font-normal',
!value && 'text-muted-foreground',

View File

@@ -27,12 +27,14 @@ import { CredentialSelector } from '../credential-selector/credential-selector'
import { ShortInput } from '../short-input'
import { type CustomTool, CustomToolModal } from './components/custom-tool-modal/custom-tool-modal'
import { ToolCommand } from './components/tool-command/tool-command'
import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandSeparator } from '@/components/ui/command'
import { Plus } from 'lucide-react'
interface ToolInputProps {
blockId: string
subBlockId: string
isPreview?: boolean
value?: StoredTool[]
previewValue?: any
}
interface StoredTool {
@@ -272,8 +274,13 @@ const shouldBePasswordField = (blockType: string, paramId: string): boolean => {
return false
}
export function ToolInput({ blockId, subBlockId, isPreview = false, value: propValue }: ToolInputProps) {
const [value, setValue] = useSubBlockValue(blockId, subBlockId, false, isPreview, propValue)
export function ToolInput({
blockId,
subBlockId,
isPreview = false,
previewValue
}: ToolInputProps) {
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
const [open, setOpen] = useState(false)
const [customToolModalOpen, setCustomToolModalOpen] = useState(false)
const [editingToolIndex, setEditingToolIndex] = useState<number | null>(null)
@@ -291,6 +298,9 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
const toolBlocks = getAllBlocks().filter((block) => block.category === 'tools')
// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : storeValue
// Custom filter function for the Command component
const customFilter = useCallback((value: string, search: string) => {
if (!search.trim()) return 1
@@ -316,6 +326,11 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
? (value as unknown as StoredTool[])
: []
// Check if a tool is already selected
const isToolAlreadySelected = (toolType: string) => {
return selectedTools.some((tool) => tool.type === toolType)
}
const handleSelectTool = (toolBlock: (typeof toolBlocks)[0]) => {
const hasOperations = hasMultipleOperations(toolBlock.type)
const operationOptions = hasOperations ? getOperationOptions(toolBlock.type) : []
@@ -344,7 +359,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
// If isWide, keep tools in the same row expanded
if (isWide) {
setValue([
setStoreValue([
...selectedTools.map((tool, index) => ({
...tool,
// Keep expanded if it's in the same row as the new tool
@@ -354,7 +369,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
])
} else {
// Original behavior for non-wide mode
setValue([...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), newTool])
setStoreValue([...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), newTool])
}
setOpen(false)
@@ -399,7 +414,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
// If isWide, keep tools in the same row expanded
if (isWide) {
setValue([
setStoreValue([
...selectedTools.map((tool, index) => ({
...tool,
// Keep expanded if it's in the same row as the new tool
@@ -409,7 +424,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
])
} else {
// Original behavior for non-wide mode
setValue([...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), newTool])
setStoreValue([...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), newTool])
}
}
@@ -430,7 +445,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
const handleSaveCustomTool = (customTool: CustomTool) => {
if (editingToolIndex !== null) {
// Update existing tool
setValue(
setStoreValue(
selectedTools.map((tool, index) =>
index === editingToolIndex
? {
@@ -450,7 +465,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
}
const handleRemoveTool = (toolType: string, toolIndex: number) => {
setValue(selectedTools.filter((_, index) => index !== toolIndex))
setStoreValue(selectedTools.filter((_, index) => index !== toolIndex))
}
// New handler for when a custom tool is completely deleted from the store
@@ -474,7 +489,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
// Update the workflow value if any tools were removed
if (updatedTools.length !== selectedTools.length) {
setValue(updatedTools)
setStoreValue(updatedTools)
}
}
@@ -492,7 +507,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
}
// Update the value in the workflow
setValue(
setStoreValue(
selectedTools.map((tool, index) =>
index === toolIndex
? {
@@ -521,7 +536,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
subBlockStore.setValue(blockId, 'parentIssue', '')
}
setValue(
setStoreValue(
selectedTools.map((tool, index) =>
index === toolIndex
? {
@@ -536,7 +551,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
}
const handleCredentialChange = (toolIndex: number, credentialId: string) => {
setValue(
setStoreValue(
selectedTools.map((tool, index) =>
index === toolIndex
? {
@@ -552,7 +567,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
}
const handleUsageControlChange = (toolIndex: number, usageControl: string) => {
setValue(
setStoreValue(
selectedTools.map((tool, index) =>
index === toolIndex
? {
@@ -565,7 +580,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
}
const toggleToolExpansion = (toolIndex: number) => {
setValue(
setStoreValue(
selectedTools.map((tool, index) =>
index === toolIndex ? { ...tool, isExpanded: !tool.isExpanded } : tool
)
@@ -589,112 +604,50 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
</div>
</div>
</PopoverTrigger>
<PopoverContent className='w-[200px] p-0' align='start'>
<ToolCommand.Root filter={customFilter}>
<ToolCommand.Input placeholder='Search tools...' onValueChange={setSearchQuery} />
<ToolCommand.List>
<ToolCommand.Empty>No tools found</ToolCommand.Empty>
<ToolCommand.Group>
<ToolCommand.Item
value='Create Tool'
<PopoverContent className="p-0 w-[200px]" align="start">
<Command filter={customFilter} className="max-h-48" shouldFilter={false}>
<CommandInput placeholder="Search for tools..." disabled={isPreview} />
<CommandList>
<CommandEmpty>No tools found.</CommandEmpty>
{toolBlocks.map((block) => (
<CommandGroup key={block.type} heading={block.name}>
<CommandItem
onSelect={() => {
if (!isPreview) {
handleSelectTool(block)
setOpen(false)
}
}}
disabled={isPreview || isToolAlreadySelected(block.type)}
>
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-2">
<IconComponent icon={block.icon} className="h-4 w-4" />
<span>{block.name}</span>
</div>
</div>
</CommandItem>
</CommandGroup>
))}
<CommandSeparator />
<CommandGroup heading="Custom Tools">
<CommandItem
onSelect={() => {
setOpen(false)
setCustomToolModalOpen(true)
if (!isPreview) {
setCustomToolModalOpen(true)
setOpen(false)
}
}}
className='mb-1 flex cursor-pointer items-center gap-2'
disabled={isPreview}
>
<div className='flex h-6 w-6 items-center justify-center rounded border border-muted-foreground/50 border-dashed bg-transparent'>
<WrenchIcon className='h-4 w-4 text-muted-foreground' />
<div className="flex items-center gap-2">
<Plus className="h-4 w-4" />
<span>Create Custom Tool</span>
</div>
<span>Create Tool</span>
</ToolCommand.Item>
{/* Display saved custom tools at the top */}
{customTools.length > 0 && (
<>
<ToolCommand.Separator />
<div className='px-2 pt-2.5 pb-0.5 font-medium text-muted-foreground text-xs'>
Custom Tools
</div>
<ToolCommand.Group className='-mx-1 -px-1'>
{customTools.map((customTool) => (
<ToolCommand.Item
key={customTool.id}
value={customTool.title}
onSelect={() => {
const newTool: StoredTool = {
type: 'custom-tool',
title: customTool.title,
params: {},
isExpanded: true,
schema: customTool.schema,
code: customTool.code,
usageControl: 'auto',
}
if (isWide) {
setValue([
...selectedTools.map((tool, index) => ({
...tool,
isExpanded:
Math.floor(selectedTools.length / 2) ===
Math.floor(index / 2),
})),
newTool,
])
} else {
setValue([
...selectedTools.map((tool) => ({
...tool,
isExpanded: false,
})),
newTool,
])
}
setOpen(false)
}}
className='flex cursor-pointer items-center gap-2'
>
<div className='flex h-6 w-6 items-center justify-center rounded bg-blue-500'>
<WrenchIcon className='h-4 w-4 text-white' />
</div>
<span className='max-w-[140px] truncate'>{customTool.title}</span>
</ToolCommand.Item>
))}
</ToolCommand.Group>
<ToolCommand.Separator />
</>
)}
{/* Display built-in tools */}
{toolBlocks.some((block) => customFilter(block.name, searchQuery || '') > 0) && (
<>
<div className='px-2 pt-2.5 pb-0.5 font-medium text-muted-foreground text-xs'>
Built-in Tools
</div>
<ToolCommand.Group className='-mx-1 -px-1'>
{toolBlocks.map((block) => (
<ToolCommand.Item
key={block.type}
value={block.name}
onSelect={() => handleSelectTool(block)}
className='flex cursor-pointer items-center gap-2'
>
<div
className='flex h-6 w-6 items-center justify-center rounded'
style={{ backgroundColor: block.bgColor }}
>
<IconComponent icon={block.icon} className='h-4 w-4 text-white' />
</div>
<span className='max-w-[140px] truncate'>{block.name}</span>
</ToolCommand.Item>
))}
</ToolCommand.Group>
</>
)}
</ToolCommand.Group>
</ToolCommand.List>
</ToolCommand.Root>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
) : (
@@ -935,50 +888,52 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
}
return (
<div key={param.id} className='relative space-y-1.5'>
<div className='flex items-center font-medium text-muted-foreground text-xs'>
{formatParamId(param.id)}
{param.optionalToolInput && !param.requiredForToolCall && (
<span className='ml-1 text-muted-foreground/60 text-xs'>
(Optional)
</span>
)}
</div>
<div className='relative'>
{useChannelSelector && channelSelectorConfig ? (
<ChannelSelectorInput
blockId={blockId}
subBlock={{
id: param.id,
type: 'channel-selector',
title: channelSelectorConfig.title || formatParamId(param.id),
provider: channelSelectorConfig.provider,
placeholder:
channelSelectorConfig.placeholder || param.description,
}}
credential={credentialForChannelSelector}
onChannelSelect={(channelId) => {
handleParamChange(toolIndex, param.id, channelId)
}}
/>
) : (
<ShortInput
blockId={blockId}
subBlockId={`${subBlockId}-param`}
placeholder={param.description}
password={shouldBePasswordField(tool.type, param.id)}
isConnecting={false}
config={{
id: `${subBlockId}-param`,
type: 'short-input',
title: param.id,
}}
value={tool.params[param.id] || ''}
onChange={(value) =>
handleParamChange(toolIndex, param.id, value)
}
/>
)}
<div key={param.id}>
<div className='relative space-y-1.5'>
<div className='flex items-center font-medium text-muted-foreground text-xs'>
{formatParamId(param.id)}
{param.optionalToolInput && !param.requiredForToolCall && (
<span className='ml-1 text-muted-foreground/60 text-xs'>
(Optional)
</span>
)}
</div>
<div className='relative'>
{useChannelSelector && channelSelectorConfig ? (
<ChannelSelectorInput
blockId={blockId}
subBlock={{
id: param.id,
type: 'channel-selector',
title: channelSelectorConfig.title || formatParamId(param.id),
provider: channelSelectorConfig.provider,
placeholder:
channelSelectorConfig.placeholder || param.description,
}}
credential={credentialForChannelSelector}
onChannelSelect={(channelId) => {
handleParamChange(toolIndex, param.id, channelId)
}}
/>
) : (
<ShortInput
blockId={blockId}
subBlockId={`${subBlockId}-param`}
placeholder={param.description}
password={shouldBePasswordField(tool.type, param.id)}
isConnecting={false}
config={{
id: `${subBlockId}-param`,
type: 'short-input',
title: param.id,
}}
value={tool.params[param.id] || ''}
onChange={(value) =>
handleParamChange(toolIndex, param.id, value)
}
/>
)}
</div>
</div>
</div>
)
@@ -989,6 +944,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
</div>
)
})}
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
@@ -1044,7 +1000,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
}
if (isWide) {
setValue([
setStoreValue([
...selectedTools.map((tool, index) => ({
...tool,
isExpanded:
@@ -1054,7 +1010,7 @@ export function ToolInput({ blockId, subBlockId, isPreview = false, value: propV
newTool,
])
} else {
setValue([
setStoreValue([
...selectedTools.map((tool) => ({
...tool,
isExpanded: false,

View File

@@ -299,7 +299,7 @@ export function WebhookConfig({
blockId,
subBlockId,
isConnecting,
isPreview = false,
isPreview = false,
value: propValue
}: WebhookConfigProps) {
const [isModalOpen, setIsModalOpen] = useState(false)
@@ -316,27 +316,38 @@ export function WebhookConfig({
const setWebhookStatus = useWorkflowStore((state) => state.setWebhookStatus)
// Get the webhook provider from the block state
const [webhookProvider, setWebhookProvider] = useSubBlockValue(blockId, 'webhookProvider', false, isPreview, propValue?.webhookProvider)
const [storeWebhookProvider, setWebhookProvider] = useSubBlockValue(blockId, 'webhookProvider')
// Store the webhook path
const [webhookPath, setWebhookPath] = useSubBlockValue(blockId, 'webhookPath', false, isPreview, propValue?.webhookPath)
const [storeWebhookPath, setWebhookPath] = useSubBlockValue(blockId, 'webhookPath')
// Store provider-specific configuration
const [providerConfig, setProviderConfig] = useSubBlockValue(blockId, 'providerConfig', false, isPreview, propValue?.providerConfig)
const [storeProviderConfig, setProviderConfig] = useSubBlockValue(blockId, 'providerConfig')
// Use prop values when available (preview mode), otherwise use store values
const webhookProvider = propValue?.webhookProvider ?? storeWebhookProvider
const webhookPath = propValue?.webhookPath ?? storeWebhookPath
const providerConfig = propValue?.providerConfig ?? storeProviderConfig
// Reset provider config when provider changes
useEffect(() => {
if (webhookProvider && !isPreview) {
if (webhookProvider) {
// Reset the provider config when the provider changes
setProviderConfig({})
}
}, [webhookProvider, setProviderConfig, isPreview])
}, [webhookProvider, setProviderConfig])
// Store the actual provider from the database
const [actualProvider, setActualProvider] = useState<string | null>(null)
// Check if webhook exists in the database
useEffect(() => {
// Skip API calls in preview mode
if (isPreview) {
setIsLoading(false)
return
}
const checkWebhook = async () => {
setIsLoading(true)
try {
@@ -386,6 +397,7 @@ export function WebhookConfig({
setWebhookPath,
setWebhookProvider,
setWebhookStatus,
isPreview,
])
const handleOpenModal = () => {
@@ -398,6 +410,9 @@ export function WebhookConfig({
}
const handleSaveWebhook = async (path: string, config: ProviderConfig) => {
// Prevent saving in preview mode
if (isPreview) return false
try {
setIsSaving(true)
setError(null)
@@ -461,7 +476,8 @@ export function WebhookConfig({
}
const handleDeleteWebhook = async () => {
if (!webhookId) return false
// Prevent deletion in preview mode
if (isPreview || !webhookId) return false
try {
setIsDeleting(true)
@@ -554,8 +570,8 @@ export function WebhookConfig({
'https://www.googleapis.com/auth/gmail.modify',
'https://www.googleapis.com/auth/gmail.labels',
]}
label='Select Gmail account'
disabled={isConnecting || isSaving || isDeleting}
label="Select Gmail account"
disabled={isConnecting || isSaving || isDeleting || isPreview}
/>
</div>
@@ -565,7 +581,7 @@ export function WebhookConfig({
size='sm'
className='flex h-10 w-full items-center bg-background font-normal text-sm'
onClick={handleOpenModal}
disabled={isConnecting || isSaving || isDeleting || !gmailCredentialId}
disabled={isConnecting || isSaving || isDeleting || !gmailCredentialId || isPreview}
>
{isLoading ? (
<div className='mr-2 h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
@@ -617,7 +633,7 @@ export function WebhookConfig({
size='sm'
className='flex h-10 w-full items-center bg-background font-normal text-sm'
onClick={handleOpenModal}
disabled={isConnecting || isSaving || isDeleting}
disabled={isConnecting || isSaving || isDeleting || isPreview}
>
{isLoading ? (
<div className='mr-2 h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />

View File

@@ -1,14 +1,10 @@
import { useCallback, useEffect, useRef } from 'react'
import { isEqual } from 'lodash'
import { getProviderFromModel } from '@/providers/utils'
import { createLogger } from '@/lib/logs/console-logger'
import { useGeneralStore } from '@/stores/settings/general/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
// Add logger for diagnostic logging
const logger = createLogger('useSubBlockValue')
/**
* Helper to handle API key auto-fill for provider-based blocks
* Used for agent, router, evaluator, and any other blocks that use LLM providers
@@ -152,8 +148,6 @@ function storeApiKeyValue(
* @param blockId The ID of the block containing the sub-block
* @param subBlockId The ID of the sub-block
* @param triggerWorkflowUpdate Whether to trigger a workflow update when the value changes
* @param isPreview Whether this is being used in preview mode
* @param directValue The direct value to use when in preview mode
* @returns A tuple containing the current value and a setter function
*/
export function useSubBlockValue<T = any>(
@@ -179,36 +173,12 @@ export function useSubBlockValue<T = any>(
// Previous model reference for detecting model changes
const prevModelRef = useRef<string | null>(null)
// Ref to track if we're in preview mode and have direct values
const previewDataRef = useRef<{
isInPreview: boolean,
directValue: T | null
}>({
isInPreview: isPreview,
directValue: directValue as T || null
})
// Get value from subblock store - always call this hook unconditionally
const storeValue = useSubBlockStore(
useCallback((state) => state.getValue(blockId, subBlockId), [blockId, subBlockId])
)
// Directly update preview data when props change
useEffect(() => {
if (isPreview && directValue !== undefined) {
previewDataRef.current = {
isInPreview: true,
directValue: directValue as T || null
};
valueRef.current = directValue as T || null;
} else if (!isPreview && previewDataRef.current.isInPreview) {
// Reset preview flag when isPreview prop changes to false
previewDataRef.current.isInPreview = false;
}
}, [isPreview, directValue, blockId, subBlockId]);
// Check if this is an API key field that could be auto-filled
const isApiKey =
subBlockId === 'apiKey' || (subBlockId?.toLowerCase().includes('apikey') ?? false)
@@ -228,46 +198,10 @@ export function useSubBlockValue<T = any>(
// Compute the modelValue based on block type
const modelValue = isProviderBasedBlock ? (modelSubBlockValue as string) : null
// Initialize valueRef on first render
useEffect(() => {
// If we're in preview mode with direct values, use those
if (previewDataRef.current.isInPreview) {
valueRef.current = previewDataRef.current.directValue;
} else {
// Otherwise use the store value or initial value
valueRef.current = storeValue !== undefined ? storeValue : initialValue;
}
}, [storeValue, initialValue, isPreview])
// Update the ref if the store value changes
// This ensures we're always working with the latest value
useEffect(() => {
// Skip updates from global store if we're using preview values
if (previewDataRef.current.isInPreview) return;
// Use deep comparison for objects to prevent unnecessary updates
if (!isEqual(valueRef.current, storeValue)) {
valueRef.current = storeValue !== undefined ? storeValue : initialValue
}
}, [storeValue, initialValue])
// Create a preview-aware setValue function
const setValueWithPreview = useCallback(
// Hook to set a value in the subblock store
const setValue = useCallback(
(newValue: T) => {
// If we're in preview mode, just update the local valueRef for display
// but don't update the global store
if (previewDataRef.current.isInPreview) {
// Only update if the value has changed
if (!isEqual(valueRef.current, newValue)) {
valueRef.current = newValue;
// Update the ref as well
previewDataRef.current.directValue = newValue;
}
// Return early without updating global state
return;
}
// For non-preview mode, use the normal setValue logic
// Use deep comparison to avoid unnecessary updates for complex objects
if (!isEqual(valueRef.current, newValue)) {
valueRef.current = newValue
@@ -296,6 +230,11 @@ export function useSubBlockValue<T = any>(
[blockId, subBlockId, blockType, isApiKey, storeValue, triggerWorkflowUpdate, modelValue]
)
// Initialize valueRef on first render
useEffect(() => {
valueRef.current = storeValue !== undefined ? storeValue : initialValue
}, [])
// When component mounts, check for existing API key in toolParamsStore
useEffect(() => {
// Skip autofill if the feature is disabled in settings
@@ -351,5 +290,14 @@ export function useSubBlockValue<T = any>(
isProviderBasedBlock,
])
return [valueRef.current as T | null, setValueWithPreview] as const
}
// Update the ref if the store value changes
// This ensures we're always working with the latest value
useEffect(() => {
// Use deep comparison for objects to prevent unnecessary updates
if (!isEqual(valueRef.current, storeValue)) {
valueRef.current = storeValue !== undefined ? storeValue : initialValue
}
}, [storeValue, initialValue])
return [valueRef.current as T | null, setValue] as const
}

View File

@@ -1,7 +1,7 @@
import { useEffect } from 'react'
import { Info } from 'lucide-react'
import { Label } from '@/components/ui/label'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { getBlock } from '@/blocks/index'
import type { SubBlockConfig } from '@/blocks/types'
@@ -27,26 +27,27 @@ import { TimeInput } from './components/time-input'
import { ToolInput } from './components/tool-input/tool-input'
import { WebhookConfig } from './components/webhook/webhook'
interface SubBlockProps {
blockId: string
config: SubBlockConfig
isConnecting: boolean
isPreview?: boolean
previewValue?: any
subBlockValues?: Record<string, any>
}
export function SubBlock({
blockId,
config,
isConnecting,
isPreview = false,
previewValue = undefined
isConnecting,
isPreview = false,
subBlockValues
}: SubBlockProps) {
const handleMouseDown = (e: React.MouseEvent) => {
e.stopPropagation()
}
const { getValue } = useSubBlockStore()
const isFieldRequired = () => {
const blockType = useWorkflowStore.getState().blocks[blockId]?.type
if (!blockType) return false
@@ -57,9 +58,14 @@ export function SubBlock({
return blockConfig.inputs[config.id]?.required === true
}
// Get preview value for this specific sub-block
const getPreviewValue = () => {
if (!isPreview || !subBlockValues) return undefined
return subBlockValues[config.id]?.value ?? null
}
const renderInput = () => {
// Get the subblock value from the config if available
const directValue = isPreview ? previewValue : undefined;
const previewValue = getPreviewValue()
switch (config.type) {
case 'short-input':
@@ -72,7 +78,7 @@ export function SubBlock({
isConnecting={isConnecting}
config={config}
isPreview={isPreview}
value={directValue}
previewValue={previewValue}
/>
)
case 'long-input':
@@ -85,7 +91,7 @@ export function SubBlock({
rows={config.rows}
config={config}
isPreview={isPreview}
value={directValue}
previewValue={previewValue}
/>
)
case 'dropdown':
@@ -96,7 +102,7 @@ export function SubBlock({
subBlockId={config.id}
options={config.options as string[]}
isPreview={isPreview}
value={directValue}
previewValue={previewValue}
/>
</div>
)
@@ -111,7 +117,7 @@ export function SubBlock({
step={config.step}
integer={config.integer}
isPreview={isPreview}
value={directValue}
previewValue={previewValue}
/>
)
case 'table':
@@ -120,7 +126,7 @@ export function SubBlock({
subBlockId={config.id}
columns={config.columns ?? []}
isPreview={isPreview}
value={directValue}
previewValue={previewValue}
/>
case 'code':
return (
@@ -132,7 +138,7 @@ export function SubBlock({
language={config.language}
generationType={config.generationType}
isPreview={isPreview}
value={directValue}
previewValue={previewValue}
/>
)
case 'switch':
@@ -141,14 +147,14 @@ export function SubBlock({
subBlockId={config.id}
title={config.title ?? ''}
isPreview={isPreview}
value={directValue}
previewValue={previewValue}
/>
case 'tool-input':
return <ToolInput
blockId={blockId}
subBlockId={config.id}
isPreview={isPreview}
value={directValue}
previewValue={previewValue}
/>
case 'checkbox-list':
return (
@@ -159,7 +165,7 @@ export function SubBlock({
options={config.options as { label: string; id: string }[]}
layout={config.layout}
isPreview={isPreview}
value={directValue}
subBlockValues={subBlockValues}
/>
)
case 'condition-input':
@@ -169,7 +175,7 @@ export function SubBlock({
subBlockId={config.id}
isConnecting={isConnecting}
isPreview={isPreview}
value={directValue}
previewValue={previewValue}
/>
)
case 'eval-input':
@@ -177,7 +183,7 @@ export function SubBlock({
blockId={blockId}
subBlockId={config.id}
isPreview={isPreview}
value={directValue}
previewValue={previewValue}
/>
case 'date-input':
return (
@@ -186,7 +192,7 @@ export function SubBlock({
subBlockId={config.id}
placeholder={config.placeholder}
isPreview={isPreview}
value={directValue}
previewValue={previewValue}
/>
)
case 'time-input':
@@ -196,7 +202,7 @@ export function SubBlock({
subBlockId={config.id}
placeholder={config.placeholder}
isPreview={isPreview}
value={directValue}
previewValue={previewValue}
/>
)
case 'file-upload':
@@ -208,7 +214,7 @@ export function SubBlock({
multiple={config.multiple === true}
maxSize={config.maxSize}
isPreview={isPreview}
value={directValue}
previewValue={previewValue}
/>
)
case 'webhook-config':
@@ -216,9 +222,9 @@ export function SubBlock({
<WebhookConfig
blockId={blockId}
subBlockId={config.id}
isConnecting={isConnecting}
isConnecting={isConnecting}
isPreview={isPreview}
value={directValue}
value={previewValue}
/>
)
case 'schedule-config':
@@ -228,60 +234,63 @@ export function SubBlock({
subBlockId={config.id}
isConnecting={isConnecting}
isPreview={isPreview}
value={directValue}
previewValue={previewValue}
/>
)
case 'oauth-input':
return (
<CredentialSelector
value={isPreview && directValue ? directValue : (typeof config.value === 'string' ? config.value : '')}
value={isPreview ? (previewValue || '') : (typeof config.value === 'string' ? config.value : '')}
onChange={(value) => {
// Use the workflow store to update the value
const event = new CustomEvent('update-subblock-value', {
detail: {
blockId,
subBlockId: config.id,
value,
},
})
window.dispatchEvent(event)
// Only allow changes in non-preview mode
if (!isPreview) {
const event = new CustomEvent('update-subblock-value', {
detail: {
blockId,
subBlockId: config.id,
value,
},
})
window.dispatchEvent(event)
}
}}
provider={config.provider as any}
requiredScopes={config.requiredScopes || []}
label={config.placeholder || 'Select a credential'}
serviceId={config.serviceId}
disabled={isPreview}
/>
)
case 'file-selector':
return <FileSelectorInput
blockId={blockId}
subBlock={config}
disabled={isConnecting}
disabled={isConnecting || isPreview}
isPreview={isPreview}
value={directValue}
previewValue={previewValue}
/>
case 'project-selector':
return <ProjectSelectorInput
blockId={blockId}
subBlock={config}
disabled={isConnecting}
disabled={isConnecting || isPreview}
isPreview={isPreview}
value={directValue}
previewValue={previewValue}
/>
case 'folder-selector':
return <FolderSelectorInput
blockId={blockId}
subBlock={config}
disabled={isConnecting}
disabled={isConnecting || isPreview}
isPreview={isPreview}
value={directValue}
previewValue={previewValue}
/>
case 'input-format':
return <InputFormat
blockId={blockId}
subBlockId={config.id}
isPreview={isPreview}
value={directValue}
previewValue={previewValue}
/>
default:
return <div>Unknown input type: {config.type}</div>
@@ -327,4 +336,4 @@ export function SubBlock({
{renderInput()}
</div>
)
}
}

View File

@@ -16,7 +16,6 @@ import { ActionBar } from './components/action-bar/action-bar'
import { ConnectionBlocks } from './components/connection-blocks/connection-blocks'
import { SubBlock } from './components/sub-block/sub-block'
interface WorkflowBlockProps {
type: string
config: BlockConfig
@@ -24,9 +23,7 @@ interface WorkflowBlockProps {
isActive?: boolean
isPending?: boolean
isPreview?: boolean
isReadOnly?: boolean
subBlockValues?: Record<string, any>
blockState?: any
}
// Combine both interfaces into a single component
@@ -42,11 +39,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
nextRunAt: string | null
lastRanAt: string | null
timezone: string
status?: string
isDisabled?: boolean
id?: string
} | null>(null)
const [isLoadingScheduleInfo, setIsLoadingScheduleInfo] = useState(false)
const [webhookInfo, setWebhookInfo] = useState<{
webhookPath: string
provider: string
@@ -66,9 +59,8 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
)
const isWide = useWorkflowStore((state) => state.blocks[id]?.isWide ?? false)
const blockHeight = useWorkflowStore((state) => state.blocks[id]?.height ?? 0)
const hasActiveSchedule = useWorkflowStore((state) => state.hasActiveSchedule ?? false)
const hasActiveWebhook = useWorkflowStore((state) => state.hasActiveWebhook ?? false)
const blockAdvancedMode = useWorkflowStore((state) => state.blocks[id]?.advancedMode ?? false)
const toggleBlockAdvancedMode = useWorkflowStore((state) => state.toggleBlockAdvancedMode)
// Workflow store actions
const updateBlockName = useWorkflowStore((state) => state.updateBlockName)
@@ -79,106 +71,49 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
const isActiveBlock = useExecutionStore((state) => state.activeBlockIds.has(id))
const isActive = dataIsActive || isActiveBlock
const reactivateSchedule = async (scheduleId: string) => {
try {
const response = await fetch(`/api/schedules/${scheduleId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ action: 'reactivate' }),
})
if (response.ok) {
fetchScheduleInfo()
} else {
console.error('Failed to reactivate schedule')
}
} catch (error) {
console.error('Error reactivating schedule:', error)
}
}
const fetchScheduleInfo = async () => {
try {
setIsLoadingScheduleInfo(true)
const workflowId = useWorkflowRegistry.getState().activeWorkflowId
if (!workflowId) return
const response = await fetch(`/api/schedules?workflowId=${workflowId}&mode=schedule`, {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache',
},
})
if (!response.ok) {
setScheduleInfo(null)
return
}
const data = await response.json()
if (!data.schedule) {
setScheduleInfo(null)
return
}
let scheduleTiming = 'Unknown schedule'
if (data.schedule.cronExpression) {
scheduleTiming = parseCronToHumanReadable(data.schedule.cronExpression)
}
const baseInfo = {
scheduleTiming,
nextRunAt: data.schedule.nextRunAt as string | null,
lastRanAt: data.schedule.lastRanAt as string | null,
timezone: data.schedule.timezone || 'UTC',
status: data.schedule.status as string,
isDisabled: data.schedule.status === 'disabled',
id: data.schedule.id as string,
}
try {
const statusRes = await fetch(`/api/schedules/${baseInfo.id}/status`, {
cache: 'no-store',
headers: { 'Cache-Control': 'no-cache' },
})
if (statusRes.ok) {
const statusData = await statusRes.json()
setScheduleInfo({
scheduleTiming: baseInfo.scheduleTiming,
nextRunAt: statusData.nextRunAt ?? baseInfo.nextRunAt,
lastRanAt: statusData.lastRanAt ?? baseInfo.lastRanAt,
timezone: baseInfo.timezone,
status: statusData.status ?? baseInfo.status,
isDisabled: statusData.isDisabled ?? baseInfo.isDisabled,
id: baseInfo.id,
})
return
}
} catch (err) {
console.error('Error fetching schedule status:', err)
}
setScheduleInfo(baseInfo)
} catch (error) {
console.error('Error fetching schedule info:', error)
setScheduleInfo(null)
} finally {
setIsLoadingScheduleInfo(false)
}
}
// Get schedule information for the tooltip
useEffect(() => {
if (type === 'starter') {
if (type === 'starter' && hasActiveSchedule) {
const fetchScheduleInfo = async () => {
try {
const workflowId = useWorkflowRegistry.getState().activeWorkflowId
if (!workflowId) return
const response = await fetch(`/api/schedules?workflowId=${workflowId}&mode=schedule`, {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache',
},
})
if (response.ok) {
const data = await response.json()
if (data.schedule) {
let scheduleTiming = 'Unknown schedule'
if (data.schedule.cronExpression) {
scheduleTiming = parseCronToHumanReadable(data.schedule.cronExpression)
}
setScheduleInfo({
scheduleTiming,
nextRunAt: data.schedule.nextRunAt,
lastRanAt: data.schedule.lastRanAt,
timezone: data.schedule.timezone || 'UTC',
})
}
}
} catch (error) {
console.error('Error fetching schedule info:', error)
}
}
fetchScheduleInfo()
} else {
} else if (!hasActiveSchedule) {
setScheduleInfo(null)
}
}, [type])
}, [type, hasActiveSchedule])
// Get webhook information for the tooltip
useEffect(() => {
if (type === 'starter' && hasActiveWebhook) {
const fetchWebhookInfo = async () => {
@@ -208,6 +143,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
}
}, [type, hasActiveWebhook])
// Update node internals when handles change
useEffect(() => {
updateNodeInternals(id)
}, [id, horizontalHandles, updateNodeInternals])
@@ -220,6 +156,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
}
}
// Add effect to observe size changes with debounced updates
useEffect(() => {
if (!contentRef.current) return
@@ -232,10 +169,12 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
}, 100)
const resizeObserver = new ResizeObserver((entries) => {
// Cancel any pending animation frame
if (rafId) {
cancelAnimationFrame(rafId)
}
// Schedule the update on the next animation frame
rafId = requestAnimationFrame(() => {
for (const entry of entries) {
const height =
@@ -261,7 +200,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
let currentRow: SubBlockConfig[] = []
let currentRowWidth = 0
// Get merged state for this block - use direct props if in preview mode
// Get merged state for this block
const blocks = useWorkflowStore.getState().blocks
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId || undefined
const isAdvancedMode = useWorkflowStore((state) => state.blocks[id]?.advancedMode ?? false)
@@ -279,9 +218,9 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
if (!block.condition) return true
// Get the values of the fields this block depends on from merged state
const fieldValue = mergedState?.[blockId]?.subBlocks[block.condition.field]?.value
const fieldValue = mergedState?.subBlocks[block.condition.field]?.value
const andFieldValue = block.condition.and
? mergedState?.[blockId]?.subBlocks[block.condition.and.field]?.value
? mergedState?.subBlocks[block.condition.and.field]?.value
: undefined
// Check if the condition value is an array
@@ -368,8 +307,9 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
}
}
// Check if this is a starter block and if we need to show schedule / webhook indicators
// Check if this is a starter block and has active schedule or webhook
const isStarterBlock = type === 'starter'
const showScheduleIndicator = isStarterBlock && hasActiveSchedule
const showWebhookIndicator = isStarterBlock && hasActiveWebhook
const getProviderName = (providerId: string): string => {
@@ -386,8 +326,6 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
return providers[providerId] || 'Webhook'
}
const shouldShowScheduleBadge = isStarterBlock && !isLoadingScheduleInfo && scheduleInfo !== null
return (
<div className='group relative'>
<Card
@@ -401,15 +339,6 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
isPending && 'ring-2 ring-amber-500',
'z-[20]'
)}
data-id={id}
data-props={data.isPreview ? JSON.stringify({
isPreview: data.isPreview,
isReadOnly: data.isReadOnly,
blockType: type,
data: {
subBlockValues: data.subBlockValues
}
}) : undefined}
>
{/* Show debug indicator for pending blocks */}
{isPending && (
@@ -502,7 +431,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
</Badge>
)}
{/* Schedule indicator badge - displayed for starter blocks with active schedules */}
{shouldShowScheduleBadge && (
{showScheduleIndicator && (
<Tooltip>
<TooltipTrigger asChild>
<Badge
@@ -533,7 +462,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
)}
/>
</div>
{scheduleInfo?.isDisabled ? 'Disabled' : 'Scheduled'}
Scheduled
</Badge>
</TooltipTrigger>
<TooltipContent side='top' className='max-w-[300px] p-4'>
@@ -598,7 +527,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
</TooltipContent>
</Tooltip>
)}
{config.subBlocks.some((block) => block.mode) && (
{config.longDescription && (
<Tooltip>
<TooltipTrigger asChild>
<Button
@@ -718,10 +647,9 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
<SubBlock
blockId={id}
config={subBlock}
isConnecting={isConnecting}
isPreview={data.isPreview}
previewValue={data.isPreview && data.subBlockValues ?
(data.subBlockValues[subBlock.id]?.value || null) : null}
isConnecting={isConnecting}
isPreview={data.isPreview}
subBlockValues={data.subBlockValues}
/>
</div>
))}
@@ -806,4 +734,4 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
</Card>
</div>
)
}
}

View File

@@ -27,23 +27,13 @@ import { WorkflowEdge } from '@/app/w/[id]/components/workflow-edge/workflow-edg
// import { LoopLabel } from '@/app/w/[id]/components/workflow-loop/components/loop-label/loop-label'
// import { createLoopNode } from '@/app/w/[id]/components/workflow-loop/workflow-loop'
import { getBlock } from '@/blocks'
import type { SubBlockConfig } from '@/blocks/types'
import { WorkflowState } from '@/stores/workflows/workflow/types'
const logger = createLogger('WorkflowPreview')
interface WorkflowPreviewProps {
// The workflow state to render
workflowState: {
blocks: Record<string, any>
edges: Array<{
id: string
source: string
target: string
sourceHandle?: string
targetHandle?: string
}>
loops: Record<string, any>
}
workflowState: WorkflowState
// Whether to show subblocks
showSubBlocks?: boolean
// Optional className for container styling
@@ -71,7 +61,6 @@ const edgeTypes: EdgeTypes = {
export function WorkflowPreview({
workflowState,
showSubBlocks = true,
className,
height = '100%',
width = '100%',
isPannable = false,
@@ -203,7 +192,7 @@ export function WorkflowPreview({
<ReactFlowProvider>
<div
style={{ height, width }}
className={cn(className, 'preview-mode')}
className={cn('preview-mode')}
>
<ReactFlow
nodes={nodes}

View File

@@ -1,5 +1,6 @@
import type { Edge } from 'reactflow'
import type { BlockOutput, SubBlockType } from '@/blocks/types'
import { Edge } from 'reactflow'
import { BlockOutput, SubBlockType } from '@/blocks/types'
import { DeploymentStatus } from '../registry/types'
export interface Position {
x: number
@@ -63,19 +64,6 @@ export interface Loop {
forEachItems?: any[] | Record<string, any> | string // Items or expression
}
export interface Parallel {
id: string
nodes: string[]
distribution?: any[] | Record<string, any> | string // Items or expression
}
export interface DeploymentStatus {
isDeployed: boolean
deployedAt?: Date
apiKey?: string
needsRedeployment?: boolean
}
export interface WorkflowState {
blocks: Record<string, BlockState>
edges: Edge[]