123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438 |
- '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 = () => {
- // 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<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)
|