index.tsx 13 KB

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