You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.tsx 16KB

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