Files
Developer 0a9588abb7 feat: implement QuestionBank CRUD with pagination and template query
- 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
2026-04-23 17:19:11 +08:00

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;