index.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. 'use client'
  2. import type { FC } from 'react'
  3. import useSWR from 'swr'
  4. import { useTranslation } from 'react-i18next'
  5. import React, { useEffect, useRef, useState } from 'react'
  6. import cn from 'classnames'
  7. import produce from 'immer'
  8. import { useBoolean, useGetState } from 'ahooks'
  9. import { useContext } from 'use-context-selector'
  10. import dayjs from 'dayjs'
  11. import HasNotSetAPIKEY from '../base/warning-mask/has-not-set-api'
  12. import FormattingChanged from '../base/warning-mask/formatting-changed'
  13. import GroupName from '../base/group-name'
  14. import CannotQueryDataset from '../base/warning-mask/cannot-query-dataset'
  15. import { AppType, ModelModeType, TransferMethod } from '@/types/app'
  16. import PromptValuePanel, { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
  17. import type { IChatItem } from '@/app/components/app/chat/type'
  18. import Chat from '@/app/components/app/chat'
  19. import ConfigContext from '@/context/debug-configuration'
  20. import { ToastContext } from '@/app/components/base/toast'
  21. import { fetchConvesationMessages, fetchSuggestedQuestions, sendChatMessage, sendCompletionMessage, stopChatMessageResponding } from '@/service/debug'
  22. import Button from '@/app/components/base/button'
  23. import type { ModelConfig as BackendModelConfig, VisionFile } from '@/types/app'
  24. import { promptVariablesToUserInputsForm } from '@/utils/model-config'
  25. import TextGeneration from '@/app/components/app/text-generate/item'
  26. import { IS_CE_EDITION } from '@/config'
  27. import type { Inputs } from '@/models/debug'
  28. import { fetchFileUploadConfig } from '@/service/common'
  29. import type { Annotation as AnnotationType } from '@/models/log'
  30. import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
  31. type IDebug = {
  32. hasSetAPIKEY: boolean
  33. onSetting: () => void
  34. inputs: Inputs
  35. }
  36. const Debug: FC<IDebug> = ({
  37. hasSetAPIKEY = true,
  38. onSetting,
  39. inputs,
  40. }) => {
  41. const { t } = useTranslation()
  42. const {
  43. appId,
  44. mode,
  45. modelModeType,
  46. hasSetBlockStatus,
  47. isAdvancedMode,
  48. promptMode,
  49. chatPromptConfig,
  50. completionPromptConfig,
  51. introduction,
  52. suggestedQuestionsAfterAnswerConfig,
  53. speechToTextConfig,
  54. citationConfig,
  55. moderationConfig,
  56. moreLikeThisConfig,
  57. formattingChanged,
  58. setFormattingChanged,
  59. conversationId,
  60. setConversationId,
  61. controlClearChatMessage,
  62. dataSets,
  63. modelConfig,
  64. completionParams,
  65. hasSetContextVar,
  66. datasetConfigs,
  67. externalDataToolsConfig,
  68. visionConfig,
  69. annotationConfig,
  70. } = useContext(ConfigContext)
  71. const { data: speech2textDefaultModel } = useDefaultModel(4)
  72. const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([])
  73. const chatListDomRef = useRef<HTMLDivElement>(null)
  74. const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig)
  75. useEffect(() => {
  76. // scroll to bottom
  77. if (chatListDomRef.current)
  78. chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight
  79. }, [chatList])
  80. const getIntroduction = () => replaceStringWithValues(introduction, modelConfig.configs.prompt_variables, inputs)
  81. useEffect(() => {
  82. if (introduction && !chatList.some(item => !item.isAnswer)) {
  83. setChatList([{
  84. id: `${Date.now()}`,
  85. content: getIntroduction(),
  86. isAnswer: true,
  87. isOpeningStatement: true,
  88. }])
  89. }
  90. }, [introduction, modelConfig.configs.prompt_variables, inputs])
  91. const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false)
  92. const [abortController, setAbortController] = useState<AbortController | null>(null)
  93. const [isShowFormattingChangeConfirm, setIsShowFormattingChangeConfirm] = useState(false)
  94. const [isShowCannotQueryDataset, setShowCannotQueryDataset] = useState(false)
  95. const [isShowSuggestion, setIsShowSuggestion] = useState(false)
  96. const [messageTaskId, setMessageTaskId] = useState('')
  97. const [hasStopResponded, setHasStopResponded, getHasStopResponded] = useGetState(false)
  98. useEffect(() => {
  99. if (formattingChanged && chatList.some(item => !item.isAnswer))
  100. setIsShowFormattingChangeConfirm(true)
  101. setFormattingChanged(false)
  102. }, [formattingChanged])
  103. const clearConversation = async () => {
  104. setConversationId(null)
  105. abortController?.abort()
  106. setResponsingFalse()
  107. setChatList(introduction
  108. ? [{
  109. id: `${Date.now()}`,
  110. content: getIntroduction(),
  111. isAnswer: true,
  112. isOpeningStatement: true,
  113. }]
  114. : [])
  115. setIsShowSuggestion(false)
  116. }
  117. const handleConfirm = () => {
  118. clearConversation()
  119. setIsShowFormattingChangeConfirm(false)
  120. }
  121. const handleCancel = () => {
  122. setIsShowFormattingChangeConfirm(false)
  123. }
  124. const { notify } = useContext(ToastContext)
  125. const logError = (message: string) => {
  126. notify({ type: 'error', message })
  127. }
  128. const checkCanSend = () => {
  129. if (isAdvancedMode && mode === AppType.chat) {
  130. if (modelModeType === ModelModeType.completion) {
  131. if (!hasSetBlockStatus.history) {
  132. notify({ type: 'error', message: t('appDebug.otherError.historyNoBeEmpty'), duration: 3000 })
  133. return false
  134. }
  135. if (!hasSetBlockStatus.query) {
  136. notify({ type: 'error', message: t('appDebug.otherError.queryNoBeEmpty'), duration: 3000 })
  137. return false
  138. }
  139. }
  140. }
  141. let hasEmptyInput = ''
  142. const requiredVars = modelConfig.configs.prompt_variables.filter(({ key, name, required }) => {
  143. const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
  144. return res
  145. }) // compatible with old version
  146. // debugger
  147. requiredVars.forEach(({ key, name }) => {
  148. if (hasEmptyInput)
  149. return
  150. if (!inputs[key])
  151. hasEmptyInput = name
  152. })
  153. if (hasEmptyInput) {
  154. logError(t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }))
  155. return false
  156. }
  157. // eslint-disable-next-line @typescript-eslint/no-use-before-define
  158. if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
  159. notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
  160. return false
  161. }
  162. return !hasEmptyInput
  163. }
  164. const doShowSuggestion = isShowSuggestion && !isResponsing
  165. const [suggestQuestions, setSuggestQuestions] = useState<string[]>([])
  166. const onSend = async (message: string, files?: VisionFile[]) => {
  167. if (isResponsing) {
  168. notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
  169. return false
  170. }
  171. if (files?.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
  172. notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
  173. return false
  174. }
  175. const postDatasets = dataSets.map(({ id }) => ({
  176. dataset: {
  177. enabled: true,
  178. id,
  179. },
  180. }))
  181. const contextVar = modelConfig.configs.prompt_variables.find(item => item.is_context_var)?.key
  182. const postModelConfig: BackendModelConfig = {
  183. pre_prompt: !isAdvancedMode ? modelConfig.configs.prompt_template : '',
  184. prompt_type: promptMode,
  185. chat_prompt_config: {},
  186. completion_prompt_config: {},
  187. user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
  188. dataset_query_variable: contextVar || '',
  189. opening_statement: introduction,
  190. more_like_this: {
  191. enabled: false,
  192. },
  193. suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
  194. speech_to_text: speechToTextConfig,
  195. retriever_resource: citationConfig,
  196. sensitive_word_avoidance: moderationConfig,
  197. external_data_tools: externalDataToolsConfig,
  198. agent_mode: {
  199. enabled: true,
  200. tools: [...postDatasets],
  201. },
  202. model: {
  203. provider: modelConfig.provider,
  204. name: modelConfig.model_id,
  205. mode: modelConfig.mode,
  206. completion_params: completionParams as any,
  207. },
  208. dataset_configs: datasetConfigs,
  209. file_upload: {
  210. image: visionConfig,
  211. },
  212. annotation_reply: annotationConfig,
  213. }
  214. if (isAdvancedMode) {
  215. postModelConfig.chat_prompt_config = chatPromptConfig
  216. postModelConfig.completion_prompt_config = completionPromptConfig
  217. }
  218. const data: Record<string, any> = {
  219. conversation_id: conversationId,
  220. inputs,
  221. query: message,
  222. model_config: postModelConfig,
  223. }
  224. if (visionConfig.enabled && files && files?.length > 0) {
  225. data.files = files.map((item) => {
  226. if (item.transfer_method === TransferMethod.local_file) {
  227. return {
  228. ...item,
  229. url: '',
  230. }
  231. }
  232. return item
  233. })
  234. }
  235. // qustion
  236. const questionId = `question-${Date.now()}`
  237. const questionItem = {
  238. id: questionId,
  239. content: message,
  240. isAnswer: false,
  241. message_files: files,
  242. }
  243. const placeholderAnswerId = `answer-placeholder-${Date.now()}`
  244. const placeholderAnswerItem = {
  245. id: placeholderAnswerId,
  246. content: '',
  247. isAnswer: true,
  248. }
  249. const newList = [...getChatList(), questionItem, placeholderAnswerItem]
  250. setChatList(newList)
  251. // answer
  252. const responseItem: IChatItem = {
  253. id: `${Date.now()}`,
  254. content: '',
  255. isAnswer: true,
  256. }
  257. let _newConversationId: null | string = null
  258. setHasStopResponded(false)
  259. setResponsingTrue()
  260. setIsShowSuggestion(false)
  261. sendChatMessage(appId, data, {
  262. getAbortController: (abortController) => {
  263. setAbortController(abortController)
  264. },
  265. onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => {
  266. responseItem.content = responseItem.content + message
  267. if (isFirstMessage && newConversationId) {
  268. setConversationId(newConversationId)
  269. _newConversationId = newConversationId
  270. }
  271. setMessageTaskId(taskId)
  272. if (messageId)
  273. responseItem.id = messageId
  274. // closesure new list is outdated.
  275. const newListWithAnswer = produce(
  276. getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
  277. (draft) => {
  278. if (!draft.find(item => item.id === questionId))
  279. draft.push({ ...questionItem })
  280. draft.push({ ...responseItem })
  281. })
  282. setChatList(newListWithAnswer)
  283. },
  284. async onCompleted(hasError?: boolean) {
  285. setResponsingFalse()
  286. if (hasError)
  287. return
  288. if (_newConversationId) {
  289. const { data }: any = await fetchConvesationMessages(appId, _newConversationId as string)
  290. const newResponseItem = data.find((item: any) => item.id === responseItem.id)
  291. if (!newResponseItem)
  292. return
  293. setChatList(produce(getChatList(), (draft) => {
  294. const index = draft.findIndex(item => item.id === responseItem.id)
  295. if (index !== -1) {
  296. const requestion = draft[index - 1]
  297. draft[index - 1] = {
  298. ...requestion,
  299. log: newResponseItem.message,
  300. }
  301. draft[index] = {
  302. ...draft[index],
  303. more: {
  304. time: dayjs.unix(newResponseItem.created_at).format('hh:mm A'),
  305. tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens,
  306. latency: newResponseItem.provider_response_latency.toFixed(2),
  307. },
  308. }
  309. }
  310. }))
  311. }
  312. if (suggestedQuestionsAfterAnswerConfig.enabled && !getHasStopResponded()) {
  313. const { data }: any = await fetchSuggestedQuestions(appId, responseItem.id)
  314. setSuggestQuestions(data)
  315. setIsShowSuggestion(true)
  316. }
  317. },
  318. onMessageEnd: (messageEnd) => {
  319. if (messageEnd.metadata?.annotation_reply) {
  320. responseItem.id = messageEnd.id
  321. responseItem.annotation = ({
  322. id: messageEnd.metadata.annotation_reply.id,
  323. authorName: messageEnd.metadata.annotation_reply.account.name,
  324. } as AnnotationType)
  325. const newListWithAnswer = produce(
  326. getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
  327. (draft) => {
  328. if (!draft.find(item => item.id === questionId))
  329. draft.push({ ...questionItem })
  330. draft.push({
  331. ...responseItem,
  332. })
  333. })
  334. setChatList(newListWithAnswer)
  335. return
  336. }
  337. responseItem.citation = messageEnd.metadata?.retriever_resources || []
  338. const newListWithAnswer = produce(
  339. getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
  340. (draft) => {
  341. if (!draft.find(item => item.id === questionId))
  342. draft.push({ ...questionItem })
  343. draft.push({ ...responseItem })
  344. })
  345. setChatList(newListWithAnswer)
  346. },
  347. onMessageReplace: (messageReplace) => {
  348. responseItem.content = messageReplace.answer
  349. },
  350. onError() {
  351. setResponsingFalse()
  352. // role back placeholder answer
  353. setChatList(produce(getChatList(), (draft) => {
  354. draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1)
  355. }))
  356. },
  357. })
  358. return true
  359. }
  360. useEffect(() => {
  361. if (controlClearChatMessage)
  362. setChatList([])
  363. }, [controlClearChatMessage])
  364. const [completionRes, setCompletionRes] = useState('')
  365. const [messageId, setMessageId] = useState<string | null>(null)
  366. const [completionFiles, setCompletionFiles] = useState<VisionFile[]>([])
  367. const sendTextCompletion = async () => {
  368. if (isResponsing) {
  369. notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
  370. return false
  371. }
  372. if (dataSets.length > 0 && !hasSetContextVar) {
  373. setShowCannotQueryDataset(true)
  374. return true
  375. }
  376. if (!checkCanSend())
  377. return
  378. const postDatasets = dataSets.map(({ id }) => ({
  379. dataset: {
  380. enabled: true,
  381. id,
  382. },
  383. }))
  384. const contextVar = modelConfig.configs.prompt_variables.find(item => item.is_context_var)?.key
  385. const postModelConfig: BackendModelConfig = {
  386. pre_prompt: !isAdvancedMode ? modelConfig.configs.prompt_template : '',
  387. prompt_type: promptMode,
  388. chat_prompt_config: {},
  389. completion_prompt_config: {},
  390. user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
  391. dataset_query_variable: contextVar || '',
  392. opening_statement: introduction,
  393. suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
  394. speech_to_text: speechToTextConfig,
  395. retriever_resource: citationConfig,
  396. sensitive_word_avoidance: moderationConfig,
  397. external_data_tools: externalDataToolsConfig,
  398. more_like_this: moreLikeThisConfig,
  399. agent_mode: {
  400. enabled: true,
  401. tools: [...postDatasets],
  402. },
  403. model: {
  404. provider: modelConfig.provider,
  405. name: modelConfig.model_id,
  406. mode: modelConfig.mode,
  407. completion_params: completionParams as any,
  408. },
  409. dataset_configs: datasetConfigs,
  410. file_upload: {
  411. image: visionConfig,
  412. },
  413. }
  414. if (isAdvancedMode) {
  415. postModelConfig.chat_prompt_config = chatPromptConfig
  416. postModelConfig.completion_prompt_config = completionPromptConfig
  417. }
  418. const data: Record<string, any> = {
  419. inputs,
  420. model_config: postModelConfig,
  421. }
  422. if (visionConfig.enabled && completionFiles && completionFiles?.length > 0) {
  423. data.files = completionFiles.map((item) => {
  424. if (item.transfer_method === TransferMethod.local_file) {
  425. return {
  426. ...item,
  427. url: '',
  428. }
  429. }
  430. return item
  431. })
  432. }
  433. setCompletionRes('')
  434. setMessageId('')
  435. let res: string[] = []
  436. setResponsingTrue()
  437. sendCompletionMessage(appId, data, {
  438. onData: (data: string, _isFirstMessage: boolean, { messageId }) => {
  439. res.push(data)
  440. setCompletionRes(res.join(''))
  441. setMessageId(messageId)
  442. },
  443. onMessageReplace: (messageReplace) => {
  444. res = [messageReplace.answer]
  445. setCompletionRes(res.join(''))
  446. },
  447. onCompleted() {
  448. setResponsingFalse()
  449. },
  450. onError() {
  451. setResponsingFalse()
  452. },
  453. })
  454. }
  455. const varList = modelConfig.configs.prompt_variables.map((item: any) => {
  456. return {
  457. label: item.key,
  458. value: inputs[item.key],
  459. }
  460. })
  461. return (
  462. <>
  463. <div className="shrink-0">
  464. <div className='flex items-center justify-between mb-2'>
  465. <div className='h2 '>{t('appDebug.inputs.title')}</div>
  466. {mode === 'chat' && (
  467. <Button className='flex items-center gap-1 !h-8 !bg-white' onClick={clearConversation}>
  468. <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
  469. <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" />
  470. </svg>
  471. <span className='text-primary-600 text-[13px] font-semibold'>{t('common.operation.refresh')}</span>
  472. </Button>
  473. )}
  474. </div>
  475. <PromptValuePanel
  476. appType={mode as AppType}
  477. onSend={sendTextCompletion}
  478. inputs={inputs}
  479. visionConfig={{
  480. ...visionConfig,
  481. image_file_size_limit: fileUploadConfigResponse?.image_file_size_limit,
  482. }}
  483. onVisionFilesChange={setCompletionFiles}
  484. />
  485. </div>
  486. <div className="flex flex-col grow">
  487. {/* Chat */}
  488. {mode === AppType.chat && (
  489. <div className="mt-[34px] h-full flex flex-col">
  490. <div className={cn(doShowSuggestion ? 'pb-[140px]' : (isResponsing ? 'pb-[113px]' : 'pb-[76px]'), 'relative mt-1.5 grow h-[200px] overflow-hidden')}>
  491. <div className="h-full overflow-y-auto overflow-x-hidden" ref={chatListDomRef}>
  492. <Chat
  493. chatList={chatList}
  494. onSend={onSend}
  495. checkCanSend={checkCanSend}
  496. feedbackDisabled
  497. useCurrentUserAvatar
  498. isResponsing={isResponsing}
  499. canStopResponsing={!!messageTaskId}
  500. abortResponsing={async () => {
  501. await stopChatMessageResponding(appId, messageTaskId)
  502. setHasStopResponded(true)
  503. setResponsingFalse()
  504. }}
  505. isShowSuggestion={doShowSuggestion}
  506. suggestionList={suggestQuestions}
  507. isShowSpeechToText={speechToTextConfig.enabled && !!speech2textDefaultModel}
  508. isShowCitation={citationConfig.enabled}
  509. isShowCitationHitInfo
  510. isShowPromptLog
  511. visionConfig={{
  512. ...visionConfig,
  513. image_file_size_limit: fileUploadConfigResponse?.image_file_size_limit,
  514. }}
  515. supportAnnotation
  516. appId={appId}
  517. onChatListChange={setChatList}
  518. />
  519. </div>
  520. </div>
  521. </div>
  522. )}
  523. {/* Text Generation */}
  524. {mode === AppType.completion && (
  525. <div className="mt-6">
  526. <GroupName name={t('appDebug.result')} />
  527. {(completionRes || isResponsing) && (
  528. <TextGeneration
  529. className="mt-2"
  530. content={completionRes}
  531. isLoading={!completionRes && isResponsing}
  532. isResponsing={isResponsing}
  533. isInstalledApp={false}
  534. messageId={messageId}
  535. isError={false}
  536. onRetry={() => { }}
  537. supportAnnotation
  538. appId={appId}
  539. varList={varList}
  540. />
  541. )}
  542. </div>
  543. )}
  544. {isShowFormattingChangeConfirm && (
  545. <FormattingChanged
  546. onConfirm={handleConfirm}
  547. onCancel={handleCancel}
  548. />
  549. )}
  550. {isShowCannotQueryDataset && (
  551. <CannotQueryDataset
  552. onConfirm={() => setShowCannotQueryDataset(false)}
  553. />
  554. )}
  555. </div>
  556. {!hasSetAPIKEY && (<HasNotSetAPIKEY isTrailFinished={!IS_CE_EDITION} onSetting={onSetting} />)}
  557. </>
  558. )
  559. }
  560. export default React.memo(Debug)