Skip to content

Commit 0a3c7a2

Browse files
DASOL.HWANGDASOL.HWANG
authored andcommitted
feat: add reasoning content toggle for assistant messages
Add toggleable reasoning_content field for assistant role messages in the Agent block's messages input component. Features: - Added reasoning_content optional field to Message interface - Added '+ Reasoning' / '− Reasoning' toggle button for assistant messages - Reasoning content field appears/hides based on toggle state - Auto-shows reasoning content when existing data contains reasoning_content - Full support for variable references, env vars, and all existing features - Maintains clean UI by showing field only when needed This enables users to add reasoning/thinking content separately from the main response content, useful for: - o1/o3 models with reasoning capabilities - Few-shot learning examples with chain-of-thought - Debugging and transparency in agent responses
1 parent 7ffc11a commit 0a3c7a2

File tree

1 file changed

+211
-22
lines changed
  • apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input

1 file changed

+211
-22
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input/messages-input.tsx

Lines changed: 211 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const MAX_TEXTAREA_HEIGHT_PX = 320
2020
interface Message {
2121
role: 'system' | 'user' | 'assistant'
2222
content: string
23+
reasoning_content?: string
2324
}
2425

2526
/**
@@ -60,6 +61,7 @@ export function MessagesInput({
6061
const [localMessages, setLocalMessages] = useState<Message[]>([{ role: 'user', content: '' }])
6162
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
6263
const [openPopoverIndex, setOpenPopoverIndex] = useState<number | null>(null)
64+
const [showReasoningContent, setShowReasoningContent] = useState<Record<number, boolean>>({})
6365
const subBlockInput = useSubBlockInput({
6466
blockId,
6567
subBlockId,
@@ -74,8 +76,24 @@ export function MessagesInput({
7476
useEffect(() => {
7577
if (isPreview && previewValue && Array.isArray(previewValue)) {
7678
setLocalMessages(previewValue)
79+
// Auto-show reasoning content if it exists
80+
const reasoningMap: Record<number, boolean> = {}
81+
previewValue.forEach((msg, idx) => {
82+
if (msg.role === 'assistant' && msg.reasoning_content) {
83+
reasoningMap[idx] = true
84+
}
85+
})
86+
setShowReasoningContent(reasoningMap)
7787
} else if (messages && Array.isArray(messages) && messages.length > 0) {
7888
setLocalMessages(messages)
89+
// Auto-show reasoning content if it exists
90+
const reasoningMap: Record<number, boolean> = {}
91+
messages.forEach((msg, idx) => {
92+
if (msg.role === 'assistant' && msg.reasoning_content) {
93+
reasoningMap[idx] = true
94+
}
95+
})
96+
setShowReasoningContent(reasoningMap)
7997
}
8098
}, [isPreview, previewValue, messages])
8199

@@ -117,6 +135,24 @@ export function MessagesInput({
117135
[localMessages, setMessages, isPreview, disabled]
118136
)
119137

138+
/**
139+
* Updates a specific message's reasoning_content
140+
*/
141+
const updateMessageReasoningContent = useCallback(
142+
(index: number, reasoning_content: string) => {
143+
if (isPreview || disabled) return
144+
145+
const updatedMessages = [...localMessages]
146+
updatedMessages[index] = {
147+
...updatedMessages[index],
148+
reasoning_content,
149+
}
150+
setLocalMessages(updatedMessages)
151+
setMessages(updatedMessages)
152+
},
153+
[localMessages, setMessages, isPreview, disabled]
154+
)
155+
120156
/**
121157
* Updates a specific message's role
122158
*/
@@ -199,6 +235,21 @@ export function MessagesInput({
199235
[localMessages, setMessages, isPreview, disabled]
200236
)
201237

238+
/**
239+
* Toggles reasoning content visibility for a message
240+
*/
241+
const toggleReasoningContent = useCallback(
242+
(index: number) => {
243+
if (isPreview || disabled) return
244+
245+
setShowReasoningContent((prev) => ({
246+
...prev,
247+
[index]: !prev[index],
248+
}))
249+
},
250+
[isPreview, disabled]
251+
)
252+
202253
/**
203254
* Capitalizes the first letter of the role
204255
*/
@@ -355,25 +406,26 @@ export function MessagesInput({
355406
className='flex cursor-pointer items-center justify-between px-[8px] pt-[6px]'
356407
onClick={(e) => handleHeaderClick(index, e)}
357408
>
358-
<Popover
359-
open={openPopoverIndex === index}
360-
onOpenChange={(open) => setOpenPopoverIndex(open ? index : null)}
361-
>
362-
<PopoverTrigger asChild>
363-
<button
364-
type='button'
365-
disabled={isPreview || disabled}
366-
className={cn(
367-
'-ml-1.5 -my-1 rounded px-1.5 py-1 font-medium text-[13px] text-[var(--text-primary)] leading-none transition-colors hover:bg-[var(--surface-5)] hover:text-[var(--text-secondary)]',
368-
(isPreview || disabled) &&
369-
'cursor-default hover:bg-transparent hover:text-[var(--text-primary)]'
370-
)}
371-
onClick={(e) => e.stopPropagation()}
372-
aria-label='Select message role'
373-
>
374-
{formatRole(message.role)}
375-
</button>
376-
</PopoverTrigger>
409+
<div className='flex items-center gap-2'>
410+
<Popover
411+
open={openPopoverIndex === index}
412+
onOpenChange={(open) => setOpenPopoverIndex(open ? index : null)}
413+
>
414+
<PopoverTrigger asChild>
415+
<button
416+
type='button'
417+
disabled={isPreview || disabled}
418+
className={cn(
419+
'-ml-1.5 -my-1 rounded px-1.5 py-1 font-medium text-[13px] text-[var(--text-primary)] leading-none transition-colors hover:bg-[var(--surface-5)] hover:text-[var(--text-secondary)]',
420+
(isPreview || disabled) &&
421+
'cursor-default hover:bg-transparent hover:text-[var(--text-primary)]'
422+
)}
423+
onClick={(e) => e.stopPropagation()}
424+
aria-label='Select message role'
425+
>
426+
{formatRole(message.role)}
427+
</button>
428+
</PopoverTrigger>
377429
<PopoverContent minWidth={140} align='start'>
378430
<div className='flex flex-col gap-[2px]'>
379431
{(['system', 'user', 'assistant'] as const).map((role) => (
@@ -390,10 +442,25 @@ export function MessagesInput({
390442
))}
391443
</div>
392444
</PopoverContent>
393-
</Popover>
445+
</Popover>
446+
</div>
394447

395448
{!isPreview && !disabled && (
396-
<div className='flex items-center'>
449+
<div className='flex items-center gap-1'>
450+
{message.role === 'assistant' && (
451+
<Button
452+
variant='ghost'
453+
onClick={(e: React.MouseEvent) => {
454+
e.stopPropagation()
455+
toggleReasoningContent(index)
456+
}}
457+
disabled={disabled}
458+
className='-my-1 h-6 px-2 text-[11px] text-[var(--text-muted)] hover:text-[var(--text-primary)]'
459+
aria-label='Toggle reasoning content'
460+
>
461+
{showReasoningContent[index] ? '− Reasoning' : '+ Reasoning'}
462+
</Button>
463+
)}
397464
{currentMessages.length > 1 && (
398465
<>
399466
<Button
@@ -452,13 +519,22 @@ export function MessagesInput({
452519

453520
{/* Content Input with overlay for variable highlighting */}
454521
<div className='relative w-full'>
522+
{message.role === 'assistant' && showReasoningContent[index] && (
523+
<div className='px-[8px] pt-[4px] pb-[2px]'>
524+
<span className='font-medium text-[11px] text-[var(--text-muted)]'>Content</span>
525+
</div>
526+
)}
455527
<textarea
456528
ref={(el) => {
457529
textareaRefs.current[fieldId] = el
458530
}}
459531
className='allow-scroll box-border min-h-[80px] w-full resize-none whitespace-pre-wrap break-words border-none bg-transparent px-[8px] pt-[8px] font-[inherit] font-medium text-sm text-transparent leading-[inherit] caret-[var(--text-primary)] outline-none placeholder:text-[var(--text-muted)] focus:outline-none focus-visible:outline-none disabled:cursor-not-allowed'
460532
rows={3}
461-
placeholder='Enter message content...'
533+
placeholder={
534+
message.role === 'assistant'
535+
? 'Enter assistant response content...'
536+
: 'Enter message content...'
537+
}
462538
value={message.content}
463539
onChange={(e) => {
464540
fieldHandlers.onChange(e)
@@ -544,6 +620,119 @@ export function MessagesInput({
544620
</div>
545621
)}
546622
</div>
623+
624+
{/* Reasoning Content Input (only for assistant role when toggled on) */}
625+
{message.role === 'assistant' && showReasoningContent[index] && (() => {
626+
const reasoningFieldId = `message-${index}-reasoning`
627+
const reasoningFieldState = subBlockInput.fieldHelpers.getFieldState(reasoningFieldId)
628+
const reasoningFieldHandlers = subBlockInput.fieldHelpers.createFieldHandlers(
629+
reasoningFieldId,
630+
message.reasoning_content || '',
631+
(newValue: string) => {
632+
updateMessageReasoningContent(index, newValue)
633+
}
634+
)
635+
636+
const handleReasoningEnvSelect = subBlockInput.fieldHelpers.createEnvVarSelectHandler(
637+
reasoningFieldId,
638+
message.reasoning_content || '',
639+
(newValue: string) => {
640+
updateMessageReasoningContent(index, newValue)
641+
}
642+
)
643+
644+
const handleReasoningTagSelect = subBlockInput.fieldHelpers.createTagSelectHandler(
645+
reasoningFieldId,
646+
message.reasoning_content || '',
647+
(newValue: string) => {
648+
updateMessageReasoningContent(index, newValue)
649+
}
650+
)
651+
652+
const reasoningTextareaRefObject = {
653+
current: textareaRefs.current[reasoningFieldId] ?? null,
654+
} as React.RefObject<HTMLTextAreaElement>
655+
656+
return (
657+
<div className='relative w-full border-t border-[var(--border-1)]'>
658+
<div className='px-[8px] pt-[8px] pb-[2px]'>
659+
<span className='font-medium text-[11px] text-[var(--text-muted)]'>Reasoning Content</span>
660+
</div>
661+
<textarea
662+
ref={(el) => {
663+
textareaRefs.current[reasoningFieldId] = el
664+
}}
665+
className='allow-scroll box-border min-h-[80px] w-full resize-none whitespace-pre-wrap break-words border-none bg-transparent px-[8px] pt-[4px] font-[inherit] font-medium text-sm text-transparent leading-[inherit] caret-[var(--text-primary)] outline-none placeholder:text-[var(--text-muted)] focus:outline-none focus-visible:outline-none disabled:cursor-not-allowed'
666+
rows={3}
667+
placeholder='Enter reasoning content (optional, for o1/o3 models)...'
668+
value={message.reasoning_content || ''}
669+
onChange={(e) => {
670+
reasoningFieldHandlers.onChange(e)
671+
autoResizeTextarea(reasoningFieldId)
672+
}}
673+
onKeyDown={reasoningFieldHandlers.onKeyDown}
674+
onDrop={reasoningFieldHandlers.onDrop}
675+
onDragOver={reasoningFieldHandlers.onDragOver}
676+
onScroll={(e) => {
677+
const overlay = overlayRefs.current[reasoningFieldId]
678+
if (overlay) {
679+
overlay.scrollTop = e.currentTarget.scrollTop
680+
overlay.scrollLeft = e.currentTarget.scrollLeft
681+
}
682+
}}
683+
disabled={isPreview || disabled}
684+
/>
685+
<div
686+
ref={(el) => {
687+
overlayRefs.current[reasoningFieldId] = el
688+
}}
689+
className='scrollbar-none pointer-events-none absolute top-[34px] left-0 box-border w-full overflow-auto whitespace-pre-wrap break-words border-none bg-transparent px-[8px] pt-[4px] font-[inherit] font-medium text-[var(--text-primary)] text-sm leading-[inherit]'
690+
>
691+
{formatDisplayText(message.reasoning_content || '', {
692+
accessiblePrefixes,
693+
highlightAll: !accessiblePrefixes,
694+
})}
695+
</div>
696+
697+
{/* Env var dropdown for reasoning content */}
698+
<EnvVarDropdown
699+
visible={reasoningFieldState.showEnvVars && !isPreview && !disabled}
700+
onSelect={handleReasoningEnvSelect}
701+
searchTerm={reasoningFieldState.searchTerm}
702+
inputValue={message.reasoning_content || ''}
703+
cursorPosition={reasoningFieldState.cursorPosition}
704+
onClose={() => subBlockInput.fieldHelpers.hideFieldDropdowns(reasoningFieldId)}
705+
workspaceId={subBlockInput.workspaceId}
706+
maxHeight='192px'
707+
inputRef={reasoningTextareaRefObject}
708+
/>
709+
710+
{/* Tag dropdown for reasoning content */}
711+
<TagDropdown
712+
visible={reasoningFieldState.showTags && !isPreview && !disabled}
713+
onSelect={handleReasoningTagSelect}
714+
blockId={blockId}
715+
activeSourceBlockId={reasoningFieldState.activeSourceBlockId}
716+
inputValue={message.reasoning_content || ''}
717+
cursorPosition={reasoningFieldState.cursorPosition}
718+
onClose={() => subBlockInput.fieldHelpers.hideFieldDropdowns(reasoningFieldId)}
719+
inputRef={reasoningTextareaRefObject}
720+
/>
721+
722+
{!isPreview && !disabled && (
723+
<div
724+
className='absolute right-1 bottom-1 flex h-4 w-4 cursor-ns-resize items-center justify-center rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] dark:bg-[var(--surface-5)]'
725+
onMouseDown={(e) => handleResizeStart(reasoningFieldId, e)}
726+
onDragStart={(e) => {
727+
e.preventDefault()
728+
}}
729+
>
730+
<ChevronsUpDown className='h-3 w-3 text-[var(--text-muted)]' />
731+
</div>
732+
)}
733+
</div>
734+
)
735+
})()}
547736
</>
548737
)
549738
})()}

0 commit comments

Comments
 (0)