index.tsx 7.3 KB

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