index.tsx 15 KB

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