@@ -2,7 +2,7 @@ import React, { useMemo, useCallback, useRef, useState, useEffect } from 'react'
22import { CloseCircleFilled , SyncOutlined } from '@ant-design/icons' ;
33import { Typography , Button , Dropdown , Input , App , Avatar , Alert , Popconfirm , Popover , theme , Tag , Image , Tooltip , Modal , Spin } from 'antd' ;
44import type { InputRef } from 'antd' ;
5- import { Pencil , Share2 , FileImage , FileCode , FileText , FileType , Bot , Brain , Lightbulb , Code , Languages , Copy , RotateCcw , User , Trash2 , ChevronLeft , ChevronRight , ChevronDown , Scissors , Paperclip , AlertCircle , X , ArrowDown , ArrowUp , ArrowLeftRight , Zap , Sparkles , TextCursorInput , GitBranch , ChartNoAxesColumn , MessageSquare , ArrowUpRight , ArrowDownRight , Coins , Clock , Timer } from 'lucide-react' ;
5+ import { Pencil , Share2 , FileImage , FileCode , FileText , FileType , Bot , Brain , Lightbulb , Code , Languages , Copy , Check , RotateCcw , User , Trash2 , ChevronLeft , ChevronRight , ChevronDown , Scissors , Paperclip , AlertCircle , X , ArrowDown , ArrowUp , ArrowLeftRight , Zap , Sparkles , TextCursorInput , GitBranch , ChartNoAxesColumn , MessageSquare , ArrowUpRight , ArrowDownRight , Coins , Clock , Timer } from 'lucide-react' ;
66import { ModelIcon } from '@lobehub/icons' ;
77import { getConvIcon } from '@/lib/convIcon' ;
88import Bubble from '@ant-design/x/es/bubble' ;
@@ -28,6 +28,7 @@ import { McpContainerNode } from './McpContainerNode';
2828import { getDistanceToHistoryTop , shouldShowScrollToBottom } from './chatScroll' ;
2929import { formatTokenCount , formatSpeed , formatDuration } from '../gateway/tokenFormat' ;
3030import { getStreamingLoadingState } from './chatStreaming' ;
31+ import { useCopyToClipboard } from '@/hooks/useCopyToClipboard' ;
3132import { buildAssistantDisplayContent , shouldHideAssistantBubble } from './toolCallDisplay' ;
3233import { ChatScrollIndicator } from './ChatScrollIndicator' ;
3334import { ChatMinimap , MinimapScrollProvider } from './ChatMinimap' ;
@@ -504,7 +505,7 @@ function ChatD2BlockNode({
504505 const { t } = useTranslation ( ) ;
505506 const containerRef = useRef < HTMLDivElement | null > ( null ) ;
506507 const [ showSource , setShowSource ] = useState ( false ) ;
507- const [ copied , setCopied ] = useState ( false ) ;
508+ const { copy : copyD2 , isCopied : d2Copied } = useCopyToClipboard ( { timeout : 1000 } ) ;
508509 const [ svgMarkup , setSvgMarkup ] = useState ( '' ) ;
509510 const [ error , setError ] = useState < string | null > ( null ) ;
510511 const [ canRenderPreview , setCanRenderPreview ] = useState ( false ) ;
@@ -652,15 +653,6 @@ function ChatD2BlockNode({
652653 } ;
653654 } , [ canRenderPreview , isDark , node . code , showSource , token . colorBgContainer , token . colorBgElevated , token . colorBorder , token . colorBorderSecondary , token . colorText , token . colorTextQuaternary , token . colorTextSecondary , token . colorTextTertiary ] ) ;
654655
655- const handleCopy = useCallback ( async ( ) => {
656- if ( typeof navigator === 'undefined' || ! navigator . clipboard ?. writeText ) {
657- return ;
658- }
659-
660- await navigator . clipboard . writeText ( node . code ) ;
661- setCopied ( true ) ;
662- window . setTimeout ( ( ) => setCopied ( false ) , 1000 ) ;
663- } , [ node . code ] ) ;
664656
665657 const handleExport = useCallback ( ( ) => {
666658 if ( ! svgMarkup ) return ;
@@ -714,8 +706,8 @@ function ChatD2BlockNode({
714706 { t ( 'common.source' ) }
715707 </ button >
716708 </ div >
717- < button type = "button" className = "d2-action-btn p-2 text-xs rounded-md transition-colors hover:bg-[var(--vscode-editor-selectionBackground)]" aria-label = { copied ? 'Copied' : 'Copy' } onClick = { ( ) => void handleCopy ( ) } >
718- < Copy size = { 14 } />
709+ < button type = "button" className = "d2-action-btn p-2 text-xs rounded-md transition-colors hover:bg-[var(--vscode-editor-selectionBackground)]" aria-label = { d2Copied ? 'Copied' : 'Copy' } onClick = { ( ) => void copyD2 ( node . code ) } >
710+ { d2Copied ? < Check size = { 14 } /> : < Copy size = { 14 } /> }
719711 </ button >
720712 { svgMarkup ? (
721713 < button type = "button" className = "d2-action-btn p-2 text-xs rounded-md transition-colors hover:bg-[var(--vscode-editor-selectionBackground)]" aria-label = "Export" onClick = { handleExport } >
@@ -1366,6 +1358,7 @@ function AssistantFooter({
13661358 const deleteMessageGroup = useConversationStore ( ( s ) => s . deleteMessageGroup ) ;
13671359 const switchMessageVersion = useConversationStore ( ( s ) => s . switchMessageVersion ) ;
13681360 const branchConversation = useConversationStore ( ( s ) => s . branchConversation ) ;
1361+ const { copy : copyAssistant , isCopied : assistantCopied } = useCopyToClipboard ( ) ;
13691362 // Branch modal state
13701363 const [ branchModalOpen , setBranchModalOpen ] = useState ( false ) ;
13711364 const [ branchAsChild , setBranchAsChild ] = useState ( false ) ;
@@ -1469,12 +1462,12 @@ function AssistantFooter({
14691462 items = { [
14701463 {
14711464 key : 'copy' ,
1472- icon : < Copy size = { 14 } /> ,
1465+ icon : assistantCopied ? < Check size = { 14 } style = { { color : token . colorSuccess } } /> : < Copy size = { 14 } /> ,
14731466 label : t ( 'chat.copy' ) ,
14741467 onItemClick : ( ) => {
1475- navigator . clipboard
1476- . writeText ( assistantCopyText )
1477- . then ( ( ) => messageApi . success ( t ( 'chat.copied' ) ) ) ;
1468+ void copyAssistant ( assistantCopyText ) . then ( ok => {
1469+ if ( ok ) messageApi . success ( t ( 'chat.copied' ) ) ;
1470+ } ) ;
14781471 } ,
14791472 } ,
14801473 {
@@ -1789,6 +1782,7 @@ export function ChatView() {
17891782 const profile = useUserProfileStore ( ( s ) => s . profile ) ;
17901783 const resolvedAvatarSrc = useResolvedAvatarSrc ( profile . avatarType , profile . avatarValue ) ;
17911784 const isDarkMode = useResolvedDarkMode ( settings . theme_mode ) ;
1785+ const { copy : copyMessage , isCopiedFor : isUserMsgCopied } = useCopyToClipboard ( ) ;
17921786 const { darkTheme : codeBlockDarkTheme , themes : codeBlockThemes } = useMemo (
17931787 ( ) => getChatCodeThemes ( settings . code_theme ) ,
17941788 [ settings . code_theme ] ,
@@ -2476,12 +2470,12 @@ export function ChatView() {
24762470 items = { [
24772471 {
24782472 key : 'copy' ,
2479- icon : < Copy size = { 14 } /> ,
2473+ icon : ( ( ) => { const ct = stripAqbotTags ( String ( bubbleData . content ?? '' ) ) ; return isUserMsgCopied ( ct ) ? < Check size = { 14 } style = { { color : token . colorSuccess } } /> : < Copy size = { 14 } /> ; } ) ( ) ,
24802474 label : t ( 'chat.copy' ) ,
24812475 onItemClick : ( ) => {
2482- navigator . clipboard
2483- . writeText ( stripAqbotTags ( String ( bubbleData . content ?? '' ) ) )
2484- . then ( ( ) => messageApi . success ( t ( 'chat.copied' ) ) ) ;
2476+ void copyMessage ( stripAqbotTags ( String ( bubbleData . content ?? '' ) ) ) . then ( ok => {
2477+ if ( ok ) messageApi . success ( t ( 'chat.copied' ) ) ;
2478+ } ) ;
24852479 } ,
24862480 } ,
24872481 {
@@ -2594,10 +2588,6 @@ export function ChatView() {
25942588 conversationId = { activeConversationId }
25952589 onSwitchVersion = { ( pid , mid ) => switchMessageVersion ( activeConversationId , pid , mid ) }
25962590 onDeleteVersion = { ( mid ) => deleteMessage ( mid ) }
2597- onCopyContent = { ( content ) => {
2598- const text = stripAqbotTags ( content ) ;
2599- navigator . clipboard . writeText ( text ) . catch ( ( ) => { } ) ;
2600- } }
26012591 streamingMessageId = { streamingMessageId }
26022592 multiModelDoneMessageIds = { multiModelDoneMessageIds }
26032593 getModelDisplayInfo = { getModelDisplayInfo }
0 commit comments