|
'use client' |
|
import type { FC } from 'react' |
|
import React, { useEffect, useRef, useState } from 'react' |
|
import { useBoolean } from 'ahooks' |
|
import { t } from 'i18next' |
|
import produce from 'immer' |
|
import cn from '@/utils/classnames' |
|
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 { sendCompletionMessage, sendWorkflowMessage, 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 type { ModerationService } from '@/models/common' |
|
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 { |
|
getProcessedFilesFromResponse, |
|
} from '@/app/components/base/file-uploader/utils' |
|
|
|
export type IResultProps = { |
|
isWorkflow: boolean |
|
isCallBatchAPI: boolean |
|
isPC: boolean |
|
isMobile: boolean |
|
isInstalledApp: boolean |
|
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 |
|
enableModeration?: boolean |
|
moderationService?: (text: string) => ReturnType<ModerationService> |
|
visionConfig: VisionSettings |
|
completionFiles: VisionFile[] |
|
siteInfo: SiteInfo | null |
|
} |
|
|
|
const Result: FC<IResultProps> = ({ |
|
isWorkflow, |
|
isCallBatchAPI, |
|
isPC, |
|
isMobile, |
|
isInstalledApp, |
|
installedAppInfo, |
|
isError, |
|
isShowTextToSpeech, |
|
promptConfig, |
|
moreLikeThisEnabled, |
|
inputs, |
|
controlSend, |
|
controlRetry, |
|
controlStopResponding, |
|
onShowRes, |
|
handleSaveMessage, |
|
taskId, |
|
onCompleted, |
|
visionConfig, |
|
completionFiles, |
|
siteInfo, |
|
}) => { |
|
const [isResponding, { setTrue: setRespondingTrue, setFalse: setRespondingFalse }] = useBoolean(false) |
|
useEffect(() => { |
|
if (controlStopResponding) |
|
setRespondingFalse() |
|
}, [controlStopResponding]) |
|
|
|
const [completionRes, doSetCompletionRes] = useState<any>('') |
|
const completionResRef = useRef<any>() |
|
const setCompletionRes = (res: any) => { |
|
completionResRef.current = res |
|
doSetCompletionRes(res) |
|
} |
|
const getCompletionRes = () => completionResRef.current |
|
const [workflowProcessData, doSetWorkflowProcessData] = useState<WorkflowProcess>() |
|
const workflowProcessDataRef = useRef<WorkflowProcess>() |
|
const setWorkflowProcessData = (data: WorkflowProcess) => { |
|
workflowProcessDataRef.current = data |
|
doSetWorkflowProcessData(data) |
|
} |
|
const getWorkflowProcessData = () => workflowProcessDataRef.current |
|
|
|
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 } }, isInstalledApp, installedAppInfo?.id) |
|
setFeedback(feedback) |
|
} |
|
|
|
const logError = (message: string) => { |
|
notify({ type: 'error', message }) |
|
} |
|
|
|
const checkCanSend = () => { |
|
|
|
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 }) => { |
|
const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null) |
|
return res |
|
}) || [] |
|
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 |
|
|
|
const data: Record<string, any> = { |
|
inputs, |
|
} |
|
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('') |
|
|
|
let res: string[] = [] |
|
let tempMessageId = '' |
|
|
|
if (!isPC) |
|
onShowRes() |
|
|
|
setRespondingTrue() |
|
let isEnd = false |
|
let isTimeout = false; |
|
(async () => { |
|
await sleep(TEXT_GENERATION_TIMEOUT_MS) |
|
if (!isEnd) { |
|
setRespondingFalse() |
|
onCompleted(getCompletionRes(), taskId, false) |
|
isTimeout = true |
|
} |
|
})() |
|
|
|
if (isWorkflow) { |
|
sendWorkflowMessage( |
|
data, |
|
{ |
|
onWorkflowStarted: ({ workflow_run_id }) => { |
|
tempMessageId = workflow_run_id |
|
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, |
|
} as any) |
|
})) |
|
}, |
|
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, |
|
} as any |
|
})) |
|
}, |
|
onNodeStarted: ({ data }) => { |
|
if (data.iteration_id) |
|
return |
|
|
|
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => { |
|
draft.expand = true |
|
draft.tracing!.push({ |
|
...data, |
|
status: NodeRunningStatus.Running, |
|
expand: true, |
|
} as any) |
|
})) |
|
}, |
|
onNodeFinished: ({ data }) => { |
|
if (data.iteration_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, |
|
} as any |
|
} |
|
})) |
|
}, |
|
onWorkflowFinished: ({ data }) => { |
|
if (isTimeout) { |
|
notify({ type: 'warning', message: t('appDebug.warningMessage.timeoutExceeded') }) |
|
return |
|
} |
|
if (data.error) { |
|
notify({ type: 'error', message: data.error }) |
|
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => { |
|
draft.status = WorkflowRunningStatus.Failed |
|
})) |
|
setRespondingFalse() |
|
onCompleted(getCompletionRes(), taskId, false) |
|
isEnd = true |
|
return |
|
} |
|
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => { |
|
draft.status = WorkflowRunningStatus.Succeeded |
|
draft.files = getProcessedFilesFromResponse(data.files || []) |
|
})) |
|
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() |
|
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, |
|
) |
|
} |
|
else { |
|
sendCompletionMessage(data, { |
|
onData: (data: string, _isFirstMessage: boolean, { messageId }) => { |
|
tempMessageId = messageId |
|
res.push(data) |
|
setCompletionRes(res.join('')) |
|
}, |
|
onCompleted: () => { |
|
if (isTimeout) { |
|
notify({ type: 'warning', message: t('appDebug.warningMessage.timeoutExceeded') }) |
|
return |
|
} |
|
setRespondingFalse() |
|
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() |
|
onCompleted(getCompletionRes(), taskId, false) |
|
isEnd = true |
|
}, |
|
}, isInstalledApp, installedAppInfo?.id) |
|
} |
|
} |
|
|
|
const [controlClearMoreLikeThis, setControlClearMoreLikeThis] = useState(0) |
|
useEffect(() => { |
|
if (controlSend) { |
|
handleSend() |
|
setControlClearMoreLikeThis(Date.now()) |
|
} |
|
}, [controlSend]) |
|
|
|
useEffect(() => { |
|
if (controlRetry) |
|
handleSend() |
|
}, [controlRetry]) |
|
|
|
const renderTextGenerationRes = () => ( |
|
<TextGenerationRes |
|
isWorkflow={isWorkflow} |
|
workflowProcessData={workflowProcessData} |
|
className='mt-3' |
|
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 ( |
|
<div className={cn(isNoData && !isCallBatchAPI && 'h-full')}> |
|
{!isCallBatchAPI && !isWorkflow && ( |
|
(isResponding && !completionRes) |
|
? ( |
|
<div className='flex h-full w-full justify-center items-center'> |
|
<Loading type='area' /> |
|
</div>) |
|
: ( |
|
<> |
|
{(isNoData) |
|
? <NoData /> |
|
: renderTextGenerationRes() |
|
} |
|
</> |
|
) |
|
)} |
|
{ |
|
!isCallBatchAPI && isWorkflow && ( |
|
(isResponding && !workflowProcessData) |
|
? ( |
|
<div className='flex h-full w-full justify-center items-center'> |
|
<Loading type='area' /> |
|
</div> |
|
) |
|
: !workflowProcessData |
|
? <NoData /> |
|
: renderTextGenerationRes() |
|
) |
|
} |
|
{isCallBatchAPI && ( |
|
<div className='mt-2'> |
|
{renderTextGenerationRes()} |
|
</div> |
|
)} |
|
</div> |
|
) |
|
} |
|
export default React.memo(Result) |
|
|