Ver Fonte

fix: chat agent mode content copy (#2418)

zxhlyh há 1 ano atrás
pai
commit
1b04382a9b

+ 8 - 7
web/app/components/base/chat/chat/answer/agent-content.tsx

@@ -1,23 +1,24 @@
 import type { FC } from 'react'
+import { memo } from 'react'
 import type {
   ChatItem,
   VisionFile,
 } from '../../types'
-import { useChatContext } from '../context'
 import { Markdown } from '@/app/components/base/markdown'
 import Thought from '@/app/components/app/chat/thought'
 import ImageGallery from '@/app/components/base/image-gallery'
+import type { Emoji } from '@/app/components/tools/types'
 
 type AgentContentProps = {
   item: ChatItem
+  responsing?: boolean
+  allToolIcons?: Record<string, string | Emoji>
 }
 const AgentContent: FC<AgentContentProps> = ({
   item,
+  responsing,
+  allToolIcons,
 }) => {
-  const {
-    allToolIcons,
-    isResponsing,
-  } = useChatContext()
   const {
     annotation,
     agent_thoughts,
@@ -45,7 +46,7 @@ const AgentContent: FC<AgentContentProps> = ({
             <Thought
               thought={thought}
               allToolIcons={allToolIcons || {}}
-              isFinished={!!thought.observation || !isResponsing}
+              isFinished={!!thought.observation || !responsing}
             />
           )}
 
@@ -58,4 +59,4 @@ const AgentContent: FC<AgentContentProps> = ({
   )
 }
 
-export default AgentContent
+export default memo(AgentContent)

+ 2 - 1
web/app/components/base/chat/chat/answer/basic-content.tsx

@@ -1,4 +1,5 @@
 import type { FC } from 'react'
+import { memo } from 'react'
 import type { ChatItem } from '../../types'
 import { Markdown } from '@/app/components/base/markdown'
 
@@ -19,4 +20,4 @@ const BasicContent: FC<BasicContentProps> = ({
   return <Markdown content={content} />
 }
 
-export default BasicContent
+export default memo(BasicContent)

+ 24 - 11
web/app/components/base/chat/chat/answer/index.tsx

@@ -1,8 +1,13 @@
-import type { FC } from 'react'
+import type {
+  FC,
+  ReactNode,
+} from 'react'
+import { memo } from 'react'
 import { useTranslation } from 'react-i18next'
-import type { ChatItem } from '../../types'
-import { useChatContext } from '../context'
-import { useCurrentAnswerIsResponsing } from '../hooks'
+import type {
+  ChatConfig,
+  ChatItem,
+} from '../../types'
 import Operation from './operation'
 import AgentContent from './agent-content'
 import BasicContent from './basic-content'
@@ -12,23 +17,27 @@ import { AnswerTriangle } from '@/app/components/base/icons/src/vender/solid/gen
 import LoadingAnim from '@/app/components/app/chat/loading-anim'
 import Citation from '@/app/components/app/chat/citation'
 import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item'
+import type { Emoji } from '@/app/components/tools/types'
 
 type AnswerProps = {
   item: ChatItem
   question: string
   index: number
+  config?: ChatConfig
+  answerIcon?: ReactNode
+  responsing?: boolean
+  allToolIcons?: Record<string, string | Emoji>
 }
 const Answer: FC<AnswerProps> = ({
   item,
   question,
   index,
+  config,
+  answerIcon,
+  responsing,
+  allToolIcons,
 }) => {
   const { t } = useTranslation()
-  const {
-    config,
-    answerIcon,
-  } = useChatContext()
-  const responsing = useCurrentAnswerIsResponsing(item.id)
   const {
     content,
     citation,
@@ -83,7 +92,11 @@ const Answer: FC<AnswerProps> = ({
             }
             {
               hasAgentThoughts && (
-                <AgentContent item={item} />
+                <AgentContent
+                  item={item}
+                  responsing={responsing}
+                  allToolIcons={allToolIcons}
+                />
               )
             }
             {
@@ -108,4 +121,4 @@ const Answer: FC<AnswerProps> = ({
   )
 }
 
-export default Answer
+export default memo(Answer)

+ 2 - 1
web/app/components/base/chat/chat/answer/more.tsx

@@ -1,4 +1,5 @@
 import type { FC } from 'react'
+import { memo } from 'react'
 import { useTranslation } from 'react-i18next'
 import type { ChatItem } from '../../types'
 import { formatNumber } from '@/utils/format'
@@ -42,4 +43,4 @@ const More: FC<MoreProps> = ({
   )
 }
 
-export default More
+export default memo(More)

+ 16 - 6
web/app/components/base/chat/chat/answer/operation.tsx

@@ -1,8 +1,11 @@
 import type { FC } from 'react'
-import { useState } from 'react'
+import {
+  memo,
+  useMemo,
+  useState,
+} from 'react'
 import { useTranslation } from 'react-i18next'
 import type { ChatItem } from '../../types'
-import { useCurrentAnswerIsResponsing } from '../hooks'
 import { useChatContext } from '../context'
 import CopyBtn from '@/app/components/app/chat/copy-btn'
 import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
@@ -34,17 +37,24 @@ const Operation: FC<OperationProps> = ({
     onFeedback,
   } = useChatContext()
   const [isShowReplyModal, setIsShowReplyModal] = useState(false)
-  const responsing = useCurrentAnswerIsResponsing(item.id)
   const {
     id,
     isOpeningStatement,
-    content,
+    content: messageContent,
     annotation,
     feedback,
+    agent_thoughts,
   } = item
   const hasAnnotation = !!annotation?.id
   const [localFeedback, setLocalFeedback] = useState(feedback)
 
+  const content = useMemo(() => {
+    if (agent_thoughts?.length)
+      return agent_thoughts.reduce((acc, cur) => acc + cur.thought, '')
+
+    return messageContent
+  }, [agent_thoughts, messageContent])
+
   const handleFeedback = async (rating: 'like' | 'dislike' | null) => {
     if (!config?.supportFeedback || !onFeedback)
       return
@@ -56,7 +66,7 @@ const Operation: FC<OperationProps> = ({
   return (
     <div className='absolute top-[-14px] right-[-14px] flex justify-end gap-1'>
       {
-        !isOpeningStatement && !responsing && (
+        !isOpeningStatement && (
           <CopyBtn
             value={content}
             className='hidden group-hover:block'
@@ -159,4 +169,4 @@ const Operation: FC<OperationProps> = ({
   )
 }
 
-export default Operation
+export default memo(Operation)

+ 2 - 1
web/app/components/base/chat/chat/answer/suggested-questions.tsx

@@ -1,4 +1,5 @@
 import type { FC } from 'react'
+import { memo } from 'react'
 import type { ChatItem } from '../../types'
 import { useChatContext } from '../context'
 
@@ -32,4 +33,4 @@ const SuggestedQuestions: FC<SuggestedQuestionsProps> = ({
   )
 }
 
-export default SuggestedQuestions
+export default memo(SuggestedQuestions)

+ 87 - 84
web/app/components/base/chat/chat/chat-input.tsx

@@ -1,5 +1,6 @@
 import type { FC } from 'react'
 import {
+  memo,
   useRef,
   useState,
 } from 'react'
@@ -126,100 +127,102 @@ const ChatInput: FC<ChatInputProps> = ({
   )
 
   return (
-    <div
-      className={`
-        relative p-[5.5px] max-h-[150px] bg-white border-[1.5px] border-gray-200 rounded-xl overflow-y-auto
-        ${isDragActive && 'border-primary-600'}
-      `}
-    >
-      {
-        visionConfig?.enabled && (
-          <>
-            <div className='absolute bottom-2 left-2 flex items-center'>
-              <ChatImageUploader
-                settings={visionConfig}
-                onUpload={onUpload}
-                disabled={files.length >= visionConfig.number_limits}
-              />
-              <div className='mx-1 w-[1px] h-4 bg-black/5' />
-            </div>
-            <div className='pl-[52px]'>
-              <ImageList
-                list={files}
-                onRemove={onRemove}
-                onReUpload={onReUpload}
-                onImageLinkLoadSuccess={onImageLinkLoadSuccess}
-                onImageLinkLoadError={onImageLinkLoadError}
-              />
-            </div>
-          </>
-        )
-      }
-      <Textarea
+    <div className='relative'>
+      <div
         className={`
-          block w-full px-2 pr-[118px] py-[7px] leading-5 max-h-none text-sm text-gray-700 outline-none appearance-none resize-none
-          ${visionConfig?.enabled && 'pl-12'}
+          p-[5.5px] max-h-[150px] bg-white border-[1.5px] border-gray-200 rounded-xl overflow-y-auto
+          ${isDragActive && 'border-primary-600'}
         `}
-        value={query}
-        onChange={handleContentChange}
-        onKeyUp={handleKeyUp}
-        onKeyDown={handleKeyDown}
-        onPaste={onPaste}
-        onDragEnter={onDragEnter}
-        onDragLeave={onDragLeave}
-        onDragOver={onDragOver}
-        onDrop={onDrop}
-        autoSize
-      />
-      <div className='absolute bottom-[7px] right-2 flex items-center h-8'>
-        <div className='flex items-center px-1 h-5 rounded-md bg-gray-100 text-xs font-medium text-gray-500'>
-          {query.trim().length}
-        </div>
+      >
         {
-          query
-            ? (
-              <div className='flex justify-center items-center ml-2 w-8 h-8 cursor-pointer hover:bg-gray-100 rounded-lg' onClick={() => setQuery('')}>
-                <XCircle className='w-4 h-4 text-[#98A2B3]' />
+          visionConfig?.enabled && (
+            <>
+              <div className='absolute bottom-2 left-2 flex items-center'>
+                <ChatImageUploader
+                  settings={visionConfig}
+                  onUpload={onUpload}
+                  disabled={files.length >= visionConfig.number_limits}
+                />
+                <div className='mx-1 w-[1px] h-4 bg-black/5' />
+              </div>
+              <div className='pl-[52px]'>
+                <ImageList
+                  list={files}
+                  onRemove={onRemove}
+                  onReUpload={onReUpload}
+                  onImageLinkLoadSuccess={onImageLinkLoadSuccess}
+                  onImageLinkLoadError={onImageLinkLoadError}
+                />
               </div>
-            )
-            : speechToTextConfig?.enabled
+            </>
+          )
+        }
+        <Textarea
+          className={`
+            block w-full px-2 pr-[118px] py-[7px] leading-5 max-h-none text-sm text-gray-700 outline-none appearance-none resize-none
+            ${visionConfig?.enabled && 'pl-12'}
+          `}
+          value={query}
+          onChange={handleContentChange}
+          onKeyUp={handleKeyUp}
+          onKeyDown={handleKeyDown}
+          onPaste={onPaste}
+          onDragEnter={onDragEnter}
+          onDragLeave={onDragLeave}
+          onDragOver={onDragOver}
+          onDrop={onDrop}
+          autoSize
+        />
+        <div className='absolute bottom-[7px] right-2 flex items-center h-8'>
+          <div className='flex items-center px-1 h-5 rounded-md bg-gray-100 text-xs font-medium text-gray-500'>
+            {query.trim().length}
+          </div>
+          {
+            query
               ? (
-                <div
-                  className='group flex justify-center items-center ml-2 w-8 h-8 hover:bg-primary-50 rounded-lg cursor-pointer'
-                  onClick={handleVoiceInputShow}
-                >
-                  <Microphone01 className='block w-4 h-4 text-gray-500 group-hover:hidden' />
-                  <Microphone01Solid className='hidden w-4 h-4 text-primary-600 group-hover:block' />
+                <div className='flex justify-center items-center ml-2 w-8 h-8 cursor-pointer hover:bg-gray-100 rounded-lg' onClick={() => setQuery('')}>
+                  <XCircle className='w-4 h-4 text-[#98A2B3]' />
                 </div>
               )
-              : null
+              : speechToTextConfig?.enabled
+                ? (
+                  <div
+                    className='group flex justify-center items-center ml-2 w-8 h-8 hover:bg-primary-50 rounded-lg cursor-pointer'
+                    onClick={handleVoiceInputShow}
+                  >
+                    <Microphone01 className='block w-4 h-4 text-gray-500 group-hover:hidden' />
+                    <Microphone01Solid className='hidden w-4 h-4 text-primary-600 group-hover:block' />
+                  </div>
+                )
+                : null
+          }
+          <div className='mx-2 w-[1px] h-4 bg-black opacity-5' />
+          {isMobile
+            ? sendBtn
+            : (
+              <TooltipPlus
+                popupContent={
+                  <div>
+                    <div>{t('common.operation.send')} Enter</div>
+                    <div>{t('common.operation.lineBreak')} Shift Enter</div>
+                  </div>
+                }
+              >
+                {sendBtn}
+              </TooltipPlus>
+            )}
+        </div>
+        {
+          voiceInputShow && (
+            <VoiceInput
+              onCancel={() => setVoiceInputShow(false)}
+              onConverted={text => setQuery(text)}
+            />
+          )
         }
-        <div className='mx-2 w-[1px] h-4 bg-black opacity-5' />
-        {isMobile
-          ? sendBtn
-          : (
-            <TooltipPlus
-              popupContent={
-                <div>
-                  <div>{t('common.operation.send')} Enter</div>
-                  <div>{t('common.operation.lineBreak')} Shift Enter</div>
-                </div>
-              }
-            >
-              {sendBtn}
-            </TooltipPlus>
-          )}
       </div>
-      {
-        voiceInputShow && (
-          <VoiceInput
-            onCancel={() => setVoiceInputShow(false)}
-            onConverted={text => setQuery(text)}
-          />
-        )
-      }
     </div>
   )
 }
 
-export default ChatInput
+export default memo(ChatInput)

+ 0 - 12
web/app/components/base/chat/chat/hooks.ts

@@ -14,7 +14,6 @@ import type {
   PromptVariable,
   VisionFile,
 } from '../types'
-import { useChatContext } from './context'
 import { TransferMethod } from '@/types/app'
 import { useToastContext } from '@/app/components/base/toast'
 import { ssePost } from '@/service/base'
@@ -507,14 +506,3 @@ export const useChat = (
     handleAnnotationRemoved,
   }
 }
-
-export const useCurrentAnswerIsResponsing = (answerId: string) => {
-  const {
-    isResponsing,
-    chatList,
-  } = useChatContext()
-
-  const isLast = answerId === chatList[chatList.length - 1]?.id
-
-  return isLast && isResponsing
-}

+ 8 - 0
web/app/components/base/chat/chat/index.tsx

@@ -140,12 +140,17 @@ const Chat: FC<ChatProps> = ({
             {
               chatList.map((item, index) => {
                 if (item.isAnswer) {
+                  const isLast = item.id === chatList[chatList.length - 1]?.id
                   return (
                     <Answer
                       key={item.id}
                       item={item}
                       question={chatList[index - 1]?.content}
                       index={index}
+                      config={config}
+                      answerIcon={answerIcon}
+                      responsing={isLast && isResponsing}
+                      allToolIcons={allToolIcons}
                     />
                   )
                 }
@@ -153,6 +158,9 @@ const Chat: FC<ChatProps> = ({
                   <Question
                     key={item.id}
                     item={item}
+                    showPromptLog={showPromptLog}
+                    questionIcon={questionIcon}
+                    isResponsing={isResponsing}
                   />
                 )
               })

+ 15 - 9
web/app/components/base/chat/chat/question.tsx

@@ -1,7 +1,12 @@
-import type { FC } from 'react'
-import { useRef } from 'react'
+import type {
+  FC,
+  ReactNode,
+} from 'react'
+import {
+  memo,
+  useRef,
+} from 'react'
 import type { ChatItem } from '../types'
-import { useChatContext } from './context'
 import { QuestionTriangle } from '@/app/components/base/icons/src/vender/solid/general'
 import { User } from '@/app/components/base/icons/src/public/avatar'
 import Log from '@/app/components/app/chat/log'
@@ -10,16 +15,17 @@ import ImageGallery from '@/app/components/base/image-gallery'
 
 type QuestionProps = {
   item: ChatItem
+  showPromptLog?: boolean
+  questionIcon?: ReactNode
+  isResponsing?: boolean
 }
 const Question: FC<QuestionProps> = ({
   item,
+  showPromptLog,
+  isResponsing,
+  questionIcon,
 }) => {
   const ref = useRef(null)
-  const {
-    showPromptLog,
-    isResponsing,
-    questionIcon,
-  } = useChatContext()
   const {
     content,
     message_files,
@@ -59,4 +65,4 @@ const Question: FC<QuestionProps> = ({
   )
 }
 
-export default Question
+export default memo(Question)

+ 2 - 1
web/app/components/base/chat/chat/try-to-ask.tsx

@@ -1,4 +1,5 @@
 import type { FC } from 'react'
+import { memo } from 'react'
 import { useTranslation } from 'react-i18next'
 import type { OnSend } from '../types'
 import { Star04 } from '@/app/components/base/icons/src/vender/solid/shapes'
@@ -51,4 +52,4 @@ const TryToAsk: FC<TryToAskProps> = ({
   )
 }
 
-export default TryToAsk
+export default memo(TryToAsk)