mirror of
https://github.com/langgenius/dify.git
synced 2024-11-16 11:42:29 +08:00
fix: chat agent mode content copy (#2418)
This commit is contained in:
parent
71e5828d41
commit
1b04382a9b
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
: speechToTextConfig?.enabled
|
||||
<div className='pl-[52px]'>
|
||||
<ImageList
|
||||
list={files}
|
||||
onRemove={onRemove}
|
||||
onReUpload={onReUpload}
|
||||
onImageLinkLoadSuccess={onImageLinkLoadSuccess}
|
||||
onImageLinkLoadError={onImageLinkLoadError}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue
Block a user