Skip to content

Commit f5c211e

Browse files
committed
feat(chat): add inputs button
1 parent 4db16b4 commit f5c211e

File tree

6 files changed

+248
-53
lines changed

6 files changed

+248
-53
lines changed

apps/sim/app/globals.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,8 @@
281281
--c-F4F4F4: #f4f4f4;
282282
--c-F5F5F5: #f5f5f5;
283283

284+
--c-CFCFCF: #cfcfcf;
285+
284286
/* Blues and cyans */
285287
--c-00B0B0: #00b0b0;
286288
--c-264F78: #264f78;

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

Lines changed: 190 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
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'
2325
import { cn } from '@/lib/utils'
2426
import { useScrollManagement } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
2527
import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution'
@@ -28,6 +30,8 @@ import { getChatPosition, useChatStore } from '@/stores/chat/store'
2830
import { useExecutionStore } from '@/stores/execution/store'
2931
import { useTerminalConsoleStore } from '@/stores/terminal'
3032
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
33+
// import { useSubBlockStore } from '@/stores/workflows/subblock/store'
34+
// import { useWorkflowStore } from '@/stores/workflows/workflow/store'
3135
import { ChatMessage, OutputSelect } from './components'
3236
import { useChatBoundarySync, useChatDrag, useChatFileUpload, useChatResize } from './hooks'
3337

@@ -124,6 +128,39 @@ const formatOutputContent = (output: any): string => {
124128
return ''
125129
}
126130

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>`
163+
127164
/**
128165
* Floating chat modal component
129166
*
@@ -140,6 +177,9 @@ export function Chat() {
140177
const params = useParams()
141178
const workspaceId = params.workspaceId as string
142179
const { activeWorkflowId } = useWorkflowRegistry()
180+
// const blocks = useWorkflowStore((state) => state.blocks)
181+
// const triggerWorkflowUpdate = useWorkflowStore((state) => state.triggerUpdate)
182+
// const setSubBlockValue = useSubBlockStore((state) => state.setValue)
143183

144184
// Chat state (UI and messages from unified store)
145185
const {
@@ -190,6 +230,69 @@ export function Chat() {
190230
handleDrop,
191231
} = useChatFileUpload()
192232

233+
/**
234+
* Resolves the unified start block for chat execution, if available.
235+
*/
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
295+
193296
// Get actual position (default if not set)
194297
const actualPosition = useMemo(
195298
() => getChatPosition(chatPosition, chatWidth, chatHeight),
@@ -564,6 +667,60 @@ export function Chat() {
564667
setIsChatOpen(false)
565668
}, [setIsChatOpen])
566669

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+
567724
// Don't render if not open
568725
if (!isChatOpen) return null
569726

@@ -583,17 +740,32 @@ export function Chat() {
583740
>
584741
{/* Header with drag handle */}
585742
<div
586-
className='flex h-[32px] flex-shrink-0 cursor-grab items-center justify-between bg-[var(--surface-1)] p-0 active:cursor-grabbing'
743+
className='flex h-[32px] flex-shrink-0 cursor-grab items-center justify-between gap-[10px] bg-[var(--surface-1)] p-0 active:cursor-grabbing'
587744
onMouseDown={handleMouseDown}
588745
>
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>
746+
<span className='flex-shrink-0 pr-[2px] font-medium text-[14px] text-[var(--text-primary)]'>
747+
Chat
748+
</span>
749+
750+
{/* Start inputs button and output selector - with max-width to prevent overflow */}
751+
<div
752+
className='ml-auto flex min-w-0 flex-shrink items-center gap-[6px]'
753+
onMouseDown={(e) => e.stopPropagation()}
754+
>
755+
{/* {shouldShowConfigureStartInputsButton && (
756+
<Badge
757+
variant='outline'
758+
className='cursor-pointer rounded-[6px] flex-none whitespace-nowrap'
759+
title='Add chat inputs to Start block'
760+
onMouseDown={(e) => {
761+
e.stopPropagation()
762+
handleConfigureStartInputs()
763+
}}
764+
>
765+
<span className='whitespace-nowrap text-[12px]'>Add inputs</span>
766+
</Badge>
767+
)} */}
594768

595-
{/* Output selector - centered with mx-auto */}
596-
<div className='mr-[6px] ml-auto' onMouseDown={(e) => e.stopPropagation()}>
597769
<OutputSelect
598770
workflowId={activeWorkflowId}
599771
selectedOutputs={selectedOutputs}
@@ -605,7 +777,7 @@ export function Chat() {
605777
/>
606778
</div>
607779

608-
<div className='flex items-center gap-[8px]'>
780+
<div className='flex flex-shrink-0 items-center gap-[8px]'>
609781
{/* More menu with actions */}
610782
<Popover variant='default'>
611783
<PopoverTrigger asChild>
@@ -628,22 +800,22 @@ export function Chat() {
628800
<PopoverItem
629801
onClick={(e) => {
630802
e.stopPropagation()
631-
if (activeWorkflowId) clearChat(activeWorkflowId)
803+
if (activeWorkflowId) exportChatCSV(activeWorkflowId)
632804
}}
633-
disabled={messages.length === 0}
805+
disabled={workflowMessages.length === 0}
634806
>
635-
<Trash className='h-[14px] w-[14px]' />
636-
<span>Clear</span>
807+
<ArrowDownToLine className='h-[13px] w-[13px]' />
808+
<span>Download</span>
637809
</PopoverItem>
638810
<PopoverItem
639811
onClick={(e) => {
640812
e.stopPropagation()
641-
if (activeWorkflowId) exportChatCSV(activeWorkflowId)
813+
if (activeWorkflowId) clearChat(activeWorkflowId)
642814
}}
643-
disabled={messages.length === 0}
815+
disabled={workflowMessages.length === 0}
644816
>
645-
<ArrowDownToLine className='h-[14px] w-[14px]' />
646-
<span>Download</span>
817+
<Trash className='h-[13px] w-[13px]' />
818+
<span>Clear</span>
647819
</PopoverItem>
648820
</PopoverScrollArea>
649821
</PopoverContent>
@@ -662,7 +834,7 @@ export function Chat() {
662834
<div className='flex-1 overflow-hidden'>
663835
{workflowMessages.length === 0 ? (
664836
<div className='flex h-full items-center justify-center text-[#8D8D8D] text-[13px]'>
665-
No messages yet
837+
Workflow input: {'<start.input>'}
666838
</div>
667839
) : (
668840
<div ref={scrollAreaRef} className='h-full overflow-y-auto overflow-x-hidden'>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/hooks/use-chat-resize.ts

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -178,16 +178,11 @@ export function useChatResize({
178178
(e: MouseEvent) => {
179179
if (!isResizingRef.current || !activeDirectionRef.current) return
180180

181-
const deltaX = e.clientX - resizeStartRef.current.x
182-
const deltaY = e.clientY - resizeStartRef.current.y
181+
let deltaX = e.clientX - resizeStartRef.current.x
182+
let deltaY = e.clientY - resizeStartRef.current.y
183183
const initial = initialStateRef.current
184184
const direction = activeDirectionRef.current
185185

186-
let newX = initial.x
187-
let newY = initial.y
188-
let newWidth = initial.width
189-
let newHeight = initial.height
190-
191186
// Get layout bounds
192187
const sidebarWidth = Number.parseInt(
193188
getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0'
@@ -199,6 +194,56 @@ export function useChatResize({
199194
getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0'
200195
)
201196

197+
// Clamp vertical drag when resizing from the top so the chat does not grow downward
198+
// after its top edge hits the top of the viewport.
199+
if (direction === 'top' || direction === 'top-left' || direction === 'top-right') {
200+
// newY = initial.y + deltaY should never be less than 0
201+
const maxUpwardDelta = initial.y
202+
if (deltaY < -maxUpwardDelta) {
203+
deltaY = -maxUpwardDelta
204+
}
205+
}
206+
207+
// Clamp vertical drag when resizing from the bottom so the chat does not grow upward
208+
// after its bottom edge hits the top of the terminal.
209+
if (direction === 'bottom' || direction === 'bottom-left' || direction === 'bottom-right') {
210+
const maxBottom = window.innerHeight - terminalHeight
211+
const initialBottom = initial.y + initial.height
212+
const maxDeltaY = maxBottom - initialBottom
213+
214+
if (deltaY > maxDeltaY) {
215+
deltaY = maxDeltaY
216+
}
217+
}
218+
219+
// Clamp horizontal drag when resizing from the left so the chat does not grow to the right
220+
// after its left edge hits the sidebar.
221+
if (direction === 'left' || direction === 'top-left' || direction === 'bottom-left') {
222+
const minLeft = sidebarWidth
223+
const minDeltaX = minLeft - initial.x
224+
225+
if (deltaX < minDeltaX) {
226+
deltaX = minDeltaX
227+
}
228+
}
229+
230+
// Clamp horizontal drag when resizing from the right so the chat does not grow to the left
231+
// after its right edge hits the panel.
232+
if (direction === 'right' || direction === 'top-right' || direction === 'bottom-right') {
233+
const maxRight = window.innerWidth - panelWidth
234+
const initialRight = initial.x + initial.width
235+
const maxDeltaX = maxRight - initialRight
236+
237+
if (deltaX > maxDeltaX) {
238+
deltaX = maxDeltaX
239+
}
240+
}
241+
242+
let newX = initial.x
243+
let newY = initial.y
244+
let newWidth = initial.width
245+
let newHeight = initial.height
246+
202247
// Calculate new dimensions based on resize direction
203248
switch (direction) {
204249
// Corners

0 commit comments

Comments
 (0)