0a9588abb7
- Add pagination support to findAll (page, limit query params) - Add findByTemplateId method to service - Add GET /by-template/:templateId endpoint to controller - Service already includes CRUD for QuestionBank and QuestionBankItem
295 lines
14 KiB
TypeScript
295 lines
14 KiB
TypeScript
|
|
import React from 'react';
|
|
import { AppSettings, ModelConfig, ModelType } from '../types';
|
|
import { useLanguage } from '../contexts/LanguageContext';
|
|
import { useConfirm } from '../contexts/ConfirmContext';
|
|
import { Settings, Database, Sliders, Layers, Cpu, ChevronRight } from 'lucide-react';
|
|
import VisionModelSelector from './VisionModelSelector';
|
|
|
|
interface ConfigPanelProps {
|
|
settings: AppSettings;
|
|
models: ModelConfig[];
|
|
onSettingsChange: (newSettings: AppSettings) => void;
|
|
onOpenSettings: () => void;
|
|
mode?: 'chat' | 'kb' | 'all';
|
|
isAdmin?: boolean;
|
|
}
|
|
|
|
const ConfigPanel: React.FC<ConfigPanelProps> = ({ settings, models, onSettingsChange, onOpenSettings, mode = 'all', isAdmin = false }) => {
|
|
const { t } = useLanguage();
|
|
const { confirm } = useConfirm();
|
|
|
|
const handleChange = (key: keyof AppSettings, value: any) => {
|
|
onSettingsChange({
|
|
...settings,
|
|
[key]: value,
|
|
});
|
|
};
|
|
|
|
const llmModels = models.filter(m => m.type === ModelType.LLM && m.isEnabled !== false && !m.supportsVision);
|
|
const embeddingModels = models.filter(m => m.type === ModelType.EMBEDDING && m.isEnabled !== false);
|
|
const rerankModels = models.filter(m => m.type === ModelType.RERANK && m.isEnabled !== false);
|
|
|
|
const showChatSettings = mode === 'chat' || mode === 'all';
|
|
const showKbSettings = mode === 'kb' || mode === 'all';
|
|
|
|
return (
|
|
<div className="flex-1 overflow-y-auto p-4 space-y-6 bg-slate-50">
|
|
{!isAdmin && (
|
|
<div className="bg-orange-50 border border-orange-200 p-3 rounded-lg mb-4">
|
|
<p className="text-xs text-orange-700 flex items-center gap-2">
|
|
<Sliders className="w-3 h-3" />
|
|
{t('onlyAdminCanModify') || "Only administrators can modify system settings."}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Model Selection (LLM) - Chat Mode Only */}
|
|
{showChatSettings && (
|
|
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
|
|
<div className="flex items-center justify-between mb-4 border-b border-slate-100 pb-2">
|
|
<div className="flex items-center gap-2 text-slate-800 font-semibold">
|
|
<Cpu className="w-4 h-4 text-blue-600" />
|
|
{t('headerModelSelection')}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-500 mb-1.5">{t('selectLLMModel')}</label>
|
|
<select
|
|
value={settings.selectedLLMId}
|
|
onChange={(e) => handleChange('selectedLLMId', e.target.value)}
|
|
disabled={!isAdmin}
|
|
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"
|
|
>
|
|
<option value="">--- {t('selectLLMModel')} ---</option>
|
|
{llmModels.map(m => (
|
|
<option key={m.id} value={m.id}>
|
|
{m.name} ({m.modelId})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Embedding Model Selection - KB Mode Only */}
|
|
{showKbSettings && (
|
|
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
|
|
<div className="flex items-center justify-between mb-4 border-b border-slate-100 pb-2">
|
|
<div className="flex items-center gap-2 text-slate-800 font-semibold">
|
|
<Cpu className="w-4 h-4 text-blue-600" />
|
|
{t('lblEmbedding')}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-500 mb-1.5">{t('lblEmbedding')}</label>
|
|
<select
|
|
value={settings.selectedEmbeddingId}
|
|
onChange={async (e) => {
|
|
const newId = e.target.value;
|
|
if (newId !== settings.selectedEmbeddingId && settings.selectedEmbeddingId) {
|
|
if (await confirm(t('confirmChangeEmbeddingModel') || "WARNING: Changing the embedding model will require re-indexing all existing files. Are you sure?")) {
|
|
handleChange('selectedEmbeddingId', newId);
|
|
}
|
|
} else {
|
|
handleChange('selectedEmbeddingId', newId);
|
|
}
|
|
}}
|
|
disabled={!isAdmin}
|
|
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"
|
|
>
|
|
<option value="">--- {t('selectEmbeddingModel')} ---</option>
|
|
{embeddingModels.map(m => (
|
|
<option key={m.id} value={m.id}>{m.name}</option>
|
|
))}
|
|
</select>
|
|
<p className="text-[10px] text-slate-400 mt-1">{t('defaultForUploads')}</p>
|
|
<p className="text-[10px] text-orange-500 mt-1">{t('embeddingModelWarning') || "Changing this setting may require clearing and re-importing your knowledge base."}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Hyperparameters - Chat Mode Only */}
|
|
{showChatSettings && (
|
|
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
|
|
<div className="flex items-center gap-2 mb-4 text-slate-800 font-semibold border-b border-slate-100 pb-2">
|
|
<Sliders className="w-4 h-4 text-pink-500" />
|
|
{t('headerHyperparams')}
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<div className="flex justify-between mb-1.5">
|
|
<label className="text-xs font-medium text-slate-500">{t('lblTemperature')}</label>
|
|
<span className="text-xs text-blue-600 font-bold">{settings.temperature}</span>
|
|
</div>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="1"
|
|
step="0.1"
|
|
value={settings.temperature}
|
|
onChange={(e) => handleChange('temperature', parseFloat(e.target.value))}
|
|
disabled={!isAdmin}
|
|
className={`w-full h-2 rounded-lg appearance-none cursor-pointer accent-blue-600 ${!isAdmin ? 'bg-slate-100' : 'bg-slate-200'}`}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-500 mb-1.5">{t('lblMaxTokens')}</label>
|
|
<input
|
|
type="number"
|
|
value={settings.maxTokens}
|
|
onChange={(e) => handleChange('maxTokens', parseInt(e.target.value))}
|
|
disabled={!isAdmin}
|
|
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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Vision Model Settings - Chat Mode Only? Or both? Assuming Chat */}
|
|
{/* Vision Model Settings - KB Only */}
|
|
{showKbSettings && <VisionModelSelector isAdmin={isAdmin} />}
|
|
|
|
{/* Retrieval Settings - KB Mode Only */}
|
|
{showKbSettings && (
|
|
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
|
|
<div className="flex items-center gap-2 mb-4 text-slate-800 font-semibold border-b border-slate-100 pb-2">
|
|
<Database className="w-4 h-4 text-green-600" />
|
|
{t('headerRetrieval')}
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<div className="flex justify-between mb-1.5">
|
|
<label className="text-xs font-medium text-slate-500">{t('lblTopK')}</label>
|
|
<span className="text-xs text-blue-600 font-bold">{settings.topK}</span>
|
|
</div>
|
|
<input
|
|
type="range"
|
|
min="1"
|
|
max="20"
|
|
step="1"
|
|
value={settings.topK}
|
|
onChange={(e) => handleChange('topK', parseInt(e.target.value))}
|
|
disabled={!isAdmin}
|
|
className={`w-full h-2 rounded-lg appearance-none cursor-pointer accent-blue-600 ${!isAdmin ? 'bg-slate-100' : 'bg-slate-200'}`}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-500 mb-1.5">{t('lblRerankRef')}</label>
|
|
<select
|
|
value={settings.selectedRerankId}
|
|
onChange={(e) => handleChange('selectedRerankId', e.target.value)}
|
|
disabled={!settings.enableRerank || !isAdmin}
|
|
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"
|
|
>
|
|
<option value="">--- {t('selectRerankModel')} ---</option>
|
|
{rerankModels.map(m => (
|
|
<option key={m.id} value={m.id}>{m.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="flex justify-between mb-1.5">
|
|
<label className="text-xs font-medium text-slate-500">{t('vectorSimilarityThreshold')}</label>
|
|
<span className="text-xs text-blue-600 font-bold">{settings.similarityThreshold}</span>
|
|
</div>
|
|
<input
|
|
type="range"
|
|
min="0.0"
|
|
max="1.0"
|
|
step="0.05"
|
|
value={settings.similarityThreshold}
|
|
onChange={(e) => handleChange('similarityThreshold', parseFloat(e.target.value))}
|
|
disabled={!isAdmin}
|
|
className={`w-full h-2 rounded-lg appearance-none cursor-pointer accent-blue-600 ${!isAdmin ? 'bg-slate-100' : 'bg-slate-200'}`}
|
|
/>
|
|
<p className="text-[10px] text-slate-400 mt-1">{t('filterLowResults')}</p>
|
|
</div>
|
|
|
|
{settings.enableRerank && (
|
|
<div>
|
|
<div className="flex justify-between mb-1.5">
|
|
<label className="text-xs font-medium text-slate-500">{t('rerankSimilarityThreshold')}</label>
|
|
<span className="text-xs text-blue-600 font-bold">{settings.rerankSimilarityThreshold}</span>
|
|
</div>
|
|
<input
|
|
type="range"
|
|
min="0.0"
|
|
max="1.0"
|
|
step="0.05"
|
|
value={settings.rerankSimilarityThreshold}
|
|
onChange={(e) => handleChange('rerankSimilarityThreshold', parseFloat(e.target.value))}
|
|
className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-pink-600"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center justify-between pt-2">
|
|
<label className="text-sm text-slate-700">{t('lblRerank')}</label>
|
|
<button
|
|
onClick={() => isAdmin && handleChange('enableRerank', !settings.enableRerank)}
|
|
disabled={!isAdmin}
|
|
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'
|
|
} ${!isAdmin ? 'cursor-not-allowed opacity-50' : ''}`}
|
|
>
|
|
<span
|
|
className={`${settings.enableRerank ? 'translate-x-6' : 'translate-x-1'
|
|
} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between pt-2">
|
|
<label className="text-sm text-slate-700">{t('fullTextSearch')}</label>
|
|
<button
|
|
onClick={() => isAdmin && handleChange('enableFullTextSearch', !settings.enableFullTextSearch)}
|
|
disabled={!isAdmin}
|
|
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'
|
|
} ${!isAdmin ? 'cursor-not-allowed opacity-50' : ''}`}
|
|
>
|
|
<span
|
|
className={`${settings.enableFullTextSearch ? 'translate-x-6' : 'translate-x-1'
|
|
} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
{settings.enableFullTextSearch && (
|
|
<div className="pt-2 animate-in fade-in slide-in-from-top-1 duration-200">
|
|
<div className="flex justify-between mb-1.5">
|
|
<label className="text-xs font-medium text-slate-500">{t('hybridVectorWeight')}</label>
|
|
<span className="text-xs text-blue-600 font-bold">{settings.hybridVectorWeight}</span>
|
|
</div>
|
|
<input
|
|
type="range"
|
|
min="0.0"
|
|
max="1.0"
|
|
step="0.05"
|
|
value={settings.hybridVectorWeight}
|
|
onChange={(e) => handleChange('hybridVectorWeight', parseFloat(e.target.value))}
|
|
disabled={!isAdmin}
|
|
className={`w-full h-2 rounded-lg appearance-none cursor-pointer accent-blue-600 ${!isAdmin ? 'bg-slate-100' : 'bg-slate-200'}`}
|
|
/>
|
|
<p className="text-[10px] text-slate-400 mt-1">{t('hybridVectorWeightDesc')}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ConfigPanel;
|