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,20 +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'
2323import { 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'
2427import { useScrollManagement } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
2528import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution'
2629import type { BlockLog , ExecutionResult } from '@/executor/types'
2730import { getChatPosition , useChatStore } from '@/stores/chat/store'
2831import { useExecutionStore } from '@/stores/execution/store'
32+ import { useOperationQueue } from '@/stores/operation-queue/store'
2933import { useTerminalConsoleStore } from '@/stores/terminal'
3034import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
35+ import { useSubBlockStore } from '@/stores/workflows/subblock/store'
36+ import { useWorkflowStore } from '@/stores/workflows/workflow/store'
3137import { ChatMessage , OutputSelect } from './components'
3238import { useChatBoundarySync , useChatDrag , useChatFileUpload , useChatResize } from './hooks'
3339
@@ -124,6 +130,14 @@ const formatOutputContent = (output: any): string => {
124130 return ''
125131}
126132
133+ interface StartInputFormatField {
134+ id ?: string
135+ name ?: string
136+ type ?: string
137+ value ?: unknown
138+ collapsed ?: boolean
139+ }
140+
127141/**
128142 * Floating chat modal component
129143 *
@@ -137,9 +151,10 @@ const formatOutputContent = (output: any): string => {
137151 * position across sessions using the floating chat store.
138152 */
139153export function Chat ( ) {
140- const params = useParams ( )
141- const workspaceId = params . workspaceId as string
142154 const { activeWorkflowId } = useWorkflowRegistry ( )
155+ const blocks = useWorkflowStore ( ( state ) => state . blocks )
156+ const triggerWorkflowUpdate = useWorkflowStore ( ( state ) => state . triggerUpdate )
157+ const setSubBlockValue = useSubBlockStore ( ( state ) => state . setValue )
143158
144159 // Chat state (UI and messages from unified store)
145160 const {
@@ -164,6 +179,8 @@ export function Chat() {
164179 const { entries } = useTerminalConsoleStore ( )
165180 const { isExecuting } = useExecutionStore ( )
166181 const { handleRunWorkflow } = useWorkflowExecution ( )
182+ const { data : session } = useSession ( )
183+ const { addToQueue } = useOperationQueue ( )
167184
168185 // Local state
169186 const [ chatMessage , setChatMessage ] = useState ( '' )
@@ -190,6 +207,71 @@ export function Chat() {
190207 handleDrop,
191208 } = useChatFileUpload ( )
192209
210+ /**
211+ * Resolves the unified start block for chat execution, if available.
212+ */
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
274+
193275 // Get actual position (default if not set)
194276 const actualPosition = useMemo (
195277 ( ) => getChatPosition ( chatPosition , chatWidth , chatHeight ) ,
@@ -564,7 +646,67 @@ export function Chat() {
564646 setIsChatOpen ( false )
565647 } , [ setIsChatOpen ] )
566648
567- // 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+
568710 if ( ! isChatOpen ) return null
569711
570712 return (
@@ -583,17 +725,32 @@ export function Chat() {
583725 >
584726 { /* Header with drag handle */ }
585727 < div
586- className = 'flex h-[32px] flex-shrink-0 cursor-grab items-center justify-between bg-[var(--surface-1)] p-0 active:cursor-grabbing'
728+ className = 'flex h-[32px] flex-shrink-0 cursor-grab items-center justify-between gap-[10px] bg-[var(--surface-1)] p-0 active:cursor-grabbing'
587729 onMouseDown = { handleMouseDown }
588730 >
589- < div className = 'flex items-center' >
590- < span className = 'flex-shrink-0 font-medium text-[14px] text-[var(--text-primary)]' >
591- Chat
592- </ span >
593- </ div >
731+ < span className = 'flex-shrink-0 pr-[2px] font-medium text-[14px] text-[var(--text-primary)]' >
732+ Chat
733+ </ span >
734+
735+ { /* Start inputs button and output selector - with max-width to prevent overflow */ }
736+ < div
737+ className = 'ml-auto flex min-w-0 flex-shrink items-center gap-[6px]'
738+ onMouseDown = { ( e ) => e . stopPropagation ( ) }
739+ >
740+ { shouldShowConfigureStartInputsButton && (
741+ < Badge
742+ variant = 'outline'
743+ className = 'flex-none cursor-pointer whitespace-nowrap rounded-[6px]'
744+ title = 'Add chat inputs to Start block'
745+ onMouseDown = { ( e ) => {
746+ e . stopPropagation ( )
747+ handleConfigureStartInputs ( )
748+ } }
749+ >
750+ < span className = 'whitespace-nowrap text-[12px]' > Add inputs</ span >
751+ </ Badge >
752+ ) }
594753
595- { /* Output selector - centered with mx-auto */ }
596- < div className = 'mr-[6px] ml-auto' onMouseDown = { ( e ) => e . stopPropagation ( ) } >
597754 < OutputSelect
598755 workflowId = { activeWorkflowId }
599756 selectedOutputs = { selectedOutputs }
@@ -605,7 +762,7 @@ export function Chat() {
605762 />
606763 </ div >
607764
608- < div className = 'flex items-center gap-[8px]' >
765+ < div className = 'flex flex-shrink-0 items-center gap-[8px]' >
609766 { /* More menu with actions */ }
610767 < Popover variant = 'default' >
611768 < PopoverTrigger asChild >
@@ -628,22 +785,22 @@ export function Chat() {
628785 < PopoverItem
629786 onClick = { ( e ) => {
630787 e . stopPropagation ( )
631- if ( activeWorkflowId ) clearChat ( activeWorkflowId )
788+ if ( activeWorkflowId ) exportChatCSV ( activeWorkflowId )
632789 } }
633- disabled = { messages . length === 0 }
790+ disabled = { workflowMessages . length === 0 }
634791 >
635- < Trash className = 'h-[14px ] w-[14px ]' />
636- < span > Clear </ span >
792+ < ArrowDownToLine className = 'h-[13px ] w-[13px ]' />
793+ < span > Download </ span >
637794 </ PopoverItem >
638795 < PopoverItem
639796 onClick = { ( e ) => {
640797 e . stopPropagation ( )
641- if ( activeWorkflowId ) exportChatCSV ( activeWorkflowId )
798+ if ( activeWorkflowId ) clearChat ( activeWorkflowId )
642799 } }
643- disabled = { messages . length === 0 }
800+ disabled = { workflowMessages . length === 0 }
644801 >
645- < ArrowDownToLine className = 'h-[14px ] w-[14px ]' />
646- < span > Download </ span >
802+ < Trash className = 'h-[13px ] w-[13px ]' />
803+ < span > Clear </ span >
647804 </ PopoverItem >
648805 </ PopoverScrollArea >
649806 </ PopoverContent >
@@ -662,7 +819,7 @@ export function Chat() {
662819 < div className = 'flex-1 overflow-hidden' >
663820 { workflowMessages . length === 0 ? (
664821 < div className = 'flex h-full items-center justify-center text-[#8D8D8D] text-[13px]' >
665- No messages yet
822+ Workflow input: { '<start.input>' }
666823 </ div >
667824 ) : (
668825 < div ref = { scrollAreaRef } className = 'h-full overflow-y-auto overflow-x-hidden' >
0 commit comments