FEAT: NEW WORKFLOW ENGINE (#3160)
Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: Yeuoly <admin@srmxy.cn> Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: StyleZhang <jasonapring2015@outlook.com> Co-authored-by: jyong <jyong@dify.ai> Co-authored-by: nite-knite <nkCoding@gmail.com> Co-authored-by: jyong <718720800@qq.com>
This commit is contained in:
@@ -17,7 +17,7 @@ const BasicContent: FC<BasicContentProps> = ({
|
||||
if (annotation?.logAnnotation)
|
||||
return <Markdown content={annotation?.logAnnotation.content || ''} />
|
||||
|
||||
return <Markdown content={content} />
|
||||
return <Markdown content={content} className={`${item.isError && '!text-[#F04438]'}`} />
|
||||
}
|
||||
|
||||
export default memo(BasicContent)
|
||||
|
@@ -2,7 +2,7 @@ import type {
|
||||
FC,
|
||||
ReactNode,
|
||||
} from 'react'
|
||||
import { memo } from 'react'
|
||||
import { memo, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
ChatConfig,
|
||||
@@ -13,7 +13,9 @@ import AgentContent from './agent-content'
|
||||
import BasicContent from './basic-content'
|
||||
import SuggestedQuestions from './suggested-questions'
|
||||
import More from './more'
|
||||
import WorkflowProcess from './workflow-process'
|
||||
import { AnswerTriangle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
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'
|
||||
@@ -27,6 +29,8 @@ type AnswerProps = {
|
||||
answerIcon?: ReactNode
|
||||
responding?: boolean
|
||||
allToolIcons?: Record<string, string | Emoji>
|
||||
showPromptLog?: boolean
|
||||
chatAnswerContainerInner?: string
|
||||
}
|
||||
const Answer: FC<AnswerProps> = ({
|
||||
item,
|
||||
@@ -36,6 +40,8 @@ const Answer: FC<AnswerProps> = ({
|
||||
answerIcon,
|
||||
responding,
|
||||
allToolIcons,
|
||||
showPromptLog,
|
||||
chatAnswerContainerInner,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
@@ -44,9 +50,33 @@ const Answer: FC<AnswerProps> = ({
|
||||
agent_thoughts,
|
||||
more,
|
||||
annotation,
|
||||
workflowProcess,
|
||||
} = item
|
||||
const hasAgentThoughts = !!agent_thoughts?.length
|
||||
|
||||
const [containerWidth, setContainerWidth] = useState(0)
|
||||
const [contentWidth, setContentWidth] = useState(0)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const getContainerWidth = () => {
|
||||
if (containerRef.current)
|
||||
setContainerWidth(containerRef.current?.clientWidth + 16)
|
||||
}
|
||||
const getContentWidth = () => {
|
||||
if (contentRef.current)
|
||||
setContentWidth(contentRef.current?.clientWidth)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getContainerWidth()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!responding)
|
||||
getContentWidth()
|
||||
}, [responding])
|
||||
|
||||
return (
|
||||
<div className='flex mb-2 last:mb-0'>
|
||||
<div className='shrink-0 relative w-10 h-10'>
|
||||
@@ -65,19 +95,43 @@ const Answer: FC<AnswerProps> = ({
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='chat-answer-container grow w-0 group ml-4'>
|
||||
<div className='relative pr-10'>
|
||||
<div className='chat-answer-container grow w-0 ml-4' ref={containerRef}>
|
||||
<div className={`group relative pr-10 ${chatAnswerContainerInner}`}>
|
||||
<AnswerTriangle className='absolute -left-2 top-0 w-2 h-3 text-gray-100' />
|
||||
<div className='group relative inline-block px-4 py-3 max-w-full bg-gray-100 rounded-b-2xl rounded-tr-2xl text-sm text-gray-900'>
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={`
|
||||
relative inline-block px-4 py-3 max-w-full bg-gray-100 rounded-b-2xl rounded-tr-2xl text-sm text-gray-900
|
||||
${workflowProcess && 'w-full'}
|
||||
`}
|
||||
>
|
||||
{annotation?.id && (
|
||||
<div
|
||||
className='absolute -top-3.5 -right-3.5 box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-[#444CE7] shadow-md group-hover:hidden'
|
||||
>
|
||||
<div className='p-1 rounded-lg bg-[#EEF4FF] '>
|
||||
<MessageFast className='w-4 h-4' />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
!responding && (
|
||||
<Operation
|
||||
hasWorkflowProcess={!!workflowProcess}
|
||||
maxSize={containerWidth - contentWidth - 4}
|
||||
contentWidth={contentWidth}
|
||||
item={item}
|
||||
question={question}
|
||||
index={index}
|
||||
showPromptLog={showPromptLog}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
workflowProcess && (
|
||||
<WorkflowProcess data={workflowProcess} hideInfo />
|
||||
)
|
||||
}
|
||||
{
|
||||
responding && !content && !hasAgentThoughts && (
|
||||
<div className='flex items-center justify-center w-6 h-5'>
|
||||
|
@@ -4,6 +4,7 @@ import {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ChatItem } from '../../types'
|
||||
import { useChatContext } from '../context'
|
||||
@@ -17,16 +18,25 @@ import {
|
||||
ThumbsUp,
|
||||
} from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
import Log from '@/app/components/app/chat/log'
|
||||
|
||||
type OperationProps = {
|
||||
item: ChatItem
|
||||
question: string
|
||||
index: number
|
||||
showPromptLog?: boolean
|
||||
maxSize: number
|
||||
contentWidth: number
|
||||
hasWorkflowProcess: boolean
|
||||
}
|
||||
const Operation: FC<OperationProps> = ({
|
||||
item,
|
||||
question,
|
||||
index,
|
||||
showPromptLog,
|
||||
maxSize,
|
||||
contentWidth,
|
||||
hasWorkflowProcess,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
@@ -63,39 +73,134 @@ const Operation: FC<OperationProps> = ({
|
||||
setLocalFeedback({ rating })
|
||||
}
|
||||
|
||||
const operationWidth = useMemo(() => {
|
||||
let width = 0
|
||||
if (!isOpeningStatement)
|
||||
width += 28
|
||||
if (!isOpeningStatement && showPromptLog)
|
||||
width += 102 + 8
|
||||
if (!isOpeningStatement && config?.text_to_speech?.enabled)
|
||||
width += 33
|
||||
if (!isOpeningStatement && config?.supportAnnotation && config?.annotation_reply?.enabled)
|
||||
width += 56 + 8
|
||||
if (config?.supportFeedback && !localFeedback?.rating && onFeedback && !isOpeningStatement)
|
||||
width += 60 + 8
|
||||
if (config?.supportFeedback && localFeedback?.rating && onFeedback && !isOpeningStatement)
|
||||
width += 28 + 8
|
||||
return width
|
||||
}, [isOpeningStatement, showPromptLog, config?.text_to_speech?.enabled, config?.supportAnnotation, config?.annotation_reply?.enabled, config?.supportFeedback, localFeedback?.rating, onFeedback])
|
||||
|
||||
const positionRight = useMemo(() => operationWidth < maxSize, [operationWidth, maxSize])
|
||||
|
||||
return (
|
||||
<div className='absolute top-[-14px] right-[-14px] flex justify-end gap-1'>
|
||||
{
|
||||
!isOpeningStatement && (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute flex justify-end gap-1',
|
||||
hasWorkflowProcess && '-top-3.5 -right-3.5',
|
||||
!positionRight && '-top-3.5 -right-3.5',
|
||||
!hasWorkflowProcess && positionRight && '!top-[9px]',
|
||||
)}
|
||||
style={(!hasWorkflowProcess && positionRight) ? { left: contentWidth + 8 } : {}}
|
||||
>
|
||||
{!isOpeningStatement && (
|
||||
<CopyBtn
|
||||
value={content}
|
||||
className='hidden group-hover:block'
|
||||
/>
|
||||
)
|
||||
}
|
||||
)}
|
||||
|
||||
{(!isOpeningStatement && config?.text_to_speech?.enabled) && (
|
||||
<AudioBtn
|
||||
value={content}
|
||||
voice={config?.text_to_speech?.voice}
|
||||
className='hidden group-hover:block'
|
||||
/>
|
||||
)}
|
||||
{(!isOpeningStatement && config?.supportAnnotation && config.annotation_reply?.enabled) && (
|
||||
<AnnotationCtrlBtn
|
||||
appId={config?.appId || ''}
|
||||
messageId={id}
|
||||
annotationId={annotation?.id || ''}
|
||||
className='hidden group-hover:block ml-1 shrink-0'
|
||||
cached={hasAnnotation}
|
||||
query={question}
|
||||
answer={content}
|
||||
onAdded={(id, authorName) => onAnnotationAdded?.(id, authorName, question, content, index)}
|
||||
onEdit={() => setIsShowReplyModal(true)}
|
||||
onRemoved={() => onAnnotationRemoved?.(index)}
|
||||
/>
|
||||
)}
|
||||
{!isOpeningStatement && (showPromptLog || config?.text_to_speech?.enabled) && (
|
||||
<div className='hidden group-hover:flex items-center w-max h-[28px] p-0.5 rounded-lg bg-white border-[0.5px] border-gray-100 shadow-md shrink-0'>
|
||||
{showPromptLog && (
|
||||
<Log logItem={item} />
|
||||
)}
|
||||
{(config?.text_to_speech?.enabled) && (
|
||||
<>
|
||||
<div className='mx-1 w-[1px] h-[14px] bg-gray-200'/>
|
||||
<AudioBtn
|
||||
value={content}
|
||||
voice={config?.text_to_speech?.voice}
|
||||
className='hidden group-hover:block'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!isOpeningStatement && config?.supportAnnotation && config.annotation_reply?.enabled) && (
|
||||
<AnnotationCtrlBtn
|
||||
appId={config?.appId || ''}
|
||||
messageId={id}
|
||||
annotationId={annotation?.id || ''}
|
||||
className='hidden group-hover:block ml-1 shrink-0'
|
||||
cached={hasAnnotation}
|
||||
query={question}
|
||||
answer={content}
|
||||
onAdded={(id, authorName) => onAnnotationAdded?.(id, authorName, question, content, index)}
|
||||
onEdit={() => setIsShowReplyModal(true)}
|
||||
onRemoved={() => onAnnotationRemoved?.(index)}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
!positionRight && annotation?.id && (
|
||||
<div
|
||||
className='relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-[#444CE7] shadow-md group-hover:hidden'
|
||||
>
|
||||
<div className='p-1 rounded-lg bg-[#EEF4FF] '>
|
||||
<MessageFast className='w-4 h-4' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
config?.supportFeedback && !localFeedback?.rating && onFeedback && !isOpeningStatement && (
|
||||
<div className='hidden group-hover:flex ml-1 shrink-0 items-center px-0.5 bg-white border-[0.5px] border-gray-100 shadow-md text-gray-500 rounded-lg'>
|
||||
<TooltipPlus popupContent={t('appDebug.operation.agree')}>
|
||||
<div
|
||||
className='flex items-center justify-center mr-0.5 w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
|
||||
onClick={() => handleFeedback('like')}
|
||||
>
|
||||
<ThumbsUp className='w-4 h-4' />
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
<TooltipPlus popupContent={t('appDebug.operation.disagree')}>
|
||||
<div
|
||||
className='flex items-center justify-center w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
|
||||
onClick={() => handleFeedback('dislike')}
|
||||
>
|
||||
<ThumbsDown className='w-4 h-4' />
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
config?.supportFeedback && localFeedback?.rating && onFeedback && !isOpeningStatement && (
|
||||
<TooltipPlus popupContent={localFeedback.rating === 'like' ? t('appDebug.operation.cancelAgree') : t('appDebug.operation.cancelDisagree')}>
|
||||
<div
|
||||
className={`
|
||||
flex items-center justify-center w-7 h-7 rounded-[10px] border-[2px] border-white cursor-pointer
|
||||
${localFeedback.rating === 'like' && 'bg-blue-50 text-blue-600'}
|
||||
${localFeedback.rating === 'dislike' && 'bg-red-100 text-red-600'}
|
||||
`}
|
||||
onClick={() => handleFeedback(null)}
|
||||
>
|
||||
{
|
||||
localFeedback.rating === 'like' && (
|
||||
<ThumbsUp className='w-4 h-4' />
|
||||
)
|
||||
}
|
||||
{
|
||||
localFeedback.rating === 'dislike' && (
|
||||
<ThumbsDown className='w-4 h-4' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<EditReplyModal
|
||||
isShow={isShowReplyModal}
|
||||
onHide={() => setIsShowReplyModal(false)}
|
||||
@@ -109,65 +214,7 @@ const Operation: FC<OperationProps> = ({
|
||||
createdAt={annotation?.created_at}
|
||||
onRemove={() => onAnnotationRemoved?.(index)}
|
||||
/>
|
||||
{
|
||||
annotation?.id && (
|
||||
<div
|
||||
className='relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-[#444CE7] shadow-md'
|
||||
>
|
||||
<div className='p-1 rounded-lg bg-[#EEF4FF] '>
|
||||
<MessageFast className='w-4 h-4' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
config?.supportFeedback && !localFeedback?.rating && onFeedback && !isOpeningStatement && (
|
||||
<div className='hidden group-hover:flex ml-1 shrink-0 items-center px-0.5 bg-white border-[0.5px] border-gray-100 shadow-md text-gray-500 rounded-lg'>
|
||||
<TooltipPlus popupContent={t('appDebug.operation.agree')}>
|
||||
<div
|
||||
className='flex items-center justify-center mr-0.5 w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
|
||||
onClick={() => handleFeedback('like')}
|
||||
>
|
||||
<ThumbsUp className='w-4 h-4' />
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
<TooltipPlus popupContent={t('appDebug.operation.disagree')}>
|
||||
<div
|
||||
className='flex items-center justify-center w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
|
||||
onClick={() => handleFeedback('dislike')}
|
||||
>
|
||||
<ThumbsDown className='w-4 h-4' />
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
config?.supportFeedback && localFeedback?.rating && onFeedback && !isOpeningStatement && (
|
||||
<TooltipPlus popupContent={localFeedback.rating === 'like' ? t('appDebug.operation.cancelAgree') : t('appDebug.operation.cancelDisagree')}>
|
||||
<div
|
||||
className={`
|
||||
flex items-center justify-center w-7 h-7 rounded-[10px] border-[2px] border-white cursor-pointer
|
||||
${localFeedback.rating === 'like' && 'bg-blue-50 text-blue-600'}
|
||||
${localFeedback.rating === 'dislike' && 'bg-red-100 text-red-600'}
|
||||
`}
|
||||
onClick={() => handleFeedback(null)}
|
||||
>
|
||||
{
|
||||
localFeedback.rating === 'like' && (
|
||||
<ThumbsUp className='w-4 h-4' />
|
||||
)
|
||||
}
|
||||
{
|
||||
localFeedback.rating === 'dislike' && (
|
||||
<ThumbsDown className='w-4 h-4' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
108
web/app/components/base/chat/chat/answer/workflow-process.tsx
Normal file
108
web/app/components/base/chat/chat/answer/workflow-process.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { WorkflowProcess } from '../../types'
|
||||
import { CheckCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { AlertCircle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
|
||||
import { Loading02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import NodePanel from '@/app/components/workflow/run/node'
|
||||
|
||||
type WorkflowProcessProps = {
|
||||
data: WorkflowProcess
|
||||
grayBg?: boolean
|
||||
expand?: boolean
|
||||
hideInfo?: boolean
|
||||
}
|
||||
const WorkflowProcessItem = ({
|
||||
data,
|
||||
grayBg,
|
||||
expand = false,
|
||||
hideInfo = false,
|
||||
}: WorkflowProcessProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [collapse, setCollapse] = useState(!expand)
|
||||
const running = data.status === WorkflowRunningStatus.Running
|
||||
const succeeded = data.status === WorkflowRunningStatus.Succeeded
|
||||
const failed = data.status === WorkflowRunningStatus.Failed || data.status === WorkflowRunningStatus.Stopped
|
||||
|
||||
const background = useMemo(() => {
|
||||
if (running && !collapse)
|
||||
return 'linear-gradient(180deg, #E1E4EA 0%, #EAECF0 100%)'
|
||||
|
||||
if (succeeded && !collapse)
|
||||
return 'linear-gradient(180deg, #ECFDF3 0%, #F6FEF9 100%)'
|
||||
|
||||
if (failed && !collapse)
|
||||
return 'linear-gradient(180deg, #FEE4E2 0%, #FEF3F2 100%)'
|
||||
}, [running, succeeded, failed, collapse])
|
||||
|
||||
useEffect(() => {
|
||||
setCollapse(!expand)
|
||||
}, [expand])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'mb-2 rounded-xl border-[0.5px] border-black/[0.08]',
|
||||
collapse ? 'py-[7px]' : hideInfo ? 'pt-2 pb-1' : 'py-2',
|
||||
collapse && (!grayBg ? 'bg-white' : 'bg-gray-50'),
|
||||
hideInfo ? 'mx-[-8px] px-1' : 'w-full px-3',
|
||||
)}
|
||||
style={{
|
||||
background,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center h-[18px] cursor-pointer',
|
||||
hideInfo && 'px-[6px]',
|
||||
)}
|
||||
onClick={() => setCollapse(!collapse)}
|
||||
>
|
||||
{
|
||||
running && (
|
||||
<Loading02 className='shrink-0 mr-1 w-3 h-3 text-[#667085] animate-spin' />
|
||||
)
|
||||
}
|
||||
{
|
||||
succeeded && (
|
||||
<CheckCircle className='shrink-0 mr-1 w-3 h-3 text-[#12B76A]' />
|
||||
)
|
||||
}
|
||||
{
|
||||
failed && (
|
||||
<AlertCircle className='shrink-0 mr-1 w-3 h-3 text-[#F04438]' />
|
||||
)
|
||||
}
|
||||
<div className='grow text-xs font-medium text-gray-700'>
|
||||
{t('workflow.common.workflowProcess')}
|
||||
</div>
|
||||
<ChevronRight className={`'ml-1 w-3 h-3 text-gray-500' ${collapse ? '' : 'rotate-90'}`} />
|
||||
</div>
|
||||
{
|
||||
!collapse && (
|
||||
<div className='mt-1.5'>
|
||||
{
|
||||
data.tracing.map(node => (
|
||||
<div key={node.id} className='mb-1 last-of-type:mb-0'>
|
||||
<NodePanel
|
||||
nodeInfo={node}
|
||||
hideInfo={hideInfo}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkflowProcessItem
|
@@ -19,6 +19,7 @@ import { useToastContext } from '@/app/components/base/toast'
|
||||
import { ssePost } from '@/service/base'
|
||||
import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
|
||||
import type { Annotation } from '@/models/log'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
|
||||
type GetAbortController = (abortController: AbortController) => void
|
||||
type SendCallback = {
|
||||
@@ -318,10 +319,21 @@ export const useChat = (
|
||||
const requestion = draft[index - 1]
|
||||
draft[index - 1] = {
|
||||
...requestion,
|
||||
log: newResponseItem.message,
|
||||
}
|
||||
draft[index] = {
|
||||
...draft[index],
|
||||
log: [
|
||||
...newResponseItem.message,
|
||||
...(newResponseItem.message[newResponseItem.message.length - 1].role !== 'assistant'
|
||||
? [
|
||||
{
|
||||
role: 'assistant',
|
||||
text: newResponseItem.answer,
|
||||
files: newResponseItem.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
more: {
|
||||
time: dayjs.unix(newResponseItem.created_at).format('hh:mm A'),
|
||||
tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens,
|
||||
@@ -422,6 +434,52 @@ export const useChat = (
|
||||
})
|
||||
handleUpdateChatList(newChatList)
|
||||
},
|
||||
onWorkflowStarted: ({ workflow_run_id, task_id }) => {
|
||||
taskIdRef.current = task_id
|
||||
responseItem.workflow_run_id = workflow_run_id
|
||||
responseItem.workflowProcess = {
|
||||
status: WorkflowRunningStatus.Running,
|
||||
tracing: [],
|
||||
}
|
||||
handleUpdateChatList(produce(chatListRef.current, (draft) => {
|
||||
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
|
||||
draft[currentIndex] = {
|
||||
...draft[currentIndex],
|
||||
...responseItem,
|
||||
}
|
||||
}))
|
||||
},
|
||||
onWorkflowFinished: ({ data }) => {
|
||||
responseItem.workflowProcess!.status = data.status as WorkflowRunningStatus
|
||||
handleUpdateChatList(produce(chatListRef.current, (draft) => {
|
||||
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
|
||||
draft[currentIndex] = {
|
||||
...draft[currentIndex],
|
||||
...responseItem,
|
||||
}
|
||||
}))
|
||||
},
|
||||
onNodeStarted: ({ data }) => {
|
||||
responseItem.workflowProcess!.tracing!.push(data as any)
|
||||
handleUpdateChatList(produce(chatListRef.current, (draft) => {
|
||||
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
|
||||
draft[currentIndex] = {
|
||||
...draft[currentIndex],
|
||||
...responseItem,
|
||||
}
|
||||
}))
|
||||
},
|
||||
onNodeFinished: ({ data }) => {
|
||||
const currentIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id)
|
||||
responseItem.workflowProcess!.tracing[currentIndex] = data as any
|
||||
handleUpdateChatList(produce(chatListRef.current, (draft) => {
|
||||
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
|
||||
draft[currentIndex] = {
|
||||
...draft[currentIndex],
|
||||
...responseItem,
|
||||
}
|
||||
}))
|
||||
},
|
||||
})
|
||||
return true
|
||||
}, [
|
||||
|
@@ -7,6 +7,7 @@ import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { debounce } from 'lodash-es'
|
||||
@@ -24,6 +25,8 @@ import { ChatContextProvider } from './context'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||
import PromptLogModal from '@/app/components/base/prompt-log-modal'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
|
||||
export type ChatProps = {
|
||||
chatList: ChatItem[]
|
||||
@@ -47,6 +50,7 @@ export type ChatProps = {
|
||||
onAnnotationRemoved?: (index: number) => void
|
||||
chatNode?: ReactNode
|
||||
onFeedback?: (messageId: string, feedback: Feedback) => void
|
||||
chatAnswerContainerInner?: string
|
||||
}
|
||||
const Chat: FC<ChatProps> = ({
|
||||
config,
|
||||
@@ -70,8 +74,11 @@ const Chat: FC<ChatProps> = ({
|
||||
onAnnotationRemoved,
|
||||
chatNode,
|
||||
onFeedback,
|
||||
chatAnswerContainerInner,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal } = useAppStore()
|
||||
const [width, setWidth] = useState(0)
|
||||
const chatContainerRef = useRef<HTMLDivElement>(null)
|
||||
const chatContainerInnerRef = useRef<HTMLDivElement>(null)
|
||||
const chatFooterRef = useRef<HTMLDivElement>(null)
|
||||
@@ -84,6 +91,9 @@ const Chat: FC<ChatProps> = ({
|
||||
}, [])
|
||||
|
||||
const handleWindowResize = useCallback(() => {
|
||||
if (chatContainerRef.current)
|
||||
setWidth(document.body.clientWidth - (chatContainerRef.current?.clientWidth + 16) - 8)
|
||||
|
||||
if (chatContainerRef.current && chatFooterRef.current)
|
||||
chatFooterRef.current.style.width = `${chatContainerRef.current.clientWidth}px`
|
||||
|
||||
@@ -182,6 +192,8 @@ const Chat: FC<ChatProps> = ({
|
||||
answerIcon={answerIcon}
|
||||
responding={isLast && isResponding}
|
||||
allToolIcons={allToolIcons}
|
||||
showPromptLog={showPromptLog}
|
||||
chatAnswerContainerInner={chatAnswerContainerInner}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -189,9 +201,7 @@ const Chat: FC<ChatProps> = ({
|
||||
<Question
|
||||
key={item.id}
|
||||
item={item}
|
||||
showPromptLog={showPromptLog}
|
||||
questionIcon={questionIcon}
|
||||
isResponding={isResponding}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@@ -238,6 +248,16 @@ const Chat: FC<ChatProps> = ({
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{showPromptLogModal && (
|
||||
<PromptLogModal
|
||||
width={width}
|
||||
currentLogItem={currentLogItem}
|
||||
onCancel={() => {
|
||||
setCurrentLogItem()
|
||||
setShowPromptLogModal(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ChatContextProvider>
|
||||
)
|
||||
|
@@ -4,28 +4,21 @@ import type {
|
||||
} from 'react'
|
||||
import {
|
||||
memo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import type { ChatItem } from '../types'
|
||||
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'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import ImageGallery from '@/app/components/base/image-gallery'
|
||||
|
||||
type QuestionProps = {
|
||||
item: ChatItem
|
||||
showPromptLog?: boolean
|
||||
questionIcon?: ReactNode
|
||||
isResponding?: boolean
|
||||
}
|
||||
const Question: FC<QuestionProps> = ({
|
||||
item,
|
||||
showPromptLog,
|
||||
isResponding,
|
||||
questionIcon,
|
||||
}) => {
|
||||
const ref = useRef(null)
|
||||
const {
|
||||
content,
|
||||
message_files,
|
||||
@@ -34,14 +27,9 @@ const Question: FC<QuestionProps> = ({
|
||||
const imgSrcs = message_files?.length ? message_files.map(item => item.url) : []
|
||||
|
||||
return (
|
||||
<div className='flex justify-end mb-2 last:mb-0 pl-10' ref={ref}>
|
||||
<div className='flex justify-end mb-2 last:mb-0 pl-10'>
|
||||
<div className='group relative mr-4'>
|
||||
<QuestionTriangle className='absolute -right-2 top-0 w-2 h-3 text-[#D1E9FF]/50' />
|
||||
{
|
||||
showPromptLog && !isResponding && (
|
||||
<Log log={item.log!} containerRef={ref} />
|
||||
)
|
||||
}
|
||||
<div className='px-4 py-3 bg-[#D1E9FF]/50 rounded-b-2xl rounded-tl-2xl text-sm text-gray-900'>
|
||||
{
|
||||
!!imgSrcs.length && (
|
||||
|
@@ -4,6 +4,8 @@ import type {
|
||||
VisionSettings,
|
||||
} from '@/types/app'
|
||||
import type { IChatItem } from '@/app/components/app/chat/type'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import type { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
|
||||
export type { VisionFile } from '@/types/app'
|
||||
export { TransferMethod } from '@/types/app'
|
||||
@@ -48,7 +50,16 @@ export type ChatConfig = Omit<ModelConfig, 'model'> & {
|
||||
supportCitationHitInfo?: boolean
|
||||
}
|
||||
|
||||
export type ChatItem = IChatItem
|
||||
export type WorkflowProcess = {
|
||||
status: WorkflowRunningStatus
|
||||
tracing: NodeTracing[]
|
||||
expand?: boolean // for UI
|
||||
}
|
||||
|
||||
export type ChatItem = IChatItem & {
|
||||
isError?: boolean
|
||||
workflowProcess?: WorkflowProcess
|
||||
}
|
||||
|
||||
export type OnSend = (message: string, files?: VisionFile[]) => void
|
||||
|
||||
|
Reference in New Issue
Block a user