Skip to content

Commit 5e75863

Browse files
committed
Notes v1
1 parent b6139d6 commit 5e75863

12 files changed

Lines changed: 436 additions & 39 deletions

File tree

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import { memo, useCallback, useEffect, useMemo, useRef } from 'react'
2+
import ReactMarkdown from 'react-markdown'
3+
import remarkGfm from 'remark-gfm'
4+
import { useUpdateNodeInternals, type NodeProps } from 'reactflow'
5+
import { cn } from '@/lib/utils'
6+
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
7+
import { useCurrentWorkflow } from '../../hooks'
8+
import { ActionBar } from '../workflow-block/components'
9+
import { useBlockState } from '../workflow-block/hooks'
10+
import type { WorkflowBlockProps } from '../workflow-block/types'
11+
import { usePanelEditorStore } from '@/stores/panel-new/editor/store'
12+
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
13+
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
14+
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
15+
16+
interface NoteBlockNodeData extends WorkflowBlockProps {}
17+
18+
const NOTE_MIN_WIDTH = 220
19+
const NOTE_MIN_HEIGHT = 140
20+
21+
const MarkdownContent = memo(function MarkdownContent({ content }: { content: string }) {
22+
return (
23+
<ReactMarkdown
24+
remarkPlugins={[remarkGfm]}
25+
components={{
26+
p: ({ children }) => (
27+
<p className='mb-2 text-sm leading-relaxed text-[#E5E5E5] last:mb-0'>{children}</p>
28+
),
29+
strong: ({ children }) => <strong className='font-semibold text-white'>{children}</strong>,
30+
em: ({ children }) => <em className='text-[#B8B8B8]'>{children}</em>,
31+
h1: ({ children }) => (
32+
<h1 className='mt-0 mb-[6px] text-lg font-semibold leading-snug text-[#E5E5E5]'>
33+
{children}
34+
</h1>
35+
),
36+
h2: ({ children }) => (
37+
<h2 className='mt-0 mb-[4px] text-base font-semibold leading-snug text-[#E5E5E5]'>
38+
{children}
39+
</h2>
40+
),
41+
h3: ({ children }) => (
42+
<h3 className='mt-0 mb-[4px] text-sm font-semibold leading-snug text-[#E5E5E5]'>
43+
{children}
44+
</h3>
45+
),
46+
a: ({ href, children }) => (
47+
<a
48+
href={href}
49+
target='_blank'
50+
rel='noopener noreferrer'
51+
className='font-medium text-[#33B4FF] underline-offset-2 hover:underline'
52+
>
53+
{children}
54+
</a>
55+
),
56+
ul: ({ children }) => (
57+
<ul className='mb-2 ml-5 list-disc text-sm leading-relaxed text-[#E5E5E5]'>{children}</ul>
58+
),
59+
ol: ({ children }) => (
60+
<ol className='mb-2 ml-5 list-decimal text-sm leading-relaxed text-[#E5E5E5]'>{children}</ol>
61+
),
62+
li: ({ children }) => <li className='mb-1 last:mb-0'>{children}</li>,
63+
code: ({ inline, children }: any) =>
64+
inline ? (
65+
<code className='rounded bg-[#393939] px-1 py-0.5 text-xs text-[#F59E0B]'>{children}</code>
66+
) : (
67+
<code className='block rounded bg-[#1A1A1A] p-2 text-xs text-[#E5E5E5]'>{children}</code>
68+
),
69+
blockquote: ({ children }) => (
70+
<blockquote className='mb-3 border-l-2 border-[#F59E0B] pl-3 italic text-[#B8B8B8]'>
71+
{children}
72+
</blockquote>
73+
),
74+
}}
75+
>
76+
{content}
77+
</ReactMarkdown>
78+
)
79+
})
80+
81+
export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps<NoteBlockNodeData>) {
82+
const { type, config, name } = data
83+
const containerRef = useRef<HTMLDivElement>(null)
84+
const sizeRef = useRef<{ width: number; height: number } | null>(null)
85+
const updateNodeInternals = useUpdateNodeInternals()
86+
const updateBlockLayoutMetrics = useWorkflowStore((state) => state.updateBlockLayoutMetrics)
87+
88+
const setCurrentBlockId = usePanelEditorStore((state) => state.setCurrentBlockId)
89+
const currentBlockId = usePanelEditorStore((state) => state.currentBlockId)
90+
const isFocused = currentBlockId === id
91+
92+
const currentWorkflow = useCurrentWorkflow()
93+
const { isEnabled, isActive, diffStatus, isDeletedBlock } = useBlockState(id, currentWorkflow, data)
94+
95+
const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
96+
const storedValues = useSubBlockStore(
97+
useCallback(
98+
(state) => {
99+
if (!activeWorkflowId) return undefined
100+
return state.workflowValues[activeWorkflowId]?.[id]
101+
},
102+
[activeWorkflowId, id]
103+
)
104+
)
105+
106+
const noteValues = useMemo(() => {
107+
if (data.isPreview && data.subBlockValues) {
108+
const previewFormatState = data.subBlockValues.format
109+
const previewContentState = data.subBlockValues.content
110+
111+
const extractedPreviewFormat =
112+
typeof previewFormatState === 'object' && previewFormatState !== null
113+
? (previewFormatState as { value?: unknown }).value
114+
: previewFormatState
115+
const extractedPreviewContent =
116+
typeof previewContentState === 'object' && previewContentState !== null
117+
? (previewContentState as { value?: unknown }).value
118+
: previewContentState
119+
120+
return {
121+
format: typeof extractedPreviewFormat === 'string' ? extractedPreviewFormat : 'plain',
122+
content: typeof extractedPreviewContent === 'string' ? extractedPreviewContent : '',
123+
}
124+
}
125+
126+
const format =
127+
storedValues && typeof storedValues.format === 'string'
128+
? storedValues.format
129+
: typeof storedValues?.format === 'object' && storedValues?.format !== null
130+
? (storedValues.format as { value?: unknown }).value
131+
: undefined
132+
const content =
133+
storedValues && typeof storedValues.content === 'string'
134+
? storedValues.content
135+
: typeof storedValues?.content === 'object' && storedValues?.content !== null
136+
? (storedValues.content as { value?: unknown }).value
137+
: undefined
138+
139+
return {
140+
format: typeof format === 'string' ? format : 'plain',
141+
content: typeof content === 'string' ? content : '',
142+
}
143+
}, [data.isPreview, data.subBlockValues, storedValues])
144+
145+
const trimmedContent = noteValues.content?.trim() ?? ''
146+
const isEmpty = trimmedContent.length === 0
147+
const showMarkdown = noteValues.format === 'markdown' && !isEmpty
148+
149+
const userPermissions = useUserPermissionsContext()
150+
151+
useEffect(() => {
152+
const element = containerRef.current
153+
if (!element) return
154+
155+
const observer = new ResizeObserver((entries) => {
156+
const entry = entries[0]
157+
if (!entry) return
158+
159+
const width = Math.max(Math.round(entry.contentRect.width), NOTE_MIN_WIDTH)
160+
const height = Math.max(Math.round(entry.contentRect.height), NOTE_MIN_HEIGHT)
161+
162+
const previous = sizeRef.current
163+
if (!previous || previous.width !== width || previous.height !== height) {
164+
sizeRef.current = { width, height }
165+
updateBlockLayoutMetrics(id, { width, height })
166+
updateNodeInternals(id)
167+
}
168+
})
169+
170+
observer.observe(element)
171+
return () => observer.disconnect()
172+
}, [id, updateBlockLayoutMetrics, updateNodeInternals])
173+
174+
const hasRing =
175+
isActive || isFocused || diffStatus === 'new' || diffStatus === 'edited' || isDeletedBlock
176+
const ringStyles = cn(
177+
hasRing && 'ring-[1.75px]',
178+
isActive && 'ring-[#8C10FF] animate-pulse-ring',
179+
isFocused && 'ring-[#33B4FF]',
180+
diffStatus === 'new' && 'ring-[#22C55F]',
181+
diffStatus === 'edited' && 'ring-[#FF6600]',
182+
isDeletedBlock && 'ring-[#EF4444]'
183+
)
184+
185+
return (
186+
<div className='group relative'>
187+
<div
188+
ref={containerRef}
189+
className={cn(
190+
'relative z-[20] w-[250px] cursor-default select-none rounded-[8px] bg-[#232323]'
191+
)}
192+
onClick={() => setCurrentBlockId(id)}
193+
>
194+
<ActionBar blockId={id} blockType={type} disabled={!userPermissions.canEdit} />
195+
196+
<div
197+
className='note-drag-handle flex cursor-grab items-center justify-between border-[#393939] border-b p-[8px] [&:active]:cursor-grabbing'
198+
onMouseDown={(event) => {
199+
event.stopPropagation()
200+
}}
201+
>
202+
<div className='flex min-w-0 flex-1 items-center gap-[10px]'>
203+
<div
204+
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
205+
style={{ backgroundColor: isEnabled ? config.bgColor : 'gray' }}
206+
>
207+
<config.icon className='h-[16px] w-[16px] text-white' />
208+
</div>
209+
<span
210+
className={cn('font-medium text-[16px]', !isEnabled && 'truncate text-[#808080]')}
211+
title={name}
212+
>
213+
{name}
214+
</span>
215+
</div>
216+
</div>
217+
218+
<div className='relative px-[12px] py-[10px]'>
219+
<div className='relative whitespace-pre-wrap break-words'>
220+
{isEmpty ? (
221+
<p className='text-sm italic text-[#868686]'>Add your note...</p>
222+
) : showMarkdown ? (
223+
<MarkdownContent content={trimmedContent} />
224+
) : (
225+
<p className='text-sm leading-relaxed text-[#E5E5E5]'>{trimmedContent}</p>
226+
)}
227+
</div>
228+
</div>
229+
{hasRing && (
230+
<div
231+
className={cn('pointer-events-none absolute inset-0 z-40 rounded-[8px]', ringStyles)}
232+
/>
233+
)}
234+
</div>
235+
</div>
236+
)
237+
})
238+

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/action-bar/action-bar.tsx

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -146,29 +146,31 @@ export const ActionBar = memo(
146146
</Tooltip.Root>
147147
)}
148148

