dify
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import Header from './header'
|
||||
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
|
||||
import { format } from '@/service/base'
|
||||
|
||||
export type IResultProps = {
|
||||
content: string
|
||||
showFeedback: boolean
|
||||
feedback: FeedbackType
|
||||
onFeedback: (feedback: FeedbackType) => void
|
||||
}
|
||||
const Result: FC<IResultProps> = ({
|
||||
content,
|
||||
showFeedback,
|
||||
feedback,
|
||||
onFeedback,
|
||||
}) => {
|
||||
return (
|
||||
<div className='h-max basis-3/4'>
|
||||
<Header result={content} showFeedback={showFeedback} feedback={feedback} onFeedback={onFeedback} />
|
||||
<div
|
||||
className='mt-4 flex w-full overflow-scroll text-sm font-normal leading-5 text-gray-900'
|
||||
style={{
|
||||
maxHeight: '70vh',
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: format(content),
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Result)
|
||||
113
dify/web/app/components/share/text-generation/result/header.tsx
Normal file
113
dify/web/app/components/share/text-generation/result/header.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ClipboardDocumentIcon, HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type IResultHeaderProps = {
|
||||
result: string
|
||||
showFeedback: boolean
|
||||
feedback: FeedbackType
|
||||
onFeedback: (feedback: FeedbackType) => void
|
||||
}
|
||||
|
||||
const Header: FC<IResultHeaderProps> = ({
|
||||
feedback,
|
||||
showFeedback,
|
||||
onFeedback,
|
||||
result,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='flex w-full items-center justify-between '>
|
||||
<div className='text-2xl font-normal leading-4 text-gray-800'>{t('share.generation.resultTitle')}</div>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<Button
|
||||
className='h-7 p-[2px] pr-2'
|
||||
onClick={() => {
|
||||
copy(result)
|
||||
Toast.notify({ type: 'success', message: 'copied' })
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<ClipboardDocumentIcon className='mr-1 h-3 w-4 text-gray-500' />
|
||||
<span className='text-xs leading-3 text-gray-500'>{t('share.generation.copy')}</span>
|
||||
</>
|
||||
</Button>
|
||||
|
||||
{showFeedback && feedback.rating && feedback.rating === 'like' && (
|
||||
<Tooltip
|
||||
popupContent="Undo Great Rating"
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback({
|
||||
rating: null,
|
||||
})
|
||||
}}
|
||||
className='flex h-7 w-7 cursor-pointer items-center justify-center rounded-md border border-primary-200 bg-primary-100 !text-primary-600 hover:border-primary-300 hover:bg-primary-200'>
|
||||
<HandThumbUpIcon width={16} height={16} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{showFeedback && feedback.rating && feedback.rating === 'dislike' && (
|
||||
<Tooltip
|
||||
popupContent="Undo Undesirable Response"
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback({
|
||||
rating: null,
|
||||
})
|
||||
}}
|
||||
className='flex h-7 w-7 cursor-pointer items-center justify-center rounded-md border border-red-200 bg-red-100 !text-red-600 hover:border-red-300 hover:bg-red-200'>
|
||||
<HandThumbDownIcon width={16} height={16} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{showFeedback && !feedback.rating && (
|
||||
<div className='flex space-x-1 rounded-lg border border-gray-200 p-[1px]'>
|
||||
<Tooltip
|
||||
popupContent="Great Rating"
|
||||
needsDelay={false}
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback({
|
||||
rating: 'like',
|
||||
})
|
||||
}}
|
||||
className='flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-gray-100'>
|
||||
<HandThumbUpIcon width={16} height={16} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
popupContent="Undesirable Response"
|
||||
needsDelay={false}
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback({
|
||||
rating: 'dislike',
|
||||
})
|
||||
}}
|
||||
className='flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-gray-100'>
|
||||
<HandThumbDownIcon width={16} height={16} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Header)
|
||||
607
dify/web/app/components/share/text-generation/result/index.tsx
Normal file
607
dify/web/app/components/share/text-generation/result/index.tsx
Normal file
@@ -0,0 +1,607 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { t } from 'i18next'
|
||||
import { produce } from 'immer'
|
||||
import TextGenerationRes from '@/app/components/app/text-generate/item'
|
||||
import NoData from '@/app/components/share/text-generation/no-data'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import { sendCompletionMessage, sendWorkflowMessage, stopChatMessageResponding, stopWorkflowMessage, updateFeedback } from '@/service/share'
|
||||
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import type { PromptConfig } from '@/models/debug'
|
||||
import type { InstalledApp } from '@/models/explore'
|
||||
import { TransferMethod, type VisionFile, type VisionSettings } from '@/types/app'
|
||||
import { NodeRunningStatus, WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import type { WorkflowProcess } from '@/app/components/base/chat/types'
|
||||
import { sleep } from '@/utils'
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
import { TEXT_GENERATION_TIMEOUT_MS } from '@/config'
|
||||
import {
|
||||
getFilesInLogs,
|
||||
getProcessedFiles,
|
||||
} from '@/app/components/base/file-uploader/utils'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import { formatBooleanInputs } from '@/utils/model-config'
|
||||
|
||||
export type IResultProps = {
|
||||
isWorkflow: boolean
|
||||
isCallBatchAPI: boolean
|
||||
isPC: boolean
|
||||
isMobile: boolean
|
||||
isInstalledApp: boolean
|
||||
appId: string
|
||||
installedAppInfo?: InstalledApp
|
||||
isError: boolean
|
||||
isShowTextToSpeech: boolean
|
||||
promptConfig: PromptConfig | null
|
||||
moreLikeThisEnabled: boolean
|
||||
inputs: Record<string, any>
|
||||
controlSend?: number
|
||||
controlRetry?: number
|
||||
controlStopResponding?: number
|
||||
onShowRes: () => void
|
||||
handleSaveMessage: (messageId: string) => void
|
||||
taskId?: number
|
||||
onCompleted: (completionRes: string, taskId?: number, success?: boolean) => void
|
||||
visionConfig: VisionSettings
|
||||
completionFiles: VisionFile[]
|
||||
siteInfo: SiteInfo | null
|
||||
onRunStart: () => void
|
||||
onRunControlChange?: (control: { onStop: () => Promise<void> | void; isStopping: boolean } | null) => void
|
||||
hideInlineStopButton?: boolean
|
||||
}
|
||||
|
||||
const Result: FC<IResultProps> = ({
|
||||
isWorkflow,
|
||||
isCallBatchAPI,
|
||||
isPC,
|
||||
isMobile,
|
||||
isInstalledApp,
|
||||
appId,
|
||||
installedAppInfo,
|
||||
isError,
|
||||
isShowTextToSpeech,
|
||||
promptConfig,
|
||||
moreLikeThisEnabled,
|
||||
inputs,
|
||||
controlSend,
|
||||
controlRetry,
|
||||
controlStopResponding,
|
||||
onShowRes,
|
||||
handleSaveMessage,
|
||||
taskId,
|
||||
onCompleted,
|
||||
visionConfig,
|
||||
completionFiles,
|
||||
siteInfo,
|
||||
onRunStart,
|
||||
onRunControlChange,
|
||||
hideInlineStopButton = false,
|
||||
}) => {
|
||||
const [isResponding, { setTrue: setRespondingTrue, setFalse: setRespondingFalse }] = useBoolean(false)
|
||||
const [completionRes, doSetCompletionRes] = useState<string>('')
|
||||
const completionResRef = useRef<string>('')
|
||||
const setCompletionRes = (res: string) => {
|
||||
completionResRef.current = res
|
||||
doSetCompletionRes(res)
|
||||
}
|
||||
const getCompletionRes = () => completionResRef.current
|
||||
const [workflowProcessData, doSetWorkflowProcessData] = useState<WorkflowProcess>()
|
||||
const workflowProcessDataRef = useRef<WorkflowProcess | undefined>(undefined)
|
||||
const setWorkflowProcessData = (data: WorkflowProcess) => {
|
||||
workflowProcessDataRef.current = data
|
||||
doSetWorkflowProcessData(data)
|
||||
}
|
||||
const getWorkflowProcessData = () => workflowProcessDataRef.current
|
||||
const [currentTaskId, setCurrentTaskId] = useState<string | null>(null)
|
||||
const [isStopping, setIsStopping] = useState(false)
|
||||
const abortControllerRef = useRef<AbortController | null>(null)
|
||||
const resetRunState = useCallback(() => {
|
||||
setCurrentTaskId(null)
|
||||
setIsStopping(false)
|
||||
abortControllerRef.current = null
|
||||
onRunControlChange?.(null)
|
||||
}, [onRunControlChange])
|
||||
|
||||
useEffect(() => {
|
||||
const abortCurrentRequest = () => {
|
||||
abortControllerRef.current?.abort()
|
||||
}
|
||||
|
||||
if (controlStopResponding) {
|
||||
abortCurrentRequest()
|
||||
setRespondingFalse()
|
||||
resetRunState()
|
||||
}
|
||||
|
||||
return abortCurrentRequest
|
||||
}, [controlStopResponding, resetRunState, setRespondingFalse])
|
||||
|
||||
const { notify } = Toast
|
||||
const isNoData = !completionRes
|
||||
|
||||
const [messageId, setMessageId] = useState<string | null>(null)
|
||||
const [feedback, setFeedback] = useState<FeedbackType>({
|
||||
rating: null,
|
||||
})
|
||||
|
||||
const handleFeedback = async (feedback: FeedbackType) => {
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, isInstalledApp, installedAppInfo?.id)
|
||||
setFeedback(feedback)
|
||||
}
|
||||
|
||||
const logError = (message: string) => {
|
||||
notify({ type: 'error', message })
|
||||
}
|
||||
|
||||
const handleStop = useCallback(async () => {
|
||||
if (!currentTaskId || isStopping)
|
||||
return
|
||||
setIsStopping(true)
|
||||
try {
|
||||
if (isWorkflow)
|
||||
await stopWorkflowMessage(appId, currentTaskId, isInstalledApp, installedAppInfo?.id || '')
|
||||
else
|
||||
await stopChatMessageResponding(appId, currentTaskId, isInstalledApp, installedAppInfo?.id || '')
|
||||
abortControllerRef.current?.abort()
|
||||
}
|
||||
catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
notify({ type: 'error', message })
|
||||
}
|
||||
finally {
|
||||
setIsStopping(false)
|
||||
}
|
||||
}, [appId, currentTaskId, installedAppInfo?.id, isInstalledApp, isStopping, isWorkflow, notify])
|
||||
|
||||
useEffect(() => {
|
||||
if (!onRunControlChange)
|
||||
return
|
||||
if (isResponding && currentTaskId) {
|
||||
onRunControlChange({
|
||||
onStop: handleStop,
|
||||
isStopping,
|
||||
})
|
||||
}
|
||||
else {
|
||||
onRunControlChange(null)
|
||||
}
|
||||
}, [currentTaskId, handleStop, isResponding, isStopping, onRunControlChange])
|
||||
|
||||
const checkCanSend = () => {
|
||||
// batch will check outer
|
||||
if (isCallBatchAPI)
|
||||
return true
|
||||
|
||||
const prompt_variables = promptConfig?.prompt_variables
|
||||
if (!prompt_variables || prompt_variables?.length === 0) {
|
||||
if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForFileUpload') })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
let hasEmptyInput = ''
|
||||
const requiredVars = prompt_variables?.filter(({ key, name, required, type }) => {
|
||||
if(type === 'boolean' || type === 'checkbox')
|
||||
return false // boolean/checkbox input is not required
|
||||
const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
|
||||
return res
|
||||
}) || [] // compatible with old version
|
||||
requiredVars.forEach(({ key, name }) => {
|
||||
if (hasEmptyInput)
|
||||
return
|
||||
|
||||
if (!inputs[key])
|
||||
hasEmptyInput = name
|
||||
})
|
||||
|
||||
if (hasEmptyInput) {
|
||||
logError(t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }))
|
||||
return false
|
||||
}
|
||||
|
||||
if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForFileUpload') })
|
||||
return false
|
||||
}
|
||||
return !hasEmptyInput
|
||||
}
|
||||
|
||||
const handleSend = async () => {
|
||||
if (isResponding) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
|
||||
return false
|
||||
}
|
||||
|
||||
if (!checkCanSend())
|
||||
return
|
||||
|
||||
// Process inputs: convert file entities to API format
|
||||
const processedInputs = { ...formatBooleanInputs(promptConfig?.prompt_variables, inputs) }
|
||||
promptConfig?.prompt_variables.forEach((variable) => {
|
||||
const value = processedInputs[variable.key]
|
||||
if (variable.type === 'file' && value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
// Convert single file entity to API format
|
||||
processedInputs[variable.key] = getProcessedFiles([value as FileEntity])[0]
|
||||
}
|
||||
else if (variable.type === 'file-list' && Array.isArray(value) && value.length > 0) {
|
||||
// Convert file entity array to API format
|
||||
processedInputs[variable.key] = getProcessedFiles(value as FileEntity[])
|
||||
}
|
||||
})
|
||||
|
||||
const data: Record<string, any> = {
|
||||
inputs: processedInputs,
|
||||
}
|
||||
if (visionConfig.enabled && completionFiles && completionFiles?.length > 0) {
|
||||
data.files = completionFiles.map((item) => {
|
||||
if (item.transfer_method === TransferMethod.local_file) {
|
||||
return {
|
||||
...item,
|
||||
url: '',
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
setMessageId(null)
|
||||
setFeedback({
|
||||
rating: null,
|
||||
})
|
||||
setCompletionRes('')
|
||||
resetRunState()
|
||||
|
||||
let res: string[] = []
|
||||
let tempMessageId = ''
|
||||
|
||||
if (!isPC) {
|
||||
onShowRes()
|
||||
onRunStart()
|
||||
}
|
||||
|
||||
setRespondingTrue()
|
||||
let isEnd = false
|
||||
let isTimeout = false;
|
||||
(async () => {
|
||||
await sleep(TEXT_GENERATION_TIMEOUT_MS)
|
||||
if (!isEnd) {
|
||||
setRespondingFalse()
|
||||
onCompleted(getCompletionRes(), taskId, false)
|
||||
resetRunState()
|
||||
isTimeout = true
|
||||
}
|
||||
})()
|
||||
|
||||
if (isWorkflow) {
|
||||
sendWorkflowMessage(
|
||||
data,
|
||||
{
|
||||
onWorkflowStarted: ({ workflow_run_id, task_id }) => {
|
||||
tempMessageId = workflow_run_id
|
||||
setCurrentTaskId(task_id || null)
|
||||
setIsStopping(false)
|
||||
setWorkflowProcessData({
|
||||
status: WorkflowRunningStatus.Running,
|
||||
tracing: [],
|
||||
expand: false,
|
||||
resultText: '',
|
||||
})
|
||||
},
|
||||
onIterationStart: ({ data }) => {
|
||||
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||
draft.expand = true
|
||||
draft.tracing!.push({
|
||||
...data,
|
||||
status: NodeRunningStatus.Running,
|
||||
expand: true,
|
||||
})
|
||||
}))
|
||||
},
|
||||
onIterationNext: () => {
|
||||
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||
draft.expand = true
|
||||
const iterations = draft.tracing.find(item => item.node_id === data.node_id
|
||||
&& (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id))!
|
||||
iterations?.details!.push([])
|
||||
}))
|
||||
},
|
||||
onIterationFinish: ({ data }) => {
|
||||
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||
draft.expand = true
|
||||
const iterationsIndex = draft.tracing.findIndex(item => item.node_id === data.node_id
|
||||
&& (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id))!
|
||||
draft.tracing[iterationsIndex] = {
|
||||
...data,
|
||||
expand: !!data.error,
|
||||
}
|
||||
}))
|
||||
},
|
||||
onLoopStart: ({ data }) => {
|
||||
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||
draft.expand = true
|
||||
draft.tracing!.push({
|
||||
...data,
|
||||
status: NodeRunningStatus.Running,
|
||||
expand: true,
|
||||
})
|
||||
}))
|
||||
},
|
||||
onLoopNext: () => {
|
||||
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||
draft.expand = true
|
||||
const loops = draft.tracing.find(item => item.node_id === data.node_id
|
||||
&& (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id))!
|
||||
loops?.details!.push([])
|
||||
}))
|
||||
},
|
||||
onLoopFinish: ({ data }) => {
|
||||
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||
draft.expand = true
|
||||
const loopsIndex = draft.tracing.findIndex(item => item.node_id === data.node_id
|
||||
&& (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id))!
|
||||
draft.tracing[loopsIndex] = {
|
||||
...data,
|
||||
expand: !!data.error,
|
||||
}
|
||||
}))
|
||||
},
|
||||
onNodeStarted: ({ data }) => {
|
||||
if (data.iteration_id)
|
||||
return
|
||||
|
||||
if (data.loop_id)
|
||||
return
|
||||
|
||||
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||
draft.expand = true
|
||||
draft.tracing!.push({
|
||||
...data,
|
||||
status: NodeRunningStatus.Running,
|
||||
expand: true,
|
||||
})
|
||||
}))
|
||||
},
|
||||
onNodeFinished: ({ data }) => {
|
||||
if (data.iteration_id)
|
||||
return
|
||||
|
||||
if (data.loop_id)
|
||||
return
|
||||
|
||||
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||
const currentIndex = draft.tracing!.findIndex(trace => trace.node_id === data.node_id
|
||||
&& (trace.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || trace.parallel_id === data.execution_metadata?.parallel_id))
|
||||
if (currentIndex > -1 && draft.tracing) {
|
||||
draft.tracing[currentIndex] = {
|
||||
...(draft.tracing[currentIndex].extras
|
||||
? { extras: draft.tracing[currentIndex].extras }
|
||||
: {}),
|
||||
...data,
|
||||
expand: !!data.error,
|
||||
}
|
||||
}
|
||||
}))
|
||||
},
|
||||
onWorkflowFinished: ({ data }) => {
|
||||
if (isTimeout) {
|
||||
notify({ type: 'warning', message: t('appDebug.warningMessage.timeoutExceeded') })
|
||||
return
|
||||
}
|
||||
const workflowStatus = data.status as WorkflowRunningStatus | undefined
|
||||
const markNodesStopped = (traces?: WorkflowProcess['tracing']) => {
|
||||
if (!traces)
|
||||
return
|
||||
const markTrace = (trace: WorkflowProcess['tracing'][number]) => {
|
||||
if ([NodeRunningStatus.Running, NodeRunningStatus.Waiting].includes(trace.status as NodeRunningStatus))
|
||||
trace.status = NodeRunningStatus.Stopped
|
||||
trace.details?.forEach(detailGroup => detailGroup.forEach(markTrace))
|
||||
trace.retryDetail?.forEach(markTrace)
|
||||
trace.parallelDetail?.children?.forEach(markTrace)
|
||||
}
|
||||
traces.forEach(markTrace)
|
||||
}
|
||||
if (workflowStatus === WorkflowRunningStatus.Stopped) {
|
||||
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||
draft.status = WorkflowRunningStatus.Stopped
|
||||
markNodesStopped(draft.tracing)
|
||||
}))
|
||||
setRespondingFalse()
|
||||
resetRunState()
|
||||
onCompleted(getCompletionRes(), taskId, false)
|
||||
isEnd = true
|
||||
return
|
||||
}
|
||||
if (data.error) {
|
||||
notify({ type: 'error', message: data.error })
|
||||
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||
draft.status = WorkflowRunningStatus.Failed
|
||||
markNodesStopped(draft.tracing)
|
||||
}))
|
||||
setRespondingFalse()
|
||||
resetRunState()
|
||||
onCompleted(getCompletionRes(), taskId, false)
|
||||
isEnd = true
|
||||
return
|
||||
}
|
||||
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||
draft.status = WorkflowRunningStatus.Succeeded
|
||||
draft.files = getFilesInLogs(data.outputs || []) as any[]
|
||||
}))
|
||||
if (!data.outputs) {
|
||||
setCompletionRes('')
|
||||
}
|
||||
else {
|
||||
setCompletionRes(data.outputs)
|
||||
const isStringOutput = Object.keys(data.outputs).length === 1 && typeof data.outputs[Object.keys(data.outputs)[0]] === 'string'
|
||||
if (isStringOutput) {
|
||||
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||
draft.resultText = data.outputs[Object.keys(data.outputs)[0]]
|
||||
}))
|
||||
}
|
||||
}
|
||||
setRespondingFalse()
|
||||
resetRunState()
|
||||
setMessageId(tempMessageId)
|
||||
onCompleted(getCompletionRes(), taskId, true)
|
||||
isEnd = true
|
||||
},
|
||||
onTextChunk: (params) => {
|
||||
const { data: { text } } = params
|
||||
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||
draft.resultText += text
|
||||
}))
|
||||
},
|
||||
onTextReplace: (params) => {
|
||||
const { data: { text } } = params
|
||||
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||
draft.resultText = text
|
||||
}))
|
||||
},
|
||||
},
|
||||
isInstalledApp,
|
||||
installedAppInfo?.id,
|
||||
).catch((error) => {
|
||||
setRespondingFalse()
|
||||
resetRunState()
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
notify({ type: 'error', message })
|
||||
})
|
||||
}
|
||||
else {
|
||||
sendCompletionMessage(data, {
|
||||
onData: (data: string, _isFirstMessage: boolean, { messageId, taskId }) => {
|
||||
tempMessageId = messageId
|
||||
if (taskId && typeof taskId === 'string' && taskId.trim() !== '')
|
||||
setCurrentTaskId(prev => prev ?? taskId)
|
||||
res.push(data)
|
||||
setCompletionRes(res.join(''))
|
||||
},
|
||||
onCompleted: () => {
|
||||
if (isTimeout) {
|
||||
notify({ type: 'warning', message: t('appDebug.warningMessage.timeoutExceeded') })
|
||||
return
|
||||
}
|
||||
setRespondingFalse()
|
||||
resetRunState()
|
||||
setMessageId(tempMessageId)
|
||||
onCompleted(getCompletionRes(), taskId, true)
|
||||
isEnd = true
|
||||
},
|
||||
onMessageReplace: (messageReplace) => {
|
||||
res = [messageReplace.answer]
|
||||
setCompletionRes(res.join(''))
|
||||
},
|
||||
onError() {
|
||||
if (isTimeout) {
|
||||
notify({ type: 'warning', message: t('appDebug.warningMessage.timeoutExceeded') })
|
||||
return
|
||||
}
|
||||
setRespondingFalse()
|
||||
resetRunState()
|
||||
onCompleted(getCompletionRes(), taskId, false)
|
||||
isEnd = true
|
||||
},
|
||||
getAbortController: (abortController) => {
|
||||
abortControllerRef.current = abortController
|
||||
},
|
||||
}, isInstalledApp, installedAppInfo?.id)
|
||||
}
|
||||
}
|
||||
|
||||
const [controlClearMoreLikeThis, setControlClearMoreLikeThis] = useState(0)
|
||||
useEffect(() => {
|
||||
if (controlSend) {
|
||||
handleSend()
|
||||
setControlClearMoreLikeThis(Date.now())
|
||||
}
|
||||
}, [controlSend])
|
||||
|
||||
useEffect(() => {
|
||||
if (controlRetry)
|
||||
handleSend()
|
||||
}, [controlRetry])
|
||||
|
||||
const renderTextGenerationRes = () => (
|
||||
<>
|
||||
{!hideInlineStopButton && isResponding && currentTaskId && (
|
||||
<div className={`mb-3 flex ${isPC ? 'justify-end' : 'justify-center'}`}>
|
||||
<Button
|
||||
variant='secondary'
|
||||
disabled={isStopping}
|
||||
onClick={handleStop}
|
||||
>
|
||||
{
|
||||
isStopping
|
||||
? <RiLoader2Line className='mr-[5px] h-3.5 w-3.5 animate-spin' />
|
||||
: <StopCircle className='mr-[5px] h-3.5 w-3.5' />
|
||||
}
|
||||
<span className='text-xs font-normal'>{t('appDebug.operation.stopResponding')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<TextGenerationRes
|
||||
isWorkflow={isWorkflow}
|
||||
workflowProcessData={workflowProcessData}
|
||||
isError={isError}
|
||||
onRetry={handleSend}
|
||||
content={completionRes}
|
||||
messageId={messageId}
|
||||
isInWebApp
|
||||
moreLikeThis={moreLikeThisEnabled}
|
||||
onFeedback={handleFeedback}
|
||||
feedback={feedback}
|
||||
onSave={handleSaveMessage}
|
||||
isMobile={isMobile}
|
||||
isInstalledApp={isInstalledApp}
|
||||
installedAppId={installedAppInfo?.id}
|
||||
isLoading={isCallBatchAPI ? (!completionRes && isResponding) : false}
|
||||
taskId={isCallBatchAPI ? ((taskId as number) < 10 ? `0${taskId}` : `${taskId}`) : undefined}
|
||||
controlClearMoreLikeThis={controlClearMoreLikeThis}
|
||||
isShowTextToSpeech={isShowTextToSpeech}
|
||||
hideProcessDetail
|
||||
siteInfo={siteInfo}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isCallBatchAPI && !isWorkflow && (
|
||||
(isResponding && !completionRes)
|
||||
? (
|
||||
<div className='flex h-full w-full items-center justify-center'>
|
||||
<Loading type='area' />
|
||||
</div>)
|
||||
: (
|
||||
<>
|
||||
{(isNoData)
|
||||
? <NoData />
|
||||
: renderTextGenerationRes()
|
||||
}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
{!isCallBatchAPI && isWorkflow && (
|
||||
(isResponding && !workflowProcessData)
|
||||
? (
|
||||
<div className='flex h-full w-full items-center justify-center'>
|
||||
<Loading type='area' />
|
||||
</div>
|
||||
)
|
||||
: !workflowProcessData
|
||||
? <NoData />
|
||||
: renderTextGenerationRes()
|
||||
)}
|
||||
{isCallBatchAPI && renderTextGenerationRes()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(Result)
|
||||
Reference in New Issue
Block a user