index.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. 'use client'
  2. import type { FC } from 'react'
  3. import React, { useEffect, useRef, useState } from 'react'
  4. import { useBoolean } from 'ahooks'
  5. import { t } from 'i18next'
  6. import produce from 'immer'
  7. import cn from '@/utils/classnames'
  8. import TextGenerationRes from '@/app/components/app/text-generate/item'
  9. import NoData from '@/app/components/share/text-generation/no-data'
  10. import Toast from '@/app/components/base/toast'
  11. import { sendCompletionMessage, sendWorkflowMessage, updateFeedback } from '@/service/share'
  12. import type { FeedbackType } from '@/app/components/base/chat/chat/type'
  13. import Loading from '@/app/components/base/loading'
  14. import type { PromptConfig } from '@/models/debug'
  15. import type { InstalledApp } from '@/models/explore'
  16. import type { ModerationService } from '@/models/common'
  17. import { TransferMethod, type VisionFile, type VisionSettings } from '@/types/app'
  18. import { NodeRunningStatus, WorkflowRunningStatus } from '@/app/components/workflow/types'
  19. import type { WorkflowProcess } from '@/app/components/base/chat/types'
  20. import { sleep } from '@/utils'
  21. import type { SiteInfo } from '@/models/share'
  22. import { TEXT_GENERATION_TIMEOUT_MS } from '@/config'
  23. export type IResultProps = {
  24. isWorkflow: boolean
  25. isCallBatchAPI: boolean
  26. isPC: boolean
  27. isMobile: boolean
  28. isInstalledApp: boolean
  29. installedAppInfo?: InstalledApp
  30. isError: boolean
  31. isShowTextToSpeech: boolean
  32. promptConfig: PromptConfig | null
  33. moreLikeThisEnabled: boolean
  34. inputs: Record<string, any>
  35. controlSend?: number
  36. controlRetry?: number
  37. controlStopResponding?: number
  38. onShowRes: () => void
  39. handleSaveMessage: (messageId: string) => void
  40. taskId?: number
  41. onCompleted: (completionRes: string, taskId?: number, success?: boolean) => void
  42. enableModeration?: boolean
  43. moderationService?: (text: string) => ReturnType<ModerationService>
  44. visionConfig: VisionSettings
  45. completionFiles: VisionFile[]
  46. siteInfo: SiteInfo | null
  47. }
  48. const Result: FC<IResultProps> = ({
  49. isWorkflow,
  50. isCallBatchAPI,
  51. isPC,
  52. isMobile,
  53. isInstalledApp,
  54. installedAppInfo,
  55. isError,
  56. isShowTextToSpeech,
  57. promptConfig,
  58. moreLikeThisEnabled,
  59. inputs,
  60. controlSend,
  61. controlRetry,
  62. controlStopResponding,
  63. onShowRes,
  64. handleSaveMessage,
  65. taskId,
  66. onCompleted,
  67. visionConfig,
  68. completionFiles,
  69. siteInfo,
  70. }) => {
  71. const [isResponding, { setTrue: setRespondingTrue, setFalse: setRespondingFalse }] = useBoolean(false)
  72. useEffect(() => {
  73. if (controlStopResponding)
  74. setRespondingFalse()
  75. }, [controlStopResponding])
  76. const [completionRes, doSetCompletionRes] = useState<any>('')
  77. const completionResRef = useRef<any>()
  78. const setCompletionRes = (res: any) => {
  79. completionResRef.current = res
  80. doSetCompletionRes(res)
  81. }
  82. const getCompletionRes = () => completionResRef.current
  83. const [workflowProcessData, doSetWorkflowProcessData] = useState<WorkflowProcess>()
  84. const workflowProcessDataRef = useRef<WorkflowProcess>()
  85. const setWorkflowProcessData = (data: WorkflowProcess) => {
  86. workflowProcessDataRef.current = data
  87. doSetWorkflowProcessData(data)
  88. }
  89. const getWorkflowProcessData = () => workflowProcessDataRef.current
  90. const { notify } = Toast
  91. const isNoData = !completionRes
  92. const [messageId, setMessageId] = useState<string | null>(null)
  93. const [feedback, setFeedback] = useState<FeedbackType>({
  94. rating: null,
  95. })
  96. const handleFeedback = async (feedback: FeedbackType) => {
  97. await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } }, isInstalledApp, installedAppInfo?.id)
  98. setFeedback(feedback)
  99. }
  100. const logError = (message: string) => {
  101. notify({ type: 'error', message })
  102. }
  103. const checkCanSend = () => {
  104. // batch will check outer
  105. if (isCallBatchAPI)
  106. return true
  107. const prompt_variables = promptConfig?.prompt_variables
  108. if (!prompt_variables || prompt_variables?.length === 0) {
  109. if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
  110. notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
  111. return false
  112. }
  113. return true
  114. }
  115. let hasEmptyInput = ''
  116. const requiredVars = prompt_variables?.filter(({ key, name, required }) => {
  117. const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
  118. return res
  119. }) || [] // compatible with old version
  120. requiredVars.forEach(({ key, name }) => {
  121. if (hasEmptyInput)
  122. return
  123. if (!inputs[key])
  124. hasEmptyInput = name
  125. })
  126. if (hasEmptyInput) {
  127. logError(t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }))
  128. return false
  129. }
  130. if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
  131. notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
  132. return false
  133. }
  134. return !hasEmptyInput
  135. }
  136. const handleSend = async () => {
  137. if (isResponding) {
  138. notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
  139. return false
  140. }
  141. if (!checkCanSend())
  142. return
  143. const data: Record<string, any> = {
  144. inputs,
  145. }
  146. if (visionConfig.enabled && completionFiles && completionFiles?.length > 0) {
  147. data.files = completionFiles.map((item) => {
  148. if (item.transfer_method === TransferMethod.local_file) {
  149. return {
  150. ...item,
  151. url: '',
  152. }
  153. }
  154. return item
  155. })
  156. }
  157. setMessageId(null)
  158. setFeedback({
  159. rating: null,
  160. })
  161. setCompletionRes('')
  162. let res: string[] = []
  163. let tempMessageId = ''
  164. if (!isPC)
  165. onShowRes()
  166. setRespondingTrue()
  167. let isEnd = false
  168. let isTimeout = false;
  169. (async () => {
  170. await sleep(TEXT_GENERATION_TIMEOUT_MS)
  171. if (!isEnd) {
  172. setRespondingFalse()
  173. onCompleted(getCompletionRes(), taskId, false)
  174. isTimeout = true
  175. }
  176. })()
  177. if (isWorkflow) {
  178. sendWorkflowMessage(
  179. data,
  180. {
  181. onWorkflowStarted: ({ workflow_run_id }) => {
  182. tempMessageId = workflow_run_id
  183. setWorkflowProcessData({
  184. status: WorkflowRunningStatus.Running,
  185. tracing: [],
  186. expand: false,
  187. resultText: '',
  188. })
  189. },
  190. onIterationStart: ({ data }) => {
  191. setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
  192. draft.expand = true
  193. draft.tracing!.push({
  194. ...data,
  195. status: NodeRunningStatus.Running,
  196. expand: true,
  197. } as any)
  198. }))
  199. },
  200. onIterationNext: () => {
  201. setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
  202. draft.expand = true
  203. const iterations = draft.tracing.find(item => item.node_id === data.node_id
  204. && (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id))!
  205. iterations?.details!.push([])
  206. }))
  207. },
  208. onIterationFinish: ({ data }) => {
  209. setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
  210. draft.expand = true
  211. const iterationsIndex = draft.tracing.findIndex(item => item.node_id === data.node_id
  212. && (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id))!
  213. draft.tracing[iterationsIndex] = {
  214. ...data,
  215. expand: !!data.error,
  216. } as any
  217. }))
  218. },
  219. onNodeStarted: ({ data }) => {
  220. if (data.iteration_id)
  221. return
  222. setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
  223. draft.expand = true
  224. draft.tracing!.push({
  225. ...data,
  226. status: NodeRunningStatus.Running,
  227. expand: true,
  228. } as any)
  229. }))
  230. },
  231. onNodeFinished: ({ data }) => {
  232. if (data.iteration_id)
  233. return
  234. setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
  235. const currentIndex = draft.tracing!.findIndex(trace => trace.node_id === data.node_id
  236. && (trace.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || trace.parallel_id === data.execution_metadata?.parallel_id))
  237. if (currentIndex > -1 && draft.tracing) {
  238. draft.tracing[currentIndex] = {
  239. ...(draft.tracing[currentIndex].extras
  240. ? { extras: draft.tracing[currentIndex].extras }
  241. : {}),
  242. ...data,
  243. expand: !!data.error,
  244. } as any
  245. }
  246. }))
  247. },
  248. onWorkflowFinished: ({ data }) => {
  249. if (isTimeout) {
  250. notify({ type: 'warning', message: t('appDebug.warningMessage.timeoutExceeded') })
  251. return
  252. }
  253. if (data.error) {
  254. notify({ type: 'error', message: data.error })
  255. setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
  256. draft.status = WorkflowRunningStatus.Failed
  257. }))
  258. setRespondingFalse()
  259. onCompleted(getCompletionRes(), taskId, false)
  260. isEnd = true
  261. return
  262. }
  263. setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
  264. draft.status = WorkflowRunningStatus.Succeeded
  265. }))
  266. if (!data.outputs) {
  267. setCompletionRes('')
  268. }
  269. else {
  270. setCompletionRes(data.outputs)
  271. const isStringOutput = Object.keys(data.outputs).length === 1 && typeof data.outputs[Object.keys(data.outputs)[0]] === 'string'
  272. if (isStringOutput) {
  273. setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
  274. draft.resultText = data.outputs[Object.keys(data.outputs)[0]]
  275. }))
  276. }
  277. }
  278. setRespondingFalse()
  279. setMessageId(tempMessageId)
  280. onCompleted(getCompletionRes(), taskId, true)
  281. isEnd = true
  282. },
  283. onTextChunk: (params) => {
  284. const { data: { text } } = params
  285. setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
  286. draft.resultText += text
  287. }))
  288. },
  289. onTextReplace: (params) => {
  290. const { data: { text } } = params
  291. setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
  292. draft.resultText = text
  293. }))
  294. },
  295. },
  296. isInstalledApp,
  297. installedAppInfo?.id,
  298. )
  299. }
  300. else {
  301. sendCompletionMessage(data, {
  302. onData: (data: string, _isFirstMessage: boolean, { messageId }) => {
  303. tempMessageId = messageId
  304. res.push(data)
  305. setCompletionRes(res.join(''))
  306. },
  307. onCompleted: () => {
  308. if (isTimeout) {
  309. notify({ type: 'warning', message: t('appDebug.warningMessage.timeoutExceeded') })
  310. return
  311. }
  312. setRespondingFalse()
  313. setMessageId(tempMessageId)
  314. onCompleted(getCompletionRes(), taskId, true)
  315. isEnd = true
  316. },
  317. onMessageReplace: (messageReplace) => {
  318. res = [messageReplace.answer]
  319. setCompletionRes(res.join(''))
  320. },
  321. onError() {
  322. if (isTimeout) {
  323. notify({ type: 'warning', message: t('appDebug.warningMessage.timeoutExceeded') })
  324. return
  325. }
  326. setRespondingFalse()
  327. onCompleted(getCompletionRes(), taskId, false)
  328. isEnd = true
  329. },
  330. }, isInstalledApp, installedAppInfo?.id)
  331. }
  332. }
  333. const [controlClearMoreLikeThis, setControlClearMoreLikeThis] = useState(0)
  334. useEffect(() => {
  335. if (controlSend) {
  336. handleSend()
  337. setControlClearMoreLikeThis(Date.now())
  338. }
  339. }, [controlSend])
  340. useEffect(() => {
  341. if (controlRetry)
  342. handleSend()
  343. }, [controlRetry])
  344. const renderTextGenerationRes = () => (
  345. <TextGenerationRes
  346. isWorkflow={isWorkflow}
  347. workflowProcessData={workflowProcessData}
  348. className='mt-3'
  349. isError={isError}
  350. onRetry={handleSend}
  351. content={completionRes}
  352. messageId={messageId}
  353. isInWebApp
  354. moreLikeThis={moreLikeThisEnabled}
  355. onFeedback={handleFeedback}
  356. feedback={feedback}
  357. onSave={handleSaveMessage}
  358. isMobile={isMobile}
  359. isInstalledApp={isInstalledApp}
  360. installedAppId={installedAppInfo?.id}
  361. isLoading={isCallBatchAPI ? (!completionRes && isResponding) : false}
  362. taskId={isCallBatchAPI ? ((taskId as number) < 10 ? `0${taskId}` : `${taskId}`) : undefined}
  363. controlClearMoreLikeThis={controlClearMoreLikeThis}
  364. isShowTextToSpeech={isShowTextToSpeech}
  365. hideProcessDetail
  366. siteInfo={siteInfo}
  367. />
  368. )
  369. return (
  370. <div className={cn(isNoData && !isCallBatchAPI && 'h-full')}>
  371. {!isCallBatchAPI && !isWorkflow && (
  372. (isResponding && !completionRes)
  373. ? (
  374. <div className='flex h-full w-full justify-center items-center'>
  375. <Loading type='area' />
  376. </div>)
  377. : (
  378. <>
  379. {(isNoData)
  380. ? <NoData />
  381. : renderTextGenerationRes()
  382. }
  383. </>
  384. )
  385. )}
  386. {
  387. !isCallBatchAPI && isWorkflow && (
  388. (isResponding && !workflowProcessData)
  389. ? (
  390. <div className='flex h-full w-full justify-center items-center'>
  391. <Loading type='area' />
  392. </div>
  393. )
  394. : !workflowProcessData
  395. ? <NoData />
  396. : renderTextGenerationRes()
  397. )
  398. }
  399. {isCallBatchAPI && (
  400. <div className='mt-2'>
  401. {renderTextGenerationRes()}
  402. </div>
  403. )}
  404. </div>
  405. )
  406. }
  407. export default React.memo(Result)