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
|
||||
Reference in New Issue
Block a user