index.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. 'use client'
  2. import type { FC } from 'react'
  3. import useSWR from 'swr'
  4. import { useTranslation } from 'react-i18next'
  5. import React, { useCallback, useEffect, useRef, useState } from 'react'
  6. import { setAutoFreeze } from 'immer'
  7. import { useBoolean } from 'ahooks'
  8. import {
  9. RiAddLine,
  10. } from '@remixicon/react'
  11. import { useContext } from 'use-context-selector'
  12. import { useShallow } from 'zustand/react/shallow'
  13. import HasNotSetAPIKEY from '../base/warning-mask/has-not-set-api'
  14. import FormattingChanged from '../base/warning-mask/formatting-changed'
  15. import GroupName from '../base/group-name'
  16. import CannotQueryDataset from '../base/warning-mask/cannot-query-dataset'
  17. import DebugWithMultipleModel from './debug-with-multiple-model'
  18. import DebugWithSingleModel from './debug-with-single-model'
  19. import type { DebugWithSingleModelRefType } from './debug-with-single-model'
  20. import type { ModelAndParameter } from './types'
  21. import {
  22. APP_CHAT_WITH_MULTIPLE_MODEL,
  23. APP_CHAT_WITH_MULTIPLE_MODEL_RESTART,
  24. } from './types'
  25. import { AppType, ModelModeType, TransferMethod } from '@/types/app'
  26. import PromptValuePanel from '@/app/components/app/configuration/prompt-value-panel'
  27. import ConfigContext from '@/context/debug-configuration'
  28. import { ToastContext } from '@/app/components/base/toast'
  29. import { sendCompletionMessage } from '@/service/debug'
  30. import Button from '@/app/components/base/button'
  31. import type { ModelConfig as BackendModelConfig, VisionFile } from '@/types/app'
  32. import { promptVariablesToUserInputsForm } from '@/utils/model-config'
  33. import TextGeneration from '@/app/components/app/text-generate/item'
  34. import { IS_CE_EDITION } from '@/config'
  35. import type { Inputs } from '@/models/debug'
  36. import { fetchFileUploadConfig } from '@/service/common'
  37. import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
  38. import { ModelFeatureEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
  39. import type { ModelParameterModalProps } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
  40. import { useEventEmitterContextContext } from '@/context/event-emitter'
  41. import { useProviderContext } from '@/context/provider-context'
  42. import PromptLogModal from '@/app/components/base/prompt-log-modal'
  43. import { useStore as useAppStore } from '@/app/components/app/store'
  44. type IDebug = {
  45. isAPIKeySet: boolean
  46. onSetting: () => void
  47. inputs: Inputs
  48. modelParameterParams: Pick<ModelParameterModalProps, 'setModel' | 'onCompletionParamsChange'>
  49. debugWithMultipleModel: boolean
  50. multipleModelConfigs: ModelAndParameter[]
  51. onMultipleModelConfigsChange: (multiple: boolean, modelConfigs: ModelAndParameter[]) => void
  52. }
  53. const Debug: FC<IDebug> = ({
  54. isAPIKeySet = true,
  55. onSetting,
  56. inputs,
  57. modelParameterParams,
  58. debugWithMultipleModel,
  59. multipleModelConfigs,
  60. onMultipleModelConfigsChange,
  61. }) => {
  62. const { t } = useTranslation()
  63. const {
  64. appId,
  65. mode,
  66. modelModeType,
  67. hasSetBlockStatus,
  68. isAdvancedMode,
  69. promptMode,
  70. chatPromptConfig,
  71. completionPromptConfig,
  72. introduction,
  73. suggestedQuestionsAfterAnswerConfig,
  74. speechToTextConfig,
  75. textToSpeechConfig,
  76. citationConfig,
  77. moderationConfig,
  78. moreLikeThisConfig,
  79. formattingChanged,
  80. setFormattingChanged,
  81. dataSets,
  82. modelConfig,
  83. completionParams,
  84. hasSetContextVar,
  85. datasetConfigs,
  86. visionConfig,
  87. setVisionConfig,
  88. } = useContext(ConfigContext)
  89. const { eventEmitter } = useEventEmitterContextContext()
  90. const { data: text2speechDefaultModel } = useDefaultModel(ModelTypeEnum.textEmbedding)
  91. const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig)
  92. useEffect(() => {
  93. setAutoFreeze(false)
  94. return () => {
  95. setAutoFreeze(true)
  96. }
  97. }, [])
  98. const [isResponding, { setTrue: setRespondingTrue, setFalse: setRespondingFalse }] = useBoolean(false)
  99. const [isShowFormattingChangeConfirm, setIsShowFormattingChangeConfirm] = useState(false)
  100. const [isShowCannotQueryDataset, setShowCannotQueryDataset] = useState(false)
  101. useEffect(() => {
  102. if (formattingChanged)
  103. setIsShowFormattingChangeConfirm(true)
  104. }, [formattingChanged])
  105. const debugWithSingleModelRef = React.useRef<DebugWithSingleModelRefType | null>(null)
  106. const handleClearConversation = () => {
  107. debugWithSingleModelRef.current?.handleRestart()
  108. }
  109. const clearConversation = async () => {
  110. if (debugWithMultipleModel) {
  111. eventEmitter?.emit({
  112. type: APP_CHAT_WITH_MULTIPLE_MODEL_RESTART,
  113. } as any)
  114. return
  115. }
  116. handleClearConversation()
  117. }
  118. const handleConfirm = () => {
  119. clearConversation()
  120. setIsShowFormattingChangeConfirm(false)
  121. setFormattingChanged(false)
  122. }
  123. const handleCancel = () => {
  124. setIsShowFormattingChangeConfirm(false)
  125. setFormattingChanged(false)
  126. }
  127. const { notify } = useContext(ToastContext)
  128. const logError = useCallback((message: string) => {
  129. notify({ type: 'error', message, duration: 3000 })
  130. }, [notify])
  131. const [completionFiles, setCompletionFiles] = useState<VisionFile[]>([])
  132. const checkCanSend = useCallback(() => {
  133. if (isAdvancedMode && mode !== AppType.completion) {
  134. if (modelModeType === ModelModeType.completion) {
  135. if (!hasSetBlockStatus.history) {
  136. notify({ type: 'error', message: t('appDebug.otherError.historyNoBeEmpty'), duration: 3000 })
  137. return false
  138. }
  139. if (!hasSetBlockStatus.query) {
  140. notify({ type: 'error', message: t('appDebug.otherError.queryNoBeEmpty'), duration: 3000 })
  141. return false
  142. }
  143. }
  144. }
  145. let hasEmptyInput = ''
  146. const requiredVars = modelConfig.configs.prompt_variables.filter(({ key, name, required, type }) => {
  147. if (type !== 'string' && type !== 'paragraph' && type !== 'select')
  148. return false
  149. const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
  150. return res
  151. }) // compatible with old version
  152. // debugger
  153. requiredVars.forEach(({ key, name }) => {
  154. if (hasEmptyInput)
  155. return
  156. if (!inputs[key])
  157. hasEmptyInput = name
  158. })
  159. if (hasEmptyInput) {
  160. logError(t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }))
  161. return false
  162. }
  163. if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
  164. notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
  165. return false
  166. }
  167. return !hasEmptyInput
  168. }, [
  169. completionFiles,
  170. hasSetBlockStatus.history,
  171. hasSetBlockStatus.query,
  172. inputs,
  173. isAdvancedMode,
  174. mode,
  175. modelConfig.configs.prompt_variables,
  176. t,
  177. logError,
  178. notify,
  179. modelModeType,
  180. ])
  181. const [completionRes, setCompletionRes] = useState('')
  182. const [messageId, setMessageId] = useState<string | null>(null)
  183. const sendTextCompletion = async () => {
  184. if (isResponding) {
  185. notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
  186. return false
  187. }
  188. if (dataSets.length > 0 && !hasSetContextVar) {
  189. setShowCannotQueryDataset(true)
  190. return true
  191. }
  192. if (!checkCanSend())
  193. return
  194. const postDatasets = dataSets.map(({ id }) => ({
  195. dataset: {
  196. enabled: true,
  197. id,
  198. },
  199. }))
  200. const contextVar = modelConfig.configs.prompt_variables.find(item => item.is_context_var)?.key
  201. const postModelConfig: BackendModelConfig = {
  202. pre_prompt: !isAdvancedMode ? modelConfig.configs.prompt_template : '',
  203. prompt_type: promptMode,
  204. chat_prompt_config: {},
  205. completion_prompt_config: {},
  206. user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
  207. dataset_query_variable: contextVar || '',
  208. opening_statement: introduction,
  209. suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
  210. speech_to_text: speechToTextConfig,
  211. retriever_resource: citationConfig,
  212. sensitive_word_avoidance: moderationConfig,
  213. more_like_this: moreLikeThisConfig,
  214. model: {
  215. provider: modelConfig.provider,
  216. name: modelConfig.model_id,
  217. mode: modelConfig.mode,
  218. completion_params: completionParams as any,
  219. },
  220. text_to_speech: {
  221. enabled: false,
  222. voice: '',
  223. language: '',
  224. },
  225. agent_mode: {
  226. enabled: false,
  227. tools: [],
  228. },
  229. dataset_configs: {
  230. ...datasetConfigs,
  231. datasets: {
  232. datasets: [...postDatasets],
  233. } as any,
  234. },
  235. file_upload: {
  236. image: visionConfig,
  237. },
  238. }
  239. if (isAdvancedMode) {
  240. postModelConfig.chat_prompt_config = chatPromptConfig
  241. postModelConfig.completion_prompt_config = completionPromptConfig
  242. }
  243. const data: Record<string, any> = {
  244. inputs,
  245. model_config: postModelConfig,
  246. }
  247. if (visionConfig.enabled && completionFiles && completionFiles?.length > 0) {
  248. data.files = completionFiles.map((item) => {
  249. if (item.transfer_method === TransferMethod.local_file) {
  250. return {
  251. ...item,
  252. url: '',
  253. }
  254. }
  255. return item
  256. })
  257. }
  258. setCompletionRes('')
  259. setMessageId('')
  260. let res: string[] = []
  261. setRespondingTrue()
  262. sendCompletionMessage(appId, data, {
  263. onData: (data: string, _isFirstMessage: boolean, { messageId }) => {
  264. res.push(data)
  265. setCompletionRes(res.join(''))
  266. setMessageId(messageId)
  267. },
  268. onMessageReplace: (messageReplace) => {
  269. res = [messageReplace.answer]
  270. setCompletionRes(res.join(''))
  271. },
  272. onCompleted() {
  273. setRespondingFalse()
  274. },
  275. onError() {
  276. setRespondingFalse()
  277. },
  278. })
  279. }
  280. const handleSendTextCompletion = () => {
  281. if (debugWithMultipleModel) {
  282. eventEmitter?.emit({
  283. type: APP_CHAT_WITH_MULTIPLE_MODEL,
  284. payload: {
  285. message: '',
  286. files: completionFiles,
  287. },
  288. } as any)
  289. return
  290. }
  291. sendTextCompletion()
  292. }
  293. const varList = modelConfig.configs.prompt_variables.map((item: any) => {
  294. return {
  295. label: item.key,
  296. value: inputs[item.key],
  297. }
  298. })
  299. const { textGenerationModelList } = useProviderContext()
  300. const handleChangeToSingleModel = (item: ModelAndParameter) => {
  301. const currentProvider = textGenerationModelList.find(modelItem => modelItem.provider === item.provider)
  302. const currentModel = currentProvider?.models.find(model => model.model === item.model)
  303. modelParameterParams.setModel({
  304. modelId: item.model,
  305. provider: item.provider,
  306. mode: currentModel?.model_properties.mode as string,
  307. features: currentModel?.features,
  308. })
  309. modelParameterParams.onCompletionParamsChange(item.parameters)
  310. onMultipleModelConfigsChange(
  311. false,
  312. [],
  313. )
  314. }
  315. const handleVisionConfigInMultipleModel = () => {
  316. if (debugWithMultipleModel && mode) {
  317. const supportedVision = multipleModelConfigs.some((modelConfig) => {
  318. const currentProvider = textGenerationModelList.find(modelItem => modelItem.provider === modelConfig.provider)
  319. const currentModel = currentProvider?.models.find(model => model.model === modelConfig.model)
  320. return currentModel?.features?.includes(ModelFeatureEnum.vision)
  321. })
  322. if (supportedVision) {
  323. setVisionConfig({
  324. ...visionConfig,
  325. enabled: true,
  326. }, true)
  327. }
  328. else {
  329. setVisionConfig({
  330. ...visionConfig,
  331. enabled: false,
  332. }, true)
  333. }
  334. }
  335. }
  336. useEffect(() => {
  337. handleVisionConfigInMultipleModel()
  338. }, [multipleModelConfigs, mode])
  339. const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal } = useAppStore(useShallow(state => ({
  340. currentLogItem: state.currentLogItem,
  341. setCurrentLogItem: state.setCurrentLogItem,
  342. showPromptLogModal: state.showPromptLogModal,
  343. setShowPromptLogModal: state.setShowPromptLogModal,
  344. })))
  345. const [width, setWidth] = useState(0)
  346. const ref = useRef<HTMLDivElement>(null)
  347. const adjustModalWidth = () => {
  348. if (ref.current)
  349. setWidth(document.body.clientWidth - (ref.current?.clientWidth + 16) - 8)
  350. }
  351. useEffect(() => {
  352. adjustModalWidth()
  353. }, [])
  354. return (
  355. <>
  356. <div className="shrink-0 pt-4 px-6">
  357. <div className='flex items-center justify-between mb-2'>
  358. <div className='h2 '>{t('appDebug.inputs.title')}</div>
  359. <div className='flex items-center'>
  360. {
  361. debugWithMultipleModel
  362. ? (
  363. <>
  364. <Button
  365. variant='secondary-accent'
  366. onClick={() => onMultipleModelConfigsChange(true, [...multipleModelConfigs, { id: `${Date.now()}`, model: '', provider: '', parameters: {} }])}
  367. disabled={multipleModelConfigs.length >= 4}
  368. >
  369. <RiAddLine className='mr-1 w-3.5 h-3.5' />
  370. {t('common.modelProvider.addModel')}({multipleModelConfigs.length}/4)
  371. </Button>
  372. <div className='mx-2 w-[1px] h-[14px] bg-gray-200' />
  373. </>
  374. )
  375. : null
  376. }
  377. {mode !== AppType.completion && (
  378. <Button variant='secondary-accent' className='gap-1' onClick={clearConversation}>
  379. <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
  380. <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" />
  381. </svg>
  382. <span className='text-primary-600 text-[13px] font-semibold'>{t('common.operation.refresh')}</span>
  383. </Button>
  384. )}
  385. </div>
  386. </div>
  387. <PromptValuePanel
  388. appType={mode as AppType}
  389. onSend={handleSendTextCompletion}
  390. inputs={inputs}
  391. visionConfig={{
  392. ...visionConfig,
  393. image_file_size_limit: fileUploadConfigResponse?.image_file_size_limit,
  394. }}
  395. onVisionFilesChange={setCompletionFiles}
  396. />
  397. </div>
  398. {
  399. debugWithMultipleModel && (
  400. <div className='grow mt-3 overflow-hidden'>
  401. <DebugWithMultipleModel
  402. multipleModelConfigs={multipleModelConfigs}
  403. onMultipleModelConfigsChange={onMultipleModelConfigsChange}
  404. onDebugWithMultipleModelChange={handleChangeToSingleModel}
  405. checkCanSend={checkCanSend}
  406. />
  407. </div>
  408. )
  409. }
  410. {
  411. !debugWithMultipleModel && (
  412. <div className="flex flex-col grow" ref={ref}>
  413. {/* Chat */}
  414. {mode !== AppType.completion && (
  415. <div className='grow h-0 overflow-hidden'>
  416. <DebugWithSingleModel
  417. ref={debugWithSingleModelRef}
  418. checkCanSend={checkCanSend}
  419. />
  420. </div>
  421. )}
  422. {/* Text Generation */}
  423. {mode === AppType.completion && (
  424. <div className="mt-6 px-6 pb-4">
  425. <GroupName name={t('appDebug.result')} />
  426. {(completionRes || isResponding) && (
  427. <TextGeneration
  428. className="mt-2"
  429. content={completionRes}
  430. isLoading={!completionRes && isResponding}
  431. isShowTextToSpeech={textToSpeechConfig.enabled && !!text2speechDefaultModel}
  432. isResponding={isResponding}
  433. isInstalledApp={false}
  434. messageId={messageId}
  435. isError={false}
  436. onRetry={() => { }}
  437. supportAnnotation
  438. appId={appId}
  439. varList={varList}
  440. />
  441. )}
  442. </div>
  443. )}
  444. {mode === AppType.completion && showPromptLogModal && (
  445. <PromptLogModal
  446. width={width}
  447. currentLogItem={currentLogItem}
  448. onCancel={() => {
  449. setCurrentLogItem()
  450. setShowPromptLogModal(false)
  451. }}
  452. />
  453. )}
  454. {isShowCannotQueryDataset && (
  455. <CannotQueryDataset
  456. onConfirm={() => setShowCannotQueryDataset(false)}
  457. />
  458. )}
  459. </div>
  460. )
  461. }
  462. {isShowFormattingChangeConfirm && (
  463. <FormattingChanged
  464. onConfirm={handleConfirm}
  465. onCancel={handleCancel}
  466. />
  467. )}
  468. {!isAPIKeySet && (<HasNotSetAPIKEY isTrailFinished={!IS_CE_EDITION} onSetting={onSetting} />)}
  469. </>
  470. )
  471. }
  472. export default React.memo(Debug)