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
This commit is contained in:
@@ -0,0 +1,562 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ModelConfig, ModelType } from '../types';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { X, Plus, Trash2, Edit2, Save, Cpu, Box, Loader2, User, Shield, Key, LogOut, Globe, Settings as SettingsIcon, Star } from 'lucide-react';
|
||||
import { userService } from '../services/userService';
|
||||
import { settingsService } from '../services/settingsService';
|
||||
import { userSettingService } from '../services/userSettingService';
|
||||
import { knowledgeGroupService } from '../services/knowledgeGroupService';
|
||||
import { modelConfigService } from '../services/modelConfigService';
|
||||
import { useConfirm } from '../contexts/ConfirmContext';
|
||||
import { AppSettings, KnowledgeGroup } from '../types';
|
||||
|
||||
|
||||
interface SettingsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
// Model Props
|
||||
models: ModelConfig[];
|
||||
authToken: string | null;
|
||||
onUpdateModels: (action: 'create' | 'update' | 'delete', model: ModelConfig) => Promise<void>;
|
||||
onLogout: () => void;
|
||||
}
|
||||
|
||||
type TabType = 'general' | 'user' | 'model';
|
||||
|
||||
export const SettingsModal: React.FC<SettingsModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
models,
|
||||
authToken,
|
||||
onUpdateModels,
|
||||
onLogout
|
||||
}) => {
|
||||
const { t, language, setLanguage } = useLanguage();
|
||||
const { confirm } = useConfirm();
|
||||
const [activeTab, setActiveTab] = useState<TabType>('general');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// --- Model Manager State ---
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [modelFormData, setModelFormData] = useState<Partial<ModelConfig>>({
|
||||
type: ModelType.LLM,
|
||||
baseUrl: 'http://localhost:11434/v1',
|
||||
modelId: 'llama3',
|
||||
name: '',
|
||||
dimensions: 1536,
|
||||
maxInputTokens: 8191,
|
||||
maxBatchSize: 2048
|
||||
});
|
||||
|
||||
// --- User Management State ---
|
||||
interface UserType {
|
||||
id: string;
|
||||
username: string;
|
||||
isAdmin: boolean;
|
||||
role?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
const [users, setUsers] = useState<UserType[]>([]);
|
||||
const [isUserLoading, setIsUserLoading] = useState(false);
|
||||
const [showAddUser, setShowAddUser] = useState(false);
|
||||
const [newUser, setNewUser] = useState({ username: '', password: '' });
|
||||
const [userSuccess, setUserSuccess] = useState('');
|
||||
|
||||
// --- Change Password State ---
|
||||
const [passwordForm, setPasswordForm] = useState({ current: '', new: '', confirm: '' });
|
||||
const [passwordSuccess, setPasswordSuccess] = useState('');
|
||||
|
||||
// --- App Settings State ---
|
||||
const [appSettings, setAppSettings] = useState<AppSettings | null>(null);
|
||||
const [knowledgeGroups, setKnowledgeGroups] = useState<KnowledgeGroup[]>([]);
|
||||
const [isSettingsLoading, setIsSettingsLoading] = useState(false);
|
||||
const [currentUser, setCurrentUser] = useState<UserType | null>(null);
|
||||
|
||||
// Reset state on open
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setActiveTab('general');
|
||||
setError(null);
|
||||
setEditingId(null);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Fetch Users when User tab is active
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (activeTab === 'user') {
|
||||
fetchUsers();
|
||||
} else if (activeTab === 'general') {
|
||||
fetchSettingsAndGroups();
|
||||
}
|
||||
}
|
||||
}, [isOpen, activeTab]);
|
||||
|
||||
const fetchSettingsAndGroups = async () => {
|
||||
if (!authToken) return;
|
||||
setIsSettingsLoading(true);
|
||||
try {
|
||||
const [settings, groups, users, personal] = await Promise.all([
|
||||
userSettingService.get(authToken),
|
||||
knowledgeGroupService.getGroups(),
|
||||
userService.getUsers().catch(() => []), // Regular users might fail this
|
||||
userSettingService.getPersonal(authToken).catch(() => null)
|
||||
]);
|
||||
setAppSettings(settings);
|
||||
setKnowledgeGroups(groups);
|
||||
|
||||
if (personal?.language && personal.language !== language) {
|
||||
setLanguage(personal.language as any);
|
||||
}
|
||||
|
||||
// Temporary way to get current user details since we lack a /me endpoint hook here
|
||||
const tokenPayload = JSON.parse(atob(authToken.split('.')[1]));
|
||||
const me = users.find(u => u.id === tokenPayload.sub) || { isAdmin: tokenPayload.role === 'SUPER_ADMIN' || tokenPayload.role === 'TENANT_ADMIN', role: tokenPayload.role };
|
||||
setCurrentUser(me as any);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch settings or groups:', error);
|
||||
} finally {
|
||||
setIsSettingsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
// --- General Tab Handlers ---
|
||||
const handleLanguageChange = async (newLanguage: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await settingsService.updateLanguage(newLanguage);
|
||||
setLanguage(newLanguage as any);
|
||||
} catch (error) {
|
||||
console.error('Failed to update language:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangePassword = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setPasswordSuccess('');
|
||||
|
||||
if (passwordForm.new !== passwordForm.confirm) {
|
||||
setError(t('passwordMismatch'));
|
||||
return;
|
||||
}
|
||||
if (passwordForm.new.length < 6) {
|
||||
setError(t('newPasswordMinLength'));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await userService.changePassword(passwordForm.current, passwordForm.new);
|
||||
setPasswordSuccess(t('passwordChangeSuccess'));
|
||||
setPasswordForm({ current: '', new: '', confirm: '' });
|
||||
} catch (err: any) {
|
||||
setError(err.message || t('passwordChangeFailed'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// --- User Tab Handlers ---
|
||||
const fetchUsers = async () => {
|
||||
setIsUserLoading(true);
|
||||
try {
|
||||
const userList = await userService.getUsers();
|
||||
setUsers(userList);
|
||||
} catch (error: any) {
|
||||
setError(error.message || t('getUserListFailed'));
|
||||
} finally {
|
||||
setIsUserLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateUser = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setUserSuccess('');
|
||||
|
||||
if (newUser.password.length < 6) {
|
||||
setError(t('passwordMinLength'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await userService.createUser(newUser.username, newUser.password);
|
||||
setUserSuccess(t('userCreatedSuccess'));
|
||||
setNewUser({ username: '', password: '' });
|
||||
setShowAddUser(false);
|
||||
fetchUsers();
|
||||
} catch (error: any) {
|
||||
setError(error.message || t('createUserFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
// --- Model Tab Handlers ---
|
||||
const handleSaveModel = async () => {
|
||||
if (!authToken) {
|
||||
setError(t('mmErrorNotAuthenticated'));
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
|
||||
if (!modelFormData.name?.trim()) { setError(t('mmErrorNameRequired')); return; }
|
||||
if (!modelFormData.modelId?.trim()) { setError(t('mmErrorModelIdRequired')); return; }
|
||||
if (!modelFormData.baseUrl?.trim()) { setError(t('mmErrorBaseUrlRequired')); return; }
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const saveData = { ...modelFormData } as ModelConfig;
|
||||
await onUpdateModels(editingId === 'new' ? 'create' : 'update', saveData);
|
||||
setEditingId(null);
|
||||
} catch (err: any) {
|
||||
setError(err.message || t('errorGeneric'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteModel = async (id: string) => {
|
||||
if (await confirm(t('confirmClear'))) {
|
||||
await onUpdateModels('delete', { id } as ModelConfig);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetDefault = async (id: string) => {
|
||||
if (!authToken) {
|
||||
setError(t('mmErrorNotAuthenticated'));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await modelConfigService.setDefault(authToken, id);
|
||||
// Reload page to fetch model list again
|
||||
window.location.reload();
|
||||
} catch (err: any) {
|
||||
setError(err.message || t('defaultSettingFailed'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeLabel = (type: ModelType) => {
|
||||
switch (type) {
|
||||
case ModelType.LLM: return t('typeLLM');
|
||||
case ModelType.EMBEDDING: return t('typeEmbedding');
|
||||
case ModelType.RERANK: return t('typeRerank');
|
||||
case ModelType.VISION: return t('typeVision');
|
||||
}
|
||||
};
|
||||
|
||||
// --- Render Functions ---
|
||||
|
||||
const renderGeneralTab = () => (
|
||||
<div className="space-y-8 animate-in slide-in-from-right duration-300">
|
||||
{/* Language section */}
|
||||
<section>
|
||||
<h3 className="text-sm font-medium text-slate-500 mb-3 flex items-center gap-2">
|
||||
<Globe className="w-4 h-4" />
|
||||
{t('languageSettings')}
|
||||
</h3>
|
||||
<div className="flex gap-2">
|
||||
{(['zh', 'en', 'ja'] as const).map((lang) => (
|
||||
<button
|
||||
key={lang}
|
||||
onClick={() => handleLanguageChange(lang)}
|
||||
disabled={isLoading}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors border ${language === lang
|
||||
? 'bg-blue-50 border-blue-200 text-blue-700'
|
||||
: 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
{lang === 'zh' ? 'Chinese' : lang === 'en' ? 'English' : 'Japanese'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
{/* Change Password Section */}
|
||||
<section className="bg-white p-6 rounded-lg border border-slate-200 shadow-sm">
|
||||
<h3 className="text-sm font-medium text-slate-800 mb-4 flex items-center gap-2">
|
||||
<Key className="w-4 h-4 text-blue-500" />
|
||||
{t('changePassword')}
|
||||
</h3>
|
||||
<form onSubmit={handleChangePassword} className="space-y-4 max-w-sm">
|
||||
<input type="hidden" name="username" value="current-user" autoComplete="username" />
|
||||
<div>
|
||||
<input
|
||||
type="password"
|
||||
name="currentPassword"
|
||||
placeholder={t('currentPassword')}
|
||||
value={passwordForm.current}
|
||||
onChange={e => setPasswordForm({ ...passwordForm, current: e.target.value })}
|
||||
className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
required
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="password"
|
||||
name="newPassword"
|
||||
placeholder={t('newPassword')}
|
||||
value={passwordForm.new}
|
||||
onChange={e => setPasswordForm({ ...passwordForm, new: e.target.value })}
|
||||
className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
required
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
placeholder={t('confirmPassword')}
|
||||
value={passwordForm.confirm}
|
||||
onChange={e => setPasswordForm({ ...passwordForm, confirm: e.target.value })}
|
||||
className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
required
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
{passwordSuccess && <p className="text-xs text-green-600">{passwordSuccess}</p>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full py-2 bg-slate-800 text-white rounded-md text-sm hover:bg-slate-900 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? <Loader2 className="w-4 h-4 animate-spin mx-auto" /> : t('confirmChange')}
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{/* Logout Section */}
|
||||
<section className="pt-4 border-t border-slate-200">
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className="flex items-center gap-2 text-red-600 hover:bg-red-50 px-4 py-2 rounded-lg transition-colors text-sm font-medium"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
{t('logout')}
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderUserTab = () => (
|
||||
<div className="space-y-4 animate-in slide-in-from-right duration-300">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="font-medium text-slate-700">{t('userList')}</h3>
|
||||
{currentUser?.role === 'SUPER_ADMIN' && (
|
||||
<button
|
||||
onClick={() => setShowAddUser(!showAddUser)}
|
||||
className="flex items-center gap-1 text-sm bg-blue-600 text-white px-3 py-1.5 rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
{t('addUser')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showAddUser && (
|
||||
<form onSubmit={handleCreateUser} className="bg-slate-50 p-4 rounded-lg border border-slate-200 mb-4 space-y-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('username')}
|
||||
value={newUser.username}
|
||||
onChange={e => setNewUser({ ...newUser, username: e.target.value })}
|
||||
className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md"
|
||||
required
|
||||
autoComplete="username"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder={t('password')}
|
||||
value={newUser.password}
|
||||
onChange={e => setNewUser({ ...newUser, password: e.target.value })}
|
||||
className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md"
|
||||
required
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button type="button" onClick={() => setShowAddUser(false)} className="text-xs text-slate-500 hover:text-slate-700 px-2 py-1">{t('cancel')}</button>
|
||||
<button type="submit" className="text-xs bg-blue-600 text-white px-3 py-1 rounded hover:bg-blue-700">{t('create')}</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
{userSuccess && <p className="text-xs text-green-600 mb-2">{userSuccess}</p>}
|
||||
|
||||
<div className="space-y-2 max-h-[60vh] overflow-y-auto">
|
||||
{users.map(user => (
|
||||
<div key={user.id} className="flex items-center justify-between p-3 bg-white border border-slate-200 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-full ${user.isAdmin ? 'bg-orange-100 text-orange-600' : 'bg-slate-100 text-slate-600'}`}>
|
||||
{user.isAdmin ? <Shield className="w-4 h-4" /> : <User className="w-4 h-4" />}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-800">{user.username}</p>
|
||||
<p className="text-xs text-slate-400">{new Date(user.createdAt).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-slate-400 bg-slate-50 px-2 py-1 rounded">
|
||||
{user.isAdmin ? t('admin') : t('user')}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderModelTab = () => (
|
||||
<div className="animate-in slide-in-from-right duration-300">
|
||||
{editingId ? (
|
||||
<div className="bg-white p-6 rounded-lg border border-blue-100 shadow-sm space-y-4">
|
||||
<h3 className="font-semibold text-slate-700 mb-4">{editingId === 'new' ? t('mmAddBtn') : t('mmEdit')}</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-500 mb-1">{t('mmFormName')} *</label>
|
||||
<input className="w-full text-sm border rounded-md px-3 py-2" value={modelFormData.name} onChange={e => setModelFormData({ ...modelFormData, name: e.target.value })} disabled={isLoading} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-500 mb-1">{t('mmFormModelId')} *</label>
|
||||
<input className="w-full text-sm border rounded-md px-3 py-2 font-mono" value={modelFormData.modelId} onChange={e => setModelFormData({ ...modelFormData, modelId: e.target.value })} disabled={isLoading} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-500 mb-1">{t('mmFormType')} *</label>
|
||||
<select className="w-full text-sm border rounded-md px-3 py-2" value={modelFormData.type} onChange={e => setModelFormData({ ...modelFormData, type: e.target.value as ModelType })} disabled={isLoading}>
|
||||
<option value={ModelType.LLM}>{t('typeLLM')}</option>
|
||||
<option value={ModelType.EMBEDDING}>{t('typeEmbedding')}</option>
|
||||
<option value={ModelType.RERANK}>{t('typeRerank')}</option>
|
||||
<option value={ModelType.VISION}>{t('typeVision')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-500 mb-1">{t('mmFormBaseUrl')} *</label>
|
||||
<input className="w-full text-sm border rounded-md px-3 py-2 font-mono" value={modelFormData.baseUrl} onChange={e => setModelFormData({ ...modelFormData, baseUrl: e.target.value })} disabled={isLoading} autoComplete="off" />
|
||||
</div>
|
||||
|
||||
{modelFormData.type === ModelType.EMBEDDING && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-500 mb-1">Max Input</label>
|
||||
<input type="number" className="w-full text-sm border rounded px-3 py-2" value={modelFormData.maxInputTokens || 8191} onChange={e => setModelFormData({ ...modelFormData, maxInputTokens: parseInt(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-500 mb-1">Dimensions</label>
|
||||
<input type="number" className="w-full text-sm border rounded px-3 py-2" value={modelFormData.dimensions || 1536} onChange={e => setModelFormData({ ...modelFormData, dimensions: parseInt(e.target.value) })} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<button onClick={() => { setEditingId(null); setError(null); }} className="px-4 py-2 text-sm text-slate-600 hover:bg-slate-100 rounded-lg">{t('mmCancel')}</button>
|
||||
<button onClick={handleSaveModel} className="px-4 py-2 text-sm bg-blue-600 text-white rounded-lg flex items-center gap-2" disabled={isLoading}>
|
||||
{isLoading && <Loader2 className="w-4 h-4 animate-spin" />}
|
||||
{t('mmSave')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{models.map(model => (
|
||||
<div key={model.id} className="bg-white border border-slate-200 rounded-lg p-3 flex justify-between items-start">
|
||||
<div className="flex gap-3 flex-1">
|
||||
<div className="w-10 h-10 rounded-lg bg-emerald-50 text-emerald-600 flex items-center justify-center"><Cpu className="w-5 h-5" /></div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-semibold text-sm text-slate-800">{model.name}</h4>
|
||||
{model.isDefault && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-amber-50 text-amber-700 text-xs font-medium rounded-full border border-amber-200">
|
||||
<Star className="w-3 h-3 fill-amber-500 text-amber-500" />
|
||||
{t('defaultBadge')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2 text-xs text-slate-500">
|
||||
<span className="bg-slate-100 px-1 rounded">{getTypeLabel(model.type)}</span>
|
||||
<span className="font-mono">{model.modelId}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<button onClick={() => { setEditingId(model.id); setModelFormData({ ...model }); }} className="p-2 text-slate-400 hover:text-blue-600"><Edit2 className="w-4 h-4" /></button>
|
||||
<button onClick={() => handleDeleteModel(model.id)} className="p-2 text-slate-400 hover:text-red-600"><Trash2 className="w-4 h-4" /></button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={() => { setEditingId('new'); setModelFormData({ type: ModelType.LLM, baseUrl: 'http://localhost:11434/v1', modelId: 'llama3', name: '', dimensions: 1536 }); }} className="w-full py-3 border-2 border-dashed border-slate-200 rounded-lg text-slate-500 hover:border-blue-400 hover:text-blue-600 flex justify-center gap-2 items-center">
|
||||
<Plus className="w-5 h-5" /> {t('mmAddBtn')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in duration-200">
|
||||
<div className="bg-white rounded-xl shadow-2xl w-full max-w-4xl h-[80vh] flex overflow-hidden">
|
||||
{/* Sidebar */}
|
||||
<div className="w-64 bg-slate-50 border-r border-slate-200 flex flex-col">
|
||||
<div className="p-6">
|
||||
<h2 className="text-xl font-bold text-slate-800 flex items-center gap-2">
|
||||
<SettingsIcon className="w-6 h-6 text-blue-600" />
|
||||
{t('settings')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 px-4 space-y-1">
|
||||
<button onClick={() => setActiveTab('general')} className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${activeTab === 'general' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-600 hover:bg-slate-100'}`}>
|
||||
<Globe className="w-5 h-5" /> {t('generalSettings')}
|
||||
</button>
|
||||
{currentUser?.role === 'SUPER_ADMIN' && (
|
||||
<button onClick={() => setActiveTab('user')} className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${activeTab === 'user' ? 'bg-white text-purple-600 shadow-sm' : 'text-slate-600 hover:bg-slate-100'}`}>
|
||||
<User className="w-5 h-5" /> {t('userManagement')}
|
||||
</button>
|
||||
)}
|
||||
{currentUser?.role !== 'USER' && (
|
||||
<button onClick={() => setActiveTab('model')} className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${activeTab === 'model' ? 'bg-white text-emerald-600 shadow-sm' : 'text-slate-600 hover:bg-slate-100'}`}>
|
||||
<Cpu className="w-5 h-5" /> {t('modelManagement')}
|
||||
</button>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Content Area */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<div className="flex items-center justify-between p-4 border-b border-slate-100">
|
||||
<h3 className="text-lg font-semibold text-slate-800">
|
||||
{activeTab === 'general' ? t('generalSettings') : activeTab === 'user' ? t('userManagement') : t('modelManagement')}
|
||||
</h3>
|
||||
<button onClick={onClose} className="p-2 hover:bg-slate-100 rounded-full transition-colors"><X className="w-5 h-5 text-slate-500" /></button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6 bg-white">
|
||||
{error && (
|
||||
<div className="mb-4 p-4 bg-red-50 text-red-700 rounded-lg flex gap-2 items-start text-sm">
|
||||
<span className="font-bold">Error:</span> {error}
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'general' && renderGeneralTab()}
|
||||
{activeTab === 'user' && currentUser?.role === 'SUPER_ADMIN' && renderUserTab()}
|
||||
{activeTab === 'model' && currentUser?.role !== 'USER' && renderModelTab()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user