Skip to content

Commit b5311d7

Browse files
committed
Options
1 parent 6750024 commit b5311d7

File tree

3 files changed

+314
-75
lines changed

3 files changed

+314
-75
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx

Lines changed: 48 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
'use client'
22

3-
import { type FC, memo, useMemo, useState } from 'react'
4-
import { Check, Copy, RotateCcw, ThumbsDown, ThumbsUp } from 'lucide-react'
3+
import { type FC, memo, useCallback, useMemo, useState } from 'react'
4+
import { RotateCcw } from 'lucide-react'
55
import { Button } from '@/components/emcn'
6-
import { ToolCall } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components'
6+
import { OptionsSelector, parseSpecialTags, ToolCall } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components'
77
import {
88
FileAttachmentDisplay,
99
SmoothStreamingText,
@@ -15,8 +15,6 @@ import CopilotMarkdownRenderer from '@/app/workspace/[workspaceId]/w/[workflowId
1515
import {
1616
useCheckpointManagement,
1717
useMessageEditing,
18-
useMessageFeedback,
19-
useSuccessTimers,
2018
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks'
2119
import { UserInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input'
2220
import { useCopilotStore } from '@/stores/panel/copilot/store'
@@ -40,6 +38,8 @@ interface CopilotMessageProps {
4038
onEditModeChange?: (isEditing: boolean, cancelCallback?: () => void) => void
4139
/** Callback when revert mode changes */
4240
onRevertModeChange?: (isReverting: boolean) => void
41+
/** Whether this is the last message in the conversation */
42+
isLastMessage?: boolean
4343
}
4444

4545
/**
@@ -59,6 +59,7 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
5959
checkpointCount = 0,
6060
onEditModeChange,
6161
onRevertModeChange,
62+
isLastMessage = false,
6263
}) => {
6364
const isUser = message.role === 'user'
6465
const isAssistant = message.role === 'assistant'
@@ -88,22 +89,6 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
8889
// UI state
8990
const [isHoveringMessage, setIsHoveringMessage] = useState(false)
9091

91-
// Success timers hook
92-
const {
93-
showCopySuccess,
94-
showUpvoteSuccess,
95-
showDownvoteSuccess,
96-
handleCopy,
97-
setShowUpvoteSuccess,
98-
setShowDownvoteSuccess,
99-
} = useSuccessTimers()
100-
101-
// Message feedback hook
102-
const { handleUpvote, handleDownvote } = useMessageFeedback(message, messages, {
103-
setShowUpvoteSuccess,
104-
setShowDownvoteSuccess,
105-
})
106-
10792
// Checkpoint management hook
10893
const {
10994
showRestoreConfirmation,
@@ -153,14 +138,6 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
153138
pendingEditRef,
154139
})
155140

156-
/**
157-
* Handles copying message content to clipboard
158-
* Uses the success timer hook to show feedback
159-
*/
160-
const handleCopyContent = () => {
161-
handleCopy(message.content)
162-
}
163-
164141
// Get clean text content with double newline parsing
165142
const cleanTextContent = useMemo(() => {
166143
if (!message.content) return ''
@@ -169,6 +146,24 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
169146
return message.content.replace(/\n{3,}/g, '\n\n')
170147
}, [message.content])
171148

149+
// Parse special tags from message content (options, plan)
150+
const parsedTags = useMemo(() => {
151+
if (!message.content || isUser) return null
152+
return parseSpecialTags(message.content)
153+
}, [message.content, isUser])
154+
155+
// Get sendMessage from store for continuation actions
156+
const sendMessage = useCopilotStore((s) => s.sendMessage)
157+
158+
// Handler for option selection
159+
const handleOptionSelect = useCallback(
160+
(_optionKey: string, optionText: string) => {
161+
// Send the option text as a message
162+
sendMessage(optionText)
163+
},
164+
[sendMessage]
165+
)
166+
172167
// Memoize content blocks to avoid re-rendering unchanged blocks
173168
const memoizedContentBlocks = useMemo(() => {
174169
if (!message.contentBlocks || message.contentBlocks.length === 0) {
@@ -179,8 +174,12 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
179174
if (block.type === 'text') {
180175
const isLastTextBlock =
181176
index === message.contentBlocks!.length - 1 && block.type === 'text'
182-
// Clean content for this text block
183-
const cleanBlockContent = block.content.replace(/\n{3,}/g, '\n\n')
177+
// Clean content for this text block - strip special tags and excessive newlines
178+
const parsed = parseSpecialTags(block.content)
179+
const cleanBlockContent = parsed.cleanContent.replace(/\n{3,}/g, '\n\n')
180+
181+
// Skip if no content after stripping tags
182+
if (!cleanBlockContent.trim()) return null
184183

185184
// Use smooth streaming for the last text block if we're streaming
186185
const shouldUseSmoothing = isStreaming && isLastTextBlock
@@ -467,47 +466,6 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
467466
</div>
468467
)}
469468

470-
{/* Action buttons for completed messages */}
471-
{!isStreaming && cleanTextContent && (
472-
<div className='flex items-center gap-[8px] pt-[8px]'>
473-
<Button
474-
onClick={handleCopyContent}
475-
variant='ghost'
476-
title='Copy'
477-
className='!h-[14px] !w-[14px] !p-0'
478-
>
479-
{showCopySuccess ? (
480-
<Check className='h-[14px] w-[14px]' strokeWidth={2} />
481-
) : (
482-
<Copy className='h-[14px] w-[14px]' strokeWidth={2} />
483-
)}
484-
</Button>
485-
<Button
486-
onClick={handleUpvote}
487-
variant='ghost'
488-
title='Upvote'
489-
className='!h-[14px] !w-[14px] !p-0'
490-
>
491-
{showUpvoteSuccess ? (
492-
<Check className='h-[14px] w-[14px]' strokeWidth={2} />
493-
) : (
494-
<ThumbsUp className='h-[14px] w-[14px]' strokeWidth={2} />
495-
)}
496-
</Button>
497-
<Button
498-
onClick={handleDownvote}
499-
variant='ghost'
500-
title='Downvote'
501-
className='!h-[14px] !w-[14px] !p-0'
502-
>
503-
{showDownvoteSuccess ? (
504-
<Check className='h-[14px] w-[14px]' strokeWidth={2} />
505-
) : (
506-
<ThumbsDown className='h-[14px] w-[14px]' strokeWidth={2} />
507-
)}
508-
</Button>
509-
</div>
510-
)}
511469

512470
{/* Citations if available */}
513471
{message.citations && message.citations.length > 0 && (
@@ -528,6 +486,19 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
528486
</div>
529487
</div>
530488
)}
489+
490+
{/* Options selector when agent presents choices */}
491+
{!isStreaming &&
492+
parsedTags?.options &&
493+
Object.keys(parsedTags.options).length > 0 && (
494+
<OptionsSelector
495+
options={parsedTags.options}
496+
onSelect={handleOptionSelect}
497+
disabled={isSendingMessage}
498+
enableKeyboardNav={isLastMessage}
499+
/>
500+
)}
501+
531502
</div>
532503
</div>
533504
)
@@ -565,6 +536,11 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
565536
return false
566537
}
567538

539+
// If isLastMessage changed, re-render (for options visibility)
540+
if (prevProps.isLastMessage !== nextProps.isLastMessage) {
541+
return false
542+
}
543+
568544
// For streaming messages, check if content actually changed
569545
if (nextProps.isStreaming) {
570546
const prevBlocks = prevMessage.contentBlocks || []

0 commit comments

Comments
 (0)