@@ -8,13 +8,15 @@ import {
88 ChipModalField ,
99 ChipModalFooter ,
1010 ChipModalHeader ,
11+ type ClipboardContent ,
1112 cn ,
1213 Duplicate ,
1314 Split ,
1415 ThumbsDown ,
1516 ThumbsUp ,
1617 Tooltip ,
1718 toast ,
19+ useCopyToClipboard ,
1820} from '@sim/emcn'
1921import { useParams , useRouter } from 'next/navigation'
2022import { isLiveAssistantMessageId } from '@/lib/copilot/chat/effective-transcript'
@@ -23,82 +25,54 @@ import { useSubmitCopilotFeedback } from '@/hooks/queries/copilot-feedback'
2325import { useForkMothershipChat } from '@/hooks/queries/mothership-chats'
2426import { useFolderStore } from '@/stores/folders/store'
2527
26- const SPECIAL_TAGS = 'thinking|options|usage_upgrade|credential|mothership-error|file|question'
27-
28- function toPlainText ( raw : string ) : string {
29- return (
30- raw
31- // Strip special tags and their contents
32- . replace ( new RegExp ( `<\\/?(${ SPECIAL_TAGS } )(?:>[\\s\\S]*?<\\/(${ SPECIAL_TAGS } )>|>)` , 'g' ) , '' )
33- // Strip markdown
34- . replace ( / ^ # { 1 , 6 } \s + / gm, '' )
35- . replace ( / \* \* ( .+ ?) \* \* / g, '$1' )
36- . replace ( / \* ( .+ ?) \* / g, '$1' )
37- . replace ( / ` { 3 } [ \s \S ] * ?` { 3 } / g, '' )
38- . replace ( / ` ( .+ ?) ` / g, '$1' )
39- . replace ( / \[ ( [ ^ \] ] + ) \] \( [ ^ ) ] + \) / g, '$1' )
40- . replace ( / ^ [ > \- * ] \s + / gm, '' )
41- . replace ( / ! \[ [ ^ \] ] * \] \( [ ^ ) ] + \) / g, '' )
42- // Normalize whitespace
43- . replace ( / \n { 3 , } / g, '\n\n' )
44- . trim ( )
45- )
46- }
47-
4828const ICON_CLASS = 'size-[14px]'
4929const BUTTON_CLASS =
5030 'flex size-[26px] items-center justify-center rounded-[6px] text-[var(--text-icon)] transition-colors hover-hover:bg-[var(--surface-hover)] focus-visible:outline-none'
5131
5232interface MessageActionsProps {
5333 content : string
34+ getCopyContent ?: ( ) => string
35+ hasCopyContent ?: boolean
36+ prepareContentForCopy ?: ( content : string ) => ClipboardContent
5437 userQuery ?: string
5538 requestId ?: string
5639 messageId ?: string
5740}
5841
5942export const MessageActions = memo ( function MessageActions ( {
6043 content,
44+ getCopyContent,
45+ hasCopyContent,
46+ prepareContentForCopy,
6147 userQuery,
6248 requestId,
6349 messageId,
6450} : MessageActionsProps ) {
6551 const router = useRouter ( )
6652 const params = useParams < { workspaceId : string } > ( )
6753 const { chatId } = useChatSurface ( )
68- const [ copied , setCopied ] = useState ( false )
54+ const { copied, copy : copyMessage } = useCopyToClipboard ( { resetMs : 1500 } )
6955 const [ copiedRequestId , setCopiedRequestId ] = useState ( false )
7056 const [ pendingFeedback , setPendingFeedback ] = useState < 'up' | 'down' | null > ( null )
7157 const [ feedbackText , setFeedbackText ] = useState ( '' )
72- const resetTimeoutRef = useRef < number | null > ( null )
7358 const requestIdTimeoutRef = useRef < number | null > ( null )
7459 const submitFeedback = useSubmitCopilotFeedback ( )
7560 const forkChat = useForkMothershipChat ( params . workspaceId )
7661
7762 useEffect ( ( ) => {
7863 return ( ) => {
79- if ( resetTimeoutRef . current !== null ) {
80- window . clearTimeout ( resetTimeoutRef . current )
81- }
8264 if ( requestIdTimeoutRef . current !== null ) {
8365 window . clearTimeout ( requestIdTimeoutRef . current )
8466 }
8567 }
8668 } , [ ] )
8769
88- const copyToClipboard = async ( ) => {
89- if ( ! content ) return
90- const text = toPlainText ( content )
91- if ( ! text ) return
92- try {
93- await navigator . clipboard . writeText ( text )
94- setCopied ( true )
95- if ( resetTimeoutRef . current !== null ) {
96- window . clearTimeout ( resetTimeoutRef . current )
97- }
98- resetTimeoutRef . current = window . setTimeout ( ( ) => setCopied ( false ) , 1500 )
99- } catch {
100- /* clipboard unavailable */
101- }
70+ const copyToClipboard = ( ) => {
71+ const contentToCopy = getCopyContent ?.( ) ?? content
72+ if ( ! contentToCopy ) return
73+ const copyContent = prepareContentForCopy ?.( contentToCopy ) ?? contentToCopy
74+ if ( typeof copyContent === 'string' && ! copyContent ) return
75+ void copyMessage ( copyContent )
10276 }
10377
10478 const copyRequestId = async ( ) => {
@@ -166,18 +140,18 @@ export const MessageActions = memo(function MessageActions({
166140 }
167141 }
168142
169- const hasContent = Boolean ( content )
143+ const canCopyContent = hasCopyContent ?? Boolean ( content )
170144 const canSubmitFeedback = Boolean ( chatId && userQuery )
171145 // A live (just-streamed) assistant message carries a synthetic id that the
172146 // persisted transcript doesn't know — forking it would 400. The button
173147 // appears once the transcript refetch swaps in the persisted message id.
174148 const canFork = Boolean ( chatId && messageId && ! isLiveAssistantMessageId ( messageId ) )
175- if ( ! hasContent && ! canSubmitFeedback && ! canFork ) return null
149+ if ( ! canCopyContent && ! canSubmitFeedback && ! canFork ) return null
176150
177151 return (
178152 < >
179153 < div className = 'flex items-center gap-0.5' >
180- { hasContent && (
154+ { canCopyContent && (
181155 < Tooltip . Root >
182156 < Tooltip . Trigger asChild >
183157 < button
0 commit comments