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:
takatost
2024-04-08 18:51:46 +08:00
committed by GitHub
parent 2fb9850af5
commit 7753ba2d37
1161 changed files with 103836 additions and 10327 deletions

View File

@@ -14,10 +14,10 @@ import Filter from './filter'
import s from './style.module.css'
import Loading from '@/app/components/base/loading'
import { fetchChatConversations, fetchCompletionConversations } from '@/service/log'
import { fetchAppDetail } from '@/service/apps'
import { APP_PAGE_LIMIT } from '@/config'
import type { App, AppMode } from '@/types/app'
export type ILogsProps = {
appId: string
appDetail: App
}
export type QueryParam = {
@@ -50,7 +50,7 @@ const EmptyElement: FC<{ appUrl: string }> = ({ appUrl }) => {
</div>
}
const Logs: FC<ILogsProps> = ({ appId }) => {
const Logs: FC<ILogsProps> = ({ appDetail }) => {
const { t } = useTranslation()
const [queryParams, setQueryParams] = useState<QueryParam>({ period: 7, annotation_status: 'all' })
const [currPage, setCurrPage] = React.useState<number>(0)
@@ -67,21 +67,26 @@ const Logs: FC<ILogsProps> = ({ appId }) => {
...omit(queryParams, ['period']),
}
const getWebAppType = (appType: AppMode) => {
if (appType !== 'completion' && appType !== 'workflow')
return 'chat'
return appType
}
// Get the app type first
const { data: appDetail } = useSWR({ url: '/apps', id: appId }, fetchAppDetail)
const isChatMode = appDetail?.mode === 'chat'
const isChatMode = appDetail.mode !== 'completion'
// When the details are obtained, proceed to the next request
const { data: chatConversations, mutate: mutateChatList } = useSWR(() => isChatMode
? {
url: `/apps/${appId}/chat-conversations`,
url: `/apps/${appDetail.id}/chat-conversations`,
params: query,
}
: null, fetchChatConversations)
const { data: completionConversations, mutate: mutateCompletionList } = useSWR(() => !isChatMode
? {
url: `/apps/${appId}/completion-conversations`,
url: `/apps/${appDetail.id}/completion-conversations`,
params: query,
}
: null, fetchCompletionConversations)
@@ -92,12 +97,12 @@ const Logs: FC<ILogsProps> = ({ appId }) => {
<div className='flex flex-col h-full'>
<p className='flex text-sm font-normal text-gray-500'>{t('appLog.description')}</p>
<div className='flex flex-col py-4 flex-1'>
<Filter appId={appId} queryParams={queryParams} setQueryParams={setQueryParams} />
<Filter appId={appDetail.id} queryParams={queryParams} setQueryParams={setQueryParams} />
{total === undefined
? <Loading type='app' />
: total > 0
? <List logs={isChatMode ? chatConversations : completionConversations} appDetail={appDetail} onRefresh={isChatMode ? mutateChatList : mutateCompletionList} />
: <EmptyElement appUrl={`${appDetail?.site.app_base_url}/${appDetail?.mode}/${appDetail?.site.access_token}`} />
: <EmptyElement appUrl={`${appDetail.site.app_base_url}/${getWebAppType(appDetail.mode)}/${appDetail.site.access_token}`} />
}
{/* Show Pagination only if the total is more than the limit */}
{(total && total > APP_PAGE_LIMIT)

View File

@@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import useSWR from 'swr'
import {
HandThumbDownIcon,
@@ -35,10 +35,13 @@ import ModelName from '@/app/components/header/account-setting/model-provider-pa
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import TextGeneration from '@/app/components/app/text-generate/item'
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
import PromptLogModal from '@/app/components/base/prompt-log-modal'
import MessageLogModal from '@/app/components/base/message-log-modal'
import { useStore as useAppStore } from '@/app/components/app/store'
type IConversationList = {
logs?: ChatConversationsResponse | CompletionConversationsResponse
appDetail?: App
appDetail: App
onRefresh: () => void
}
@@ -80,7 +83,6 @@ const getFormattedChatList = (messages: ChatMessage[]) => {
id: `question-${item.id}`,
content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query
isAnswer: false,
log: item.message as any,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
})
newChatList.push({
@@ -92,6 +94,19 @@ const getFormattedChatList = (messages: ChatMessage[]) => {
feedbackDisabled: false,
isAnswer: true,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
log: [
...item.message,
...(item.message[item.message.length - 1]?.role !== 'assistant'
? [
{
role: 'assistant',
text: item.answer,
files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
},
]
: []),
],
workflow_run_id: item.workflow_run_id,
more: {
time: dayjs.unix(item.created_at).format('hh:mm A'),
tokens: item.answer_tokens + item.message_tokens,
@@ -133,6 +148,7 @@ type IDetailPanel<T> = {
function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionConversationFullDetailResponse>({ detail, onFeedback }: IDetailPanel<T>) {
const { onClose, appDetail } = useContext(DrawerContext)
const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showMessageLogModal, setShowMessageLogModal } = useAppStore()
const { t } = useTranslation()
const [items, setItems] = React.useState<IChatItem[]>([])
const [hasMore, setHasMore] = useState(true)
@@ -175,11 +191,12 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
}
useEffect(() => {
if (appDetail?.id && detail.id && appDetail?.mode === 'chat')
if (appDetail?.id && detail.id && appDetail?.mode !== 'completion')
fetchData()
}, [appDetail?.id, detail.id, appDetail?.mode])
const isChatMode = appDetail?.mode === 'chat'
const isChatMode = appDetail?.mode !== 'completion'
const isAdvanced = appDetail?.mode === 'advanced-chat'
const targetTone = TONE_LIST.find((item: any) => {
let res = true
@@ -189,21 +206,21 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
return res
})?.name ?? 'custom'
const modelName = (detail.model_config as any).model.name
const provideName = (detail.model_config as any).model.provider as any
const modelName = (detail.model_config as any).model?.name
const provideName = (detail.model_config as any).model?.provider as any
const {
currentModel,
currentProvider,
} = useTextGenerationCurrentProviderAndModelAndModelList(
{ provider: provideName, model: modelName },
)
const varList = (detail.model_config as any).user_input_form.map((item: any) => {
const varList = (detail.model_config as any).user_input_form?.map((item: any) => {
const itemContent = item[Object.keys(item)[0]]
return {
label: itemContent.variable,
value: varValues[itemContent.variable] || detail.message?.inputs?.[itemContent.variable],
}
})
}) || []
const message_files = (!isChatMode && detail.message.message_files && detail.message.message_files.length > 0)
? detail.message.message_files.map((item: any) => item.url)
: []
@@ -220,144 +237,182 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
return value
}
return (<div className='rounded-xl border-[0.5px] border-gray-200 h-full flex flex-col overflow-auto'>
{/* Panel Header */}
<div className='border-b border-gray-100 py-4 px-6 flex items-center justify-between'>
<div>
<div className='text-gray-500 text-[10px] leading-[14px]'>{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}</div>
<div className='text-gray-700 text-[13px] leading-[18px]'>{isChatMode ? detail.id?.split('-').slice(-1)[0] : dayjs.unix(detail.created_at).format(t('appLog.dateTimeFormat') as string)}</div>
</div>
<div className='flex items-center flex-wrap gap-y-1 justify-end'>
<div
className={cn('mr-2 flex items-center border h-8 px-2 space-x-2 rounded-lg bg-indigo-25 border-[#2A87F5]')}
>
<ModelIcon
className='!w-5 !h-5'
provider={currentProvider}
modelName={currentModel?.model}
/>
<ModelName
modelItem={currentModel!}
showMode
/>
const [width, setWidth] = useState(0)
const ref = useRef<HTMLDivElement>(null)
const adjustModalWidth = () => {
if (ref.current)
setWidth(document.body.clientWidth - (ref.current?.clientWidth + 16) - 8)
}
useEffect(() => {
adjustModalWidth()
}, [])
return (
<div ref={ref} className='rounded-xl border-[0.5px] border-gray-200 h-full flex flex-col overflow-auto'>
{/* Panel Header */}
<div className='border-b border-gray-100 py-4 px-6 flex items-center justify-between'>
<div>
<div className='text-gray-500 text-[10px] leading-[14px]'>{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}</div>
<div className='text-gray-700 text-[13px] leading-[18px]'>{isChatMode ? detail.id?.split('-').slice(-1)[0] : dayjs.unix(detail.created_at).format(t('appLog.dateTimeFormat') as string)}</div>
</div>
<Popover
position='br'
className='!w-[280px]'
btnClassName='mr-4 !bg-gray-50 !py-1.5 !px-2.5 border-none font-normal'
btnElement={<>
<span className='text-[13px]'>{targetTone}</span>
<InformationCircleIcon className='h-4 w-4 text-gray-800 ml-1.5' />
</>}
htmlContent={<div className='w-[280px]'>
<div className='flex justify-between py-2 px-4 font-medium text-sm text-gray-700'>
<span>Tone of responses</span>
<div>{targetTone}</div>
</div>
{['temperature', 'top_p', 'presence_penalty', 'max_tokens', 'stop'].map((param: string, index: number) => {
return <div className='flex justify-between py-2 px-4 bg-gray-50' key={index}>
<span className='text-xs text-gray-700'>{PARAM_MAP[param as keyof typeof PARAM_MAP]}</span>
<span className='text-gray-800 font-medium text-xs'>{getParamValue(param)}</span>
<div className='flex items-center flex-wrap gap-y-1 justify-end'>
{!isAdvanced && (
<>
<div
className={cn('mr-2 flex items-center border h-8 px-2 space-x-2 rounded-lg bg-indigo-25 border-[#2A87F5]')}
>
<ModelIcon
className='!w-5 !h-5'
provider={currentProvider}
modelName={currentModel?.model}
/>
<ModelName
modelItem={currentModel!}
showMode
/>
</div>
})}
</div>}
/>
<div className='w-6 h-6 rounded-lg flex items-center justify-center hover:cursor-pointer hover:bg-gray-100'>
<XMarkIcon className='w-4 h-4 text-gray-500' onClick={onClose} />
<Popover
position='br'
className='!w-[280px]'
btnClassName='mr-4 !bg-gray-50 !py-1.5 !px-2.5 border-none font-normal'
btnElement={<>
<span className='text-[13px]'>{targetTone}</span>
<InformationCircleIcon className='h-4 w-4 text-gray-800 ml-1.5' />
</>}
htmlContent={<div className='w-[280px]'>
<div className='flex justify-between py-2 px-4 font-medium text-sm text-gray-700'>
<span>Tone of responses</span>
<div>{targetTone}</div>
</div>
{['temperature', 'top_p', 'presence_penalty', 'max_tokens', 'stop'].map((param: string, index: number) => {
return <div className='flex justify-between py-2 px-4 bg-gray-50' key={index}>
<span className='text-xs text-gray-700'>{PARAM_MAP[param as keyof typeof PARAM_MAP]}</span>
<span className='text-gray-800 font-medium text-xs'>{getParamValue(param)}</span>
</div>
})}
</div>}
/>
</>
)}
<div className='w-6 h-6 rounded-lg flex items-center justify-center hover:cursor-pointer hover:bg-gray-100'>
<XMarkIcon className='w-4 h-4 text-gray-500' onClick={onClose} />
</div>
</div>
</div>
</div>
{/* Panel Body */}
{(varList.length > 0 || (!isChatMode && message_files.length > 0)) && (
<div className='px-6 pt-4 pb-2'>
<VarPanel
varList={varList}
message_files={message_files}
/>
</div>
)}
{!isChatMode
? <div className="px-6 py-4">
<div className='flex h-[18px] items-center space-x-3'>
<div className='leading-[18px] text-xs font-semibold text-gray-500 uppercase'>{t('appLog.table.header.output')}</div>
<div className='grow h-[1px]' style={{
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, rgb(243, 244, 246) 100%)',
}}></div>
{/* Panel Body */}
{(varList.length > 0 || (!isChatMode && message_files.length > 0)) && (
<div className='px-6 pt-4 pb-2'>
<VarPanel
varList={varList}
message_files={message_files}
/>
</div>
<TextGeneration
className='mt-2'
content={detail.message.answer}
messageId={detail.message.id}
isError={false}
onRetry={() => { }}
isInstalledApp={false}
supportFeedback
feedback={detail.message.feedbacks.find((item: any) => item.from_source === 'admin')}
onFeedback={feedback => onFeedback(detail.message.id, feedback)}
supportAnnotation
isShowTextToSpeech
appId={appDetail?.id}
varList={varList}
/>
</div>
: items.length < 8
? <div className="px-2.5 pt-4 mb-4">
<Chat
chatList={items}
isHideSendInput={true}
onFeedback={onFeedback}
displayScene='console'
isShowPromptLog
)}
{!isChatMode
? <div className="px-6 py-4">
<div className='flex h-[18px] items-center space-x-3'>
<div className='leading-[18px] text-xs font-semibold text-gray-500 uppercase'>{t('appLog.table.header.output')}</div>
<div className='grow h-[1px]' style={{
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, rgb(243, 244, 246) 100%)',
}}></div>
</div>
<TextGeneration
className='mt-2'
content={detail.message.answer}
messageId={detail.message.id}
isError={false}
onRetry={() => { }}
isInstalledApp={false}
supportFeedback
feedback={detail.message.feedbacks.find((item: any) => item.from_source === 'admin')}
onFeedback={feedback => onFeedback(detail.message.id, feedback)}
supportAnnotation
isShowTextToSpeech
appId={appDetail?.id}
onChatListChange={setItems}
varList={varList}
/>
</div>
: <div
className="px-2.5 py-4"
id="scrollableDiv"
style={{
height: 1000, // Specify a value
overflow: 'auto',
display: 'flex',
flexDirection: 'column-reverse',
}}>
{/* Put the scroll bar always on the bottom */}
<InfiniteScroll
scrollableTarget="scrollableDiv"
dataLength={items.length}
next={fetchData}
hasMore={hasMore}
loader={<div className='text-center text-gray-400 text-xs'>{t('appLog.detail.loading')}...</div>}
// endMessage={<div className='text-center'>Nothing more to show</div>}
// below props only if you need pull down functionality
refreshFunction={fetchData}
pullDownToRefresh
pullDownToRefreshThreshold={50}
// pullDownToRefreshContent={
// <div className='text-center'>Pull down to refresh</div>
// }
// releaseToRefreshContent={
// <div className='text-center'>Release to refresh</div>
// }
// To put endMessage and loader to the top.
style={{ display: 'flex', flexDirection: 'column-reverse' }}
inverse={true}
>
: items.length < 8
? <div className="px-2.5 pt-4 mb-4">
<Chat
chatList={items}
isHideSendInput={true}
onFeedback={onFeedback}
displayScene='console'
isShowPromptLog
supportAnnotation
isShowTextToSpeech
appId={appDetail?.id}
onChatListChange={setItems}
/>
</InfiniteScroll>
</div>
}
</div>)
</div>
: <div
className="px-2.5 py-4"
id="scrollableDiv"
style={{
height: 1000, // Specify a value
overflow: 'auto',
display: 'flex',
flexDirection: 'column-reverse',
}}>
{/* Put the scroll bar always on the bottom */}
<InfiniteScroll
scrollableTarget="scrollableDiv"
dataLength={items.length}
next={fetchData}
hasMore={hasMore}
loader={<div className='text-center text-gray-400 text-xs'>{t('appLog.detail.loading')}...</div>}
// endMessage={<div className='text-center'>Nothing more to show</div>}
// below props only if you need pull down functionality
refreshFunction={fetchData}
pullDownToRefresh
pullDownToRefreshThreshold={50}
// pullDownToRefreshContent={
// <div className='text-center'>Pull down to refresh</div>
// }
// releaseToRefreshContent={
// <div className='text-center'>Release to refresh</div>
// }
// To put endMessage and loader to the top.
style={{ display: 'flex', flexDirection: 'column-reverse' }}
inverse={true}
>
<Chat
chatList={items}
isHideSendInput={true}
onFeedback={onFeedback}
displayScene='console'
isShowPromptLog
/>
</InfiniteScroll>
</div>
}
{showPromptLogModal && (
<PromptLogModal
width={width}
currentLogItem={currentLogItem}
onCancel={() => {
setCurrentLogItem()
setShowPromptLogModal(false)
}}
/>
)}
{showMessageLogModal && (
<MessageLogModal
width={width}
currentLogItem={currentLogItem}
onCancel={() => {
setCurrentLogItem()
setShowMessageLogModal(false)
}}
/>
)}
</div>
)
}
/**
@@ -460,7 +515,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
const [showDrawer, setShowDrawer] = useState<boolean>(false) // Whether to display the chat details drawer
const [currentConversation, setCurrentConversation] = useState<ChatConversationGeneralDetail | CompletionConversationGeneralDetail | undefined>() // Currently selected conversation
const isChatMode = appDetail?.mode === 'chat' // Whether the app is a chat app
const isChatMode = appDetail.mode !== 'completion' // Whether the app is a chat app
// Annotated data needs to be highlighted
const renderTdValue = (value: string | number | null, isEmptyStyle: boolean, isHighlight = false, annotation?: LogAnnotation) => {
@@ -559,8 +614,8 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
appDetail,
}}>
{isChatMode
? <ChatConversationDetailComp appId={appDetail?.id} conversationId={currentConversation?.id} />
: <CompletionConversationDetailComp appId={appDetail?.id} conversationId={currentConversation?.id} />
? <ChatConversationDetailComp appId={appDetail.id} conversationId={currentConversation?.id} />
: <CompletionConversationDetailComp appId={appDetail.id} conversationId={currentConversation?.id} />
}
</DrawerContext.Provider>
</Drawer>