Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

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