149-
<Tooltip.Root>
150-
<Tooltip.Trigger asChild>
151-
<Button
152-
variant='ghost'
153-
onClick={() => {
154-
if (!disabled) {
155-
collaborativeToggleBlockHandles(blockId)
156-
}
157-
}}
158-
className='h-[30px] w-[30px] rounded-[8px] bg-[#363636] p-0 text-[#868686] hover:bg-[#33B4FF] hover:text-[#1B1B1B] dark:text-[#868686] dark:hover:bg-[#33B4FF] dark:hover:text-[#1B1B1B]'
159-
disabled={disabled}
160-
>
161-
{horizontalHandles ? (
162-
<ArrowLeftRight className='h-[14px] w-[14px]' />
163-
) : (
164-
<ArrowUpDown className='h-[14px] w-[14px]' />
165-
)}
166-
</Button>
167-
</Tooltip.Trigger>
168-
<Tooltip.Content side='right'>
169-
{getTooltipMessage(horizontalHandles ? 'Vertical Ports' : 'Horizontal Ports')}
170-
</Tooltip.Content>
171-
</Tooltip.Root>
149+
{blockType !== 'note' && (
150+
<Tooltip.Root>
151+
<Tooltip.Trigger asChild>
152+
<Button
153+
variant='ghost'
154+
onClick={() => {
155+
if (!disabled) {
156+
collaborativeToggleBlockHandles(blockId)
157+
}
158+
}}
159+
className='h-[30px] w-[30px] rounded-[8px] bg-[#363636] p-0 text-[#868686] hover:bg-[#33B4FF] hover:text-[#1B1B1B] dark:text-[#868686] dark:hover:bg-[#33B4FF] dark:hover:text-[#1B1B1B]'
160+
disabled={disabled}
161+
>
162+
{horizontalHandles ? (
163+
<ArrowLeftRight className='h-[14px] w-[14px]' />
164+
) : (
165+
<ArrowUpDown className='h-[14px] w-[14px]' />
166+
)}
167+
</Button>
168+
</Tooltip.Trigger>
169+
<Tooltip.Content side='right'>
170+
{getTooltipMessage(horizontalHandles ? 'Vertical Ports' : 'Horizontal Ports')}
171+
</Tooltip.Content>
172+
</Tooltip.Root>
173+
)}
172174

