index.tsx 11 KB

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