index.tsx 6.6 KB

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