@@ -15,6 +15,7 @@ import {
1515 ThumbsUp ,
1616 Tooltip ,
1717 toast ,
18+ useCopyToClipboard ,
1819} from '@sim/emcn'
1920import { useParams , useRouter } from 'next/navigation'
2021import { isLiveAssistantMessageId } from '@/lib/copilot/chat/effective-transcript'
@@ -23,82 +24,54 @@ import { useSubmitCopilotFeedback } from '@/hooks/queries/copilot-feedback'
2324import { useForkMothershipChat } from '@/hooks/queries/mothership-chats'
2425import { useFolderStore } from '@/stores/folders/store'
2526
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-
4827const ICON_CLASS = 'size-[14px]'
4928const BUTTON_CLASS =
5029 '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'
5130
5231interface MessageActionsProps {
5332 content : string
33+ getCopyContent ?: ( ) => string
34+ hasCopyContent ?: boolean
35+ prepareContentForCopy ?: ( content : string ) => string
5436 userQuery ?: string
5537 requestId ?: string
5638 messageId ?: string
5739}
5840
5941export const MessageActions = memo ( function MessageActions ( {
6042 content,
43+ getCopyContent,
44+ hasCopyContent,
45+ prepareContentForCopy,
6146 userQuery,
6247 requestId,
6348 messageId,
6449} : MessageActionsProps ) {
6550 const router = useRouter ( )
6651 const params = useParams < { workspaceId : string } > ( )
6752 const { chatId } = useChatSurface ( )
68- const [ copied , setCopied ] = useState ( false )
53+ const { copied, copy : copyMessage } = useCopyToClipboard ( { resetMs : 1500 } )
6954 const [ copiedRequestId , setCopiedRequestId ] = useState ( false )
7055 const [ pendingFeedback , setPendingFeedback ] = useState < 'up' | 'down' | null > ( null )
7156 const [ feedbackText , setFeedbackText ] = useState ( '' )
72- const resetTimeoutRef = useRef < number | null > ( null )
7357 const requestIdTimeoutRef = useRef < number | null > ( null )
7458 const submitFeedback = useSubmitCopilotFeedback ( )
7559 const forkChat = useForkMothershipChat ( params . workspaceId )
7660
7761 useEffect ( ( ) => {
7862 return ( ) => {
79- if ( resetTimeoutRef . current !== null ) {
80- window . clearTimeout ( resetTimeoutRef . current )
81- }
8263 if ( requestIdTimeoutRef . current !== null ) {
8364 window . clearTimeout ( requestIdTimeoutRef . current )
8465 }
8566 }
8667 } , [ ] )
8768
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- }
69+ const copyToClipboard = ( ) => {
70+ const contentToCopy = getCopyContent ?.( ) ?? content
71+ if ( ! contentToCopy ) return
72+ const markdown = prepareContentForCopy ?.( contentToCopy ) ?? contentToCopy
73+ if ( ! markdown ) return
74+ void copyMessage ( markdown )
10275 }
10376
10477 const copyRequestId = async ( ) => {
@@ -166,18 +139,18 @@ export const MessageActions = memo(function MessageActions({
166139 }
167140 }
168141
169- const hasContent = Boolean ( content )
142+ const canCopyContent = hasCopyContent ?? Boolean ( content )
170143 const canSubmitFeedback = Boolean ( chatId && userQuery )
171144 // A live (just-streamed) assistant message carries a synthetic id that the
172145 // persisted transcript doesn't know — forking it would 400. The button
173146 // appears once the transcript refetch swaps in the persisted message id.
174147 const canFork = Boolean ( chatId && messageId && ! isLiveAssistantMessageId ( messageId ) )
175- if ( ! hasContent && ! canSubmitFeedback && ! canFork ) return null
148+ if ( ! canCopyContent && ! canSubmitFeedback && ! canFork ) return null
176149
177150 return (
178151 < >
179152 < div className = 'flex items-center gap-0.5' >
180- { hasContent && (
153+ { canCopyContent && (
181154 < Tooltip . Root >
182155 < Tooltip . Trigger asChild >
183156 < button
0 commit comments