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'
55import { 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'
77import {
88 FileAttachmentDisplay ,
99 SmoothStreamingText ,
@@ -15,8 +15,6 @@ import CopilotMarkdownRenderer from '@/app/workspace/[workspaceId]/w/[workflowId
1515import {
1616 useCheckpointManagement ,
1717 useMessageEditing ,
18- useMessageFeedback ,
19- useSuccessTimers ,
2018} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks'
2119import { UserInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input'
2220import { 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