22
33import { type KeyboardEvent , useCallback , useEffect , useMemo , useRef , useState } from 'react'
44import { AlertCircle , ArrowDownToLine , ArrowUp , MoreVertical , Paperclip , X } from 'lucide-react'
5- import { useParams } from 'next/navigation'
65import {
76 Badge ,
87 Button ,
@@ -14,24 +13,27 @@ import {
1413 PopoverTrigger ,
1514 Trash ,
1615} from '@/components/emcn'
16+ import { useSession } from '@/lib/auth-client'
1717import { createLogger } from '@/lib/logs/console/logger'
1818import {
1919 extractBlockIdFromOutputId ,
2020 extractPathFromOutputId ,
2121 parseOutputContentSafely ,
2222} from '@/lib/response-format'
23- // import { START_BLOCK_RESERVED_FIELDS } from '@/lib/workflows/types'
24- // import { StartBlockPath, TriggerUtils } from '@/lib/workflows/triggers'
2523import { cn } from '@/lib/utils'
24+ import { normalizeInputFormatValue } from '@/lib/workflows/input-format-utils'
25+ import { StartBlockPath , TriggerUtils } from '@/lib/workflows/triggers'
26+ import { START_BLOCK_RESERVED_FIELDS } from '@/lib/workflows/types'
2627import { useScrollManagement } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
2728import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution'
2829import type { BlockLog , ExecutionResult } from '@/executor/types'
2930import { getChatPosition , useChatStore } from '@/stores/chat/store'
3031import { useExecutionStore } from '@/stores/execution/store'
32+ import { useOperationQueue } from '@/stores/operation-queue/store'
3133import { useTerminalConsoleStore } from '@/stores/terminal'
3234import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
33- // import { useSubBlockStore } from '@/stores/workflows/subblock/store'
34- // import { useWorkflowStore } from '@/stores/workflows/workflow/store'
35+ import { useSubBlockStore } from '@/stores/workflows/subblock/store'
36+ import { useWorkflowStore } from '@/stores/workflows/workflow/store'
3537import { ChatMessage , OutputSelect } from './components'
3638import { useChatBoundarySync , useChatDrag , useChatFileUpload , useChatResize } from './hooks'
3739
@@ -128,38 +130,13 @@ const formatOutputContent = (output: any): string => {
128130 return ''
129131}
130132
131- // interface StartInputFormatField {
132- // id?: string
133- // name?: string
134- // type?: string
135- // value?: unknown
136- // collapsed?: boolean
137- // }
138-
139- // /**
140- // * Normalizes an input format value into a list of valid fields.
141- // *
142- // * @param value - Raw input format value from subblock state.
143- // * @returns Array of fields with non-empty names.
144- // */
145- // const normalizeStartInputFormat = (value: unknown): StartInputFormatField[] => {
146- // if (!Array.isArray(value)) {
147- // return []
148- // }
149-
150- // return value.filter((field): field is StartInputFormatField => {
151- // if (!field || typeof field !== 'object') {
152- // return false
153- // }
154-
155- // const name = (field as StartInputFormatField).name
156- // return typeof name === 'string' && name.trim() !== ''
157- // })
158- // }
159-
160- const CHAT_START_EXAMPLE_JSON = `"input": string,
161- "conversationId": string,
162- "files": array<File>`
133+ interface StartInputFormatField {
134+ id ?: string
135+ name ?: string
136+ type ?: string
137+ value ?: unknown
138+ collapsed ?: boolean
139+ }
163140
164141/**
165142 * Floating chat modal component
@@ -174,12 +151,10 @@ const CHAT_START_EXAMPLE_JSON = `"input": string,
174151 * position across sessions using the floating chat store.
175152 */
176153export function Chat ( ) {
177- const params = useParams ( )
178- const workspaceId = params . workspaceId as string
179154 const { activeWorkflowId } = useWorkflowRegistry ( )
180- // const blocks = useWorkflowStore((state) => state.blocks)
181- // const triggerWorkflowUpdate = useWorkflowStore((state) => state.triggerUpdate)
182- // const setSubBlockValue = useSubBlockStore((state) => state.setValue)
155+ const blocks = useWorkflowStore ( ( state ) => state . blocks )
156+ const triggerWorkflowUpdate = useWorkflowStore ( ( state ) => state . triggerUpdate )
157+ const setSubBlockValue = useSubBlockStore ( ( state ) => state . setValue )
183158
184159 // Chat state (UI and messages from unified store)
185160 const {
@@ -204,6 +179,8 @@ export function Chat() {
204179 const { entries } = useTerminalConsoleStore ( )
205180 const { isExecuting } = useExecutionStore ( )
206181 const { handleRunWorkflow } = useWorkflowExecution ( )
182+ const { data : session } = useSession ( )
183+ const { addToQueue } = useOperationQueue ( )
207184
208185 // Local state
209186 const [ chatMessage , setChatMessage ] = useState ( '' )
@@ -233,65 +210,67 @@ export function Chat() {
233210 /**
234211 * Resolves the unified start block for chat execution, if available.
235212 */
236- // const startBlockCandidate = useMemo(() => {
237- // if (!activeWorkflowId) {
238- // return null
239- // }
240-
241- // if (!blocks || Object.keys(blocks).length === 0) {
242- // return null
243- // }
244-
245- // const candidate = TriggerUtils.findStartBlock(blocks, 'chat')
246- // if (!candidate || candidate.path !== StartBlockPath.UNIFIED) {
247- // return null
248- // }
249-
250- // return candidate
251- // }, [activeWorkflowId, blocks])
252-
253- // const startBlockId = startBlockCandidate?.blockId ?? null
254-
255- // /**
256- // * Reads the current input format for the unified start block from the subblock store,
257- // * falling back to the workflow store if no explicit value is stored yet.
258- // */
259- // const startBlockInputFormat = useSubBlockStore((state) => {
260- // if (!activeWorkflowId || !startBlockId) {
261- // return null
262- // }
263-
264- // const workflowValues = state.workflowValues[activeWorkflowId]
265- // const fromStore = workflowValues?.[startBlockId]?.inputFormat
266- // if (fromStore !== undefined && fromStore !== null) {
267- // return fromStore
268- // }
269-
270- // const startBlock = blocks[startBlockId]
271- // return startBlock?.subBlocks?.inputFormat?.value ?? null
272- // })
273-
274- // /**
275- // * Determines which reserved start inputs are missing from the input format.
276- // */
277- // const missingStartReservedFields = useMemo(() => {
278- // if (!startBlockId) {
279- // return START_BLOCK_RESERVED_FIELDS
280- // }
281-
282- // const normalizedFields = normalizeStartInputFormat(startBlockInputFormat)
283- // const existingNames = new Set(
284- // normalizedFields
285- // .map((field) => field.name)
286- // .filter((name): name is string => typeof name === 'string' && name.trim() !== '')
287- // .map((name) => name.trim())
288- // )
289-
290- // return START_BLOCK_RESERVED_FIELDS.filter((fieldName) => !existingNames.has(fieldName))
291- // }, [startBlockId, startBlockInputFormat])
292-
293- // const shouldShowConfigureStartInputsButton =
294- // Boolean(startBlockId) && missingStartReservedFields.length > 0
213+ const startBlockCandidate = useMemo ( ( ) => {
214+ if ( ! activeWorkflowId ) {
215+ return null
216+ }
217+
218+ if ( ! blocks || Object . keys ( blocks ) . length === 0 ) {
219+ return null
220+ }
221+
222+ const candidate = TriggerUtils . findStartBlock ( blocks , 'chat' )
223+ if ( ! candidate || candidate . path !== StartBlockPath . UNIFIED ) {
224+ return null
225+ }
226+
227+ return candidate
228+ } , [ activeWorkflowId , blocks ] )
229+
230+ const startBlockId = startBlockCandidate ?. blockId ?? null
231+
232+ /**
233+ * Reads the current input format for the unified start block from the subblock store,
234+ * falling back to the workflow store if no explicit value is stored yet.
235+ */
236+ const startBlockInputFormat = useSubBlockStore ( ( state ) => {
237+ if ( ! activeWorkflowId || ! startBlockId ) {
238+ return null
239+ }
240+
241+ const workflowValues = state . workflowValues [ activeWorkflowId ]
242+ const fromStore = workflowValues ?. [ startBlockId ] ?. inputFormat
243+ if ( fromStore !== undefined && fromStore !== null ) {
244+ return fromStore
245+ }
246+
247+ const startBlock = blocks [ startBlockId ]
248+ return startBlock ?. subBlocks ?. inputFormat ?. value ?? null
249+ } )
250+
251+ /**
252+ * Determines which reserved start inputs are missing from the input format.
253+ */
254+ const missingStartReservedFields = useMemo ( ( ) => {
255+ if ( ! startBlockId ) {
256+ return START_BLOCK_RESERVED_FIELDS
257+ }
258+
259+ const normalizedFields = normalizeInputFormatValue ( startBlockInputFormat )
260+ const existingNames = new Set (
261+ normalizedFields
262+ . map ( ( field ) => field . name )
263+ . filter ( ( name ) : name is string => typeof name === 'string' && name . trim ( ) !== '' )
264+ . map ( ( name ) => name . trim ( ) . toLowerCase ( ) )
265+ )
266+
267+ return START_BLOCK_RESERVED_FIELDS . filter (
268+ ( fieldName ) => ! existingNames . has ( fieldName . toLowerCase ( ) )
269+ )
270+ } , [ startBlockId , startBlockInputFormat ] )
271+
272+ const shouldShowConfigureStartInputsButton =
273+ Boolean ( startBlockId ) && missingStartReservedFields . length > 0
295274
296275 // Get actual position (default if not set)
297276 const actualPosition = useMemo (
@@ -667,61 +646,67 @@ export function Chat() {
667646 setIsChatOpen ( false )
668647 } , [ setIsChatOpen ] )
669648
670- // /**
671- // * Adds any missing reserved inputs (input, conversationId, files) to the unified start block.
672- // */
673- // const handleConfigureStartInputs = useCallback(() => {
674- // if (!activeWorkflowId || !startBlockId) {
675- // logger.warn('Cannot configure start inputs: missing active workflow ID or start block ID')
676- // return
677- // }
678-
679- // try {
680- // const existingFields = Array.isArray(startBlockInputFormat)
681- // ? [...startBlockInputFormat]
682- // : []
683-
684- // const normalizedExisting = normalizeStartInputFormat(existingFields)
685- // const existingNames = new Set(
686- // normalizedExisting
687- // .map((field) => field.name)
688- // .filter((name): name is string => typeof name === 'string' && name.trim() !== '')
689- // .map((name) => name.trim())
690- // )
691-
692- // const updatedFields: StartInputFormatField[] = [...existingFields]
693-
694- // missingStartReservedFields.forEach((fieldName) => {
695- // if (existingNames.has(fieldName)) {
696- // return
697- // }
698-
699- // const defaultType = fieldName === 'files' ? 'files' : 'string'
700-
701- // updatedFields.push({
702- // id: crypto.randomUUID(),
703- // name: fieldName,
704- // type: defaultType,
705- // value: '',
706- // collapsed: false,
707- // })
708- // })
709-
710- // setSubBlockValue(startBlockId, 'inputFormat', updatedFields)
711- // triggerWorkflowUpdate()
712- // } catch (error) {
713- // logger.error('Failed to configure start block reserved inputs', error)
714- // }
715- // }, [
716- // activeWorkflowId,
717- // missingStartReservedFields,
718- // setSubBlockValue,
719- // startBlockId,
720- // startBlockInputFormat,
721- // triggerWorkflowUpdate,
722- // ])
723-
724- // Don't render if not open
649+ /**
650+ * Adds any missing reserved inputs (input, conversationId, files) to the unified start block.
651+ */
652+ const handleConfigureStartInputs = useCallback ( ( ) => {
653+ if ( ! activeWorkflowId || ! startBlockId ) {
654+ logger . warn ( 'Cannot configure start inputs: missing active workflow ID or start block ID' )
655+ return
656+ }
657+
658+ try {
659+ const normalizedExisting = normalizeInputFormatValue ( startBlockInputFormat )
660+
661+ const newReservedFields : StartInputFormatField [ ] = missingStartReservedFields . map (
662+ ( fieldName ) => {
663+ const defaultType = fieldName === 'files' ? 'files' : 'string'
664+
665+ return {
666+ id : crypto . randomUUID ( ) ,
667+ name : fieldName ,
668+ type : defaultType ,
669+ value : '' ,
670+ collapsed : false ,
671+ }
672+ }
673+ )
674+
675+ const updatedFields : StartInputFormatField [ ] = [ ...newReservedFields , ...normalizedExisting ]
676+
677+ setSubBlockValue ( startBlockId , 'inputFormat' , updatedFields )
678+
679+ const userId = session ?. user ?. id || 'unknown'
680+ addToQueue ( {
681+ id : crypto . randomUUID ( ) ,
682+ operation : {
683+ operation : 'subblock-update' ,
684+ target : 'subblock' ,
685+ payload : {
686+ blockId : startBlockId ,
687+ subblockId : 'inputFormat' ,
688+ value : updatedFields ,
689+ } ,
690+ } ,
691+ workflowId : activeWorkflowId ,
692+ userId,
693+ } )
694+
695+ triggerWorkflowUpdate ( )
696+ } catch ( error ) {
697+ logger . error ( 'Failed to configure start block reserved inputs' , error )
698+ }
699+ } , [
700+ activeWorkflowId ,
701+ missingStartReservedFields ,
702+ setSubBlockValue ,
703+ startBlockId ,
704+ startBlockInputFormat ,
705+ triggerWorkflowUpdate ,
706+ session ,
707+ addToQueue ,
708+ ] )
709+
725710 if ( ! isChatOpen ) return null
726711
727712 return (
@@ -752,10 +737,10 @@ export function Chat() {
752737 className = 'ml-auto flex min-w-0 flex-shrink items-center gap-[6px]'
753738 onMouseDown = { ( e ) => e . stopPropagation ( ) }
754739 >
755- { /* { shouldShowConfigureStartInputsButton && (
740+ { shouldShowConfigureStartInputsButton && (
756741 < Badge
757742 variant = 'outline'
758- className='cursor-pointer rounded-[6px] flex-none whitespace-nowrap '
743+ className = 'flex-none cursor-pointer whitespace-nowrap rounded-[6px]'
759744 title = 'Add chat inputs to Start block'
760745 onMouseDown = { ( e ) => {
761746 e . stopPropagation ( )
@@ -764,7 +749,7 @@ export function Chat() {
764749 >
765750 < span className = 'whitespace-nowrap text-[12px]' > Add inputs</ span >
766751 </ Badge >
767- )} */ }
752+ ) }
768753
769754 < OutputSelect
770755 workflowId = { activeWorkflowId }
0 commit comments