173175
{!isStarterBlock && (
174176
<Tooltip.Root>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { Chat } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/ch
1919
import { UserAvatarStack } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/user-avatar-stack/user-avatar-stack'
2020
import { ErrorBoundary } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/error/index'
2121
import { Panel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/panel-new'
22+
import { NoteBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block'
2223
import { SubflowNodeComponent } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node'
2324
import { Terminal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal'
2425
import { TrainingControls } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/training-controls/training-controls'
@@ -55,6 +56,7 @@ const logger = createLogger('Workflow')
5556
// Define custom node and edge types - memoized outside component to prevent re-creation
5657
const nodeTypes: NodeTypes = {
5758
workflowBlock: WorkflowBlock,
59+
noteBlock: NoteBlock,
5860
subflowNode: SubflowNodeComponent,
5961
}
6062
const edgeTypes: EdgeTypes = {
@@ -1298,13 +1300,32 @@ const WorkflowContent = React.memo(() => {
12981300
const isActive = activeBlockIds.has(block.id)
12991301
const isPending = isDebugging && pendingBlocks.includes(block.id)
13001302

1303+
const measuredWidth =
1304+
typeof block.layout?.measuredWidth === 'number' ? block.layout.measuredWidth : undefined
1305+
const measuredHeight =
1306+
typeof block.layout?.measuredHeight === 'number' ? block.layout.measuredHeight : undefined
1307+
1308+
const nodeType = block.type === 'note' ? 'noteBlock' : 'workflowBlock'
1309+
const dragHandle =
1310+
block.type === 'note' ? '.note-drag-handle' : '.workflow-drag-handle'
1311+
1312+
const defaultWidth =
1313+
block.type === 'note'
1314+
? Math.max(measuredWidth ?? block.data?.width ?? 260, 200)
1315+
: 250
1316+
1317+
const defaultHeight =
1318+
block.type === 'note'
1319+
? Math.max(measuredHeight ?? block.height ?? 160, 120)
1320+
: Math.max(block.height || 100, 100)
1321+
13011322
// Create stable node object - React Flow will handle shallow comparison
13021323
nodeArray.push({
13031324
id: block.id,
1304-
type: 'workflowBlock',
1325+
type: nodeType,
13051326
position,
13061327
parentId: block.data?.parentId,
1307-
dragHandle: '.workflow-drag-handle',
1328+
dragHandle,
13081329
extent: (() => {
13091330
// Clamp children to subflow body (exclude header)
13101331
const parentId = block.data?.parentId as string | undefined
@@ -1332,8 +1353,8 @@ const WorkflowContent = React.memo(() => {
13321353
isPending,
13331354
},
13341355
// Include dynamic dimensions for container resizing calculations (must match rendered size)
1335-
width: 250, // Standard width - matches w-[250px] in workflow-block.tsx
1336-
height: Math.max(block.height || 100, 100), // Use actual height with minimum
1356+
width: defaultWidth,
1357+
height: defaultHeight,
13371358
})
13381359
})
13391360

apps/sim/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import 'reactflow/dist/style.css'
1515

1616
import { createLogger } from '@/lib/logs/console/logger'
1717
import { cn } from '@/lib/utils'
18+
import { NoteBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block'
1819
import { SubflowNodeComponent } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node'
1920
import { WorkflowBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block'
2021
import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge'
@@ -39,6 +40,7 @@ interface WorkflowPreviewProps {
3940
// Define node types - the components now handle preview mode internally
4041
const nodeTypes: NodeTypes = {
4142
workflowBlock: WorkflowBlock,
43+
noteBlock: NoteBlock,
4244
subflowNode: SubflowNodeComponent,
4345
}
4446

@@ -179,9 +181,11 @@ export function WorkflowPreview({
179181

180182
const subBlocksClone = block.subBlocks ? cloneDeep(block.subBlocks) : {}
181183

184+
const nodeType = block.type === 'note' ? 'noteBlock' : 'workflowBlock'
185+
182186
nodeArray.push({
183187
id: blockId,
184-
type: 'workflowBlock',
188+
type: nodeType,
185189
position: absolutePosition,
186190
draggable: false,
187191
data: {
@@ -204,9 +208,11 @@ export function WorkflowPreview({
204208
const childConfig = getBlock(childBlock.type)
205209

206210
if (childConfig) {
211+
const childNodeType = childBlock.type === 'note' ? 'noteBlock' : 'workflowBlock'
212+
207213
nodeArray.push({
208214
id: childId,
209-
type: 'workflowBlock',
215+
type: childNodeType,
210216
position: {
211217
x: block.position.x + 50,
212218
y: block.position.y + (childBlock.position?.y || 100),

0 commit comments

Comments
 (0)