'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 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 visionConfig: VisionSettings completionFiles: VisionFile[] siteInfo: SiteInfo | null } const Result: FC = ({ 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('') const completionResRef = useRef() const setCompletionRes = (res: any) => { completionResRef.current = res doSetCompletionRes(res) } const getCompletionRes = () => completionResRef.current const [workflowProcessData, doSetWorkflowProcessData] = useState() const workflowProcessDataRef = useRef() const setWorkflowProcessData = (data: WorkflowProcess) => { workflowProcessDataRef.current = data doSetWorkflowProcessData(data) } const getWorkflowProcessData = () => workflowProcessDataRef.current const { notify } = Toast const isNoData = !completionRes const [messageId, setMessageId] = useState(null) const [feedback, setFeedback] = useState({ 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 = () => { // 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 }) => { 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 const data: Record = { 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 = () => ( ) return (
{!isCallBatchAPI && !isWorkflow && ( (isResponding && !completionRes) ? (
) : ( <> {(isNoData) ? : renderTextGenerationRes() } ) )} { !isCallBatchAPI && isWorkflow && ( (isResponding && !workflowProcessData) ? (
) : !workflowProcessData ? : renderTextGenerationRes() ) } {isCallBatchAPI && (
{renderTextGenerationRes()}
)}
) } export default React.memo(Result)