index.tsx 12 KB

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