Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

index.tsx 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. 'use client'
  2. import type { FC } from 'react'
  3. import { useTranslation } from 'react-i18next'
  4. import React, { useEffect, useRef, useState } from 'react'
  5. import cn from 'classnames'
  6. import produce from 'immer'
  7. import { useBoolean, useGetState } from 'ahooks'
  8. import { useContext } from 'use-context-selector'
  9. import dayjs from 'dayjs'
  10. import HasNotSetAPIKEY from '../base/warning-mask/has-not-set-api'
  11. import FormattingChanged from '../base/warning-mask/formatting-changed'
  12. import GroupName from '../base/group-name'
  13. import { AppType } from '@/types/app'
  14. import PromptValuePanel, { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
  15. import type { IChatItem } from '@/app/components/app/chat'
  16. import Chat from '@/app/components/app/chat'
  17. import ConfigContext from '@/context/debug-configuration'
  18. import { ToastContext } from '@/app/components/base/toast'
  19. import { fetchConvesationMessages, fetchSuggestedQuestions, sendChatMessage, sendCompletionMessage, stopChatMessageResponding } from '@/service/debug'
  20. import Button from '@/app/components/base/button'
  21. import type { ModelConfig as BackendModelConfig } from '@/types/app'
  22. import { promptVariablesToUserInputsForm } from '@/utils/model-config'
  23. import TextGeneration from '@/app/components/app/text-generate/item'
  24. import { IS_CE_EDITION } from '@/config'
  25. import { useProviderContext } from '@/context/provider-context'
  26. type IDebug = {
  27. hasSetAPIKEY: boolean
  28. onSetting: () => void
  29. }
  30. const Debug: FC<IDebug> = ({
  31. hasSetAPIKEY = true,
  32. onSetting,
  33. }) => {
  34. const { t } = useTranslation()
  35. const {
  36. appId,
  37. mode,
  38. introduction,
  39. suggestedQuestionsAfterAnswerConfig,
  40. speechToTextConfig,
  41. moreLikeThisConfig,
  42. inputs,
  43. // setInputs,
  44. formattingChanged,
  45. setFormattingChanged,
  46. conversationId,
  47. setConversationId,
  48. controlClearChatMessage,
  49. dataSets,
  50. modelConfig,
  51. completionParams,
  52. } = useContext(ConfigContext)
  53. const { currentProvider } = useProviderContext()
  54. const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([])
  55. const chatListDomRef = useRef<HTMLDivElement>(null)
  56. useEffect(() => {
  57. // scroll to bottom
  58. if (chatListDomRef.current)
  59. chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight
  60. }, [chatList])
  61. const getIntroduction = () => replaceStringWithValues(introduction, modelConfig.configs.prompt_variables, inputs)
  62. useEffect(() => {
  63. if (introduction && !chatList.some(item => !item.isAnswer)) {
  64. setChatList([{
  65. id: `${Date.now()}`,
  66. content: getIntroduction(),
  67. isAnswer: true,
  68. isOpeningStatement: true,
  69. }])
  70. }
  71. }, [introduction, modelConfig.configs.prompt_variables, inputs])
  72. const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false)
  73. const [abortController, setAbortController] = useState<AbortController | null>(null)
  74. const [isShowFormattingChangeConfirm, setIsShowFormattingChangeConfirm] = useState(false)
  75. const [isShowSuggestion, setIsShowSuggestion] = useState(false)
  76. const [messageTaskId, setMessageTaskId] = useState('')
  77. const [hasStopResponded, setHasStopResponded, getHasStopResponded] = useGetState(false)
  78. useEffect(() => {
  79. if (formattingChanged && chatList.some(item => !item.isAnswer))
  80. setIsShowFormattingChangeConfirm(true)
  81. setFormattingChanged(false)
  82. }, [formattingChanged])
  83. const clearConversation = async () => {
  84. setConversationId(null)
  85. abortController?.abort()
  86. setResponsingFalse()
  87. setChatList(introduction
  88. ? [{
  89. id: `${Date.now()}`,
  90. content: getIntroduction(),
  91. isAnswer: true,
  92. isOpeningStatement: true,
  93. }]
  94. : [])
  95. setIsShowSuggestion(false)
  96. }
  97. const handleConfirm = () => {
  98. clearConversation()
  99. setIsShowFormattingChangeConfirm(false)
  100. }
  101. const handleCancel = () => {
  102. setIsShowFormattingChangeConfirm(false)
  103. }
  104. const { notify } = useContext(ToastContext)
  105. const logError = (message: string) => {
  106. notify({ type: 'error', message })
  107. }
  108. const checkCanSend = () => {
  109. let hasEmptyInput = ''
  110. const requiredVars = modelConfig.configs.prompt_variables.filter(({ key, name, required }) => {
  111. const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
  112. return res
  113. }) // compatible with old version
  114. // debugger
  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. return !hasEmptyInput
  126. }
  127. const doShowSuggestion = isShowSuggestion && !isResponsing
  128. const [suggestQuestions, setSuggestQuestions] = useState<string[]>([])
  129. const onSend = async (message: string) => {
  130. if (isResponsing) {
  131. notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
  132. return false
  133. }
  134. const postDatasets = dataSets.map(({ id }) => ({
  135. dataset: {
  136. enabled: true,
  137. id,
  138. },
  139. }))
  140. const postModelConfig: BackendModelConfig = {
  141. pre_prompt: modelConfig.configs.prompt_template,
  142. user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
  143. opening_statement: introduction,
  144. more_like_this: {
  145. enabled: false,
  146. },
  147. suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
  148. speech_to_text: speechToTextConfig,
  149. agent_mode: {
  150. enabled: true,
  151. tools: [...postDatasets],
  152. },
  153. model: {
  154. provider: modelConfig.provider,
  155. name: modelConfig.model_id,
  156. completion_params: completionParams as any,
  157. },
  158. }
  159. const data = {
  160. conversation_id: conversationId,
  161. inputs,
  162. query: message,
  163. model_config: postModelConfig,
  164. }
  165. // qustion
  166. const questionId = `question-${Date.now()}`
  167. const questionItem = {
  168. id: questionId,
  169. content: message,
  170. isAnswer: false,
  171. }
  172. const placeholderAnswerId = `answer-placeholder-${Date.now()}`
  173. const placeholderAnswerItem = {
  174. id: placeholderAnswerId,
  175. content: '',
  176. isAnswer: true,
  177. }
  178. const newList = [...getChatList(), questionItem, placeholderAnswerItem]
  179. setChatList(newList)
  180. // answer
  181. const responseItem = {
  182. id: `${Date.now()}`,
  183. content: '',
  184. isAnswer: true,
  185. }
  186. let _newConversationId: null | string = null
  187. setHasStopResponded(false)
  188. setResponsingTrue()
  189. setIsShowSuggestion(false)
  190. sendChatMessage(appId, data, {
  191. getAbortController: (abortController) => {
  192. setAbortController(abortController)
  193. },
  194. onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => {
  195. responseItem.content = responseItem.content + message
  196. if (isFirstMessage && newConversationId) {
  197. setConversationId(newConversationId)
  198. _newConversationId = newConversationId
  199. }
  200. setMessageTaskId(taskId)
  201. if (messageId)
  202. responseItem.id = messageId
  203. // closesure new list is outdated.
  204. const newListWithAnswer = produce(
  205. getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
  206. (draft) => {
  207. if (!draft.find(item => item.id === questionId))
  208. draft.push({ ...questionItem })
  209. draft.push({ ...responseItem })
  210. })
  211. setChatList(newListWithAnswer)
  212. },
  213. async onCompleted(hasError?: boolean) {
  214. setResponsingFalse()
  215. if (hasError)
  216. return
  217. if (_newConversationId) {
  218. const { data }: any = await fetchConvesationMessages(appId, _newConversationId as string)
  219. const newResponseItem = data.find((item: any) => item.id === responseItem.id)
  220. if (!newResponseItem)
  221. return
  222. setChatList(produce(getChatList(), (draft) => {
  223. const index = draft.findIndex(item => item.id === responseItem.id)
  224. if (index !== -1) {
  225. draft[index] = {
  226. ...draft[index],
  227. more: {
  228. time: dayjs.unix(newResponseItem.created_at).format('hh:mm A'),
  229. tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens,
  230. latency: newResponseItem.provider_response_latency.toFixed(2),
  231. },
  232. }
  233. }
  234. }))
  235. }
  236. if (suggestedQuestionsAfterAnswerConfig.enabled && !getHasStopResponded()) {
  237. const { data }: any = await fetchSuggestedQuestions(appId, responseItem.id)
  238. setSuggestQuestions(data)
  239. setIsShowSuggestion(true)
  240. }
  241. },
  242. onError() {
  243. setResponsingFalse()
  244. // role back placeholder answer
  245. setChatList(produce(getChatList(), (draft) => {
  246. draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1)
  247. }))
  248. },
  249. })
  250. return true
  251. }
  252. useEffect(() => {
  253. if (controlClearChatMessage)
  254. setChatList([])
  255. }, [controlClearChatMessage])
  256. const [completionQuery, setCompletionQuery] = useState('')
  257. const [completionRes, setCompletionRes] = useState('')
  258. const sendTextCompletion = async () => {
  259. if (isResponsing) {
  260. notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
  261. return false
  262. }
  263. if (!checkCanSend())
  264. return
  265. if (!completionQuery) {
  266. logError(t('appDebug.errorMessage.queryRequired'))
  267. return false
  268. }
  269. const postDatasets = dataSets.map(({ id }) => ({
  270. dataset: {
  271. enabled: true,
  272. id,
  273. },
  274. }))
  275. const postModelConfig: BackendModelConfig = {
  276. pre_prompt: modelConfig.configs.prompt_template,
  277. user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
  278. opening_statement: introduction,
  279. suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
  280. speech_to_text: speechToTextConfig,
  281. more_like_this: moreLikeThisConfig,
  282. agent_mode: {
  283. enabled: true,
  284. tools: [...postDatasets],
  285. },
  286. model: {
  287. provider: modelConfig.provider,
  288. name: modelConfig.model_id,
  289. completion_params: completionParams as any,
  290. },
  291. }
  292. const data = {
  293. inputs,
  294. query: completionQuery,
  295. model_config: postModelConfig,
  296. }
  297. setCompletionRes('')
  298. const res: string[] = []
  299. setResponsingTrue()
  300. sendCompletionMessage(appId, data, {
  301. onData: (data: string) => {
  302. res.push(data)
  303. setCompletionRes(res.join(''))
  304. },
  305. onCompleted() {
  306. setResponsingFalse()
  307. },
  308. onError() {
  309. setResponsingFalse()
  310. },
  311. })
  312. }
  313. return (
  314. <>
  315. <div className="shrink-0">
  316. <div className='flex items-center justify-between mb-2'>
  317. <div className='h2 '>{t('appDebug.inputs.title')}</div>
  318. {mode === 'chat' && (
  319. <Button className='flex items-center gap-1 !h-8 !bg-white' onClick={clearConversation}>
  320. <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
  321. <path d="M2.66663 2.66629V5.99963H3.05463M3.05463 5.99963C3.49719 4.90505 4.29041 3.98823 5.30998 3.39287C6.32954 2.7975 7.51783 2.55724 8.68861 2.70972C9.85938 2.8622 10.9465 3.39882 11.7795 4.23548C12.6126 5.07213 13.1445 6.16154 13.292 7.33296M3.05463 5.99963H5.99996M13.3333 13.333V9.99963H12.946M12.946 9.99963C12.5028 11.0936 11.7093 12.0097 10.6898 12.6045C9.67038 13.1993 8.48245 13.4393 7.31203 13.2869C6.1416 13.1344 5.05476 12.5982 4.22165 11.7621C3.38854 10.926 2.8562 9.83726 2.70796 8.66629M12.946 9.99963H9.99996" stroke="#1C64F2" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
  322. </svg>
  323. <span className='text-primary-600 text-[13px] font-semibold'>{t('common.operation.refresh')}</span>
  324. </Button>
  325. )}
  326. </div>
  327. <PromptValuePanel
  328. appType={mode as AppType}
  329. value={completionQuery}
  330. onChange={setCompletionQuery}
  331. onSend={sendTextCompletion}
  332. />
  333. </div>
  334. <div className="flex flex-col grow">
  335. {/* Chat */}
  336. {mode === AppType.chat && (
  337. <div className="mt-[34px] h-full flex flex-col">
  338. <div className={cn(doShowSuggestion ? 'pb-[140px]' : (isResponsing ? 'pb-[113px]' : 'pb-[76px]'), 'relative mt-1.5 grow h-[200px] overflow-hidden')}>
  339. <div className="h-full overflow-y-auto overflow-x-hidden" ref={chatListDomRef}>
  340. <Chat
  341. chatList={chatList}
  342. onSend={onSend}
  343. checkCanSend={checkCanSend}
  344. feedbackDisabled
  345. useCurrentUserAvatar
  346. isResponsing={isResponsing}
  347. canStopResponsing={!!messageTaskId}
  348. abortResponsing={async () => {
  349. await stopChatMessageResponding(appId, messageTaskId)
  350. setHasStopResponded(true)
  351. setResponsingFalse()
  352. }}
  353. isShowSuggestion={doShowSuggestion}
  354. suggestionList={suggestQuestions}
  355. isShowSpeechToText={speechToTextConfig.enabled && currentProvider?.provider_name === 'openai'}
  356. />
  357. </div>
  358. </div>
  359. </div>
  360. )}
  361. {/* Text Generation */}
  362. {mode === AppType.completion && (
  363. <div className="mt-6">
  364. <GroupName name={t('appDebug.result')} />
  365. {(completionRes || isResponsing) && (
  366. <TextGeneration
  367. className="mt-2"
  368. content={completionRes}
  369. isLoading={!completionRes && isResponsing}
  370. isInstalledApp={false}
  371. />
  372. )}
  373. </div>
  374. )}
  375. {isShowFormattingChangeConfirm && (
  376. <FormattingChanged
  377. onConfirm={handleConfirm}
  378. onCancel={handleCancel}
  379. />
  380. )}
  381. </div>
  382. {!hasSetAPIKEY && (<HasNotSetAPIKEY isTrailFinished={!IS_CE_EDITION} onSetting={onSetting} />)}
  383. </>
  384. )
  385. }
  386. export default React.memo(Debug)