index.tsx 14 KB

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