Unverified Commit 1b04382a authored by zxhlyh's avatar zxhlyh Committed by GitHub

fix: chat agent mode content copy (#2418)

parent 71e5828d
import type { FC } from 'react' import type { FC } from 'react'
import { memo } from 'react'
import type { import type {
ChatItem, ChatItem,
VisionFile, VisionFile,
} from '../../types' } from '../../types'
import { useChatContext } from '../context'
import { Markdown } from '@/app/components/base/markdown' import { Markdown } from '@/app/components/base/markdown'
import Thought from '@/app/components/app/chat/thought' import Thought from '@/app/components/app/chat/thought'
import ImageGallery from '@/app/components/base/image-gallery' import ImageGallery from '@/app/components/base/image-gallery'
import type { Emoji } from '@/app/components/tools/types'
type AgentContentProps = { type AgentContentProps = {
item: ChatItem item: ChatItem
responsing?: boolean
allToolIcons?: Record<string, string | Emoji>
} }
const AgentContent: FC<AgentContentProps> = ({ const AgentContent: FC<AgentContentProps> = ({
item, item,
responsing,
allToolIcons,
}) => { }) => {
const {
allToolIcons,
isResponsing,
} = useChatContext()
const { const {
annotation, annotation,
agent_thoughts, agent_thoughts,
...@@ -45,7 +46,7 @@ const AgentContent: FC<AgentContentProps> = ({ ...@@ -45,7 +46,7 @@ const AgentContent: FC<AgentContentProps> = ({
<Thought <Thought
thought={thought} thought={thought}
allToolIcons={allToolIcons || {}} allToolIcons={allToolIcons || {}}
isFinished={!!thought.observation || !isResponsing} isFinished={!!thought.observation || !responsing}
/> />
)} )}
...@@ -58,4 +59,4 @@ const AgentContent: FC<AgentContentProps> = ({ ...@@ -58,4 +59,4 @@ const AgentContent: FC<AgentContentProps> = ({
) )
} }
export default AgentContent export default memo(AgentContent)
import type { FC } from 'react' import type { FC } from 'react'
import { memo } from 'react'
import type { ChatItem } from '../../types' import type { ChatItem } from '../../types'
import { Markdown } from '@/app/components/base/markdown' import { Markdown } from '@/app/components/base/markdown'
...@@ -19,4 +20,4 @@ const BasicContent: FC<BasicContentProps> = ({ ...@@ -19,4 +20,4 @@ const BasicContent: FC<BasicContentProps> = ({
return <Markdown content={content} /> return <Markdown content={content} />
} }
export default BasicContent export default memo(BasicContent)
import type { FC } from 'react' import type {
FC,
ReactNode,
} from 'react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { ChatItem } from '../../types' import type {
import { useChatContext } from '../context' ChatConfig,
import { useCurrentAnswerIsResponsing } from '../hooks' ChatItem,
} from '../../types'
import Operation from './operation' import Operation from './operation'
import AgentContent from './agent-content' import AgentContent from './agent-content'
import BasicContent from './basic-content' import BasicContent from './basic-content'
...@@ -12,23 +17,27 @@ import { AnswerTriangle } from '@/app/components/base/icons/src/vender/solid/gen ...@@ -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 LoadingAnim from '@/app/components/app/chat/loading-anim'
import Citation from '@/app/components/app/chat/citation' import Citation from '@/app/components/app/chat/citation'
import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item' import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item'
import type { Emoji } from '@/app/components/tools/types'
type AnswerProps = { type AnswerProps = {
item: ChatItem item: ChatItem
question: string question: string
index: number index: number
config?: ChatConfig
answerIcon?: ReactNode
responsing?: boolean
allToolIcons?: Record<string, string | Emoji>
} }
const Answer: FC<AnswerProps> = ({ const Answer: FC<AnswerProps> = ({
item, item,
question, question,
index, index,
config,
answerIcon,
responsing,
allToolIcons,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const {
config,
answerIcon,
} = useChatContext()
const responsing = useCurrentAnswerIsResponsing(item.id)
const { const {
content, content,
citation, citation,
...@@ -83,7 +92,11 @@ const Answer: FC<AnswerProps> = ({ ...@@ -83,7 +92,11 @@ const Answer: FC<AnswerProps> = ({
} }
{ {
hasAgentThoughts && ( hasAgentThoughts && (
<AgentContent item={item} /> <AgentContent
item={item}
responsing={responsing}
allToolIcons={allToolIcons}
/>
) )
} }
{ {
...@@ -108,4 +121,4 @@ const Answer: FC<AnswerProps> = ({ ...@@ -108,4 +121,4 @@ const Answer: FC<AnswerProps> = ({
) )
} }
export default Answer export default memo(Answer)
import type { FC } from 'react' import type { FC } from 'react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { ChatItem } from '../../types' import type { ChatItem } from '../../types'
import { formatNumber } from '@/utils/format' import { formatNumber } from '@/utils/format'
...@@ -42,4 +43,4 @@ const More: FC<MoreProps> = ({ ...@@ -42,4 +43,4 @@ const More: FC<MoreProps> = ({
) )
} }
export default More export default memo(More)
import type { FC } from 'react' import type { FC } from 'react'
import { useState } from 'react' import {
memo,
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { ChatItem } from '../../types' import type { ChatItem } from '../../types'
import { useCurrentAnswerIsResponsing } from '../hooks'
import { useChatContext } from '../context' import { useChatContext } from '../context'
import CopyBtn from '@/app/components/app/chat/copy-btn' import CopyBtn from '@/app/components/app/chat/copy-btn'
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication' import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
...@@ -34,17 +37,24 @@ const Operation: FC<OperationProps> = ({ ...@@ -34,17 +37,24 @@ const Operation: FC<OperationProps> = ({
onFeedback, onFeedback,
} = useChatContext() } = useChatContext()
const [isShowReplyModal, setIsShowReplyModal] = useState(false) const [isShowReplyModal, setIsShowReplyModal] = useState(false)
const responsing = useCurrentAnswerIsResponsing(item.id)
const { const {
id, id,
isOpeningStatement, isOpeningStatement,
content, content: messageContent,
annotation, annotation,
feedback, feedback,
agent_thoughts,
} = item } = item
const hasAnnotation = !!annotation?.id const hasAnnotation = !!annotation?.id
const [localFeedback, setLocalFeedback] = useState(feedback) 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) => { const handleFeedback = async (rating: 'like' | 'dislike' | null) => {
if (!config?.supportFeedback || !onFeedback) if (!config?.supportFeedback || !onFeedback)
return return
...@@ -56,7 +66,7 @@ const Operation: FC<OperationProps> = ({ ...@@ -56,7 +66,7 @@ const Operation: FC<OperationProps> = ({
return ( return (
<div className='absolute top-[-14px] right-[-14px] flex justify-end gap-1'> <div className='absolute top-[-14px] right-[-14px] flex justify-end gap-1'>
{ {
!isOpeningStatement && !responsing && ( !isOpeningStatement && (
<CopyBtn <CopyBtn
value={content} value={content}
className='hidden group-hover:block' className='hidden group-hover:block'
...@@ -159,4 +169,4 @@ const Operation: FC<OperationProps> = ({ ...@@ -159,4 +169,4 @@ const Operation: FC<OperationProps> = ({
) )
} }
export default Operation export default memo(Operation)
import type { FC } from 'react' import type { FC } from 'react'
import { memo } from 'react'
import type { ChatItem } from '../../types' import type { ChatItem } from '../../types'
import { useChatContext } from '../context' import { useChatContext } from '../context'
...@@ -32,4 +33,4 @@ const SuggestedQuestions: FC<SuggestedQuestionsProps> = ({ ...@@ -32,4 +33,4 @@ const SuggestedQuestions: FC<SuggestedQuestionsProps> = ({
) )
} }
export default SuggestedQuestions export default memo(SuggestedQuestions)
import type { FC } from 'react' import type { FC } from 'react'
import { import {
memo,
useRef, useRef,
useState, useState,
} from 'react' } from 'react'
...@@ -126,100 +127,102 @@ const ChatInput: FC<ChatInputProps> = ({ ...@@ -126,100 +127,102 @@ const ChatInput: FC<ChatInputProps> = ({
) )
return ( return (
<div <div className='relative'>
className={` <div
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
className={` 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 p-[5.5px] max-h-[150px] bg-white border-[1.5px] border-gray-200 rounded-xl overflow-y-auto
${visionConfig?.enabled && 'pl-12'} ${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 visionConfig?.enabled && (
? ( <>
<div className='flex justify-center items-center ml-2 w-8 h-8 cursor-pointer hover:bg-gray-100 rounded-lg' onClick={() => setQuery('')}> <div className='absolute bottom-2 left-2 flex items-center'>
<XCircle className='w-4 h-4 text-[#98A2B3]' /> <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> </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 <div className='flex justify-center items-center ml-2 w-8 h-8 cursor-pointer hover:bg-gray-100 rounded-lg' onClick={() => setQuery('')}>
className='group flex justify-center items-center ml-2 w-8 h-8 hover:bg-primary-50 rounded-lg cursor-pointer' <XCircle className='w-4 h-4 text-[#98A2B3]' />
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> </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> </div>
{
voiceInputShow && (
<VoiceInput
onCancel={() => setVoiceInputShow(false)}
onConverted={text => setQuery(text)}
/>
)
}
</div> </div>
) )
} }
export default ChatInput export default memo(ChatInput)
...@@ -14,7 +14,6 @@ import type { ...@@ -14,7 +14,6 @@ import type {
PromptVariable, PromptVariable,
VisionFile, VisionFile,
} from '../types' } from '../types'
import { useChatContext } from './context'
import { TransferMethod } from '@/types/app' import { TransferMethod } from '@/types/app'
import { useToastContext } from '@/app/components/base/toast' import { useToastContext } from '@/app/components/base/toast'
import { ssePost } from '@/service/base' import { ssePost } from '@/service/base'
...@@ -507,14 +506,3 @@ export const useChat = ( ...@@ -507,14 +506,3 @@ export const useChat = (
handleAnnotationRemoved, handleAnnotationRemoved,
} }
} }
export const useCurrentAnswerIsResponsing = (answerId: string) => {
const {
isResponsing,
chatList,
} = useChatContext()
const isLast = answerId === chatList[chatList.length - 1]?.id
return isLast && isResponsing
}
...@@ -140,12 +140,17 @@ const Chat: FC<ChatProps> = ({ ...@@ -140,12 +140,17 @@ const Chat: FC<ChatProps> = ({
{ {
chatList.map((item, index) => { chatList.map((item, index) => {
if (item.isAnswer) { if (item.isAnswer) {
const isLast = item.id === chatList[chatList.length - 1]?.id
return ( return (
<Answer <Answer
key={item.id} key={item.id}
item={item} item={item}
question={chatList[index - 1]?.content} question={chatList[index - 1]?.content}
index={index} index={index}
config={config}
answerIcon={answerIcon}
responsing={isLast && isResponsing}
allToolIcons={allToolIcons}
/> />
) )
} }
...@@ -153,6 +158,9 @@ const Chat: FC<ChatProps> = ({ ...@@ -153,6 +158,9 @@ const Chat: FC<ChatProps> = ({
<Question <Question
key={item.id} key={item.id}
item={item} item={item}
showPromptLog={showPromptLog}
questionIcon={questionIcon}
isResponsing={isResponsing}
/> />
) )
}) })
......
import type { FC } from 'react' import type {
import { useRef } from 'react' FC,
ReactNode,
} from 'react'
import {
memo,
useRef,
} from 'react'
import type { ChatItem } from '../types' import type { ChatItem } from '../types'
import { useChatContext } from './context'
import { QuestionTriangle } from '@/app/components/base/icons/src/vender/solid/general' import { QuestionTriangle } from '@/app/components/base/icons/src/vender/solid/general'
import { User } from '@/app/components/base/icons/src/public/avatar' import { User } from '@/app/components/base/icons/src/public/avatar'
import Log from '@/app/components/app/chat/log' import Log from '@/app/components/app/chat/log'
...@@ -10,16 +15,17 @@ import ImageGallery from '@/app/components/base/image-gallery' ...@@ -10,16 +15,17 @@ import ImageGallery from '@/app/components/base/image-gallery'
type QuestionProps = { type QuestionProps = {
item: ChatItem item: ChatItem
showPromptLog?: boolean
questionIcon?: ReactNode
isResponsing?: boolean
} }
const Question: FC<QuestionProps> = ({ const Question: FC<QuestionProps> = ({
item, item,
showPromptLog,
isResponsing,
questionIcon,
}) => { }) => {
const ref = useRef(null) const ref = useRef(null)
const {
showPromptLog,
isResponsing,
questionIcon,
} = useChatContext()
const { const {
content, content,
message_files, message_files,
...@@ -59,4 +65,4 @@ const Question: FC<QuestionProps> = ({ ...@@ -59,4 +65,4 @@ const Question: FC<QuestionProps> = ({
) )
} }
export default Question export default memo(Question)
import type { FC } from 'react' import type { FC } from 'react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { OnSend } from '../types' import type { OnSend } from '../types'
import { Star04 } from '@/app/components/base/icons/src/vender/solid/shapes' import { Star04 } from '@/app/components/base/icons/src/vender/solid/shapes'
...@@ -51,4 +52,4 @@ const TryToAsk: FC<TryToAskProps> = ({ ...@@ -51,4 +52,4 @@ const TryToAsk: FC<TryToAskProps> = ({
) )
} }
export default TryToAsk export default memo(TryToAsk)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment