index.tsx 16 KB

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