index.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  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 cn from 'classnames'
  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, updateFeedback } from '@/service/share'
  11. import type { Feedbacktype } from '@/app/components/app/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 type { ModerationService } from '@/models/common'
  16. import { TransferMethod, type VisionFile, type VisionSettings } from '@/types/app'
  17. export type IResultProps = {
  18. isCallBatchAPI: boolean
  19. isPC: boolean
  20. isMobile: boolean
  21. isInstalledApp: boolean
  22. installedAppInfo?: InstalledApp
  23. isError: boolean
  24. isShowTextToSpeech: boolean
  25. promptConfig: PromptConfig | null
  26. moreLikeThisEnabled: boolean
  27. inputs: Record<string, any>
  28. controlSend?: number
  29. controlRetry?: number
  30. controlStopResponding?: number
  31. onShowRes: () => void
  32. handleSaveMessage: (messageId: string) => void
  33. taskId?: number
  34. onCompleted: (completionRes: string, taskId?: number, success?: boolean) => void
  35. enableModeration?: boolean
  36. moderationService?: (text: string) => ReturnType<ModerationService>
  37. visionConfig: VisionSettings
  38. completionFiles: VisionFile[]
  39. }
  40. const Result: FC<IResultProps> = ({
  41. isCallBatchAPI,
  42. isPC,
  43. isMobile,
  44. isInstalledApp,
  45. installedAppInfo,
  46. isError,
  47. isShowTextToSpeech,
  48. promptConfig,
  49. moreLikeThisEnabled,
  50. inputs,
  51. controlSend,
  52. controlRetry,
  53. controlStopResponding,
  54. onShowRes,
  55. handleSaveMessage,
  56. taskId,
  57. onCompleted,
  58. visionConfig,
  59. completionFiles,
  60. }) => {
  61. const [isResponding, { setTrue: setRespondingTrue, setFalse: setRespondingFalse }] = useBoolean(false)
  62. useEffect(() => {
  63. if (controlStopResponding)
  64. setRespondingFalse()
  65. }, [controlStopResponding])
  66. const [completionRes, doSetCompletionRes] = useState('')
  67. const completionResRef = useRef('')
  68. const setCompletionRes = (res: string) => {
  69. completionResRef.current = res
  70. doSetCompletionRes(res)
  71. }
  72. const getCompletionRes = () => completionResRef.current
  73. const { notify } = Toast
  74. const isNoData = !completionRes
  75. const [messageId, setMessageId] = useState<string | null>(null)
  76. const [feedback, setFeedback] = useState<Feedbacktype>({
  77. rating: null,
  78. })
  79. const handleFeedback = async (feedback: Feedbacktype) => {
  80. await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } }, isInstalledApp, installedAppInfo?.id)
  81. setFeedback(feedback)
  82. }
  83. const logError = (message: string) => {
  84. notify({ type: 'error', message })
  85. }
  86. const checkCanSend = () => {
  87. // batch will check outer
  88. if (isCallBatchAPI)
  89. return true
  90. const prompt_variables = promptConfig?.prompt_variables
  91. if (!prompt_variables || prompt_variables?.length === 0) {
  92. if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
  93. notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
  94. return false
  95. }
  96. return true
  97. }
  98. let hasEmptyInput = ''
  99. const requiredVars = prompt_variables?.filter(({ key, name, required }) => {
  100. const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
  101. return res
  102. }) || [] // compatible with old version
  103. requiredVars.forEach(({ key, name }) => {
  104. if (hasEmptyInput)
  105. return
  106. if (!inputs[key])
  107. hasEmptyInput = name
  108. })
  109. if (hasEmptyInput) {
  110. logError(t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }))
  111. return false
  112. }
  113. if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
  114. notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
  115. return false
  116. }
  117. return !hasEmptyInput
  118. }
  119. const handleSend = async () => {
  120. if (isResponding) {
  121. notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
  122. return false
  123. }
  124. if (!checkCanSend())
  125. return
  126. const data: Record<string, any> = {
  127. inputs,
  128. }
  129. if (visionConfig.enabled && completionFiles && completionFiles?.length > 0) {
  130. data.files = completionFiles.map((item) => {
  131. if (item.transfer_method === TransferMethod.local_file) {
  132. return {
  133. ...item,
  134. url: '',
  135. }
  136. }
  137. return item
  138. })
  139. }
  140. setMessageId(null)
  141. setFeedback({
  142. rating: null,
  143. })
  144. setCompletionRes('')
  145. let res: string[] = []
  146. let tempMessageId = ''
  147. if (!isPC)
  148. onShowRes()
  149. setRespondingTrue()
  150. const startTime = Date.now()
  151. let isTimeout = false
  152. const runId = setInterval(() => {
  153. if (Date.now() - startTime > 1000 * 60) { // 1min timeout
  154. clearInterval(runId)
  155. setRespondingFalse()
  156. onCompleted(getCompletionRes(), taskId, false)
  157. isTimeout = true
  158. }
  159. }, 1000)
  160. sendCompletionMessage(data, {
  161. onData: (data: string, _isFirstMessage: boolean, { messageId }) => {
  162. tempMessageId = messageId
  163. res.push(data)
  164. setCompletionRes(res.join(''))
  165. },
  166. onCompleted: () => {
  167. if (isTimeout)
  168. return
  169. setRespondingFalse()
  170. setMessageId(tempMessageId)
  171. onCompleted(getCompletionRes(), taskId, true)
  172. clearInterval(runId)
  173. },
  174. onMessageReplace: (messageReplace) => {
  175. res = [messageReplace.answer]
  176. setCompletionRes(res.join(''))
  177. },
  178. onError() {
  179. if (isTimeout)
  180. return
  181. setRespondingFalse()
  182. onCompleted(getCompletionRes(), taskId, false)
  183. clearInterval(runId)
  184. },
  185. }, isInstalledApp, installedAppInfo?.id)
  186. }
  187. const [controlClearMoreLikeThis, setControlClearMoreLikeThis] = useState(0)
  188. useEffect(() => {
  189. if (controlSend) {
  190. handleSend()
  191. setControlClearMoreLikeThis(Date.now())
  192. }
  193. }, [controlSend])
  194. useEffect(() => {
  195. if (controlRetry)
  196. handleSend()
  197. }, [controlRetry])
  198. const renderTextGenerationRes = () => (
  199. <TextGenerationRes
  200. className='mt-3'
  201. isError={isError}
  202. onRetry={handleSend}
  203. content={completionRes}
  204. messageId={messageId}
  205. isInWebApp
  206. moreLikeThis={moreLikeThisEnabled}
  207. onFeedback={handleFeedback}
  208. feedback={feedback}
  209. onSave={handleSaveMessage}
  210. isMobile={isMobile}
  211. isInstalledApp={isInstalledApp}
  212. installedAppId={installedAppInfo?.id}
  213. isLoading={isCallBatchAPI ? (!completionRes && isResponding) : false}
  214. taskId={isCallBatchAPI ? ((taskId as number) < 10 ? `0${taskId}` : `${taskId}`) : undefined}
  215. controlClearMoreLikeThis={controlClearMoreLikeThis}
  216. isShowTextToSpeech={isShowTextToSpeech}
  217. />
  218. )
  219. return (
  220. <div className={cn(isNoData && !isCallBatchAPI && 'h-full')}>
  221. {!isCallBatchAPI && (
  222. (isResponding && !completionRes)
  223. ? (
  224. <div className='flex h-full w-full justify-center items-center'>
  225. <Loading type='area' />
  226. </div>)
  227. : (
  228. <>
  229. {isNoData
  230. ? <NoData />
  231. : renderTextGenerationRes()
  232. }
  233. </>
  234. )
  235. )}
  236. {isCallBatchAPI && (
  237. <div className='mt-2'>
  238. {renderTextGenerationRes()}
  239. </div>
  240. )}
  241. </div>
  242. )
  243. }
  244. export default React.memo(Result)