@@ -634,11 +634,50 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
634634
635635 const handlePaste = useCallback (
636636 async ( e : React . ClipboardEvent ) => {
637+ const hasHtml = Array . from ( e . clipboardData . types ) . includes ( "text/html" )
638+ if ( hasHtml && navigator . clipboard ?. read ) {
639+ e . preventDefault ( )
640+ try {
641+ const clipboardItems = await navigator . clipboard . read ( )
642+ const htmlItem = clipboardItems . find ( ( item ) => item . types . includes ( "text/html" ) )
643+ if ( htmlItem ) {
644+ const htmlBlob = await htmlItem . getType ( "text/html" )
645+ const htmlText = await htmlBlob . text ( )
646+ const parser = new DOMParser ( )
647+ const doc = parser . parseFromString ( htmlText , "text/html" )
648+ const plainText = doc . body . textContent ?. trim ( ) || ""
649+ const imgElements = doc . querySelectorAll ( "img" )
650+ const imageSrcs = Array . from ( imgElements )
651+ . map ( ( img ) => img . src )
652+ . filter ( ( src ) => src . startsWith ( "data:image/" ) )
653+ const availableSlots = MAX_IMAGES_PER_MESSAGE - selectedImages . length
654+ const newImages = imageSrcs . slice ( 0 , availableSlots )
655+ if ( imageSrcs . length > newImages . length ) {
656+ console . warn (
657+ `只能粘贴 ${ availableSlots } 张图片,已忽略剩余 ${
658+ imageSrcs . length - newImages . length
659+ } 张`,
660+ )
661+ }
662+ if ( plainText ) {
663+ const newValue =
664+ inputValue . slice ( 0 , cursorPosition ) + plainText + inputValue . slice ( cursorPosition )
665+ setInputValue ( newValue )
666+ const newCursorPosition = cursorPosition + plainText . length
667+ setCursorPosition ( newCursorPosition )
668+ setIntendedCursorPosition ( newCursorPosition )
669+ }
670+ if ( newImages . length > 0 ) {
671+ setSelectedImages ( ( prev ) => [ ...prev , ...newImages ] )
672+ }
673+ return
674+ }
675+ } catch ( err ) {
676+ console . warn ( "Rich text paste failed, falling back to legacy paste." , err )
677+ }
678+ }
637679 const items = e . clipboardData . items
638-
639680 const pastedText = e . clipboardData . getData ( "text" )
640- // Check if the pasted content is a URL, add space after so user
641- // can easily delete if they don't want it.
642681 const urlRegex = / ^ \S + : \/ \/ \S + $ /
643682 if ( urlRegex . test ( pastedText . trim ( ) ) ) {
644683 e . preventDefault ( )
@@ -650,39 +689,29 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
650689 setCursorPosition ( newCursorPosition )
651690 setIntendedCursorPosition ( newCursorPosition )
652691 setShowContextMenu ( false )
653-
654- // Scroll to new cursor position.
655692 setTimeout ( ( ) => {
656693 if ( textAreaRef . current ) {
657694 textAreaRef . current . blur ( )
658695 textAreaRef . current . focus ( )
659696 }
660697 } , 0 )
661-
662698 return
663699 }
664-
665700 const acceptedTypes = [ "png" , "jpeg" , "webp" ]
666-
667701 const imageItems = Array . from ( items ) . filter ( ( item ) => {
668702 const [ type , subtype ] = item . type . split ( "/" )
669703 return type === "image" && acceptedTypes . includes ( subtype )
670704 } )
671-
672705 if ( ! shouldDisableImages && imageItems . length > 0 ) {
673706 e . preventDefault ( )
674-
675707 const imagePromises = imageItems . map ( ( item ) => {
676708 return new Promise < string | null > ( ( resolve ) => {
677709 const blob = item . getAsFile ( )
678-
679710 if ( ! blob ) {
680711 resolve ( null )
681712 return
682713 }
683-
684714 const reader = new FileReader ( )
685-
686715 reader . onloadend = ( ) => {
687716 if ( reader . error ) {
688717 console . error ( t ( "chat:errorReadingFile" ) , reader . error )
@@ -692,22 +721,30 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
692721 resolve ( typeof result === "string" ? result : null )
693722 }
694723 }
695-
696724 reader . readAsDataURL ( blob )
697725 } )
698726 } )
699-
700727 const imageDataArray = await Promise . all ( imagePromises )
701728 const dataUrls = imageDataArray . filter ( ( dataUrl ) : dataUrl is string => dataUrl !== null )
702-
703729 if ( dataUrls . length > 0 ) {
704730 setSelectedImages ( ( prevImages ) => [ ...prevImages , ...dataUrls ] . slice ( 0 , MAX_IMAGES_PER_MESSAGE ) )
705731 } else {
706732 console . warn ( t ( "chat:noValidImages" ) )
707733 }
708734 }
709735 } ,
710- [ shouldDisableImages , setSelectedImages , cursorPosition , setInputValue , inputValue , t ] ,
736+ [
737+ shouldDisableImages ,
738+ setSelectedImages ,
739+ cursorPosition ,
740+ setInputValue ,
741+ inputValue ,
742+ t ,
743+ selectedImages ,
744+ setIntendedCursorPosition ,
745+ setShowContextMenu ,
746+ setCursorPosition ,
747+ ] ,
711748 )
712749
713750 const handleMenuMouseDown = useCallback ( ( ) => {
0 commit comments