@@ -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'
@@ -24,48 +25,11 @@ import { useForkMothershipChat } from '@/hooks/queries/mothership-chats'
2425import { useFolderStore } from '@/stores/folders/store'
2526
2627const SPECIAL_TAGS = 'thinking|options|usage_upgrade|credential|mothership-error|file|question'
27- const FENCED_CODE_BLOCK_PATTERN =
28- / ^ { 0 , 3 } ( ` { 3 , } ) [ ^ \r \n ] * \r ? \n ( [ \s \S ] * ?) (?: ^ { 0 , 3 } \1` * [ \t ] * \r ? $ | (? ! [ \s \S ] ) ) | ^ { 0 , 3 } ( ~ { 3 , } ) [ ^ \r \n ] * \r ? \n ( [ \s \S ] * ?) (?: ^ { 0 , 3 } \3~ * [ \t ] * \r ? $ | (? ! [ \s \S ] ) ) / gm
29- const CODE_BLOCK_PLACEHOLDER_PATTERN = / \u0000 c o d e - b l o c k - ( \d + ) \u0000 / g
3028
31- export function toPlainText ( raw : string ) : string {
32- const codeBlocks : string [ ] = [ ]
33- const contentWithCodePlaceholders = raw . replace (
34- FENCED_CODE_BLOCK_PATTERN ,
35- (
36- _match : string ,
37- _backtickFence : string | undefined ,
38- backtickCode : string | undefined ,
39- _tildeFence : string | undefined ,
40- tildeCode : string | undefined
41- ) => {
42- const code = backtickCode ?? tildeCode ?? ''
43- const index = codeBlocks . push ( code . replace ( / \r ? \n $ / , '' ) ) - 1
44- return `\u0000code-block-${ index } \u0000`
45- }
46- )
47- const contentWithoutSpecialTags = contentWithCodePlaceholders . replace (
48- new RegExp ( `<\\/?(${ SPECIAL_TAGS } )(?:>[\\s\\S]*?<\\/(${ SPECIAL_TAGS } )>|>)` , 'g' ) ,
49- ''
50- )
51-
52- return (
53- contentWithoutSpecialTags
54- // Strip markdown
55- . replace ( / ^ # { 1 , 6 } \s + / gm, '' )
56- . replace ( / \* \* ( .+ ?) \* \* / g, '$1' )
57- . replace ( / \* ( .+ ?) \* / g, '$1' )
58- . replace ( / ` ( .+ ?) ` / g, '$1' )
59- . replace ( / \[ ( [ ^ \] ] + ) \] \( [ ^ ) ] + \) / g, '$1' )
60- . replace ( / ^ [ > \- * ] \s + / gm, '' )
61- . replace ( / ! \[ [ ^ \] ] * \] \( [ ^ ) ] + \) / g, '' )
62- // Normalize whitespace
63- . replace ( / \n { 3 , } / g, '\n\n' )
64- . trim ( )
65- . replace ( CODE_BLOCK_PLACEHOLDER_PATTERN , ( _match : string , index : string ) => {
66- return codeBlocks [ Number ( index ) ] ?? ''
67- } )
68- )
29+ export function toCopyableMarkdown ( raw : string ) : string {
30+ return raw
31+ . replace ( new RegExp ( `<\\/?(${ SPECIAL_TAGS } )(?:>[\\s\\S]*?<\\/(${ SPECIAL_TAGS } )>|>)` , 'g' ) , '' )
32+ . trim ( )
6933}
7034
7135const ICON_CLASS = 'size-[14px]'
@@ -88,40 +52,27 @@ export const MessageActions = memo(function MessageActions({
8852 const router = useRouter ( )
8953 const params = useParams < { workspaceId : string } > ( )
9054 const { chatId } = useChatSurface ( )
91- const [ copied , setCopied ] = useState ( false )
55+ const { copied, copy : copyMessage } = useCopyToClipboard ( { resetMs : 1500 } )
9256 const [ copiedRequestId , setCopiedRequestId ] = useState ( false )
9357 const [ pendingFeedback , setPendingFeedback ] = useState < 'up' | 'down' | null > ( null )
9458 const [ feedbackText , setFeedbackText ] = useState ( '' )
95- const resetTimeoutRef = useRef < number | null > ( null )
9659 const requestIdTimeoutRef = useRef < number | null > ( null )
9760 const submitFeedback = useSubmitCopilotFeedback ( )
9861 const forkChat = useForkMothershipChat ( params . workspaceId )
9962
10063 useEffect ( ( ) => {
10164 return ( ) => {
102- if ( resetTimeoutRef . current !== null ) {
103- window . clearTimeout ( resetTimeoutRef . current )
104- }
10565 if ( requestIdTimeoutRef . current !== null ) {
10666 window . clearTimeout ( requestIdTimeoutRef . current )
10767 }
10868 }
10969 } , [ ] )
11070
111- const copyToClipboard = async ( ) => {
71+ const copyToClipboard = ( ) => {
11272 if ( ! content ) return
113- const text = toPlainText ( content )
114- if ( ! text ) return
115- try {
116- await navigator . clipboard . writeText ( text )
117- setCopied ( true )
118- if ( resetTimeoutRef . current !== null ) {
119- window . clearTimeout ( resetTimeoutRef . current )
120- }
121- resetTimeoutRef . current = window . setTimeout ( ( ) => setCopied ( false ) , 1500 )
122- } catch {
123- /* clipboard unavailable */
124- }
73+ const markdown = toCopyableMarkdown ( content )
74+ if ( ! markdown ) return
75+ void copyMessage ( markdown )
12576 }
12677
12778 const copyRequestId = async ( ) => {
0 commit comments