From 1c60cfc1608617f88c6057338324cd3d3af4476e Mon Sep 17 00:00:00 2001 From: aadamgough Date: Mon, 29 Dec 2025 16:09:58 -0800 Subject: [PATCH 01/14] added larger live deployment preview --- .../components/general/general.tsx | 44 ++- .../expanded-preview/expanded-preview.tsx | 76 +++++ .../components/expanded-preview/index.ts | 1 + .../workflow-preview/components/index.ts | 2 + .../components/pinned-sub-blocks/index.ts | 1 + .../pinned-sub-blocks/pinned-sub-blocks.tsx | 295 ++++++++++++++++++ 6 files changed, 410 insertions(+), 9 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/expanded-preview/expanded-preview.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/expanded-preview/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx index 129c92fc33..314a84c26d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' +import { Maximize2 } from 'lucide-react' import { Button, Label, @@ -13,6 +14,7 @@ import { } from '@/components/emcn' import { Skeleton } from '@/components/ui' import type { WorkflowDeploymentVersionResponse } from '@/lib/workflows/persistence/utils' +import { ExpandedWorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/components' import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview' import type { WorkflowState } from '@/stores/workflows/workflow/types' import { Versions } from './components' @@ -49,6 +51,7 @@ export function GeneralDeploy({ const [previewMode, setPreviewMode] = useState('active') const [showLoadDialog, setShowLoadDialog] = useState(false) const [showPromoteDialog, setShowPromoteDialog] = useState(false) + const [showExpandedPreview, setShowExpandedPreview] = useState(false) const [versionToLoad, setVersionToLoad] = useState(null) const [versionToPromote, setVersionToPromote] = useState(null) @@ -219,15 +222,25 @@ export function GeneralDeploy({ }} > {workflowToShow ? ( - + <> + + + ) : (
Deploy your workflow to see a preview @@ -304,6 +317,19 @@ export function GeneralDeploy({ + + {workflowToShow && ( + setShowExpandedPreview(false)} + workflowState={workflowToShow} + title={ + previewMode === 'selected' && selectedVersionInfo + ? selectedVersionInfo.name || `v${selectedVersion}` + : 'Live Workflow' + } + /> + )} ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/expanded-preview/expanded-preview.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/expanded-preview/expanded-preview.tsx new file mode 100644 index 0000000000..4990823764 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/expanded-preview/expanded-preview.tsx @@ -0,0 +1,76 @@ +'use client' + +import { useState } from 'react' +import { Modal, ModalBody, ModalContent, ModalHeader } from '@/components/emcn' +import { PinnedSubBlocks } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks' +import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview' +import type { WorkflowState } from '@/stores/workflows/workflow/types' + +interface ExpandedWorkflowPreviewProps { + /** Whether the modal is open */ + isOpen: boolean + /** Callback when closing the modal */ + onClose: () => void + /** The workflow state to display */ + workflowState: WorkflowState + /** Title for the modal header */ + title?: string +} + +/** + * Expanded workflow preview modal with clickable blocks. + * Shows the workflow preview at full size with a pinned panel + * displaying subblock values when a block is clicked. + */ +export function ExpandedWorkflowPreview({ + isOpen, + onClose, + workflowState, + title = 'Workflow Preview', +}: ExpandedWorkflowPreviewProps) { + const [selectedBlockId, setSelectedBlockId] = useState(null) + + const selectedBlock = selectedBlockId ? workflowState.blocks?.[selectedBlockId] : null + + const handleNodeClick = (blockId: string) => { + // Toggle selection if clicking the same block + if (selectedBlockId === blockId) { + setSelectedBlockId(null) + } else { + setSelectedBlockId(blockId) + } + } + + const handleClose = () => { + setSelectedBlockId(null) + onClose() + } + + const handleClosePanel = () => { + setSelectedBlockId(null) + } + + return ( + + + {title} + + +
+ +
+ + {selectedBlock && } +
+
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/expanded-preview/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/expanded-preview/index.ts new file mode 100644 index 0000000000..01f8493ae9 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/expanded-preview/index.ts @@ -0,0 +1 @@ +export { ExpandedWorkflowPreview } from './expanded-preview' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/index.ts new file mode 100644 index 0000000000..685922e47a --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/index.ts @@ -0,0 +1,2 @@ +export { ExpandedWorkflowPreview } from './expanded-preview' +export { PinnedSubBlocks } from './pinned-sub-blocks' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/index.ts new file mode 100644 index 0000000000..f09485b5c3 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/index.ts @@ -0,0 +1 @@ +export { PinnedSubBlocks } from './pinned-sub-blocks' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx new file mode 100644 index 0000000000..8d2dd4657b --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx @@ -0,0 +1,295 @@ +'use client' + +import { useState } from 'react' +import { ChevronDown, ChevronUp, Maximize2, X, Zap } from 'lucide-react' +import { Badge, Button } from '@/components/emcn' +import { cn } from '@/lib/core/utils/cn' +import { getBlock } from '@/blocks' +import type { BlockConfig, SubBlockConfig } from '@/blocks/types' +import type { BlockState } from '@/stores/workflows/workflow/types' + +/** + * Expandable section for displaying large subblock values. + * Supports inline expansion and fullscreen modal view. + */ +function ExpandableValue({ title, value }: { title: string; value: unknown }) { + const [isExpanded, setIsExpanded] = useState(false) + const [isModalOpen, setIsModalOpen] = useState(false) + + const displayValue = formatSubBlockValue(value) + const isLargeValue = displayValue.length > 100 || displayValue.includes('\n') + + if (!isLargeValue) { + return ( +
+
+          {displayValue}
+        
+
+ ) + } + + return ( + <> +
+
+ + +
+
+
+            {displayValue}
+          
+
+
+ + {isModalOpen && ( +
+
+
+

{title}

+ +
+
+
+                {displayValue}
+              
+
+
+
+ )} + + ) +} + +/** + * Format a subblock value for display. + * Handles various types including objects, arrays, booleans, etc. + */ +function formatSubBlockValue(value: unknown): string { + if (value === null || value === undefined || value === '') { + return '—' + } + + if (typeof value === 'boolean') { + return value ? 'Yes' : 'No' + } + + if (typeof value === 'object') { + try { + return JSON.stringify(value, null, 2) + } catch { + return String(value) + } + } + + return String(value) +} + +/** + * Get display label for a subblock type. + */ +function getSubBlockTypeLabel(type: string): string { + const typeLabels: Record = { + 'short-input': 'Text', + 'long-input': 'Text Area', + dropdown: 'Select', + combobox: 'Combobox', + slider: 'Slider', + table: 'Table', + code: 'Code', + switch: 'Toggle', + 'tool-input': 'Tool', + 'checkbox-list': 'Checkboxes', + 'grouped-checkbox-list': 'Grouped Checkboxes', + 'condition-input': 'Condition', + 'eval-input': 'Evaluation', + 'time-input': 'Time', + 'oauth-input': 'OAuth', + 'webhook-config': 'Webhook', + 'schedule-info': 'Schedule', + 'file-selector': 'File', + 'project-selector': 'Project', + 'channel-selector': 'Channel', + 'user-selector': 'User', + 'folder-selector': 'Folder', + 'knowledge-base-selector': 'Knowledge Base', + 'knowledge-tag-filters': 'Tag Filters', + 'document-selector': 'Document', + 'document-tag-entry': 'Document Tags', + 'mcp-server-selector': 'MCP Server', + 'mcp-tool-selector': 'MCP Tool', + 'mcp-dynamic-args': 'MCP Args', + 'input-format': 'Input Format', + 'response-format': 'Response Format', + 'trigger-save': 'Trigger', + 'file-upload': 'File Upload', + 'input-mapping': 'Input Mapping', + 'variables-input': 'Variables', + 'messages-input': 'Messages', + 'workflow-selector': 'Workflow', + 'workflow-input-mapper': 'Workflow Input', + text: 'Text', + } + + return typeLabels[type] || type +} + +/** + * Individual subblock row showing label, type, and value. + */ +function SubBlockRow({ + subBlockConfig, + value, +}: { + subBlockConfig: SubBlockConfig + value: unknown +}) { + const title = subBlockConfig.title || subBlockConfig.id + const typeLabel = getSubBlockTypeLabel(subBlockConfig.type) + const hasValue = value !== null && value !== undefined && value !== '' + + return ( +
+
+ {title} + + {typeLabel} + +
+ {hasValue ? ( + + ) : ( +
+ No value configured +
+ )} +
+ ) +} + +interface PinnedSubBlocksProps { + /** The block state containing subblock values */ + block: BlockState + /** Callback when closing the panel */ + onClose: () => void +} + +/** + * Pinned panel displaying all subblock values for a selected block. + * Overlays the workflow preview canvas in the top-right corner. + */ +export function PinnedSubBlocks({ block, onClose }: PinnedSubBlocksProps) { + const blockConfig = getBlock(block.type) as BlockConfig | undefined + + if (!blockConfig) { + return ( +
+
+
+ + + {block.name || 'Unknown Block'} + +
+ +
+
+

Block configuration not found.

+
+
+ ) + } + + // Get visible subblocks (filter out hidden ones) + const visibleSubBlocks = blockConfig.subBlocks.filter( + (subBlock) => !subBlock.hidden && !subBlock.hideFromPreview + ) + + // Get subblock values from block state + const subBlockValues = block.subBlocks || {} + + return ( +
+ {/* Header */} +
+
+
+ + {block.name || blockConfig.name} + +
+ +
+ + {/* Block Type Badge */} +
+ {blockConfig.name} + {block.enabled === false && ( + + Disabled + + )} +
+ + {/* Subblock Values */} +
+ {visibleSubBlocks.length > 0 ? ( + visibleSubBlocks.map((subBlock, index) => { + const valueObj = subBlockValues[subBlock.id] + const value = valueObj?.value !== undefined ? valueObj.value : valueObj + + return ( + + ) + }) + ) : ( +
+

+ No configurable fields for this block. +

+
+ )} +
+
+ ) +} From 618a9445c4df780fb7e86cdee5a34d155c5e8085 Mon Sep 17 00:00:00 2001 From: aadamgough Date: Mon, 29 Dec 2025 19:17:56 -0800 Subject: [PATCH 02/14] edited subblock UI --- .../pinned-sub-blocks/pinned-sub-blocks.tsx | 115 +++++++++--------- 1 file changed, 56 insertions(+), 59 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx index 8d2dd4657b..b4ace6a550 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx @@ -115,57 +115,56 @@ function formatSubBlockValue(value: unknown): string { } /** - * Get display label for a subblock type. + * Evaluate whether a subblock's condition is met based on current values. + * Returns true if the subblock should be visible. */ -function getSubBlockTypeLabel(type: string): string { - const typeLabels: Record = { - 'short-input': 'Text', - 'long-input': 'Text Area', - dropdown: 'Select', - combobox: 'Combobox', - slider: 'Slider', - table: 'Table', - code: 'Code', - switch: 'Toggle', - 'tool-input': 'Tool', - 'checkbox-list': 'Checkboxes', - 'grouped-checkbox-list': 'Grouped Checkboxes', - 'condition-input': 'Condition', - 'eval-input': 'Evaluation', - 'time-input': 'Time', - 'oauth-input': 'OAuth', - 'webhook-config': 'Webhook', - 'schedule-info': 'Schedule', - 'file-selector': 'File', - 'project-selector': 'Project', - 'channel-selector': 'Channel', - 'user-selector': 'User', - 'folder-selector': 'Folder', - 'knowledge-base-selector': 'Knowledge Base', - 'knowledge-tag-filters': 'Tag Filters', - 'document-selector': 'Document', - 'document-tag-entry': 'Document Tags', - 'mcp-server-selector': 'MCP Server', - 'mcp-tool-selector': 'MCP Tool', - 'mcp-dynamic-args': 'MCP Args', - 'input-format': 'Input Format', - 'response-format': 'Response Format', - 'trigger-save': 'Trigger', - 'file-upload': 'File Upload', - 'input-mapping': 'Input Mapping', - 'variables-input': 'Variables', - 'messages-input': 'Messages', - 'workflow-selector': 'Workflow', - 'workflow-input-mapper': 'Workflow Input', - text: 'Text', +function evaluateCondition( + condition: SubBlockConfig['condition'], + subBlockValues: Record +): boolean { + if (!condition) return true + + const actualCondition = typeof condition === 'function' ? condition() : condition + + const fieldValueObj = subBlockValues[actualCondition.field] + const fieldValue = + fieldValueObj && typeof fieldValueObj === 'object' && 'value' in fieldValueObj + ? (fieldValueObj as { value: unknown }).value + : fieldValueObj + + const conditionValues = Array.isArray(actualCondition.value) + ? actualCondition.value + : [actualCondition.value] + + let isMatch = conditionValues.some((v) => v === fieldValue) + + if (actualCondition.not) { + isMatch = !isMatch } - return typeLabels[type] || type + if (actualCondition.and && isMatch) { + const andFieldValueObj = subBlockValues[actualCondition.and.field] + const andFieldValue = + andFieldValueObj && typeof andFieldValueObj === 'object' && 'value' in andFieldValueObj + ? (andFieldValueObj as { value: unknown }).value + : andFieldValueObj + + const andConditionValues = Array.isArray(actualCondition.and.value) + ? actualCondition.and.value + : [actualCondition.and.value] + + let andMatch = andConditionValues.some((v) => v === andFieldValue) + + if (actualCondition.and.not) { + andMatch = !andMatch + } + + isMatch = isMatch && andMatch + } + + return isMatch } -/** - * Individual subblock row showing label, type, and value. - */ function SubBlockRow({ subBlockConfig, value, @@ -174,17 +173,11 @@ function SubBlockRow({ value: unknown }) { const title = subBlockConfig.title || subBlockConfig.id - const typeLabel = getSubBlockTypeLabel(subBlockConfig.type) const hasValue = value !== null && value !== undefined && value !== '' return (
-
- {title} - - {typeLabel} - -
+ {title} {hasValue ? ( ) : ( @@ -231,14 +224,18 @@ export function PinnedSubBlocks({ block, onClose }: PinnedSubBlocksProps) { ) } - // Get visible subblocks (filter out hidden ones) - const visibleSubBlocks = blockConfig.subBlocks.filter( - (subBlock) => !subBlock.hidden && !subBlock.hideFromPreview - ) - - // Get subblock values from block state const subBlockValues = block.subBlocks || {} + const visibleSubBlocks = blockConfig.subBlocks.filter((subBlock) => { + if (subBlock.hidden || subBlock.hideFromPreview) return false + + if (subBlock.condition) { + return evaluateCondition(subBlock.condition, subBlockValues) + } + + return true + }) + return (
{/* Header */} From ff2e38ae9c5f4603edb56c633f62f999a6b117f7 Mon Sep 17 00:00:00 2001 From: aadamgough Date: Mon, 29 Dec 2025 19:20:34 -0800 Subject: [PATCH 03/14] removed comments --- .../expanded-preview/expanded-preview.tsx | 10 ---------- .../pinned-sub-blocks/pinned-sub-blocks.tsx | 14 -------------- 2 files changed, 24 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/expanded-preview/expanded-preview.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/expanded-preview/expanded-preview.tsx index 4990823764..ccc9bf6279 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/expanded-preview/expanded-preview.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/expanded-preview/expanded-preview.tsx @@ -7,21 +7,12 @@ import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/work import type { WorkflowState } from '@/stores/workflows/workflow/types' interface ExpandedWorkflowPreviewProps { - /** Whether the modal is open */ isOpen: boolean - /** Callback when closing the modal */ onClose: () => void - /** The workflow state to display */ workflowState: WorkflowState - /** Title for the modal header */ title?: string } -/** - * Expanded workflow preview modal with clickable blocks. - * Shows the workflow preview at full size with a pinned panel - * displaying subblock values when a block is clicked. - */ export function ExpandedWorkflowPreview({ isOpen, onClose, @@ -33,7 +24,6 @@ export function ExpandedWorkflowPreview({ const selectedBlock = selectedBlockId ? workflowState.blocks?.[selectedBlockId] : null const handleNodeClick = (blockId: string) => { - // Toggle selection if clicking the same block if (selectedBlockId === blockId) { setSelectedBlockId(null) } else { diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx index b4ace6a550..8283e7f3b4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx @@ -8,10 +8,6 @@ import { getBlock } from '@/blocks' import type { BlockConfig, SubBlockConfig } from '@/blocks/types' import type { BlockState } from '@/stores/workflows/workflow/types' -/** - * Expandable section for displaying large subblock values. - * Supports inline expansion and fullscreen modal view. - */ function ExpandableValue({ title, value }: { title: string; value: unknown }) { const [isExpanded, setIsExpanded] = useState(false) const [isModalOpen, setIsModalOpen] = useState(false) @@ -90,10 +86,6 @@ function ExpandableValue({ title, value }: { title: string; value: unknown }) { ) } -/** - * Format a subblock value for display. - * Handles various types including objects, arrays, booleans, etc. - */ function formatSubBlockValue(value: unknown): string { if (value === null || value === undefined || value === '') { return '—' @@ -190,16 +182,10 @@ function SubBlockRow({ } interface PinnedSubBlocksProps { - /** The block state containing subblock values */ block: BlockState - /** Callback when closing the panel */ onClose: () => void } -/** - * Pinned panel displaying all subblock values for a selected block. - * Overlays the workflow preview canvas in the top-right corner. - */ export function PinnedSubBlocks({ block, onClose }: PinnedSubBlocksProps) { const blockConfig = getBlock(block.type) as BlockConfig | undefined From 3e5b8fccbd24337a8a7c7f7560408b6b30779503 Mon Sep 17 00:00:00 2001 From: aadamgough Date: Mon, 29 Dec 2025 19:33:16 -0800 Subject: [PATCH 04/14] removed carrot --- .../pinned-sub-blocks/pinned-sub-blocks.tsx | 40 +++---------------- 1 file changed, 6 insertions(+), 34 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx index 8283e7f3b4..63a49238ec 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx @@ -1,60 +1,32 @@ 'use client' import { useState } from 'react' -import { ChevronDown, ChevronUp, Maximize2, X, Zap } from 'lucide-react' +import { Maximize2, X, Zap } from 'lucide-react' import { Badge, Button } from '@/components/emcn' -import { cn } from '@/lib/core/utils/cn' import { getBlock } from '@/blocks' import type { BlockConfig, SubBlockConfig } from '@/blocks/types' import type { BlockState } from '@/stores/workflows/workflow/types' function ExpandableValue({ title, value }: { title: string; value: unknown }) { - const [isExpanded, setIsExpanded] = useState(false) const [isModalOpen, setIsModalOpen] = useState(false) const displayValue = formatSubBlockValue(value) const isLargeValue = displayValue.length > 100 || displayValue.includes('\n') - if (!isLargeValue) { - return ( -
-
-          {displayValue}
-        
-
- ) - } - return ( <> -
-
+
+ {isLargeValue && ( - -
-
+ )} +
             {displayValue}
           
From fa952df895526992ba1b40f494921822d2e04c6e Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 7 Jan 2026 20:40:08 -0800 Subject: [PATCH 05/14] updated styling to match existing subblocks --- apps/sim/app/chat/[identifier]/chat.tsx | 31 +- .../frozen-canvas/frozen-canvas.tsx | 480 +----------------- .../expanded-preview/expanded-preview.tsx | 30 +- .../pinned-sub-blocks/pinned-sub-blocks.tsx | 336 +++++++----- 4 files changed, 244 insertions(+), 633 deletions(-) diff --git a/apps/sim/app/chat/[identifier]/chat.tsx b/apps/sim/app/chat/[identifier]/chat.tsx index 0a39af6657..0a43ea1849 100644 --- a/apps/sim/app/chat/[identifier]/chat.tsx +++ b/apps/sim/app/chat/[identifier]/chat.tsx @@ -175,7 +175,6 @@ export default function ChatClient({ identifier }: { identifier: string }) { const distanceFromBottom = scrollHeight - scrollTop - clientHeight setShowScrollButton(distanceFromBottom > 100) - // Track if user is manually scrolling during streaming if (isStreamingResponse && !isUserScrollingRef.current) { setUserHasScrolled(true) } @@ -191,13 +190,10 @@ export default function ChatClient({ identifier }: { identifier: string }) { return () => container.removeEventListener('scroll', handleScroll) }, [handleScroll]) - // Reset user scroll tracking when streaming starts useEffect(() => { if (isStreamingResponse) { - // Reset userHasScrolled when streaming starts setUserHasScrolled(false) - // Give a small delay to distinguish between programmatic scroll and user scroll isUserScrollingRef.current = true setTimeout(() => { isUserScrollingRef.current = false @@ -215,7 +211,6 @@ export default function ChatClient({ identifier }: { identifier: string }) { }) if (!response.ok) { - // Check if auth is required if (response.status === 401) { const errorData = await response.json() @@ -236,7 +231,6 @@ export default function ChatClient({ identifier }: { identifier: string }) { throw new Error(`Failed to load chat configuration: ${response.status}`) } - // Reset auth required state when authentication is successful setAuthRequired(null) const data = await response.json() @@ -260,7 +254,6 @@ export default function ChatClient({ identifier }: { identifier: string }) { } } - // Fetch chat config on mount and generate new conversation ID useEffect(() => { fetchChatConfig() setConversationId(uuidv4()) @@ -285,7 +278,6 @@ export default function ChatClient({ identifier }: { identifier: string }) { }, 800) } - // Handle sending a message const handleSendMessage = async ( messageParam?: string, isVoiceInput = false, @@ -308,7 +300,6 @@ export default function ChatClient({ identifier }: { identifier: string }) { filesCount: files?.length, }) - // Reset userHasScrolled when sending a new message setUserHasScrolled(false) const userMessage: ChatMessage = { @@ -325,24 +316,20 @@ export default function ChatClient({ identifier }: { identifier: string }) { })), } - // Add the user's message to the chat setMessages((prev) => [...prev, userMessage]) setInputValue('') setIsLoading(true) - // Scroll to show only the user's message and loading indicator setTimeout(() => { scrollToMessage(userMessage.id, true) }, 100) - // Create abort controller for request cancellation const abortController = new AbortController() const timeoutId = setTimeout(() => { abortController.abort() }, CHAT_REQUEST_TIMEOUT_MS) try { - // Send structured payload to maintain chat context const payload: any = { input: typeof userMessage.content === 'string' @@ -351,7 +338,6 @@ export default function ChatClient({ identifier }: { identifier: string }) { conversationId, } - // Add files if present (convert to base64 for JSON transmission) if (files && files.length > 0) { payload.files = await Promise.all( files.map(async (file) => ({ @@ -379,7 +365,6 @@ export default function ChatClient({ identifier }: { identifier: string }) { signal: abortController.signal, }) - // Clear timeout since request succeeded clearTimeout(timeoutId) if (!response.ok) { @@ -392,7 +377,6 @@ export default function ChatClient({ identifier }: { identifier: string }) { throw new Error('Response body is missing') } - // Use the streaming hook with audio support const shouldPlayAudio = isVoiceInput || isVoiceFirstMode const audioHandler = shouldPlayAudio ? createAudioStreamHandler( @@ -421,7 +405,6 @@ export default function ChatClient({ identifier }: { identifier: string }) { } ) } catch (error: any) { - // Clear timeout in case of error clearTimeout(timeoutId) if (error.name === 'AbortError') { @@ -442,7 +425,6 @@ export default function ChatClient({ identifier }: { identifier: string }) { } } - // Stop audio when component unmounts or when streaming is stopped useEffect(() => { return () => { stopAudio() @@ -452,28 +434,23 @@ export default function ChatClient({ identifier }: { identifier: string }) { } }, [stopAudio]) - // Voice interruption - stop audio when user starts speaking const handleVoiceInterruption = useCallback(() => { stopAudio() - // Stop any ongoing streaming response if (isStreamingResponse) { stopStreaming(setMessages) } }, [isStreamingResponse, stopStreaming, setMessages, stopAudio]) - // Handle voice mode activation const handleVoiceStart = useCallback(() => { setIsVoiceFirstMode(true) }, []) - // Handle exiting voice mode const handleExitVoiceMode = useCallback(() => { setIsVoiceFirstMode(false) - stopAudio() // Stop any playing audio when exiting + stopAudio() }, [stopAudio]) - // Handle voice transcript from voice-first interface const handleVoiceTranscript = useCallback( (transcript: string) => { logger.info('Received voice transcript:', transcript) @@ -482,14 +459,11 @@ export default function ChatClient({ identifier }: { identifier: string }) { [handleSendMessage] ) - // If error, show error message using the extracted component if (error) { return } - // If authentication is required, use the extracted components if (authRequired) { - // Get title and description from the URL params or use defaults const title = new URLSearchParams(window.location.search).get('title') || 'chat' const primaryColor = new URLSearchParams(window.location.search).get('color') || 'var(--brand-primary-hover-hex)' @@ -526,12 +500,10 @@ export default function ChatClient({ identifier }: { identifier: string }) { } } - // Loading state while fetching config using the extracted component if (!chatConfig) { return } - // Voice-first mode interface if (isVoiceFirstMode) { return ( {/* Header component */} diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/frozen-canvas/frozen-canvas.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/frozen-canvas/frozen-canvas.tsx index b48bf8b63c..245e74b048 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/frozen-canvas/frozen-canvas.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/frozen-canvas/frozen-canvas.tsx @@ -2,367 +2,21 @@ import { useEffect, useState } from 'react' import { createLogger } from '@sim/logger' -import { - AlertCircle, - ChevronDown, - ChevronLeft, - ChevronRight, - ChevronUp, - Clock, - DollarSign, - Hash, - Loader2, - Maximize2, - X, - Zap, -} from 'lucide-react' -import { Badge, Modal, ModalBody, ModalContent, ModalHeader } from '@/components/emcn' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { AlertCircle, Loader2 } from 'lucide-react' +import { Modal, ModalBody, ModalContent, ModalHeader } from '@/components/emcn' import { redactApiKeys } from '@/lib/core/security/redaction' import { cn } from '@/lib/core/utils/cn' +import { PinnedSubBlocks } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks' import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview' import type { WorkflowState } from '@/stores/workflows/workflow/types' const logger = createLogger('FrozenCanvas') -function ExpandableDataSection({ title, data }: { title: string; data: any }) { - const [isExpanded, setIsExpanded] = useState(false) - const [isModalOpen, setIsModalOpen] = useState(false) - - const jsonString = JSON.stringify(data, null, 2) - const isLargeData = jsonString.length > 500 || jsonString.split('\n').length > 10 - - return ( - <> -
-
-

{title}

-
- {isLargeData && ( - - )} - -
-
-
-
-            {jsonString}
-          
-
-
- - {isModalOpen && ( -
-
-
-

{title}

- -
-
-
-                {jsonString}
-              
-
-
-
- )} - - ) -} - -function formatExecutionData(executionData: any) { - const { - inputData, - outputData, - cost, - tokens, - durationMs, - status, - blockName, - blockType, - errorMessage, - errorStackTrace, - } = executionData - - return { - blockName: blockName || 'Unknown Block', - blockType: blockType || 'unknown', - status, - duration: durationMs ? `${durationMs}ms` : 'N/A', - input: redactApiKeys(inputData || {}), - output: redactApiKeys(outputData || {}), - errorMessage, - errorStackTrace, - cost: cost - ? { - input: cost.input || 0, - output: cost.output || 0, - total: cost.total || 0, - } - : null, - tokens: tokens - ? { - input: tokens.input || tokens.prompt || 0, - output: tokens.output || tokens.completion || 0, - total: tokens.total || 0, - } - : null, - } -} - -function getCurrentIterationData(blockExecutionData: any) { - if (blockExecutionData.iterations && Array.isArray(blockExecutionData.iterations)) { - const currentIndex = blockExecutionData.currentIteration ?? 0 - return { - executionData: blockExecutionData.iterations[currentIndex], - currentIteration: currentIndex, - totalIterations: blockExecutionData.totalIterations ?? blockExecutionData.iterations.length, - hasMultipleIterations: blockExecutionData.iterations.length > 1, - } - } - - return { - executionData: blockExecutionData, - currentIteration: 0, - totalIterations: 1, - hasMultipleIterations: false, - } -} - -function PinnedLogs({ - executionData, - blockId, - workflowState, - onClose, -}: { - executionData: any | null - blockId: string - workflowState: any - onClose: () => void -}) { - const [currentIterationIndex, setCurrentIterationIndex] = useState(0) - - useEffect(() => { - setCurrentIterationIndex(0) - }, [executionData]) - - if (!executionData) { - const blockInfo = workflowState?.blocks?.[blockId] - const formatted = { - blockName: blockInfo?.name || 'Unknown Block', - blockType: blockInfo?.type || 'unknown', - status: 'not_executed', - } - - return ( - - -
- - - {formatted.blockName} - - -
-
- {formatted.blockType} - not executed -
-
- - -
-
- This block was not executed because the workflow failed before reaching it. -
-
-
-
- ) - } - - const iterationInfo = getCurrentIterationData({ - ...executionData, - currentIteration: currentIterationIndex, - }) - - const formatted = formatExecutionData(iterationInfo.executionData) - const totalIterations = executionData.iterations?.length || 1 - - const goToPreviousIteration = () => { - if (currentIterationIndex > 0) { - setCurrentIterationIndex(currentIterationIndex - 1) - } - } - - const goToNextIteration = () => { - if (currentIterationIndex < totalIterations - 1) { - setCurrentIterationIndex(currentIterationIndex + 1) - } - } - - return ( - - -
- - - {formatted.blockName} - - -
-
-
- - {formatted.blockType} - - {formatted.status} -
- - {iterationInfo.hasMultipleIterations && ( -
- - - {iterationInfo.totalIterations !== undefined - ? `${currentIterationIndex + 1} / ${iterationInfo.totalIterations}` - : `${currentIterationIndex + 1}`} - - -
- )} -
-
- - -
-
- - {formatted.duration} -
- - {formatted.cost && formatted.cost.total > 0 && ( -
- - - ${formatted.cost.total.toFixed(5)} - -
- )} - - {formatted.tokens && formatted.tokens.total > 0 && ( -
- - - {formatted.tokens.total} tokens - -
- )} -
- - - - - - {formatted.cost && formatted.cost.total > 0 && ( -
-

- Cost Breakdown -

-
-
- Input: - ${formatted.cost.input.toFixed(5)} -
-
- Output: - ${formatted.cost.output.toFixed(5)} -
-
- Total: - ${formatted.cost.total.toFixed(5)} -
-
-
- )} - - {formatted.tokens && formatted.tokens.total > 0 && ( -
-

- Token Usage -

-
-
- Input: - {formatted.tokens.input} -
-
- Output: - {formatted.tokens.output} -
-
- Total: - {formatted.tokens.total} -
-
-
- )} -
-
- ) +interface BlockExecutionData { + input: unknown + output: unknown + status: string + durationMs: number } interface FrozenCanvasData { @@ -406,30 +60,22 @@ export function FrozenCanvas({ onClose, }: FrozenCanvasProps) { const [data, setData] = useState(null) - const [blockExecutions, setBlockExecutions] = useState>({}) + const [blockExecutions, setBlockExecutions] = useState>({}) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) - const [pinnedBlockId, setPinnedBlockId] = useState(null) - // Process traceSpans to create blockExecutions map useEffect(() => { if (traceSpans && Array.isArray(traceSpans)) { - const blockExecutionMap: Record = {} - - logger.debug('Processing trace spans for frozen canvas:', { traceSpans }) + const blockExecutionMap: Record = {} - // Recursively collect all spans with blockId from the trace spans tree const collectBlockSpans = (spans: any[]): any[] => { const blockSpans: any[] = [] for (const span of spans) { - // If this span has a blockId, it's a block execution if (span.blockId) { blockSpans.push(span) } - - // Recursively check children if (span.children && Array.isArray(span.children)) { blockSpans.push(...collectBlockSpans(span.children)) } @@ -439,77 +85,15 @@ export function FrozenCanvas({ } const allBlockSpans = collectBlockSpans(traceSpans) - logger.debug('Collected all block spans:', allBlockSpans) - - // Group spans by blockId - const traceSpansByBlockId = allBlockSpans.reduce((acc: any, span: any) => { - if (span.blockId) { - if (!acc[span.blockId]) { - acc[span.blockId] = [] - } - acc[span.blockId].push(span) - } - return acc - }, {}) - - logger.debug('Grouped trace spans by blockId:', traceSpansByBlockId) - for (const [blockId, spans] of Object.entries(traceSpansByBlockId)) { - const spanArray = spans as any[] - - const iterations = spanArray.map((span: any) => { - // Extract error information from span output if status is error - let errorMessage = null - let errorStackTrace = null - - if (span.status === 'error' && span.output) { - // Error information can be in different formats in the output - if (typeof span.output === 'string') { - errorMessage = span.output - } else if (span.output.error) { - errorMessage = span.output.error - errorStackTrace = span.output.stackTrace || span.output.stack - } else if (span.output.message) { - errorMessage = span.output.message - errorStackTrace = span.output.stackTrace || span.output.stack - } else { - // Fallback: stringify the entire output for error cases - errorMessage = JSON.stringify(span.output) - } + for (const span of allBlockSpans) { + if (span.blockId && !blockExecutionMap[span.blockId]) { + blockExecutionMap[span.blockId] = { + input: redactApiKeys(span.input || {}), + output: redactApiKeys(span.output || {}), + status: span.status || 'unknown', + durationMs: span.duration || 0, } - - return { - id: span.id, - blockId: span.blockId, - blockName: span.name, - blockType: span.type, - status: span.status, - startedAt: span.startTime, - endedAt: span.endTime, - durationMs: span.duration, - inputData: span.input, - outputData: span.output, - errorMessage, - errorStackTrace, - cost: span.cost || { - input: null, - output: null, - total: null, - }, - tokens: span.tokens || { - input: null, - output: null, - total: null, - }, - modelUsed: span.model || null, - metadata: {}, - } - }) - - blockExecutionMap[blockId] = { - iterations, - currentIteration: 0, - totalIterations: iterations.length, } } @@ -606,11 +190,9 @@ export function FrozenCanvas({ } return ( - <> -
+
+ {/* Canvas area */} +
- {pinnedBlockId && ( - setPinnedBlockId(null)} + executionData={blockExecutions[pinnedBlockId]} /> )} - +
) } if (isModal) { return ( - + Workflow State - -
-
- {renderContent()} -
-
-
+ {renderContent()}
) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/expanded-preview/expanded-preview.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/expanded-preview/expanded-preview.tsx index ccc9bf6279..068ec2f5c3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/expanded-preview/expanded-preview.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/expanded-preview/expanded-preview.tsx @@ -45,20 +45,24 @@ export function ExpandedWorkflowPreview({ {title} - -
- -
+ +
+ {/* Canvas area */} +
+ +
- {selectedBlock && } + {/* Sidebar - attached to right side */} + {selectedBlock && } +
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx index 63a49238ec..c76a1ee7b0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx @@ -1,86 +1,16 @@ 'use client' import { useState } from 'react' -import { Maximize2, X, Zap } from 'lucide-react' +import { ChevronDown, ChevronUp, Maximize2, X } from 'lucide-react' +import { ReactFlowProvider } from 'reactflow' import { Badge, Button } from '@/components/emcn' +import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components' import { getBlock } from '@/blocks' import type { BlockConfig, SubBlockConfig } from '@/blocks/types' import type { BlockState } from '@/stores/workflows/workflow/types' -function ExpandableValue({ title, value }: { title: string; value: unknown }) { - const [isModalOpen, setIsModalOpen] = useState(false) - - const displayValue = formatSubBlockValue(value) - const isLargeValue = displayValue.length > 100 || displayValue.includes('\n') - - return ( - <> -
- {isLargeValue && ( - - )} -
-
-            {displayValue}
-          
-
-
- - {isModalOpen && ( -
-
-
-

{title}

- -
-
-
-                {displayValue}
-              
-
-
-
- )} - - ) -} - -function formatSubBlockValue(value: unknown): string { - if (value === null || value === undefined || value === '') { - return '—' - } - - if (typeof value === 'boolean') { - return value ? 'Yes' : 'No' - } - - if (typeof value === 'object') { - try { - return JSON.stringify(value, null, 2) - } catch { - return String(value) - } - } - - return String(value) -} - /** * Evaluate whether a subblock's condition is met based on current values. - * Returns true if the subblock should be visible. */ function evaluateCondition( condition: SubBlockConfig['condition'], @@ -129,50 +59,144 @@ function evaluateCondition( return isMatch } -function SubBlockRow({ - subBlockConfig, - value, -}: { - subBlockConfig: SubBlockConfig - value: unknown -}) { - const title = subBlockConfig.title || subBlockConfig.id - const hasValue = value !== null && value !== undefined && value !== '' +/** + * Format a value for display + */ +function formatValue(value: unknown): string { + if (value === null || value === undefined || value === '') { + return '—' + } + if (typeof value === 'boolean') { + return value ? 'Yes' : 'No' + } + if (typeof value === 'object') { + try { + return JSON.stringify(value, null, 2) + } catch { + return String(value) + } + } + return String(value) +} + +/** + * Collapsible section for execution data (input/output) + * Starts collapsed, can be expanded inline or opened in modal + */ +function ExecutionDataSection({ title, data }: { title: string; data: unknown }) { + const [isExpanded, setIsExpanded] = useState(false) + const [isModalOpen, setIsModalOpen] = useState(false) + + const displayValue = formatValue(data) return ( -
- {title} - {hasValue ? ( - - ) : ( -
- No value configured + <> +
+ {/* Header - always visible */} + + {isExpanded ? ( + + ) : ( + + )} +
+ + + {/* Content - shown when expanded */} + {isExpanded && ( +
+
+
+                {displayValue}
+              
+
+
+ )} +
+ + {/* Full-screen modal */} + {isModalOpen && ( +
+
+
+

{title}

+ +
+
+
+                {displayValue}
+              
+
+
)} -
+ ) } +/** + * Icon component for rendering block icons + */ +function IconComponent({ icon: Icon, className }: { icon: any; className?: string }) { + if (!Icon) return null + return +} + +interface ExecutionData { + input?: unknown + output?: unknown + status?: string + durationMs?: number +} + interface PinnedSubBlocksProps { block: BlockState onClose: () => void + executionData?: ExecutionData } -export function PinnedSubBlocks({ block, onClose }: PinnedSubBlocksProps) { +/** + * Readonly sidebar panel showing block configuration using SubBlock components. + */ +function PinnedSubBlocksContent({ block, onClose, executionData }: PinnedSubBlocksProps) { const blockConfig = getBlock(block.type) as BlockConfig | undefined + const subBlockValues = block.subBlocks || {} if (!blockConfig) { return ( -
-
+
+
- - +
+ {block.name || 'Unknown Block'}
@@ -182,69 +206,105 @@ export function PinnedSubBlocks({ block, onClose }: PinnedSubBlocksProps) { ) } - const subBlockValues = block.subBlocks || {} - const visibleSubBlocks = blockConfig.subBlocks.filter((subBlock) => { if (subBlock.hidden || subBlock.hideFromPreview) return false - + if (subBlock.mode === 'trigger') return false if (subBlock.condition) { return evaluateCondition(subBlock.condition, subBlockValues) } - return true }) return ( -
- {/* Header */} -
+
+ {/* Header - styled like editor */} +
- + > + +
+ {block.name || blockConfig.name} + {block.enabled === false && ( + + Disabled + + )}
- {/* Block Type Badge */} -
- {blockConfig.name} - {block.enabled === false && ( - - Disabled - - )} -
- - {/* Subblock Values */} -
- {visibleSubBlocks.length > 0 ? ( - visibleSubBlocks.map((subBlock, index) => { - const valueObj = subBlockValues[subBlock.id] - const value = valueObj?.value !== undefined ? valueObj.value : valueObj - - return ( - - ) - }) - ) : ( -
-

- No configurable fields for this block. -

+ {/* Scrollable content */} +
+ {/* Execution Input/Output (if provided) */} + {executionData && + (executionData.input !== undefined || executionData.output !== undefined) ? ( +
+ {executionData.input !== undefined && ( + + )} + {executionData.output !== undefined && ( + + )}
- )} + ) : null} + + {/* Subblock Values - Using SubBlock components in preview mode */} +
+ {visibleSubBlocks.length > 0 ? ( +
+ {visibleSubBlocks.map((subBlockConfig, index) => ( +
+ + {index < visibleSubBlocks.length - 1 && ( +
+
+
+ )} +
+ ))} +
+ ) : ( +
+

+ No configurable fields for this block. +

+
+ )} +
) } + +/** + * Pinned sub-blocks panel wrapped in ReactFlowProvider for hook compatibility. + */ +export function PinnedSubBlocks(props: PinnedSubBlocksProps) { + return ( + + + + ) +} From 92285001dd201bf5e483a2bdbb7b6a4ed0c9c6ac Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 8 Jan 2026 13:58:59 -0800 Subject: [PATCH 06/14] enriched workflow preview --- apps/sim/app/templates/[id]/template.tsx | 2 +- .../templates/components/template-card.tsx | 2 +- .../[workspaceId]/logs/components/index.ts | 2 +- .../execution-snapshot.tsx} | 52 +- .../components/execution-snapshot/index.ts | 1 + .../components/frozen-canvas/index.ts | 1 - .../components/log-details/log-details.tsx | 16 +- .../log-row-context-menu.tsx | 11 + .../app/workspace/[workspaceId]/logs/logs.tsx | 143 ++--- .../templates/components/template-card.tsx | 5 +- .../components/cursors/cursors.tsx | 2 +- .../components/general/general.tsx | 59 +- .../components/template/template.tsx | 2 +- .../components/long-input/long-input.tsx | 5 +- .../components/short-input/short-input.tsx | 3 +- .../components/slider-input/slider-input.tsx | 7 +- .../workflow-block/workflow-block.tsx | 4 +- .../workflow-edge/workflow-edge.tsx | 41 +- .../components/block-details-sidebar.tsx | 581 ++++++++++++++++++ .../components/block.tsx} | 3 - .../components/subflow.tsx} | 2 - .../w/components/preview/index.ts | 2 + .../preview.tsx} | 95 ++- .../credential-sets/credential-sets.tsx | 4 +- .../components/team-members/team-members.tsx | 2 +- .../workflow-item/avatars/avatars.tsx | 2 +- .../invite-modal/components/index.ts | 5 - .../components/invite-modal/index.ts | 6 + .../components/invite-modal/invite-modal.tsx | 4 +- .../workspace-header/workspace-header.tsx | 2 +- .../expanded-preview/expanded-preview.tsx | 70 --- .../components/expanded-preview/index.ts | 1 - .../workflow-preview/components/index.ts | 2 - .../components/pinned-sub-blocks/index.ts | 1 - .../pinned-sub-blocks/pinned-sub-blocks.tsx | 310 ---------- .../[workspaceId]/w/utils/get-user-color.ts | 54 -- .../emcn/components/checkbox/checkbox.tsx | 2 +- .../emcn/components/combobox/combobox.tsx | 8 +- .../components/date-picker/date-picker.tsx | 1 + .../emcn/components/slider/slider.tsx | 5 +- .../emcn/components/switch/switch.tsx | 6 +- .../{presence-colors.ts => colors.ts} | 45 ++ 42 files changed, 945 insertions(+), 626 deletions(-) rename apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/{frozen-canvas/frozen-canvas.tsx => execution-snapshot/execution-snapshot.tsx} (80%) create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/frozen-canvas/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block-details-sidebar.tsx rename apps/sim/app/workspace/[workspaceId]/w/components/{workflow-preview/workflow-preview-block.tsx => preview/components/block.tsx} (96%) rename apps/sim/app/workspace/[workspaceId]/w/components/{workflow-preview/workflow-preview-subflow.tsx => preview/components/subflow.tsx} (96%) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/preview/index.ts rename apps/sim/app/workspace/[workspaceId]/w/components/{workflow-preview/workflow-preview.tsx => preview/preview.tsx} (80%) delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/expanded-preview/expanded-preview.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/expanded-preview/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/utils/get-user-color.ts rename apps/sim/lib/workspaces/{presence-colors.ts => colors.ts} (65%) diff --git a/apps/sim/app/templates/[id]/template.tsx b/apps/sim/app/templates/[id]/template.tsx index 55752a5531..fe26fd1558 100644 --- a/apps/sim/app/templates/[id]/template.tsx +++ b/apps/sim/app/templates/[id]/template.tsx @@ -36,7 +36,7 @@ import { useSession } from '@/lib/auth/auth-client' import { cn } from '@/lib/core/utils/cn' import { getBaseUrl } from '@/lib/core/utils/urls' import type { CredentialRequirement } from '@/lib/workflows/credentials/credential-extractor' -import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview' +import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/preview' import { getBlock } from '@/blocks/registry' import { useStarTemplate, useTemplate } from '@/hooks/queries/templates' diff --git a/apps/sim/app/templates/components/template-card.tsx b/apps/sim/app/templates/components/template-card.tsx index 071d01b6e5..32daadfcf3 100644 --- a/apps/sim/app/templates/components/template-card.tsx +++ b/apps/sim/app/templates/components/template-card.tsx @@ -4,7 +4,7 @@ import { Star, User } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' import { VerifiedBadge } from '@/components/ui/verified-badge' import { cn } from '@/lib/core/utils/cn' -import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview' +import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/preview' import { getBlock } from '@/blocks/registry' import { useStarTemplate } from '@/hooks/queries/templates' import type { WorkflowState } from '@/stores/workflows/workflow/types' diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/index.ts b/apps/sim/app/workspace/[workspaceId]/logs/components/index.ts index cc6f091ed9..1a907cfd89 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/index.ts @@ -1,7 +1,7 @@ export { Dashboard } from './dashboard' export { LogDetails } from './log-details' +export { ExecutionSnapshot } from './log-details/components/execution-snapshot' export { FileCards } from './log-details/components/file-download' -export { FrozenCanvas } from './log-details/components/frozen-canvas' export { TraceSpans } from './log-details/components/trace-spans' export { LogRowContextMenu } from './log-row-context-menu' export { LogsList } from './logs-list' diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/frozen-canvas/frozen-canvas.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx similarity index 80% rename from apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/frozen-canvas/frozen-canvas.tsx rename to apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx index 245e74b048..24c76817dd 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/frozen-canvas/frozen-canvas.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx @@ -6,11 +6,13 @@ import { AlertCircle, Loader2 } from 'lucide-react' import { Modal, ModalBody, ModalContent, ModalHeader } from '@/components/emcn' import { redactApiKeys } from '@/lib/core/security/redaction' import { cn } from '@/lib/core/utils/cn' -import { PinnedSubBlocks } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks' -import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview' +import { + BlockDetailsSidebar, + WorkflowPreview, +} from '@/app/workspace/[workspaceId]/w/components/preview' import type { WorkflowState } from '@/stores/workflows/workflow/types' -const logger = createLogger('FrozenCanvas') +const logger = createLogger('ExecutionSnapshot') interface BlockExecutionData { input: unknown @@ -19,7 +21,7 @@ interface BlockExecutionData { durationMs: number } -interface FrozenCanvasData { +interface ExecutionSnapshotData { executionId: string workflowId: string workflowState: WorkflowState @@ -38,7 +40,7 @@ interface FrozenCanvasData { } } -interface FrozenCanvasProps { +interface ExecutionSnapshotProps { executionId: string traceSpans?: any[] className?: string @@ -49,7 +51,7 @@ interface FrozenCanvasProps { onClose?: () => void } -export function FrozenCanvas({ +export function ExecutionSnapshot({ executionId, traceSpans, className, @@ -58,8 +60,8 @@ export function FrozenCanvas({ isModal = false, isOpen = false, onClose, -}: FrozenCanvasProps) { - const [data, setData] = useState(null) +}: ExecutionSnapshotProps) { + const [data, setData] = useState(null) const [blockExecutions, setBlockExecutions] = useState>({}) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) @@ -109,15 +111,15 @@ export function FrozenCanvas({ const response = await fetch(`/api/logs/execution/${executionId}`) if (!response.ok) { - throw new Error(`Failed to fetch frozen canvas data: ${response.statusText}`) + throw new Error(`Failed to fetch execution snapshot data: ${response.statusText}`) } const result = await response.json() setData(result) - logger.debug(`Loaded frozen canvas data for execution: ${executionId}`) + logger.debug(`Loaded execution snapshot data for execution: ${executionId}`) } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Unknown error' - logger.error('Failed to fetch frozen canvas data:', err) + logger.error('Failed to fetch execution snapshot data:', err) setError(errorMessage) } finally { setLoading(false) @@ -136,7 +138,7 @@ export function FrozenCanvas({ >
- Loading frozen canvas... + Loading execution snapshot...
) @@ -150,7 +152,7 @@ export function FrozenCanvas({ >
- Failed to load frozen canvas: {error} + Failed to load execution snapshot: {error}
) @@ -190,9 +192,14 @@ export function FrozenCanvas({ } return ( -
- {/* Canvas area */} -
+
+
{ - setPinnedBlockId(blockId) + // Toggle: clicking same block closes sidebar, clicking different block switches + setPinnedBlockId((prev) => (prev === blockId ? null : blockId)) }} + cursorStyle='pointer' + executedBlocks={blockExecutions} />
- - {/* Sidebar - attached to right side */} {pinnedBlockId && data.workflowState.blocks[pinnedBlockId] && ( - setPinnedBlockId(null)} executionData={blockExecutions[pinnedBlockId]} + allBlockExecutions={blockExecutions} + workflowBlocks={data.workflowState.blocks} + isExecutionMode /> )}
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/index.ts b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/index.ts new file mode 100644 index 0000000000..a80bf4e337 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/index.ts @@ -0,0 +1 @@ +export { ExecutionSnapshot } from './execution-snapshot' diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/frozen-canvas/index.ts b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/frozen-canvas/index.ts deleted file mode 100644 index 7e9eba695b..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/frozen-canvas/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { FrozenCanvas } from './frozen-canvas' diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx index 611f1b8976..000e1be1ad 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx @@ -5,7 +5,11 @@ import { ChevronUp, X } from 'lucide-react' import { Button, Eye } from '@/components/emcn' import { ScrollArea } from '@/components/ui/scroll-area' import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants' -import { FileCards, FrozenCanvas, TraceSpans } from '@/app/workspace/[workspaceId]/logs/components' +import { + ExecutionSnapshot, + FileCards, + TraceSpans, +} from '@/app/workspace/[workspaceId]/logs/components' import { useLogDetailsResize } from '@/app/workspace/[workspaceId]/logs/hooks' import { formatDate, @@ -49,7 +53,7 @@ export const LogDetails = memo(function LogDetails({ hasNext = false, hasPrev = false, }: LogDetailsProps) { - const [isFrozenCanvasOpen, setIsFrozenCanvasOpen] = useState(false) + const [isExecutionSnapshotOpen, setIsExecutionSnapshotOpen] = useState(false) const scrollAreaRef = useRef(null) const panelWidth = useLogDetailsUIStore((state) => state.panelWidth) const { handleMouseDown } = useLogDetailsResize() @@ -266,7 +270,7 @@ export const LogDetails = memo(function LogDetails({ Workflow State
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx index 9bdfbd1cba..f25a71732f 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx @@ -18,6 +18,7 @@ interface LogRowContextMenuProps { log: WorkflowLog | null onCopyExecutionId: () => void onOpenWorkflow: () => void + onOpenPreview: () => void onToggleWorkflowFilter: () => void onClearAllFilters: () => void isFilteredByThisWorkflow: boolean @@ -36,6 +37,7 @@ export function LogRowContextMenu({ log, onCopyExecutionId, onOpenWorkflow, + onOpenPreview, onToggleWorkflowFilter, onClearAllFilters, isFilteredByThisWorkflow, @@ -78,6 +80,15 @@ export function LogRowContextMenu({ > Open Workflow + { + onOpenPreview() + onClose() + }} + > + Open Preview + {/* Filter actions */} diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index eb2abd939c..063a350615 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -18,6 +18,7 @@ import type { WorkflowLog } from '@/stores/logs/filters/types' import { useUserPermissionsContext } from '../providers/workspace-permissions-provider' import { Dashboard, + ExecutionSnapshot, LogDetails, LogRowContextMenu, LogsList, @@ -59,8 +60,7 @@ export default function Logs() { setWorkspaceId(workspaceId) }, [workspaceId, setWorkspaceId]) - const [selectedLog, setSelectedLog] = useState(null) - const [selectedLogIndex, setSelectedLogIndex] = useState(-1) + const [selectedLogId, setSelectedLogId] = useState(null) const [isSidebarOpen, setIsSidebarOpen] = useState(false) const selectedRowRef = useRef(null) const loaderRef = useRef(null) @@ -90,6 +90,13 @@ export default function Logs() { const [contextMenuLog, setContextMenuLog] = useState(null) const contextMenuRef = useRef(null) + const [isPreviewOpen, setIsPreviewOpen] = useState(false) + const [previewLogId, setPreviewLogId] = useState(null) + + // Single query for the "active" log detail - used by both sidebar and preview + const activeLogId = isPreviewOpen ? previewLogId : selectedLogId + const activeLogQuery = useLogDetail(activeLogId ?? undefined) + const logFilters = useMemo( () => ({ timeRange, @@ -129,19 +136,25 @@ export default function Logs() { refetchInterval: isLive ? 5000 : false, }) - const logDetailQuery = useLogDetail(selectedLog?.id) - - const mergedSelectedLog = useMemo(() => { - if (!selectedLog) return null - if (!logDetailQuery.data) return selectedLog - return { ...selectedLog, ...logDetailQuery.data } - }, [selectedLog, logDetailQuery.data]) - const logs = useMemo(() => { if (!logsQuery.data?.pages) return [] return logsQuery.data.pages.flatMap((page) => page.logs) }, [logsQuery.data?.pages]) + // Derive selected log and index from the logs array + const selectedLogIndex = useMemo( + () => (selectedLogId ? logs.findIndex((l) => l.id === selectedLogId) : -1), + [logs, selectedLogId] + ) + const selectedLogFromList = selectedLogIndex >= 0 ? logs[selectedLogIndex] : null + + // Merge list data with detail query data for sidebar + const selectedLog = useMemo(() => { + if (!selectedLogFromList) return null + if (!activeLogQuery.data || isPreviewOpen) return selectedLogFromList + return { ...selectedLogFromList, ...activeLogQuery.data } + }, [selectedLogFromList, activeLogQuery.data, isPreviewOpen]) + useFolders(workspaceId) useEffect(() => { @@ -150,89 +163,41 @@ export default function Logs() { } }, [debouncedSearchQuery, setStoreSearchQuery]) - const prevSelectedLogRef = useRef(null) - - useEffect(() => { - if (!selectedLog?.id || logs.length === 0) return - - const updatedLog = logs.find((l) => l.id === selectedLog.id) - if (!updatedLog) return - - const prevLog = prevSelectedLogRef.current - - const hasStatusChange = - prevLog?.id === updatedLog.id && - (updatedLog.duration !== prevLog.duration || updatedLog.status !== prevLog.status) - - if (updatedLog !== selectedLog) { - setSelectedLog(updatedLog) - prevSelectedLogRef.current = updatedLog - } - - const newIndex = logs.findIndex((l) => l.id === selectedLog.id) - if (newIndex !== selectedLogIndex) { - setSelectedLogIndex(newIndex) - } - - if (hasStatusChange) { - logDetailQuery.refetch() - } - }, [logs, selectedLog?.id, selectedLogIndex, logDetailQuery]) - + // Refetch detail when live mode is active useEffect(() => { - if (!isLive || !selectedLog?.id) return - - const interval = setInterval(() => { - logDetailQuery.refetch() - }, 5000) - + if (!isLive || !selectedLogId) return + const interval = setInterval(() => activeLogQuery.refetch(), 5000) return () => clearInterval(interval) - }, [isLive, selectedLog?.id, logDetailQuery]) + }, [isLive, selectedLogId, activeLogQuery]) const handleLogClick = useCallback( (log: WorkflowLog) => { - if (selectedLog?.id === log.id && isSidebarOpen) { + if (selectedLogId === log.id && isSidebarOpen) { setIsSidebarOpen(false) - setSelectedLog(null) - setSelectedLogIndex(-1) - prevSelectedLogRef.current = null + setSelectedLogId(null) return } - - setSelectedLog(log) - prevSelectedLogRef.current = log - const index = logs.findIndex((l) => l.id === log.id) - setSelectedLogIndex(index) + setSelectedLogId(log.id) setIsSidebarOpen(true) }, - [selectedLog?.id, isSidebarOpen, logs] + [selectedLogId, isSidebarOpen] ) const handleNavigateNext = useCallback(() => { if (selectedLogIndex < logs.length - 1) { - const nextIndex = selectedLogIndex + 1 - setSelectedLogIndex(nextIndex) - const nextLog = logs[nextIndex] - setSelectedLog(nextLog) - prevSelectedLogRef.current = nextLog + setSelectedLogId(logs[selectedLogIndex + 1].id) } }, [selectedLogIndex, logs]) const handleNavigatePrev = useCallback(() => { if (selectedLogIndex > 0) { - const prevIndex = selectedLogIndex - 1 - setSelectedLogIndex(prevIndex) - const prevLog = logs[prevIndex] - setSelectedLog(prevLog) - prevSelectedLogRef.current = prevLog + setSelectedLogId(logs[selectedLogIndex - 1].id) } }, [selectedLogIndex, logs]) const handleCloseSidebar = useCallback(() => { setIsSidebarOpen(false) - setSelectedLog(null) - setSelectedLogIndex(-1) - prevSelectedLogRef.current = null + setSelectedLogId(null) }, []) const handleLogContextMenu = useCallback((e: React.MouseEvent, log: WorkflowLog) => { @@ -271,6 +236,13 @@ export default function Logs() { setSearchQuery('') }, [resetFilters, setSearchQuery]) + const handleOpenPreview = useCallback(() => { + if (contextMenuLog?.id) { + setPreviewLogId(contextMenuLog.id) + setIsPreviewOpen(true) + } + }, [contextMenuLog]) + const contextMenuWorkflowId = contextMenuLog?.workflow?.id || contextMenuLog?.workflowId const isFilteredByThisWorkflow = Boolean( contextMenuWorkflowId && workflowIds.length === 1 && workflowIds[0] === contextMenuWorkflowId @@ -298,10 +270,10 @@ export default function Logs() { setIsVisuallyRefreshing(true) setTimeout(() => setIsVisuallyRefreshing(false), REFRESH_SPINNER_DURATION_MS) logsQuery.refetch() - if (selectedLog?.id) { - logDetailQuery.refetch() + if (selectedLogId) { + activeLogQuery.refetch() } - }, [logsQuery, logDetailQuery, selectedLog?.id]) + }, [logsQuery, activeLogQuery, selectedLogId]) const handleToggleLive = useCallback(() => { const newIsLive = !isLive @@ -391,11 +363,10 @@ export default function Logs() { if (isSearchOpenRef.current) return if (logs.length === 0) return + // Select first log if none selected and arrow key pressed if (selectedLogIndex === -1 && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) { e.preventDefault() - setSelectedLogIndex(0) - setSelectedLog(logs[0]) - prevSelectedLogRef.current = logs[0] + setSelectedLogId(logs[0].id) return } @@ -409,7 +380,7 @@ export default function Logs() { handleNavigateNext() } - if (e.key === 'Enter' && selectedLog) { + if (e.key === 'Enter' && selectedLogId) { e.preventDefault() setIsSidebarOpen(!isSidebarOpen) } @@ -417,7 +388,7 @@ export default function Logs() { window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) - }, [logs, selectedLogIndex, isSidebarOpen, selectedLog, handleNavigateNext, handleNavigatePrev]) + }, [logs, selectedLogIndex, isSidebarOpen, selectedLogId, handleNavigateNext, handleNavigatePrev]) const isDashboardView = viewMode === 'dashboard' @@ -509,7 +480,7 @@ export default function Logs() { ) : ( + + {isPreviewOpen && activeLogQuery.data?.executionId && ( + { + setIsPreviewOpen(false) + setPreviewLogId(null) + }} + /> + )}
) } diff --git a/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx b/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx index 1b75649229..730c82e54d 100644 --- a/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx +++ b/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx @@ -1,16 +1,13 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { createLogger } from '@sim/logger' import { Star, User } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' import { VerifiedBadge } from '@/components/ui/verified-badge' import { cn } from '@/lib/core/utils/cn' -import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview' +import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/preview' import { getBlock } from '@/blocks/registry' import { useStarTemplate } from '@/hooks/queries/templates' import type { WorkflowState } from '@/stores/workflows/workflow/types' -const logger = createLogger('TemplateCard') - interface TemplateCardProps { id: string title: string diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/cursors/cursors.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/cursors/cursors.tsx index 23de16a25b..1cd5831b69 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/cursors/cursors.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/cursors/cursors.tsx @@ -3,8 +3,8 @@ import { memo, useMemo } from 'react' import { useViewport } from 'reactflow' import { useSession } from '@/lib/auth/auth-client' +import { getUserColor } from '@/lib/workspaces/colors' import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' -import { getUserColor } from '@/app/workspace/[workspaceId]/w/utils/get-user-color' import { useSocket } from '@/app/workspace/providers/socket-provider' interface CursorPoint { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx index 314a84c26d..b569b7bc7a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx @@ -14,8 +14,10 @@ import { } from '@/components/emcn' import { Skeleton } from '@/components/ui' import type { WorkflowDeploymentVersionResponse } from '@/lib/workflows/persistence/utils' -import { ExpandedWorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/components' -import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview' +import { + BlockDetailsSidebar, + WorkflowPreview, +} from '@/app/workspace/[workspaceId]/w/components/preview' import type { WorkflowState } from '@/stores/workflows/workflow/types' import { Versions } from './components' @@ -52,6 +54,7 @@ export function GeneralDeploy({ const [showLoadDialog, setShowLoadDialog] = useState(false) const [showPromoteDialog, setShowPromoteDialog] = useState(false) const [showExpandedPreview, setShowExpandedPreview] = useState(false) + const [expandedSelectedBlockId, setExpandedSelectedBlockId] = useState(null) const [versionToLoad, setVersionToLoad] = useState(null) const [versionToPromote, setVersionToPromote] = useState(null) @@ -319,16 +322,48 @@ export function GeneralDeploy({ {workflowToShow && ( - setShowExpandedPreview(false)} - workflowState={workflowToShow} - title={ - previewMode === 'selected' && selectedVersionInfo - ? selectedVersionInfo.name || `v${selectedVersion}` - : 'Live Workflow' - } - /> + { + if (!open) { + setExpandedSelectedBlockId(null) + } + setShowExpandedPreview(open) + }} + > + + + {previewMode === 'selected' && selectedVersionInfo + ? selectedVersionInfo.name || `v${selectedVersion}` + : 'Live Workflow'} + + +
+
+ { + setExpandedSelectedBlockId( + expandedSelectedBlockId === blockId ? null : blockId + ) + }} + cursorStyle='pointer' + /> +
+ {expandedSelectedBlockId && workflowToShow.blocks?.[expandedSelectedBlockId] && ( + setExpandedSelectedBlockId(null)} + /> + )} +
+
+
+
)} ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/template/template.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/template/template.tsx index f5b15f522c..3bd4301250 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/template/template.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/template/template.tsx @@ -18,7 +18,7 @@ import { Skeleton, TagInput } from '@/components/ui' import { useSession } from '@/lib/auth/auth-client' import { cn } from '@/lib/core/utils/cn' import { captureAndUploadOGImage, OG_IMAGE_HEIGHT, OG_IMAGE_WIDTH } from '@/lib/og' -import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview' +import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/preview' import { useCreateTemplate, useDeleteTemplate, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/long-input/long-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/long-input/long-input.tsx index 541fb538aa..2cd1b039b3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/long-input/long-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/long-input/long-input.tsx @@ -332,7 +332,10 @@ export function LongInput({ />
{formattedText}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slider-input/slider-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slider-input/slider-input.tsx index 8b947a6af3..673669356e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slider-input/slider-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slider-input/slider-input.tsx @@ -1,5 +1,6 @@ import { useEffect } from 'react' import { Slider } from '@/components/emcn/components/slider/slider' +import { cn } from '@/lib/core/utils/cn' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' interface SliderInputProps { @@ -58,15 +59,17 @@ export function SliderInput({ const percentage = ((normalizedValue - min) / (max - min)) * 100 + const isDisabled = isPreview || disabled + return ( -
+
)} - + {!data.isPreview && ( + + )} {shouldShowDefaultHandles && } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx index 14e64108c8..dada1e459b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx @@ -55,7 +55,11 @@ const WorkflowEdgeComponent = ({ const dataSourceHandle = (data as { sourceHandle?: string } | undefined)?.sourceHandle const isErrorEdge = (sourceHandle ?? dataSourceHandle) === 'error' - const edgeRunStatus = lastRunEdges.get(id) + // Check for execution status from both live execution store and preview data + const previewExecutionStatus = ( + data as { executionStatus?: 'success' | 'error' | 'not-executed' } | undefined + )?.executionStatus + const edgeRunStatus = previewExecutionStatus || lastRunEdges.get(id) // Memoize diff status calculation to avoid recomputing on every render const edgeDiffStatus = useMemo((): EdgeDiffStatus => { @@ -87,18 +91,39 @@ const WorkflowEdgeComponent = ({ // Memoize edge style to prevent object recreation const edgeStyle = useMemo(() => { let color = 'var(--workflow-edge)' - if (edgeDiffStatus === 'deleted') color = 'var(--text-error)' - else if (isErrorEdge) color = 'var(--text-error)' - else if (edgeDiffStatus === 'new') color = 'var(--brand-tertiary)' - else if (edgeRunStatus === 'success') color = 'var(--border-success)' - else if (edgeRunStatus === 'error') color = 'var(--text-error)' + let opacity = 1 + + if (edgeDiffStatus === 'deleted') { + color = 'var(--text-error)' + opacity = 0.7 + } else if (isErrorEdge) { + color = 'var(--text-error)' + } else if (edgeDiffStatus === 'new') { + color = 'var(--brand-tertiary)' + } else if (edgeRunStatus === 'success') { + color = 'var(--border-success)' + } else if (edgeRunStatus === 'error') { + color = 'var(--text-error)' + } else if (edgeRunStatus === 'not-executed') { + opacity = 0.3 + } + + if (isSelected) { + opacity = 0.5 + } return { ...(style ?? {}), - strokeWidth: edgeDiffStatus ? 3 : isSelected ? 2.5 : 2, + strokeWidth: edgeDiffStatus + ? 3 + : edgeRunStatus === 'success' || edgeRunStatus === 'error' + ? 2.5 + : isSelected + ? 2.5 + : 2, stroke: color, strokeDasharray: edgeDiffStatus === 'deleted' ? '10,5' : undefined, - opacity: edgeDiffStatus === 'deleted' ? 0.7 : isSelected ? 0.5 : 1, + opacity, } }, [style, edgeDiffStatus, isSelected, isErrorEdge, edgeRunStatus]) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block-details-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block-details-sidebar.tsx new file mode 100644 index 0000000000..efc9f97093 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block-details-sidebar.tsx @@ -0,0 +1,581 @@ +'use client' + +import { useMemo, useState } from 'react' +import clsx from 'clsx' +import { ChevronDown as ChevronDownIcon, X } from 'lucide-react' +import { ReactFlowProvider } from 'reactflow' +import { Badge, Button, ChevronDown, Code } from '@/components/emcn' +import { cn } from '@/lib/core/utils/cn' +import { extractReferencePrefixes } from '@/lib/workflows/sanitization/references' +import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components' +import { getBlock } from '@/blocks' +import type { BlockConfig, SubBlockConfig } from '@/blocks/types' +import type { BlockState } from '@/stores/workflows/workflow/types' + +/** + * Evaluate whether a subblock's condition is met based on current values. + */ +function evaluateCondition( + condition: SubBlockConfig['condition'], + subBlockValues: Record +): boolean { + if (!condition) return true + + const actualCondition = typeof condition === 'function' ? condition() : condition + + const fieldValueObj = subBlockValues[actualCondition.field] + const fieldValue = + fieldValueObj && typeof fieldValueObj === 'object' && 'value' in fieldValueObj + ? (fieldValueObj as { value: unknown }).value + : fieldValueObj + + const conditionValues = Array.isArray(actualCondition.value) + ? actualCondition.value + : [actualCondition.value] + + let isMatch = conditionValues.some((v) => v === fieldValue) + + if (actualCondition.not) { + isMatch = !isMatch + } + + if (actualCondition.and && isMatch) { + const andFieldValueObj = subBlockValues[actualCondition.and.field] + const andFieldValue = + andFieldValueObj && typeof andFieldValueObj === 'object' && 'value' in andFieldValueObj + ? (andFieldValueObj as { value: unknown }).value + : andFieldValueObj + + const andConditionValues = Array.isArray(actualCondition.and.value) + ? actualCondition.and.value + : [actualCondition.and.value] + + let andMatch = andConditionValues.some((v) => v === andFieldValue) + + if (actualCondition.and.not) { + andMatch = !andMatch + } + + isMatch = isMatch && andMatch + } + + return isMatch +} + +/** + * Format a value for display as JSON string + */ +function formatValueAsJson(value: unknown): string { + if (value === null || value === undefined || value === '') { + return '—' + } + if (typeof value === 'object') { + try { + return JSON.stringify(value, null, 2) + } catch { + return String(value) + } + } + return String(value) +} + +interface ResolvedVariable { + tag: string + value: string +} + +/** + * Navigate a path in an object (e.g., ['response', 'data'] in { response: { data: 'value' } }) + */ +function navigatePath(obj: unknown, path: string[]): unknown { + let current: unknown = obj + for (const part of path) { + if (current === null || current === undefined) return undefined + if (typeof current !== 'object') return undefined + current = (current as Record)[part] + } + return current +} + +/** + * Normalize a block name for comparison (lowercase, remove spaces) + */ +function normalizeName(name: string): string { + return name.toLowerCase().replace(/\s+/g, '') +} + +/** + * Extract all variable references from nested subblock values + */ +function extractAllReferencesFromSubBlocks(subBlockValues: Record): string[] { + const refs = new Set() + + const processValue = (value: unknown) => { + if (typeof value === 'string') { + const extracted = extractReferencePrefixes(value) + extracted.forEach((ref) => refs.add(ref.raw)) + } else if (Array.isArray(value)) { + value.forEach(processValue) + } else if (value && typeof value === 'object') { + // Process ALL properties of objects to find nested references + Object.values(value).forEach(processValue) + } + } + + Object.values(subBlockValues).forEach(processValue) + return Array.from(refs) +} + +/** + * Format a value for inline display (single line, truncated) + */ +function formatInlineValue(value: unknown): string { + if (value === null || value === undefined) return 'null' + if (typeof value === 'string') return value + if (typeof value === 'number' || typeof value === 'boolean') return String(value) + if (typeof value === 'object') { + try { + return JSON.stringify(value) + } catch { + return String(value) + } + } + return String(value) +} + +interface ExecutionDataSectionProps { + title: string + data: unknown + isError?: boolean +} + +/** + * Collapsible section for execution data (input/output) + * Uses Code.Viewer for proper syntax highlighting matching the logs UI + */ +function ExecutionDataSection({ title, data, isError = false }: ExecutionDataSectionProps) { + const [isExpanded, setIsExpanded] = useState(false) + + const jsonString = useMemo(() => { + if (!data) return '' + return formatValueAsJson(data) + }, [data]) + + const isEmpty = jsonString === '—' || jsonString === '' + + return ( +
+
setIsExpanded(!isExpanded)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + setIsExpanded(!isExpanded) + } + }} + role='button' + tabIndex={0} + aria-expanded={isExpanded} + aria-label={`${isExpanded ? 'Collapse' : 'Expand'} ${title.toLowerCase()}`} + > + + {title} + + +
+ + {isExpanded && ( + <> + {isEmpty ? ( +
+ No data +
+ ) : ( + + )} + + )} +
+ ) +} + +/** + * Section showing resolved variable references - styled like the connections section in editor + */ +function ResolvedConnectionsSection({ variables }: { variables: ResolvedVariable[] }) { + const [isCollapsed, setIsCollapsed] = useState(false) + + if (variables.length === 0) return null + + return ( +
+ {/* Header with Chevron */} +
setIsCollapsed(!isCollapsed)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + setIsCollapsed(!isCollapsed) + } + }} + role='button' + tabIndex={0} + aria-label={isCollapsed ? 'Expand connections' : 'Collapse connections'} + > + +
Connections
+
+ + {/* Content */} + {!isCollapsed && ( +
+ {variables.map((variable) => ( +
+
+ + {variable.tag} + + + {variable.value} + +
+
+ ))} +
+ )} +
+ ) +} + +/** + * Icon component for rendering block icons + */ +function IconComponent({ icon: Icon, className }: { icon: any; className?: string }) { + if (!Icon) return null + return +} + +interface ExecutionData { + input?: unknown + output?: unknown + status?: string + durationMs?: number +} + +interface BlockDetailsSidebarProps { + block: BlockState + executionData?: ExecutionData + /** All block execution data for resolving variable references */ + allBlockExecutions?: Record + /** All workflow blocks for mapping block names to IDs */ + workflowBlocks?: Record + /** When true, shows "Not Executed" badge if no executionData is provided */ + isExecutionMode?: boolean + /** Optional close handler - if not provided, no close button is shown */ + onClose?: () => void +} + +/** + * Format duration for display + */ +function formatDuration(ms: number): string { + if (ms < 1000) return `${ms}ms` + return `${(ms / 1000).toFixed(2)}s` +} + +/** + * Readonly sidebar panel showing block configuration using SubBlock components. + */ +function BlockDetailsSidebarContent({ + block, + executionData, + allBlockExecutions, + workflowBlocks, + isExecutionMode = false, + onClose, +}: BlockDetailsSidebarProps) { + const blockConfig = getBlock(block.type) as BlockConfig | undefined + const subBlockValues = block.subBlocks || {} + + const blockNameToId = useMemo(() => { + const map = new Map() + if (workflowBlocks) { + for (const [blockId, blockData] of Object.entries(workflowBlocks)) { + if (blockData.name) { + map.set(normalizeName(blockData.name), blockId) + } + } + } + return map + }, [workflowBlocks]) + + const resolveReference = useMemo(() => { + return (reference: string): unknown => { + if (!allBlockExecutions || !workflowBlocks) return undefined + + const inner = reference.slice(1, -1) // Remove < and > + const parts = inner.split('.') + if (parts.length < 1) return undefined + + const [blockName, ...pathParts] = parts + const normalizedBlockName = normalizeName(blockName) + + const blockId = blockNameToId.get(normalizedBlockName) + if (!blockId) return undefined + + const blockExecution = allBlockExecutions[blockId] + if (!blockExecution?.output) return undefined + + if (pathParts.length === 0) { + return blockExecution.output + } + + return navigatePath(blockExecution.output, pathParts) + } + }, [allBlockExecutions, workflowBlocks, blockNameToId]) + + const resolvedVariables = useMemo((): ResolvedVariable[] => { + const allRefs = extractAllReferencesFromSubBlocks(subBlockValues) + if (allRefs.length === 0) return [] + + const results: ResolvedVariable[] = [] + const seen = new Set() + + for (const ref of allRefs) { + if (seen.has(ref)) continue + seen.add(ref) + + // Try to resolve from block executions if available + const resolvedValue = resolveReference(ref) + results.push({ + tag: ref, + value: resolvedValue !== undefined ? formatInlineValue(resolvedValue) : '—', + }) + } + + return results + }, [subBlockValues, resolveReference]) + + if (!blockConfig) { + return ( +
+
+
+ + {block.name || 'Unknown Block'} + +
+
+

Block configuration not found.

+
+
+ ) + } + + const visibleSubBlocks = blockConfig.subBlocks.filter((subBlock) => { + if (subBlock.hidden || subBlock.hideFromPreview) return false + if (subBlock.mode === 'trigger') return false + if (subBlock.condition) { + return evaluateCondition(subBlock.condition, subBlockValues) + } + return true + }) + + const statusVariant = + executionData?.status === 'error' + ? 'red' + : executionData?.status === 'success' + ? 'green' + : 'gray' + + return ( +
+ {/* Header - styled like editor */} +
+
+ +
+ + {block.name || blockConfig.name} + + {block.enabled === false && ( + + Disabled + + )} + {onClose && ( + + )} +
+ + {/* Scrollable content */} +
+ {/* Not Executed Banner - shown when in execution mode but block wasn't executed */} + {isExecutionMode && !executionData && ( +
+
+ + Not Executed + +
+
+ )} + + {/* Execution Input/Output (if provided) */} + {executionData && + (executionData.input !== undefined || executionData.output !== undefined) ? ( +
+ {/* Execution Status & Duration Header */} + {(executionData.status || executionData.durationMs !== undefined) && ( +
+ {executionData.status && ( + + {executionData.status} + + )} + {executionData.durationMs !== undefined && ( + + {formatDuration(executionData.durationMs)} + + )} +
+ )} + + {/* Divider between Status/Duration and Input/Output */} + {(executionData.status || executionData.durationMs !== undefined) && + (executionData.input !== undefined || executionData.output !== undefined) && ( +
+ )} + + {/* Input Section */} + {executionData.input !== undefined && ( + + )} + + {/* Divider between Input and Output */} + {executionData.input !== undefined && executionData.output !== undefined && ( +
+ )} + + {/* Output Section */} + {executionData.output !== undefined && ( + + )} +
+ ) : null} + + {/* Subblock Values - Using SubBlock components in preview mode */} +
+ {/* CSS override to show full opacity and prevent interaction instead of dimmed disabled state */} + + {visibleSubBlocks.length > 0 ? ( +
+ {visibleSubBlocks.map((subBlockConfig, index) => ( +
+ + {index < visibleSubBlocks.length - 1 && ( +
+
+
+ )} +
+ ))} +
+ ) : ( +
+

+ No configurable fields for this block. +

+
+ )} +
+ + {/* Resolved Variables Section - Shows what variable references resolved to */} + {resolvedVariables.length > 0 && ( + + )} +
+
+ ) +} + +/** + * Block details sidebar wrapped in ReactFlowProvider for hook compatibility. + */ +export function BlockDetailsSidebar(props: BlockDetailsSidebarProps) { + return ( + + + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block.tsx similarity index 96% rename from apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview-block.tsx rename to apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block.tsx index d8986c7c47..874f1975a5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block.tsx @@ -29,10 +29,8 @@ function WorkflowPreviewBlockInner({ data }: NodeProps } const IconComponent = blockConfig.icon - // Hide input handle for triggers, starters, or blocks in trigger mode const isStarterOrTrigger = blockConfig.category === 'triggers' || type === 'starter' || isTrigger - // Get visible subblocks from config (no fetching, just config structure) const visibleSubBlocks = useMemo(() => { if (!blockConfig.subBlocks) return [] @@ -48,7 +46,6 @@ function WorkflowPreviewBlockInner({ data }: NodeProps const hasSubBlocks = visibleSubBlocks.length > 0 const showErrorRow = !isStarterOrTrigger - // Handle styles based on orientation const horizontalHandleClass = '!border-none !bg-[var(--surface-7)] !h-5 !w-[7px] !rounded-[2px]' const verticalHandleClass = '!border-none !bg-[var(--surface-7)] !h-[7px] !w-5 !rounded-[2px]' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview-subflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/subflow.tsx similarity index 96% rename from apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview-subflow.tsx rename to apps/sim/app/workspace/[workspaceId]/w/components/preview/components/subflow.tsx index a292d661ea..67befddbda 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview-subflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/subflow.tsx @@ -26,11 +26,9 @@ function WorkflowPreviewSubflowInner({ data }: NodeProps } /** @@ -105,10 +110,9 @@ export function WorkflowPreview({ onNodeClick, lightweight = false, cursorStyle = 'grab', + executedBlocks, }: WorkflowPreviewProps) { - // Use lightweight node types for better performance in template cards const nodeTypes = lightweight ? lightweightNodeTypes : fullNodeTypes - // Check if the workflow state is valid const isValidWorkflowState = workflowState?.blocks && workflowState.edges const blocksStructure = useMemo(() => { @@ -178,9 +182,7 @@ export function WorkflowPreview({ const absolutePosition = calculateAbsolutePosition(block, workflowState.blocks) - // Lightweight mode: create minimal node data for performance if (lightweight) { - // Handle loops and parallels as subflow nodes if (block.type === 'loop' || block.type === 'parallel') { nodeArray.push({ id: blockId, @@ -197,7 +199,6 @@ export function WorkflowPreview({ return } - // Regular blocks nodeArray.push({ id: blockId, type: 'workflowBlock', @@ -214,7 +215,6 @@ export function WorkflowPreview({ return } - // Full mode: create detailed node data for interactive previews if (block.type === 'loop') { nodeArray.push({ id: block.id, @@ -265,11 +265,24 @@ export function WorkflowPreview({ const nodeType = block.type === 'note' ? 'noteBlock' : 'workflowBlock' + // Determine execution status for this block + let executionStatus: ExecutionStatus | undefined + if (executedBlocks) { + const blockExecution = executedBlocks[blockId] + if (blockExecution) { + executionStatus = blockExecution.status === 'error' ? 'error' : 'success' + } else { + // Block exists but wasn't executed + executionStatus = 'not-executed' + } + } + nodeArray.push({ id: blockId, type: nodeType, position: absolutePosition, draggable: false, + className: executionStatus ? `execution-${executionStatus}` : undefined, data: { type: block.type, config: blockConfig, @@ -278,6 +291,7 @@ export function WorkflowPreview({ canEdit: false, isPreview: true, subBlockValues: block.subBlocks ?? {}, + executionStatus, }, }) @@ -326,21 +340,45 @@ export function WorkflowPreview({ workflowState.blocks, isValidWorkflowState, lightweight, + executedBlocks, ]) const edges: Edge[] = useMemo(() => { if (!isValidWorkflowState) return [] - return (workflowState.edges || []).map((edge) => ({ - id: edge.id, - source: edge.source, - target: edge.target, - sourceHandle: edge.sourceHandle, - targetHandle: edge.targetHandle, - })) - }, [edgesStructure, workflowState.edges, isValidWorkflowState]) + return (workflowState.edges || []).map((edge) => { + // Determine if this edge was part of the execution path + let executionStatus: ExecutionStatus | undefined + if (executedBlocks) { + const sourceExecuted = executedBlocks[edge.source] + const targetExecuted = executedBlocks[edge.target] + + // Edge was traversed if both source and target blocks were executed + if (sourceExecuted && targetExecuted) { + // If the target had an error, mark the edge leading to it as error + if (targetExecuted.status === 'error') { + executionStatus = 'error' + } else { + executionStatus = 'success' + } + } else { + // Edge wasn't traversed + executionStatus = 'not-executed' + } + } + + return { + id: edge.id, + source: edge.source, + target: edge.target, + sourceHandle: edge.sourceHandle, + targetHandle: edge.targetHandle, + data: executionStatus ? { executionStatus } : undefined, + className: executionStatus === 'not-executed' ? 'execution-not-executed' : undefined, + } + }) + }, [edgesStructure, workflowState.edges, isValidWorkflowState, executedBlocks]) - // Handle migrated logs that don't have complete workflow state if (!isValidWorkflowState) { return (
- {cursorStyle && ( - - )} + void - workflowState: WorkflowState - title?: string -} - -export function ExpandedWorkflowPreview({ - isOpen, - onClose, - workflowState, - title = 'Workflow Preview', -}: ExpandedWorkflowPreviewProps) { - const [selectedBlockId, setSelectedBlockId] = useState(null) - - const selectedBlock = selectedBlockId ? workflowState.blocks?.[selectedBlockId] : null - - const handleNodeClick = (blockId: string) => { - if (selectedBlockId === blockId) { - setSelectedBlockId(null) - } else { - setSelectedBlockId(blockId) - } - } - - const handleClose = () => { - setSelectedBlockId(null) - onClose() - } - - const handleClosePanel = () => { - setSelectedBlockId(null) - } - - return ( - - - {title} - - -
- {/* Canvas area */} -
- -
- - {/* Sidebar - attached to right side */} - {selectedBlock && } -
-
-
-
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/expanded-preview/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/expanded-preview/index.ts deleted file mode 100644 index 01f8493ae9..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/expanded-preview/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ExpandedWorkflowPreview } from './expanded-preview' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/index.ts deleted file mode 100644 index 685922e47a..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ExpandedWorkflowPreview } from './expanded-preview' -export { PinnedSubBlocks } from './pinned-sub-blocks' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/index.ts deleted file mode 100644 index f09485b5c3..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { PinnedSubBlocks } from './pinned-sub-blocks' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx deleted file mode 100644 index c76a1ee7b0..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/components/pinned-sub-blocks/pinned-sub-blocks.tsx +++ /dev/null @@ -1,310 +0,0 @@ -'use client' - -import { useState } from 'react' -import { ChevronDown, ChevronUp, Maximize2, X } from 'lucide-react' -import { ReactFlowProvider } from 'reactflow' -import { Badge, Button } from '@/components/emcn' -import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components' -import { getBlock } from '@/blocks' -import type { BlockConfig, SubBlockConfig } from '@/blocks/types' -import type { BlockState } from '@/stores/workflows/workflow/types' - -/** - * Evaluate whether a subblock's condition is met based on current values. - */ -function evaluateCondition( - condition: SubBlockConfig['condition'], - subBlockValues: Record -): boolean { - if (!condition) return true - - const actualCondition = typeof condition === 'function' ? condition() : condition - - const fieldValueObj = subBlockValues[actualCondition.field] - const fieldValue = - fieldValueObj && typeof fieldValueObj === 'object' && 'value' in fieldValueObj - ? (fieldValueObj as { value: unknown }).value - : fieldValueObj - - const conditionValues = Array.isArray(actualCondition.value) - ? actualCondition.value - : [actualCondition.value] - - let isMatch = conditionValues.some((v) => v === fieldValue) - - if (actualCondition.not) { - isMatch = !isMatch - } - - if (actualCondition.and && isMatch) { - const andFieldValueObj = subBlockValues[actualCondition.and.field] - const andFieldValue = - andFieldValueObj && typeof andFieldValueObj === 'object' && 'value' in andFieldValueObj - ? (andFieldValueObj as { value: unknown }).value - : andFieldValueObj - - const andConditionValues = Array.isArray(actualCondition.and.value) - ? actualCondition.and.value - : [actualCondition.and.value] - - let andMatch = andConditionValues.some((v) => v === andFieldValue) - - if (actualCondition.and.not) { - andMatch = !andMatch - } - - isMatch = isMatch && andMatch - } - - return isMatch -} - -/** - * Format a value for display - */ -function formatValue(value: unknown): string { - if (value === null || value === undefined || value === '') { - return '—' - } - if (typeof value === 'boolean') { - return value ? 'Yes' : 'No' - } - if (typeof value === 'object') { - try { - return JSON.stringify(value, null, 2) - } catch { - return String(value) - } - } - return String(value) -} - -/** - * Collapsible section for execution data (input/output) - * Starts collapsed, can be expanded inline or opened in modal - */ -function ExecutionDataSection({ title, data }: { title: string; data: unknown }) { - const [isExpanded, setIsExpanded] = useState(false) - const [isModalOpen, setIsModalOpen] = useState(false) - - const displayValue = formatValue(data) - - return ( - <> -
- {/* Header - always visible */} - - {isExpanded ? ( - - ) : ( - - )} -
- - - {/* Content - shown when expanded */} - {isExpanded && ( -
-
-
-                {displayValue}
-              
-
-
- )} -
- - {/* Full-screen modal */} - {isModalOpen && ( -
-
-
-

{title}

- -
-
-
-                {displayValue}
-              
-
-
-
- )} - - ) -} - -/** - * Icon component for rendering block icons - */ -function IconComponent({ icon: Icon, className }: { icon: any; className?: string }) { - if (!Icon) return null - return -} - -interface ExecutionData { - input?: unknown - output?: unknown - status?: string - durationMs?: number -} - -interface PinnedSubBlocksProps { - block: BlockState - onClose: () => void - executionData?: ExecutionData -} - -/** - * Readonly sidebar panel showing block configuration using SubBlock components. - */ -function PinnedSubBlocksContent({ block, onClose, executionData }: PinnedSubBlocksProps) { - const blockConfig = getBlock(block.type) as BlockConfig | undefined - const subBlockValues = block.subBlocks || {} - - if (!blockConfig) { - return ( -
-
-
-
- - {block.name || 'Unknown Block'} - -
- -
-
-

Block configuration not found.

-
-
- ) - } - - const visibleSubBlocks = blockConfig.subBlocks.filter((subBlock) => { - if (subBlock.hidden || subBlock.hideFromPreview) return false - if (subBlock.mode === 'trigger') return false - if (subBlock.condition) { - return evaluateCondition(subBlock.condition, subBlockValues) - } - return true - }) - - return ( -
- {/* Header - styled like editor */} -
-
-
- -
- - {block.name || blockConfig.name} - - {block.enabled === false && ( - - Disabled - - )} -
- -
- - {/* Scrollable content */} -
- {/* Execution Input/Output (if provided) */} - {executionData && - (executionData.input !== undefined || executionData.output !== undefined) ? ( -
- {executionData.input !== undefined && ( - - )} - {executionData.output !== undefined && ( - - )} -
- ) : null} - - {/* Subblock Values - Using SubBlock components in preview mode */} -
- {visibleSubBlocks.length > 0 ? ( -
- {visibleSubBlocks.map((subBlockConfig, index) => ( -
- - {index < visibleSubBlocks.length - 1 && ( -
-
-
- )} -
- ))} -
- ) : ( -
-

- No configurable fields for this block. -

-
- )} -
-
-
- ) -} - -/** - * Pinned sub-blocks panel wrapped in ReactFlowProvider for hook compatibility. - */ -export function PinnedSubBlocks(props: PinnedSubBlocksProps) { - return ( - - - - ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/utils/get-user-color.ts b/apps/sim/app/workspace/[workspaceId]/w/utils/get-user-color.ts deleted file mode 100644 index 95e99c0919..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/utils/get-user-color.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * User color palette matching terminal.tsx RUN_ID_COLORS - * These colors are used consistently across cursors, avatars, and terminal run IDs - */ -export const USER_COLORS = [ - '#4ADE80', // Green - '#F472B6', // Pink - '#60C5FF', // Blue - '#FF8533', // Orange - '#C084FC', // Purple - '#FCD34D', // Yellow -] as const - -/** - * Hash a user ID to generate a consistent numeric index - * - * @param userId - The user ID to hash - * @returns A positive integer - */ -function hashUserId(userId: string): number { - return Math.abs(Array.from(userId).reduce((acc, char) => acc + char.charCodeAt(0), 0)) -} - -/** - * Gets a consistent color for a user based on their ID. - * The same user will always get the same color across cursors, avatars, and terminal. - * - * @param userId - The unique user identifier - * @returns A hex color string - */ -export function getUserColor(userId: string): string { - const hash = hashUserId(userId) - return USER_COLORS[hash % USER_COLORS.length] -} - -/** - * Creates a stable mapping of user IDs to color indices for a list of users. - * Useful when you need to maintain consistent color assignments across renders. - * - * @param userIds - Array of user IDs to map - * @returns Map of user ID to color index - */ -export function createUserColorMap(userIds: string[]): Map { - const colorMap = new Map() - let colorIndex = 0 - - for (const userId of userIds) { - if (!colorMap.has(userId)) { - colorMap.set(userId, colorIndex++) - } - } - - return colorMap -} diff --git a/apps/sim/components/emcn/components/checkbox/checkbox.tsx b/apps/sim/components/emcn/components/checkbox/checkbox.tsx index 6e5b6f64c7..c32ba636c3 100644 --- a/apps/sim/components/emcn/components/checkbox/checkbox.tsx +++ b/apps/sim/components/emcn/components/checkbox/checkbox.tsx @@ -23,7 +23,7 @@ import { cn } from '@/lib/core/utils/cn' * ``` */ const checkboxVariants = cva( - 'peer shrink-0 rounded-sm border border-[var(--border-1)] bg-[var(--surface-4)] ring-offset-background transition-colors hover:border-[var(--border-muted)] hover:bg-[var(--surface-7)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-[var(--text-muted)] data-[state=checked]:bg-[var(--text-muted)] data-[state=checked]:text-white dark:bg-[var(--surface-5)] dark:data-[state=checked]:border-[var(--surface-7)] dark:data-[state=checked]:bg-[var(--surface-7)] dark:data-[state=checked]:text-[var(--text-primary)] dark:hover:border-[var(--surface-7)] dark:hover:bg-[var(--border-1)]', + 'peer shrink-0 rounded-sm border border-[var(--border-1)] bg-[var(--surface-4)] ring-offset-background transition-colors hover:border-[var(--border-muted)] hover:bg-[var(--surface-7)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 data-[state=checked]:border-[var(--text-muted)] data-[state=checked]:bg-[var(--text-muted)] data-[state=checked]:text-white dark:bg-[var(--surface-5)] dark:data-[state=checked]:border-[var(--surface-7)] dark:data-[state=checked]:bg-[var(--surface-7)] dark:data-[state=checked]:text-[var(--text-primary)] dark:hover:border-[var(--surface-7)] dark:hover:bg-[var(--border-1)]', { variants: { size: { diff --git a/apps/sim/components/emcn/components/combobox/combobox.tsx b/apps/sim/components/emcn/components/combobox/combobox.tsx index b72db43c00..19dc56a343 100644 --- a/apps/sim/components/emcn/components/combobox/combobox.tsx +++ b/apps/sim/components/emcn/components/combobox/combobox.tsx @@ -467,7 +467,12 @@ const Combobox = forwardRef( {...inputProps} /> {(overlayContent || SelectedIcon) && ( -
+
{overlayContent ? ( overlayContent ) : ( @@ -505,6 +510,7 @@ const Combobox = forwardRef( className={cn( comboboxVariants({ variant, size }), 'relative cursor-pointer items-center justify-between', + disabled && 'cursor-not-allowed opacity-50', className )} onClick={handleToggle} diff --git a/apps/sim/components/emcn/components/date-picker/date-picker.tsx b/apps/sim/components/emcn/components/date-picker/date-picker.tsx index 3196852bde..1fb2616dad 100644 --- a/apps/sim/components/emcn/components/date-picker/date-picker.tsx +++ b/apps/sim/components/emcn/components/date-picker/date-picker.tsx @@ -844,6 +844,7 @@ const DatePicker = React.forwardRef((props, ref className={cn( datePickerVariants({ variant, size }), 'relative cursor-pointer items-center justify-between', + disabled && 'cursor-not-allowed opacity-50', className )} onClick={handleTriggerClick} diff --git a/apps/sim/components/emcn/components/slider/slider.tsx b/apps/sim/components/emcn/components/slider/slider.tsx index d6ccc54b72..71a84a9120 100644 --- a/apps/sim/components/emcn/components/slider/slider.tsx +++ b/apps/sim/components/emcn/components/slider/slider.tsx @@ -16,12 +16,13 @@ export interface SliderProps extends React.ComponentPropsWithoutRef, SliderProps>( - ({ className, ...props }, ref) => ( + ({ className, disabled, ...props }, ref) => ( , React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( +>(({ className, disabled, ...props }, ref) => ( { + const colorMap = new Map() + let colorIndex = 0 + + for (const userId of userIds) { + if (!colorMap.has(userId)) { + colorMap.set(userId, colorIndex++) + } + } + + return colorMap +} From 8b98dc752dea99c15e69009460f90c1a97a3a603 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 8 Jan 2026 14:13:16 -0800 Subject: [PATCH 07/14] fix connetion in log preview --- .../workflow-edge/workflow-edge.tsx | 2 - .../components/block-details-sidebar.tsx | 204 +++++++++++++----- .../w/components/preview/preview.tsx | 3 - 3 files changed, 152 insertions(+), 57 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx index dada1e459b..783b0b8f7b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx @@ -104,8 +104,6 @@ const WorkflowEdgeComponent = ({ color = 'var(--border-success)' } else if (edgeRunStatus === 'error') { color = 'var(--text-error)' - } else if (edgeRunStatus === 'not-executed') { - opacity = 0.3 } if (isSelected) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block-details-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block-details-sidebar.tsx index efc9f97093..a2764859da 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block-details-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block-details-sidebar.tsx @@ -10,6 +10,8 @@ import { extractReferencePrefixes } from '@/lib/workflows/sanitization/reference import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components' import { getBlock } from '@/blocks' import type { BlockConfig, SubBlockConfig } from '@/blocks/types' +import { normalizeName } from '@/executor/constants' +import { navigatePath } from '@/executor/variables/resolvers/reference' import type { BlockState } from '@/stores/workflows/workflow/types' /** @@ -82,26 +84,16 @@ function formatValueAsJson(value: unknown): string { interface ResolvedVariable { tag: string value: string + blockName: string + blockType: string + fieldPath: string } -/** - * Navigate a path in an object (e.g., ['response', 'data'] in { response: { data: 'value' } }) - */ -function navigatePath(obj: unknown, path: string[]): unknown { - let current: unknown = obj - for (const part of path) { - if (current === null || current === undefined) return undefined - if (typeof current !== 'object') return undefined - current = (current as Record)[part] - } - return current -} - -/** - * Normalize a block name for comparison (lowercase, remove spaces) - */ -function normalizeName(name: string): string { - return name.toLowerCase().replace(/\s+/g, '') +interface ResolvedConnection { + blockId: string + blockName: string + blockType: string + fields: Array<{ path: string; value: string; tag: string }> } /** @@ -117,8 +109,11 @@ function extractAllReferencesFromSubBlocks(subBlockValues: Record>(() => { + // Start with all blocks expanded + return new Set(connections.map((c) => c.blockId)) + }) + + if (connections.length === 0) return null - if (variables.length === 0) return null + const toggleBlock = (blockId: string) => { + setExpandedBlocks((prev) => { + const next = new Set(prev) + if (next.has(blockId)) { + next.delete(blockId) + } else { + next.add(blockId) + } + return next + }) + } return (
@@ -249,25 +260,86 @@ function ResolvedConnectionsSection({ variables }: { variables: ResolvedVariable
Connections
- {/* Content */} + {/* Content - styled like ConnectionBlocks */} {!isCollapsed && (
- {variables.map((variable) => ( -
-
{ + const blockConfig = getBlock(connection.blockType) + const Icon = blockConfig?.icon + const bgColor = blockConfig?.bgColor || '#6B7280' + const isExpanded = expandedBlocks.has(connection.blockId) + const hasFields = connection.fields.length > 0 + + return ( +
+ {/* Block header - styled like ConnectionItem */} +
hasFields && toggleBlock(connection.blockId)} + > +
+ {Icon && ( + + )} +
+ + {connection.blockName} + + {hasFields && ( + + )} +
+ + {/* Fields - styled like FieldItem but showing resolved values */} + {isExpanded && hasFields && ( +
+
+ {connection.fields.map((field) => ( +
+ + {field.path} + + + {field.value} + +
+ ))} +
)} - > - - {variable.tag} - - - {variable.value} -
-
- ))} + ) + })}
)}
@@ -361,27 +433,55 @@ function BlockDetailsSidebarContent({ } }, [allBlockExecutions, workflowBlocks, blockNameToId]) - const resolvedVariables = useMemo((): ResolvedVariable[] => { - const allRefs = extractAllReferencesFromSubBlocks(subBlockValues) - if (allRefs.length === 0) return [] + // Group resolved variables by source block for display + const resolvedConnections = useMemo((): ResolvedConnection[] => { + if (!allBlockExecutions || !workflowBlocks) return [] - const results: ResolvedVariable[] = [] + const allRefs = extractAllReferencesFromSubBlocks(subBlockValues) const seen = new Set() + const blockMap = new Map() for (const ref of allRefs) { if (seen.has(ref)) continue - seen.add(ref) - // Try to resolve from block executions if available + // Parse reference: + const inner = ref.slice(1, -1) + const parts = inner.split('.') + if (parts.length < 1) continue + + const [blockName, ...pathParts] = parts + const normalizedBlockName = normalizeName(blockName) + const blockId = blockNameToId.get(normalizedBlockName) + if (!blockId) continue + + const sourceBlock = workflowBlocks[blockId] + if (!sourceBlock) continue + const resolvedValue = resolveReference(ref) - results.push({ + if (resolvedValue === undefined) continue + + seen.add(ref) + + // Get or create block entry + if (!blockMap.has(blockId)) { + blockMap.set(blockId, { + blockId, + blockName: sourceBlock.name || blockName, + blockType: sourceBlock.type, + fields: [], + }) + } + + const connection = blockMap.get(blockId)! + connection.fields.push({ + path: pathParts.join('.') || 'output', + value: formatInlineValue(resolvedValue), tag: ref, - value: resolvedValue !== undefined ? formatInlineValue(resolvedValue) : '—', }) } - return results - }, [subBlockValues, resolveReference]) + return Array.from(blockMap.values()) + }, [subBlockValues, allBlockExecutions, workflowBlocks, blockNameToId, resolveReference]) if (!blockConfig) { return ( @@ -559,12 +659,12 @@ function BlockDetailsSidebarContent({
)}
- - {/* Resolved Variables Section - Shows what variable references resolved to */} - {resolvedVariables.length > 0 && ( - - )}
+ + {/* Resolved Variables Section - Pinned at bottom, outside scrollable area */} + {resolvedConnections.length > 0 && ( + + )}
) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx index 00c63cf891..d0a2f87679 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx @@ -413,9 +413,6 @@ export function WorkflowPreview({ border-radius: 8px; box-shadow: 0 0 0 4px var(--text-error); } - .preview-mode .react-flow__node.execution-not-executed { - opacity: 0.4; - } `} Date: Thu, 8 Jan 2026 14:35:30 -0800 Subject: [PATCH 08/14] cleanup --- .../execution-snapshot/execution-snapshot.tsx | 17 +++++-- .../components/short-input/short-input.tsx | 2 +- .../components/block-details-sidebar.tsx | 31 ++++++------- .../w/components/preview/preview.tsx | 45 +------------------ 4 files changed, 30 insertions(+), 65 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx index 24c76817dd..7aed4ba6a6 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx @@ -14,6 +14,15 @@ import type { WorkflowState } from '@/stores/workflows/workflow/types' const logger = createLogger('ExecutionSnapshot') +interface TraceSpan { + blockId?: string + input?: unknown + output?: unknown + status?: string + duration?: number + children?: TraceSpan[] +} + interface BlockExecutionData { input: unknown output: unknown @@ -42,7 +51,7 @@ interface ExecutionSnapshotData { interface ExecutionSnapshotProps { executionId: string - traceSpans?: any[] + traceSpans?: TraceSpan[] className?: string height?: string | number width?: string | number @@ -59,7 +68,7 @@ export function ExecutionSnapshot({ width = '100%', isModal = false, isOpen = false, - onClose, + onClose = () => {}, }: ExecutionSnapshotProps) { const [data, setData] = useState(null) const [blockExecutions, setBlockExecutions] = useState>({}) @@ -71,8 +80,8 @@ export function ExecutionSnapshot({ if (traceSpans && Array.isArray(traceSpans)) { const blockExecutionMap: Record = {} - const collectBlockSpans = (spans: any[]): any[] => { - const blockSpans: any[] = [] + const collectBlockSpans = (spans: TraceSpan[]): TraceSpan[] => { + const blockSpans: TraceSpan[] = [] for (const span of spans) { if (span.blockId) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx index 173478d80a..b2027a361d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx @@ -375,7 +375,7 @@ export function ShortInput({ className={cn( 'pointer-events-none absolute inset-0 flex items-center overflow-x-auto bg-transparent px-[8px] py-[6px] font-medium font-sans text-foreground text-sm [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden', showCopyButton ? 'pr-14' : 'pr-3', - disabled && 'opacity-50' + (isPreview || disabled) && 'opacity-50' )} >
{formattedText}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block-details-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block-details-sidebar.tsx index a2764859da..756e68fed2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block-details-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block-details-sidebar.tsx @@ -1,7 +1,6 @@ 'use client' import { useMemo, useState } from 'react' -import clsx from 'clsx' import { ChevronDown as ChevronDownIcon, X } from 'lucide-react' import { ReactFlowProvider } from 'reactflow' import { Badge, Button, ChevronDown, Code } from '@/components/emcn' @@ -9,7 +8,7 @@ import { cn } from '@/lib/core/utils/cn' import { extractReferencePrefixes } from '@/lib/workflows/sanitization/references' import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components' import { getBlock } from '@/blocks' -import type { BlockConfig, SubBlockConfig } from '@/blocks/types' +import type { BlockConfig, BlockIcon, SubBlockConfig } from '@/blocks/types' import { normalizeName } from '@/executor/constants' import { navigatePath } from '@/executor/variables/resolvers/reference' import type { BlockState } from '@/stores/workflows/workflow/types' @@ -81,14 +80,6 @@ function formatValueAsJson(value: unknown): string { return String(value) } -interface ResolvedVariable { - tag: string - value: string - blockName: string - blockType: string - fieldPath: string -} - interface ResolvedConnection { blockId: string blockName: string @@ -255,7 +246,7 @@ function ResolvedConnectionsSection({ connections }: { connections: ResolvedConn aria-label={isCollapsed ? 'Expand connections' : 'Collapse connections'} >
Connections
@@ -274,7 +265,7 @@ function ResolvedConnectionsSection({ connections }: { connections: ResolvedConn
{/* Block header - styled like ConnectionItem */}
{Icon && ( {hasFields && ( } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx index d0a2f87679..bdcd6fe9fc 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx @@ -217,7 +217,7 @@ export function WorkflowPreview({ if (block.type === 'loop') { nodeArray.push({ - id: block.id, + id: blockId, type: 'subflowNode', position: absolutePosition, parentId: block.data?.parentId, @@ -238,7 +238,7 @@ export function WorkflowPreview({ if (block.type === 'parallel') { nodeArray.push({ - id: block.id, + id: blockId, type: 'subflowNode', position: absolutePosition, parentId: block.data?.parentId, @@ -265,14 +265,12 @@ export function WorkflowPreview({ const nodeType = block.type === 'note' ? 'noteBlock' : 'workflowBlock' - // Determine execution status for this block let executionStatus: ExecutionStatus | undefined if (executedBlocks) { const blockExecution = executedBlocks[blockId] if (blockExecution) { executionStatus = blockExecution.status === 'error' ? 'error' : 'success' } else { - // Block exists but wasn't executed executionStatus = 'not-executed' } } @@ -294,41 +292,6 @@ export function WorkflowPreview({ executionStatus, }, }) - - if (block.type === 'loop') { - const childBlocks = Object.entries(workflowState.blocks || {}).filter( - ([_, childBlock]) => childBlock.data?.parentId === blockId - ) - - childBlocks.forEach(([childId, childBlock]) => { - const childConfig = getBlock(childBlock.type) - - if (childConfig) { - const childNodeType = childBlock.type === 'note' ? 'noteBlock' : 'workflowBlock' - - nodeArray.push({ - id: childId, - type: childNodeType, - position: { - x: block.position.x + 50, - y: block.position.y + (childBlock.position?.y || 100), - }, - data: { - type: childBlock.type, - config: childConfig, - name: childBlock.name, - blockState: childBlock, - showSubBlocks, - isChild: true, - parentId: blockId, - canEdit: false, - isPreview: true, - }, - draggable: false, - }) - } - }) - } }) return nodeArray @@ -347,22 +310,18 @@ export function WorkflowPreview({ if (!isValidWorkflowState) return [] return (workflowState.edges || []).map((edge) => { - // Determine if this edge was part of the execution path let executionStatus: ExecutionStatus | undefined if (executedBlocks) { const sourceExecuted = executedBlocks[edge.source] const targetExecuted = executedBlocks[edge.target] - // Edge was traversed if both source and target blocks were executed if (sourceExecuted && targetExecuted) { - // If the target had an error, mark the edge leading to it as error if (targetExecuted.status === 'error') { executionStatus = 'error' } else { executionStatus = 'success' } } else { - // Edge wasn't traversed executionStatus = 'not-executed' } } From 3ea69afd1c73ab846618167da4817612b27f59af Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 8 Jan 2026 18:27:37 -0800 Subject: [PATCH 09/14] ack PR comments --- .../execution-snapshot/execution-snapshot.tsx | 22 +++++++++++++++---- .../components/general/general.tsx | 2 +- .../components/block-details-sidebar.tsx | 4 ++-- .../w/components/preview/preview.tsx | 8 ++++--- .../components/invite-modal/invite-modal.tsx | 5 +++-- 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx index 7aed4ba6a6..659c0338e8 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx @@ -30,10 +30,25 @@ interface BlockExecutionData { durationMs: number } +/** + * Migrated logs have special properties to indicate they came from the old logging system + */ +interface MigratedWorkflowState extends WorkflowState { + _migrated: true + _note?: string +} + +/** + * Type guard to check if a workflow state is from a migrated log + */ +function isMigratedWorkflowState(state: WorkflowState): state is MigratedWorkflowState { + return (state as MigratedWorkflowState)._migrated === true +} + interface ExecutionSnapshotData { executionId: string workflowId: string - workflowState: WorkflowState + workflowState: WorkflowState | MigratedWorkflowState executionMetadata: { trigger: string startedAt: string @@ -178,8 +193,7 @@ export function ExecutionSnapshot({ ) } - const isMigratedLog = (data.workflowState as any)?._migrated === true - if (isMigratedLog) { + if (isMigratedWorkflowState(data.workflowState)) { return (
- Note: {(data.workflowState as any)?._note} + Note: {data.workflowState._note}
) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx index b569b7bc7a..43a423a31c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx @@ -338,7 +338,7 @@ export function GeneralDeploy({ : 'Live Workflow'} -
+
+
@@ -513,7 +513,7 @@ function BlockDetailsSidebarContent({ : 'gray' return ( -
+
{/* Header - styled like editor */}