ConfigPanel.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. import React from 'react';
  2. import { AppSettings, ModelConfig, ModelType } from '../types';
  3. import { useLanguage } from '../contexts/LanguageContext';
  4. import { useConfirm } from '../contexts/ConfirmContext';
  5. import { Settings, Database, Sliders, Layers, Cpu, ChevronRight } from 'lucide-react';
  6. import VisionModelSelector from './VisionModelSelector';
  7. interface ConfigPanelProps {
  8. settings: AppSettings;
  9. models: ModelConfig[];
  10. onSettingsChange: (newSettings: AppSettings) => void;
  11. onOpenSettings: () => void;
  12. mode?: 'chat' | 'kb' | 'all';
  13. isAdmin?: boolean;
  14. }
  15. const ConfigPanel: React.FC<ConfigPanelProps> = ({ settings, models, onSettingsChange, onOpenSettings, mode = 'all', isAdmin = false }) => {
  16. const { t } = useLanguage();
  17. const { confirm } = useConfirm();
  18. const handleChange = (key: keyof AppSettings, value: any) => {
  19. onSettingsChange({
  20. ...settings,
  21. [key]: value,
  22. });
  23. };
  24. const llmModels = models.filter(m => m.type === ModelType.LLM && m.isEnabled !== false && !m.supportsVision);
  25. const embeddingModels = models.filter(m => m.type === ModelType.EMBEDDING && m.isEnabled !== false);
  26. const rerankModels = models.filter(m => m.type === ModelType.RERANK && m.isEnabled !== false);
  27. const showChatSettings = mode === 'chat' || mode === 'all';
  28. const showKbSettings = mode === 'kb' || mode === 'all';
  29. return (
  30. <div className="flex-1 overflow-y-auto p-4 space-y-6 bg-slate-50">
  31. {!isAdmin && (
  32. <div className="bg-orange-50 border border-orange-200 p-3 rounded-lg mb-4">
  33. <p className="text-xs text-orange-700 flex items-center gap-2">
  34. <Sliders className="w-3 h-3" />
  35. {t('onlyAdminCanModify') || "Only administrators can modify system settings."}
  36. </p>
  37. </div>
  38. )}
  39. {/* Model Selection (LLM) - Chat Mode Only */}
  40. {showChatSettings && (
  41. <div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
  42. <div className="flex items-center justify-between mb-4 border-b border-slate-100 pb-2">
  43. <div className="flex items-center gap-2 text-slate-800 font-semibold">
  44. <Cpu className="w-4 h-4 text-blue-600" />
  45. {t('headerModelSelection')}
  46. </div>
  47. </div>
  48. <div className="space-y-4">
  49. <div>
  50. <label className="block text-xs font-medium text-slate-500 mb-1.5">{t('selectLLMModel')}</label>
  51. <select
  52. value={settings.selectedLLMId}
  53. onChange={(e) => handleChange('selectedLLMId', e.target.value)}
  54. disabled={!isAdmin}
  55. className="w-full text-sm bg-slate-50 border border-slate-200 rounded-lg px-3 py-2 text-slate-700 focus:outline-none focus:border-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
  56. >
  57. <option value="">--- {t('selectLLMModel')} ---</option>
  58. {llmModels.map(m => (
  59. <option key={m.id} value={m.id}>
  60. {m.name} ({m.modelId})
  61. </option>
  62. ))}
  63. </select>
  64. </div>
  65. </div>
  66. </div>
  67. )}
  68. {/* Embedding Model Selection - KB Mode Only */}
  69. {showKbSettings && (
  70. <div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
  71. <div className="flex items-center justify-between mb-4 border-b border-slate-100 pb-2">
  72. <div className="flex items-center gap-2 text-slate-800 font-semibold">
  73. <Cpu className="w-4 h-4 text-blue-600" />
  74. {t('lblEmbedding')}
  75. </div>
  76. </div>
  77. <div className="space-y-4">
  78. <div>
  79. <label className="block text-xs font-medium text-slate-500 mb-1.5">{t('lblEmbedding')}</label>
  80. <select
  81. value={settings.selectedEmbeddingId}
  82. onChange={async (e) => {
  83. const newId = e.target.value;
  84. if (newId !== settings.selectedEmbeddingId && settings.selectedEmbeddingId) {
  85. if (await confirm(t('confirmChangeEmbeddingModel') || "WARNING: Changing the embedding model will require re-indexing all existing files. Are you sure?")) {
  86. handleChange('selectedEmbeddingId', newId);
  87. }
  88. } else {
  89. handleChange('selectedEmbeddingId', newId);
  90. }
  91. }}
  92. disabled={!isAdmin}
  93. className="w-full text-sm bg-slate-50 border border-slate-200 rounded-lg px-3 py-2 text-slate-700 focus:outline-none focus:border-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
  94. >
  95. <option value="">--- {t('selectEmbeddingModel')} ---</option>
  96. {embeddingModels.map(m => (
  97. <option key={m.id} value={m.id}>{m.name}</option>
  98. ))}
  99. </select>
  100. <p className="text-[10px] text-slate-400 mt-1">{t('defaultForUploads')}</p>
  101. <p className="text-[10px] text-orange-500 mt-1">{t('embeddingModelWarning') || "Changing this setting may require clearing and re-importing your knowledge base."}</p>
  102. </div>
  103. </div>
  104. </div>
  105. )}
  106. {/* Hyperparameters - Chat Mode Only */}
  107. {showChatSettings && (
  108. <div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
  109. <div className="flex items-center gap-2 mb-4 text-slate-800 font-semibold border-b border-slate-100 pb-2">
  110. <Sliders className="w-4 h-4 text-pink-500" />
  111. {t('headerHyperparams')}
  112. </div>
  113. <div className="space-y-4">
  114. <div>
  115. <div className="flex justify-between mb-1.5">
  116. <label className="text-xs font-medium text-slate-500">{t('lblTemperature')}</label>
  117. <span className="text-xs text-blue-600 font-bold">{settings.temperature}</span>
  118. </div>
  119. <input
  120. type="range"
  121. min="0"
  122. max="1"
  123. step="0.1"
  124. value={settings.temperature}
  125. onChange={(e) => handleChange('temperature', parseFloat(e.target.value))}
  126. disabled={!isAdmin}
  127. className={`w-full h-2 rounded-lg appearance-none cursor-pointer accent-blue-600 ${!isAdmin ? 'bg-slate-100' : 'bg-slate-200'}`}
  128. />
  129. </div>
  130. <div>
  131. <label className="block text-xs font-medium text-slate-500 mb-1.5">{t('lblMaxTokens')}</label>
  132. <input
  133. type="number"
  134. value={settings.maxTokens}
  135. onChange={(e) => handleChange('maxTokens', parseInt(e.target.value))}
  136. disabled={!isAdmin}
  137. className="w-full text-sm bg-slate-50 border border-slate-200 rounded-lg px-3 py-2 text-slate-700 focus:outline-none focus:border-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
  138. />
  139. </div>
  140. </div>
  141. </div>
  142. )}
  143. {/* Vision Model Settings - Chat Mode Only? Or both? Assuming Chat */}
  144. {/* Vision Model Settings - KB Only */}
  145. {showKbSettings && <VisionModelSelector isAdmin={isAdmin} />}
  146. {/* Retrieval Settings - KB Mode Only */}
  147. {showKbSettings && (
  148. <div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
  149. <div className="flex items-center gap-2 mb-4 text-slate-800 font-semibold border-b border-slate-100 pb-2">
  150. <Database className="w-4 h-4 text-green-600" />
  151. {t('headerRetrieval')}
  152. </div>
  153. <div className="space-y-4">
  154. <div>
  155. <div className="flex justify-between mb-1.5">
  156. <label className="text-xs font-medium text-slate-500">{t('lblTopK')}</label>
  157. <span className="text-xs text-blue-600 font-bold">{settings.topK}</span>
  158. </div>
  159. <input
  160. type="range"
  161. min="1"
  162. max="20"
  163. step="1"
  164. value={settings.topK}
  165. onChange={(e) => handleChange('topK', parseInt(e.target.value))}
  166. disabled={!isAdmin}
  167. className={`w-full h-2 rounded-lg appearance-none cursor-pointer accent-blue-600 ${!isAdmin ? 'bg-slate-100' : 'bg-slate-200'}`}
  168. />
  169. </div>
  170. <div>
  171. <label className="block text-xs font-medium text-slate-500 mb-1.5">{t('lblRerankRef')}</label>
  172. <select
  173. value={settings.selectedRerankId}
  174. onChange={(e) => handleChange('selectedRerankId', e.target.value)}
  175. disabled={!settings.enableRerank || !isAdmin}
  176. className="w-full text-sm bg-slate-50 border border-slate-200 rounded-lg px-3 py-2 text-slate-700 focus:outline-none focus:border-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
  177. >
  178. <option value="">--- {t('selectRerankModel')} ---</option>
  179. {rerankModels.map(m => (
  180. <option key={m.id} value={m.id}>{m.name}</option>
  181. ))}
  182. </select>
  183. </div>
  184. <div>
  185. <div className="flex justify-between mb-1.5">
  186. <label className="text-xs font-medium text-slate-500">{t('vectorSimilarityThreshold')}</label>
  187. <span className="text-xs text-blue-600 font-bold">{settings.similarityThreshold}</span>
  188. </div>
  189. <input
  190. type="range"
  191. min="0.0"
  192. max="1.0"
  193. step="0.05"
  194. value={settings.similarityThreshold}
  195. onChange={(e) => handleChange('similarityThreshold', parseFloat(e.target.value))}
  196. disabled={!isAdmin}
  197. className={`w-full h-2 rounded-lg appearance-none cursor-pointer accent-blue-600 ${!isAdmin ? 'bg-slate-100' : 'bg-slate-200'}`}
  198. />
  199. <p className="text-[10px] text-slate-400 mt-1">{t('filterLowResults')}</p>
  200. </div>
  201. {settings.enableRerank && (
  202. <div>
  203. <div className="flex justify-between mb-1.5">
  204. <label className="text-xs font-medium text-slate-500">{t('rerankSimilarityThreshold')}</label>
  205. <span className="text-xs text-blue-600 font-bold">{settings.rerankSimilarityThreshold}</span>
  206. </div>
  207. <input
  208. type="range"
  209. min="0.0"
  210. max="1.0"
  211. step="0.05"
  212. value={settings.rerankSimilarityThreshold}
  213. onChange={(e) => handleChange('rerankSimilarityThreshold', parseFloat(e.target.value))}
  214. className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-pink-600"
  215. />
  216. </div>
  217. )}
  218. <div className="flex items-center justify-between pt-2">
  219. <label className="text-sm text-slate-700">{t('lblRerank')}</label>
  220. <button
  221. onClick={() => isAdmin && handleChange('enableRerank', !settings.enableRerank)}
  222. disabled={!isAdmin}
  223. className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none ${settings.enableRerank ? 'bg-blue-600' : 'bg-slate-300'
  224. } ${!isAdmin ? 'cursor-not-allowed opacity-50' : ''}`}
  225. >
  226. <span
  227. className={`${settings.enableRerank ? 'translate-x-6' : 'translate-x-1'
  228. } inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}
  229. />
  230. </button>
  231. </div>
  232. <div className="flex items-center justify-between pt-2">
  233. <label className="text-sm text-slate-700">{t('fullTextSearch')}</label>
  234. <button
  235. onClick={() => isAdmin && handleChange('enableFullTextSearch', !settings.enableFullTextSearch)}
  236. disabled={!isAdmin}
  237. className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none ${settings.enableFullTextSearch ? 'bg-blue-600' : 'bg-slate-300'
  238. } ${!isAdmin ? 'cursor-not-allowed opacity-50' : ''}`}
  239. >
  240. <span
  241. className={`${settings.enableFullTextSearch ? 'translate-x-6' : 'translate-x-1'
  242. } inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}
  243. />
  244. </button>
  245. </div>
  246. {settings.enableFullTextSearch && (
  247. <div className="pt-2 animate-in fade-in slide-in-from-top-1 duration-200">
  248. <div className="flex justify-between mb-1.5">
  249. <label className="text-xs font-medium text-slate-500">{t('hybridVectorWeight')}</label>
  250. <span className="text-xs text-blue-600 font-bold">{settings.hybridVectorWeight}</span>
  251. </div>
  252. <input
  253. type="range"
  254. min="0.0"
  255. max="1.0"
  256. step="0.05"
  257. value={settings.hybridVectorWeight}
  258. onChange={(e) => handleChange('hybridVectorWeight', parseFloat(e.target.value))}
  259. disabled={!isAdmin}
  260. className={`w-full h-2 rounded-lg appearance-none cursor-pointer accent-blue-600 ${!isAdmin ? 'bg-slate-100' : 'bg-slate-200'}`}
  261. />
  262. <p className="text-[10px] text-slate-400 mt-1">{t('hybridVectorWeightDesc')}</p>
  263. </div>
  264. )}
  265. </div>
  266. </div>
  267. )}
  268. </div>
  269. );
  270. };
  271. export default ConfigPanel;