SettingsModal.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. import React, { useState, useEffect } from 'react';
  2. import { ModelConfig, ModelType } from '../types';
  3. import { useLanguage } from '../contexts/LanguageContext';
  4. import { X, Plus, Trash2, Edit2, Save, Cpu, Box, Loader2, User, Shield, Key, LogOut, Globe, Settings as SettingsIcon, Star } from 'lucide-react';
  5. import { userService } from '../services/userService';
  6. import { settingsService } from '../services/settingsService';
  7. import { userSettingService } from '../services/userSettingService';
  8. import { knowledgeGroupService } from '../services/knowledgeGroupService';
  9. import { modelConfigService } from '../services/modelConfigService';
  10. import { useConfirm } from '../contexts/ConfirmContext';
  11. import { AppSettings, KnowledgeGroup } from '../types';
  12. interface SettingsModalProps {
  13. isOpen: boolean;
  14. onClose: () => void;
  15. // Model Props
  16. models: ModelConfig[];
  17. authToken: string | null;
  18. onUpdateModels: (action: 'create' | 'update' | 'delete', model: ModelConfig) => Promise<void>;
  19. onLogout: () => void;
  20. }
  21. type TabType = 'general' | 'user' | 'model';
  22. export const SettingsModal: React.FC<SettingsModalProps> = ({
  23. isOpen,
  24. onClose,
  25. models,
  26. authToken,
  27. onUpdateModels,
  28. onLogout
  29. }) => {
  30. const { t, language, setLanguage } = useLanguage();
  31. const { confirm } = useConfirm();
  32. const [activeTab, setActiveTab] = useState<TabType>('general');
  33. const [isLoading, setIsLoading] = useState(false);
  34. const [error, setError] = useState<string | null>(null);
  35. // --- Model Manager State ---
  36. const [editingId, setEditingId] = useState<string | null>(null);
  37. const [modelFormData, setModelFormData] = useState<Partial<ModelConfig>>({
  38. type: ModelType.LLM,
  39. baseUrl: 'http://localhost:11434/v1',
  40. modelId: 'llama3',
  41. name: '',
  42. dimensions: 1536,
  43. maxInputTokens: 8191,
  44. maxBatchSize: 2048
  45. });
  46. // --- User Management State ---
  47. interface UserType {
  48. id: string;
  49. username: string;
  50. isAdmin: boolean;
  51. role?: string;
  52. createdAt: string;
  53. }
  54. const [users, setUsers] = useState<UserType[]>([]);
  55. const [isUserLoading, setIsUserLoading] = useState(false);
  56. const [showAddUser, setShowAddUser] = useState(false);
  57. const [newUser, setNewUser] = useState({ username: '', password: '' });
  58. const [userSuccess, setUserSuccess] = useState('');
  59. // --- Change Password State ---
  60. const [passwordForm, setPasswordForm] = useState({ current: '', new: '', confirm: '' });
  61. const [passwordSuccess, setPasswordSuccess] = useState('');
  62. // --- App Settings State ---
  63. const [appSettings, setAppSettings] = useState<AppSettings | null>(null);
  64. const [knowledgeGroups, setKnowledgeGroups] = useState<KnowledgeGroup[]>([]);
  65. const [isSettingsLoading, setIsSettingsLoading] = useState(false);
  66. const [currentUser, setCurrentUser] = useState<UserType | null>(null);
  67. // Reset state on open
  68. useEffect(() => {
  69. if (isOpen) {
  70. setActiveTab('general');
  71. setError(null);
  72. setEditingId(null);
  73. }
  74. }, [isOpen]);
  75. // Fetch Users when User tab is active
  76. useEffect(() => {
  77. if (isOpen) {
  78. if (activeTab === 'user') {
  79. fetchUsers();
  80. } else if (activeTab === 'general') {
  81. fetchSettingsAndGroups();
  82. }
  83. }
  84. }, [isOpen, activeTab]);
  85. const fetchSettingsAndGroups = async () => {
  86. if (!authToken) return;
  87. setIsSettingsLoading(true);
  88. try {
  89. const [settings, groups, users] = await Promise.all([
  90. userSettingService.get(authToken),
  91. knowledgeGroupService.getGroups(),
  92. userService.getUsers().catch(() => []) // Regular users might fail this
  93. ]);
  94. setAppSettings(settings);
  95. setKnowledgeGroups(groups);
  96. // Temporary way to get current user details since we lack a /me endpoint hook here
  97. const tokenPayload = JSON.parse(atob(authToken.split('.')[1]));
  98. const me = users.find(u => u.id === tokenPayload.sub) || { isAdmin: tokenPayload.role === 'SUPER_ADMIN' || tokenPayload.role === 'TENANT_ADMIN', role: tokenPayload.role };
  99. setCurrentUser(me as any);
  100. } catch (error) {
  101. console.error('Failed to fetch settings or groups:', error);
  102. } finally {
  103. setIsSettingsLoading(false);
  104. }
  105. };
  106. if (!isOpen) return null;
  107. // --- General Tab Handlers ---
  108. const handleLanguageChange = async (newLanguage: string) => {
  109. setIsLoading(true);
  110. try {
  111. await settingsService.updateLanguage(newLanguage);
  112. setLanguage(newLanguage as any);
  113. } catch (error) {
  114. console.error('Failed to update language:', error);
  115. } finally {
  116. setIsLoading(false);
  117. }
  118. };
  119. const handleChangePassword = async (e: React.FormEvent) => {
  120. e.preventDefault();
  121. setError(null);
  122. setPasswordSuccess('');
  123. if (passwordForm.new !== passwordForm.confirm) {
  124. setError(t('passwordMismatch'));
  125. return;
  126. }
  127. if (passwordForm.new.length < 6) {
  128. setError(t('newPasswordMinLength'));
  129. return;
  130. }
  131. setIsLoading(true);
  132. try {
  133. await userService.changePassword(passwordForm.current, passwordForm.new);
  134. setPasswordSuccess(t('passwordChangeSuccess'));
  135. setPasswordForm({ current: '', new: '', confirm: '' });
  136. } catch (err: any) {
  137. setError(err.message || t('passwordChangeFailed'));
  138. } finally {
  139. setIsLoading(false);
  140. }
  141. };
  142. // --- User Tab Handlers ---
  143. const fetchUsers = async () => {
  144. setIsUserLoading(true);
  145. try {
  146. const userList = await userService.getUsers();
  147. setUsers(userList);
  148. } catch (error: any) {
  149. setError(error.message || t('getUserListFailed'));
  150. } finally {
  151. setIsUserLoading(false);
  152. }
  153. };
  154. const handleCreateUser = async (e: React.FormEvent) => {
  155. e.preventDefault();
  156. setError('');
  157. setUserSuccess('');
  158. if (newUser.password.length < 6) {
  159. setError(t('passwordMinLength'));
  160. return;
  161. }
  162. try {
  163. await userService.createUser(newUser.username, newUser.password);
  164. setUserSuccess(t('userCreatedSuccess'));
  165. setNewUser({ username: '', password: '' });
  166. setShowAddUser(false);
  167. fetchUsers();
  168. } catch (error: any) {
  169. setError(error.message || t('createUserFailed'));
  170. }
  171. };
  172. // --- Model Tab Handlers ---
  173. const handleSaveModel = async () => {
  174. if (!authToken) {
  175. setError(t('mmErrorNotAuthenticated'));
  176. return;
  177. }
  178. setError(null);
  179. if (!modelFormData.name?.trim()) { setError(t('mmErrorNameRequired')); return; }
  180. if (!modelFormData.modelId?.trim()) { setError(t('mmErrorModelIdRequired')); return; }
  181. if (!modelFormData.baseUrl?.trim()) { setError(t('mmErrorBaseUrlRequired')); return; }
  182. setIsLoading(true);
  183. try {
  184. const saveData = { ...modelFormData } as ModelConfig;
  185. await onUpdateModels(editingId === 'new' ? 'create' : 'update', saveData);
  186. setEditingId(null);
  187. } catch (err: any) {
  188. setError(err.message || t('errorGeneric'));
  189. } finally {
  190. setIsLoading(false);
  191. }
  192. };
  193. const handleDeleteModel = async (id: string) => {
  194. if (await confirm(t('confirmClear'))) {
  195. await onUpdateModels('delete', { id } as ModelConfig);
  196. }
  197. };
  198. const handleSetDefault = async (id: string) => {
  199. if (!authToken) {
  200. setError(t('mmErrorNotAuthenticated'));
  201. return;
  202. }
  203. setIsLoading(true);
  204. try {
  205. await modelConfigService.setDefault(authToken, id);
  206. // Reload page to fetch model list again
  207. window.location.reload();
  208. } catch (err: any) {
  209. setError(err.message || t('defaultSettingFailed'));
  210. } finally {
  211. setIsLoading(false);
  212. }
  213. };
  214. const getTypeLabel = (type: ModelType) => {
  215. switch (type) {
  216. case ModelType.LLM: return t('typeLLM');
  217. case ModelType.EMBEDDING: return t('typeEmbedding');
  218. case ModelType.RERANK: return t('typeRerank');
  219. }
  220. };
  221. // --- Render Functions ---
  222. const renderGeneralTab = () => (
  223. <div className="space-y-8 animate-in slide-in-from-right duration-300">
  224. {/* Language section */}
  225. <section>
  226. <h3 className="text-sm font-medium text-slate-500 mb-3 flex items-center gap-2">
  227. <Globe className="w-4 h-4" />
  228. {t('languageSettings')}
  229. </h3>
  230. <div className="flex gap-2">
  231. {(['zh', 'en', 'ja'] as const).map((lang) => (
  232. <button
  233. key={lang}
  234. onClick={() => handleLanguageChange(lang)}
  235. disabled={isLoading}
  236. className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors border ${language === lang
  237. ? 'bg-blue-50 border-blue-200 text-blue-700'
  238. : 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50'
  239. }`}
  240. >
  241. {lang === 'zh' ? 'Chinese' : lang === 'en' ? 'English' : 'Japanese'}
  242. </button>
  243. ))}
  244. </div>
  245. </section>
  246. {/* Change Password Section */}
  247. <section className="bg-white p-6 rounded-lg border border-slate-200 shadow-sm">
  248. <h3 className="text-sm font-medium text-slate-800 mb-4 flex items-center gap-2">
  249. <Key className="w-4 h-4 text-blue-500" />
  250. {t('changePassword')}
  251. </h3>
  252. <form onSubmit={handleChangePassword} className="space-y-4 max-w-sm">
  253. <div>
  254. <input
  255. type="password"
  256. placeholder={t('currentPassword')}
  257. value={passwordForm.current}
  258. onChange={e => setPasswordForm({ ...passwordForm, current: e.target.value })}
  259. className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none"
  260. required
  261. />
  262. </div>
  263. <div>
  264. <input
  265. type="password"
  266. placeholder={t('newPassword')}
  267. value={passwordForm.new}
  268. onChange={e => setPasswordForm({ ...passwordForm, new: e.target.value })}
  269. className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none"
  270. required
  271. />
  272. </div>
  273. <div>
  274. <input
  275. type="password"
  276. placeholder={t('confirmPassword')}
  277. value={passwordForm.confirm}
  278. onChange={e => setPasswordForm({ ...passwordForm, confirm: e.target.value })}
  279. className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none"
  280. required
  281. />
  282. </div>
  283. {passwordSuccess && <p className="text-xs text-green-600">{passwordSuccess}</p>}
  284. <button
  285. type="submit"
  286. disabled={isLoading}
  287. className="w-full py-2 bg-slate-800 text-white rounded-md text-sm hover:bg-slate-900 transition-colors disabled:opacity-50"
  288. >
  289. {isLoading ? <Loader2 className="w-4 h-4 animate-spin mx-auto" /> : t('confirmChange')}
  290. </button>
  291. </form>
  292. </section>
  293. {/* Logout Section */}
  294. <section className="pt-4 border-t border-slate-200">
  295. <button
  296. onClick={onLogout}
  297. 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"
  298. >
  299. <LogOut className="w-4 h-4" />
  300. {t('logout')}
  301. </button>
  302. </section>
  303. </div>
  304. );
  305. const renderUserTab = () => (
  306. <div className="space-y-4 animate-in slide-in-from-right duration-300">
  307. <div className="flex justify-between items-center mb-4">
  308. <h3 className="font-medium text-slate-700">{t('userList')}</h3>
  309. {currentUser?.role === 'SUPER_ADMIN' && (
  310. <button
  311. onClick={() => setShowAddUser(!showAddUser)}
  312. 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"
  313. >
  314. <Plus className="w-4 h-4" />
  315. {t('addUser')}
  316. </button>
  317. )}
  318. </div>
  319. {showAddUser && (
  320. <form onSubmit={handleCreateUser} className="bg-slate-50 p-4 rounded-lg border border-slate-200 mb-4 space-y-3">
  321. <input
  322. type="text"
  323. placeholder={t('username')}
  324. value={newUser.username}
  325. onChange={e => setNewUser({ ...newUser, username: e.target.value })}
  326. className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md"
  327. required
  328. />
  329. <input
  330. type="password"
  331. placeholder={t('password')}
  332. value={newUser.password}
  333. onChange={e => setNewUser({ ...newUser, password: e.target.value })}
  334. className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md"
  335. required
  336. />
  337. <div className="flex justify-end gap-2">
  338. <button type="button" onClick={() => setShowAddUser(false)} className="text-xs text-slate-500 hover:text-slate-700 px-2 py-1">{t('cancel')}</button>
  339. <button type="submit" className="text-xs bg-blue-600 text-white px-3 py-1 rounded hover:bg-blue-700">{t('create')}</button>
  340. </div>
  341. </form>
  342. )}
  343. {userSuccess && <p className="text-xs text-green-600 mb-2">{userSuccess}</p>}
  344. <div className="space-y-2 max-h-[60vh] overflow-y-auto">
  345. {users.map(user => (
  346. <div key={user.id} className="flex items-center justify-between p-3 bg-white border border-slate-200 rounded-lg">
  347. <div className="flex items-center gap-3">
  348. <div className={`p-2 rounded-full ${user.isAdmin ? 'bg-orange-100 text-orange-600' : 'bg-slate-100 text-slate-600'}`}>
  349. {user.isAdmin ? <Shield className="w-4 h-4" /> : <User className="w-4 h-4" />}
  350. </div>
  351. <div>
  352. <p className="text-sm font-medium text-slate-800">{user.username}</p>
  353. <p className="text-xs text-slate-400">{new Date(user.createdAt).toLocaleDateString()}</p>
  354. </div>
  355. </div>
  356. <span className="text-xs text-slate-400 bg-slate-50 px-2 py-1 rounded">
  357. {user.isAdmin ? t('admin') : t('user')}
  358. </span>
  359. </div>
  360. ))}
  361. </div>
  362. </div>
  363. );
  364. const renderModelTab = () => (
  365. <div className="animate-in slide-in-from-right duration-300">
  366. {editingId ? (
  367. <div className="bg-white p-6 rounded-lg border border-blue-100 shadow-sm space-y-4">
  368. <h3 className="font-semibold text-slate-700 mb-4">{editingId === 'new' ? t('mmAddBtn') : t('mmEdit')}</h3>
  369. <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
  370. <div>
  371. <label className="block text-xs font-medium text-slate-500 mb-1">{t('mmFormName')} *</label>
  372. <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} />
  373. </div>
  374. <div>
  375. <label className="block text-xs font-medium text-slate-500 mb-1">{t('mmFormModelId')} *</label>
  376. <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} />
  377. </div>
  378. </div>
  379. <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
  380. <div>
  381. <label className="block text-xs font-medium text-slate-500 mb-1">{t('mmFormType')} *</label>
  382. <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}>
  383. <option value={ModelType.LLM}>{t('typeLLM')}</option>
  384. <option value={ModelType.EMBEDDING}>{t('typeEmbedding')}</option>
  385. <option value={ModelType.RERANK}>{t('typeRerank')}</option>
  386. <option value={ModelType.VISION}>{t('typeVision')}</option>
  387. </select>
  388. </div>
  389. </div>
  390. <div>
  391. <label className="block text-xs font-medium text-slate-500 mb-1">{t('mmFormBaseUrl')} *</label>
  392. <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" />
  393. </div>
  394. {modelFormData.type === ModelType.EMBEDDING && (
  395. <div className="grid grid-cols-2 gap-4">
  396. <div>
  397. <label className="block text-xs font-medium text-slate-500 mb-1">Max Input</label>
  398. <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) })} />
  399. </div>
  400. <div>
  401. <label className="block text-xs font-medium text-slate-500 mb-1">Dimensions</label>
  402. <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) })} />
  403. </div>
  404. </div>
  405. )}
  406. <div className="flex justify-end gap-2 pt-2">
  407. <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>
  408. <button onClick={handleSaveModel} className="px-4 py-2 text-sm bg-blue-600 text-white rounded-lg flex items-center gap-2" disabled={isLoading}>
  409. {isLoading && <Loader2 className="w-4 h-4 animate-spin" />}
  410. {t('mmSave')}
  411. </button>
  412. </div>
  413. </div>
  414. ) : (
  415. <div className="space-y-3">
  416. {models.map(model => (
  417. <div key={model.id} className="bg-white border border-slate-200 rounded-lg p-3 flex justify-between items-start">
  418. <div className="flex gap-3 flex-1">
  419. <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>
  420. <div className="flex-1">
  421. <div className="flex items-center gap-2">
  422. <h4 className="font-semibold text-sm text-slate-800">{model.name}</h4>
  423. {model.isDefault && (
  424. <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">
  425. <Star className="w-3 h-3 fill-amber-500 text-amber-500" />
  426. {t('defaultBadge')}
  427. </span>
  428. )}
  429. </div>
  430. <div className="flex gap-2 text-xs text-slate-500">
  431. <span className="bg-slate-100 px-1 rounded">{getTypeLabel(model.type)}</span>
  432. <span className="font-mono">{model.modelId}</span>
  433. </div>
  434. </div>
  435. </div>
  436. <div className="flex gap-1">
  437. <button onClick={() => { setEditingId(model.id); setModelFormData({ ...model }); }} className="p-2 text-slate-400 hover:text-blue-600"><Edit2 className="w-4 h-4" /></button>
  438. <button onClick={() => handleDeleteModel(model.id)} className="p-2 text-slate-400 hover:text-red-600"><Trash2 className="w-4 h-4" /></button>
  439. </div>
  440. </div>
  441. ))}
  442. <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">
  443. <Plus className="w-5 h-5" /> {t('mmAddBtn')}
  444. </button>
  445. </div>
  446. )}
  447. </div>
  448. );
  449. return (
  450. <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">
  451. <div className="bg-white rounded-xl shadow-2xl w-full max-w-4xl h-[80vh] flex overflow-hidden">
  452. {/* Sidebar */}
  453. <div className="w-64 bg-slate-50 border-r border-slate-200 flex flex-col">
  454. <div className="p-6">
  455. <h2 className="text-xl font-bold text-slate-800 flex items-center gap-2">
  456. <SettingsIcon className="w-6 h-6 text-blue-600" />
  457. {t('settings')}
  458. </h2>
  459. </div>
  460. <nav className="flex-1 px-4 space-y-1">
  461. <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'}`}>
  462. <Globe className="w-5 h-5" /> {t('generalSettings')}
  463. </button>
  464. <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'}`}>
  465. <User className="w-5 h-5" /> {t('userManagement')}
  466. </button>
  467. {currentUser?.role !== 'USER' && (
  468. <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'}`}>
  469. <Cpu className="w-5 h-5" /> {t('modelManagement')}
  470. </button>
  471. )}
  472. </nav>
  473. </div>
  474. {/* Content Area */}
  475. <div className="flex-1 flex flex-col min-w-0">
  476. <div className="flex items-center justify-between p-4 border-b border-slate-100">
  477. <h3 className="text-lg font-semibold text-slate-800">
  478. {activeTab === 'general' ? t('generalSettings') : activeTab === 'user' ? t('userManagement') : t('modelManagement')}
  479. </h3>
  480. <button onClick={onClose} className="p-2 hover:bg-slate-100 rounded-full transition-colors"><X className="w-5 h-5 text-slate-500" /></button>
  481. </div>
  482. <div className="flex-1 overflow-y-auto p-6 bg-white">
  483. {error && (
  484. <div className="mb-4 p-4 bg-red-50 text-red-700 rounded-lg flex gap-2 items-start text-sm">
  485. <span className="font-bold">Error:</span> {error}
  486. </div>
  487. )}
  488. {activeTab === 'general' && renderGeneralTab()}
  489. {activeTab === 'user' && renderUserTab()}
  490. {activeTab === 'model' && currentUser?.role !== 'USER' && renderModelTab()}
  491. </div>
  492. </div>
  493. </div>
  494. </div>
  495. );
  496. };