SettingsView.tsx 116 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870
  1. import React, { useState, useEffect } from 'react';
  2. import { ModelConfig, ModelType, AppSettings, KnowledgeGroup } 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, ToggleLeft, ToggleRight, Database, Sparkles, ChevronRight, Lock, Building2, BookOpen } from 'lucide-react';
  5. import { motion, AnimatePresence } from 'framer-motion';
  6. import { userService } from '../../services/userService';
  7. // import { settingsService } from '../../services/settingsService';
  8. import { userSettingService } from '../../services/userSettingService';
  9. import { knowledgeGroupService } from '../../services/knowledgeGroupService';
  10. import { useConfirm } from '../../contexts/ConfirmContext';
  11. import { useToast } from '../../contexts/ToastContext';
  12. interface SettingsViewProps {
  13. // Model Props
  14. models: ModelConfig[];
  15. authToken: string | null;
  16. onUpdateModels: (action: 'create' | 'update' | 'delete', model: ModelConfig) => Promise<void>;
  17. isAdmin?: boolean; // Added isAdmin prop
  18. currentUser?: any; // Added current user prop
  19. initialTab?: TabType;
  20. }
  21. type TabType = 'general' | 'user' | 'model' | 'tenants' | 'knowledge_base';
  22. export const SettingsView: React.FC<SettingsViewProps> = ({
  23. models,
  24. authToken,
  25. onUpdateModels,
  26. isAdmin = false,
  27. currentUser,
  28. initialTab = 'general',
  29. }) => {
  30. const { t, language, setLanguage } = useLanguage();
  31. const { confirm } = useConfirm();
  32. const { showError, showSuccess } = useToast();
  33. const [activeTab, setActiveTab] = useState<TabType>('general');
  34. const [isLoading, setIsLoading] = useState(false);
  35. const [error, setError] = useState<string | null>(null);
  36. // --- Model Manager State ---
  37. const [editingId, setEditingId] = useState<string | null>(null);
  38. const [modelFormData, setModelFormData] = useState<Partial<ModelConfig>>({
  39. type: ModelType.LLM,
  40. baseUrl: 'http://localhost:11434/v1',
  41. modelId: 'llama3',
  42. name: '',
  43. dimensions: 1536,
  44. apiKey: '',
  45. maxInputTokens: 8191,
  46. maxBatchSize: 2048
  47. });
  48. // --- User Management State ---
  49. interface UserType {
  50. id: string;
  51. username: string;
  52. isAdmin: boolean;
  53. role?: string;
  54. tenantId?: string;
  55. createdAt: string;
  56. tenantMembers?: Array<{ tenantId: string; role: string; tenant?: { id: string; name: string } }>;
  57. }
  58. const [users, setUsers] = useState<UserType[]>([]);
  59. const [isUserLoading, setIsUserLoading] = useState(false);
  60. const [showAddUser, setShowAddUser] = useState(false);
  61. const [newUser, setNewUser] = useState({ username: '', password: '', role: 'USER' });
  62. const [userSuccess, setUserSuccess] = useState('');
  63. // --- Change Password State ---
  64. const [passwordForm, setPasswordForm] = useState({ current: '', new: '', confirm: '' });
  65. const [passwordSuccess, setPasswordSuccess] = useState('');
  66. // --- App Settings State ---
  67. const [appSettings, setAppSettings] = useState<AppSettings | null>(null);
  68. const [knowledgeGroups, setKnowledgeGroups] = useState<KnowledgeGroup[]>([]);
  69. const [isSettingsLoading, setIsSettingsLoading] = useState(false);
  70. const [enabledModelIds, setEnabledModelIds] = useState<string[]>([]);
  71. // --- Tenant Admin Binding Search State ---
  72. const [bindingTenantId, setBindingTenantId] = useState<string | null>(null);
  73. const [userSearchQuery, setUserSearchQuery] = useState('');
  74. // --- Manage Members Modal State ---
  75. const [managingMembersTenantId, setManagingMembersTenantId] = useState<string | null>(null);
  76. const [tenantMembers, setTenantMembers] = useState<any[]>([]);
  77. const [memberUserSearch, setMemberUserSearch] = useState('');
  78. const [isMembersLoading, setIsMembersLoading] = useState(false);
  79. useEffect(() => {
  80. if (initialTab) {
  81. setActiveTab(initialTab);
  82. }
  83. }, [initialTab]);
  84. // ユーザー一覧の取得(ユーザータブがアクティブな場合)
  85. useEffect(() => {
  86. if (activeTab === 'user') {
  87. fetchUsers();
  88. } else if (activeTab === 'general') {
  89. fetchSettingsAndGroups();
  90. } else if (activeTab === 'model' && (isAdmin || currentUser?.role === 'SUPER_ADMIN')) {
  91. // Model tab initialization
  92. } else if (activeTab === 'tenants' && currentUser?.role === 'SUPER_ADMIN') {
  93. fetchTenantsData();
  94. fetchUsers(); // Ensure users are loaded for admin binding
  95. } else if (activeTab === 'knowledge_base' && (currentUser?.role === 'TENANT_ADMIN' || currentUser?.role === 'SUPER_ADMIN')) {
  96. fetchKnowledgeBaseSettings();
  97. }
  98. }, [activeTab, currentUser]);
  99. const [kbSettings, setKbSettings] = useState<any>(null);
  100. const [localKbSettings, setLocalKbSettings] = useState<any>(null);
  101. const [isSavingKbSettings, setIsSavingKbSettings] = useState(false);
  102. const fetchKnowledgeBaseSettings = async () => {
  103. if (!authToken) return;
  104. setIsLoading(true);
  105. try {
  106. const res = await fetch('/api/v1/admin/settings', {
  107. headers: { 'Authorization': `Bearer ${authToken}` }
  108. });
  109. if (res.ok) {
  110. const data = await res.json();
  111. setKbSettings(data);
  112. setLocalKbSettings(data);
  113. }
  114. } catch (error) {
  115. console.error(error);
  116. } finally {
  117. setIsLoading(false);
  118. }
  119. };
  120. const handleUpdateKbSettings = (key: string, value: any) => {
  121. setLocalKbSettings((prev: any) => ({ ...prev, [key]: value }));
  122. };
  123. const handleSaveKbSettings = async () => {
  124. if (!authToken || !localKbSettings) return;
  125. setIsSavingKbSettings(true);
  126. try {
  127. const res = await fetch('/api/v1/admin/settings', {
  128. method: 'PUT',
  129. headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
  130. body: JSON.stringify(localKbSettings)
  131. });
  132. if (res.ok) {
  133. setKbSettings(localKbSettings);
  134. showSuccess('Knowledge Base settings saved');
  135. } else {
  136. showError('Failed to save settings');
  137. }
  138. } catch (error) {
  139. showError('Error saving settings');
  140. } finally {
  141. setIsSavingKbSettings(false);
  142. }
  143. };
  144. const handleCancelKbSettings = () => {
  145. setLocalKbSettings(kbSettings);
  146. };
  147. const fetchSettingsAndGroups = async () => {
  148. if (!authToken) return;
  149. setIsSettingsLoading(true);
  150. try {
  151. const [settings, groups] = await Promise.all([
  152. userSettingService.get(authToken),
  153. knowledgeGroupService.getGroups()
  154. ]);
  155. setAppSettings(settings);
  156. setKnowledgeGroups(groups);
  157. } catch (error) {
  158. console.error('Failed to fetch settings or groups:', error);
  159. } finally {
  160. setIsSettingsLoading(false);
  161. }
  162. };
  163. // --- 一般タブのハンドラー ---
  164. const handleChangePassword = async (e: React.FormEvent) => {
  165. e.preventDefault();
  166. setError(null);
  167. setPasswordSuccess('');
  168. if (passwordForm.new !== passwordForm.confirm) {
  169. setError(t('passwordMismatch'));
  170. return;
  171. }
  172. if (passwordForm.new.length < 6) {
  173. setError(t('newPasswordMinLength'));
  174. return;
  175. }
  176. setIsLoading(true);
  177. try {
  178. await userService.changePassword(passwordForm.current, passwordForm.new);
  179. setPasswordSuccess(t('passwordChangeSuccess'));
  180. setPasswordForm({ current: '', new: '', confirm: '' });
  181. } catch (err: any) {
  182. setError(err.message || t('passwordChangeFailed'));
  183. } finally {
  184. setIsLoading(false);
  185. }
  186. };
  187. // --- ユーザータブのハンドラー ---
  188. const fetchUsers = async () => {
  189. setIsUserLoading(true);
  190. try {
  191. const userList = await userService.getUsers();
  192. setUsers(userList);
  193. } catch (error: any) {
  194. setError(error.message || t('getUserListFailed'));
  195. } finally {
  196. setIsUserLoading(false);
  197. }
  198. };
  199. const handleCreateUser = async (e: React.FormEvent) => {
  200. e.preventDefault();
  201. setError('');
  202. setUserSuccess('');
  203. if (newUser.password.length < 6) {
  204. setError(t('passwordMinLength'));
  205. return;
  206. }
  207. try {
  208. await userService.createUser(newUser.username, newUser.password, newUser.role);
  209. setUserSuccess(t('userCreatedSuccess'));
  210. setNewUser({ username: '', password: '', role: 'USER' });
  211. setShowAddUser(false);
  212. fetchUsers();
  213. } catch (error: any) {
  214. setError(error.message || t('createUserFailed'));
  215. }
  216. };
  217. const [passwordChangeUserData, setPasswordChangeUserData] = useState<{ userId: string, newPassword: string } | null>(null);
  218. // --- Edit Role State ---
  219. const [roleChangeUserData, setRoleChangeUserData] = useState<{ userId: string, newRole: string } | null>(null);
  220. const handleToggleUserAdmin = async (userId: string, newAdminStatus: boolean) => {
  221. try {
  222. await userService.updateUser(userId, newAdminStatus);
  223. // ユーザーリストを再取得
  224. fetchUsers();
  225. setUserSuccess(newAdminStatus ? t('userPromotedToAdmin') : t('userDemotedFromAdmin'));
  226. } catch (error: any) {
  227. setError(error.message || t('updateUserFailed'));
  228. }
  229. };
  230. const handleUserPasswordChange = async () => {
  231. if (!passwordChangeUserData || !passwordChangeUserData.newPassword) return;
  232. try {
  233. // Update user password
  234. await userService.updateUserInfo(passwordChangeUserData.userId, { password: passwordChangeUserData.newPassword });
  235. setUserSuccess(t('passwordChangeSuccess'));
  236. setPasswordChangeUserData(null);
  237. fetchUsers(); // Refresh the user list
  238. } catch (error: any) {
  239. setError(error.message || t('passwordChangeFailed'));
  240. }
  241. };
  242. // --- Tenants Management State (Migrated) ---
  243. const [stats, setStats] = useState({ tenants: 0, users: 0 });
  244. const [tenants, setTenants] = useState<any[]>([]);
  245. const [tenantAdmins, setTenantAdmins] = useState<any[]>([]);
  246. const [isTenantsLoading, setIsTenantsLoading] = useState(false);
  247. const [showCreateTenant, setShowCreateTenant] = useState(false);
  248. const [editingTenant, setEditingTenant] = useState<any | null>(null);
  249. const [newTenant, setNewTenant] = useState({ name: '', domain: '', adminUserId: '' });
  250. const fetchTenantMembers = async (tenantId: string) => {
  251. setIsMembersLoading(true);
  252. try {
  253. const headers = { 'Authorization': `Bearer ${authToken}`, 'x-api-key': authToken || '' };
  254. const res = await fetch(`/api/v1/tenants/${tenantId}/members`, { headers });
  255. if (res.ok) {
  256. setTenantMembers(await res.json());
  257. }
  258. } catch (e) {
  259. console.error(e);
  260. } finally {
  261. setIsMembersLoading(false);
  262. }
  263. };
  264. const handleAddMember = async (tenantId: string, userId: string) => {
  265. try {
  266. const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}`, 'x-api-key': authToken || '' };
  267. const res = await fetch(`/api/v1/tenants/${tenantId}/members`, {
  268. method: 'POST',
  269. headers,
  270. body: JSON.stringify({ userId, role: 'USER' }),
  271. });
  272. if (res.ok) {
  273. showSuccess('User added to organization');
  274. fetchTenantMembers(tenantId);
  275. fetchTenantsData();
  276. fetchUsers();
  277. } else {
  278. const errData = await res.json().catch(() => ({}));
  279. showError(errData.message || 'Failed to add member');
  280. }
  281. } catch (e) {
  282. showError('Error adding member');
  283. }
  284. };
  285. const handleRemoveMember = async (tenantId: string, userId: string) => {
  286. try {
  287. const headers = { 'Authorization': `Bearer ${authToken}`, 'x-api-key': authToken || '' };
  288. const res = await fetch(`/api/v1/tenants/${tenantId}/members/${userId}`, {
  289. method: 'DELETE',
  290. headers,
  291. });
  292. if (res.ok || res.status === 204) {
  293. showSuccess('User removed from organization');
  294. fetchTenantMembers(tenantId);
  295. fetchTenantsData();
  296. fetchUsers();
  297. } else {
  298. showError('Failed to remove member');
  299. }
  300. } catch (e) {
  301. showError('Error removing member');
  302. }
  303. };
  304. const fetchTenantsData = async () => {
  305. setIsTenantsLoading(true);
  306. try {
  307. const headers = { 'Authorization': `Bearer ${authToken}`, 'x-api-key': authToken || '' };
  308. const [tenRes, admRes] = await Promise.all([
  309. fetch('/api/v1/tenants', { headers }),
  310. fetch('/api/v1/admin/users', { headers })
  311. ]);
  312. if (tenRes.ok) {
  313. const data = await tenRes.json();
  314. setTenants(data);
  315. setStats(s => ({ ...s, tenants: data.length }));
  316. }
  317. if (admRes.ok) {
  318. const data = await admRes.json();
  319. setTenantAdmins(data.filter((u: any) => u.role === 'TENANT_ADMIN' || u.role === 'SUPER_ADMIN'));
  320. setStats(s => ({ ...s, users: data.length }));
  321. }
  322. } catch (e) {
  323. console.error(e);
  324. } finally {
  325. setIsTenantsLoading(false);
  326. }
  327. };
  328. const handleCreateTenant = async (e: React.FormEvent) => {
  329. e.preventDefault();
  330. try {
  331. if (editingTenant) {
  332. const res = await fetch(`/api/v1/tenants/${editingTenant.id}`, {
  333. method: 'PUT',
  334. headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}`, 'x-api-key': authToken || '' },
  335. body: JSON.stringify({ name: newTenant.name, domain: newTenant.domain })
  336. });
  337. if (res.ok) {
  338. setEditingTenant(null);
  339. setNewTenant({ name: '', domain: '', adminUserId: '' });
  340. fetchTenantsData();
  341. showSuccess('Tenant updated successfully');
  342. } else {
  343. showError('Failed to update tenant');
  344. }
  345. } else {
  346. const res = await fetch('/api/v1/tenants', {
  347. method: 'POST',
  348. headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}`, 'x-api-key': authToken || '' },
  349. body: JSON.stringify(newTenant)
  350. });
  351. if (res.ok) {
  352. setShowCreateTenant(false);
  353. setNewTenant({ name: '', domain: '', adminUserId: '' });
  354. fetchTenantsData();
  355. showSuccess('Tenant created successfully');
  356. } else {
  357. showError('Failed to create tenant');
  358. }
  359. }
  360. } catch (e) {
  361. showError('Error processing tenant');
  362. }
  363. };
  364. const handleDeleteTenant = async (tenantId: string) => {
  365. if (!(await confirm('Are you sure you want to delete this tenant? All associated data will be removed.'))) return;
  366. try {
  367. const res = await fetch(`/api/v1/tenants/${tenantId}`, {
  368. method: 'DELETE',
  369. headers: { 'Authorization': `Bearer ${authToken}`, 'x-api-key': authToken || '' }
  370. });
  371. if (res.ok) {
  372. showSuccess('Tenant deleted successfully');
  373. fetchTenantsData();
  374. } else {
  375. showError('Failed to delete tenant');
  376. }
  377. } catch (e) {
  378. showError('Error deleting tenant');
  379. }
  380. };
  381. const handleBindAdmin = async (tenantId: string, userId: string): Promise<boolean> => {
  382. if (!userId) return false;
  383. try {
  384. const res = await fetch(`/api/v1/tenants/${tenantId}/admin`, {
  385. method: 'PUT',
  386. headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}`, 'x-api-key': authToken || '' },
  387. body: JSON.stringify({ userId })
  388. });
  389. if (res.ok) {
  390. showSuccess('Admin bound successfully');
  391. // Refresh tenants data and users to ensure all states are in sync
  392. await Promise.all([
  393. fetchTenantsData(),
  394. fetchUsers()
  395. ]);
  396. return true;
  397. } else {
  398. showError('Failed to bind admin');
  399. return false;
  400. }
  401. } catch (e) {
  402. showError('Error binding admin');
  403. return false;
  404. }
  405. };
  406. const handleToggleNotebookFeature = async (tenantId: string, currentEnabled: boolean) => {
  407. try {
  408. const res = await fetch(`/api/tenants/${tenantId}/settings`, {
  409. method: 'PUT',
  410. headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
  411. body: JSON.stringify({ isNotebookEnabled: !currentEnabled })
  412. });
  413. if (res.ok) {
  414. showSuccess('Feature updated successfully');
  415. fetchTenantsData();
  416. }
  417. } catch (e) {
  418. showError('Failed to update feature');
  419. }
  420. };
  421. const handleUserRoleChange = async () => {
  422. if (!roleChangeUserData) return;
  423. try {
  424. await userService.updateUserInfo(roleChangeUserData.userId, { role: roleChangeUserData.newRole });
  425. showSuccess('Role updated successfully.');
  426. setRoleChangeUserData(null);
  427. fetchUsers();
  428. } catch (error: any) {
  429. showError(error.response?.data?.message || 'Failed to update user role.');
  430. }
  431. };
  432. const handleDeleteUser = async (userId: string) => {
  433. if (!confirm(t('confirmDeleteUser') || "Are you sure you want to delete this user?")) return;
  434. try {
  435. await userService.deleteUser(userId);
  436. showSuccess(t('userDeletedSuccessfully'));
  437. fetchUsers();
  438. } catch (error: any) {
  439. showError(error.message || t('deleteUserFailed'));
  440. }
  441. };
  442. // --- モデルタブのハンドラー ---
  443. const handleSaveModel = async () => {
  444. if (!authToken) {
  445. setError(t('mmErrorNotAuthenticated'));
  446. return;
  447. }
  448. setError(null);
  449. if (!modelFormData.name?.trim()) { setError(t('mmErrorNameRequired')); return; }
  450. if (!modelFormData.modelId?.trim()) { setError(t('mmErrorModelIdRequired')); return; }
  451. if (!modelFormData.baseUrl?.trim()) { setError(t('mmErrorBaseUrlRequired')); return; }
  452. setIsLoading(true);
  453. try {
  454. const saveData = { ...modelFormData } as ModelConfig;
  455. await onUpdateModels(editingId === 'new' ? 'create' : 'update', saveData);
  456. setEditingId(null);
  457. } catch (err: any) {
  458. setError(err.message || t('errorGeneric'));
  459. } finally {
  460. setIsLoading(false);
  461. }
  462. };
  463. const handleToggleModel = async (model: ModelConfig) => {
  464. if (currentUser?.role === 'TENANT_ADMIN') {
  465. const newEnabledIds = enabledModelIds.includes(model.id)
  466. ? enabledModelIds.filter(id => id !== model.id)
  467. : [...enabledModelIds, model.id];
  468. try {
  469. await fetch('/api/v1/admin/settings', {
  470. method: 'PUT',
  471. headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
  472. body: JSON.stringify({ enabledModelIds: newEnabledIds })
  473. });
  474. setEnabledModelIds(newEnabledIds);
  475. showSuccess('Settings updated successfully');
  476. } catch (error) {
  477. console.error(error);
  478. showError(t('errorGeneric'));
  479. }
  480. return;
  481. }
  482. try {
  483. await onUpdateModels('update', { ...model, isEnabled: !model.isEnabled });
  484. } catch (error) {
  485. console.error('Failed to toggle model:', error);
  486. }
  487. };
  488. const handleDeleteModel = async (id: string) => {
  489. if (await confirm(t('confirmClear'))) {
  490. await onUpdateModels('delete', { id } as ModelConfig);
  491. }
  492. };
  493. const getTypeLabel = (type: ModelType) => {
  494. switch (type) {
  495. case ModelType.LLM: return t('typeLLM');
  496. case ModelType.EMBEDDING: return t('typeEmbedding');
  497. case ModelType.RERANK: return t('typeRerank');
  498. }
  499. };
  500. // --- レンダリング関数 ---
  501. const renderGeneralTab = () => (
  502. <div className="space-y-8 animate-in slide-in-from-right duration-300 max-w-2xl">
  503. {/* パスワード変更セクション */}
  504. <section className="bg-white p-6 rounded-lg border border-slate-200 shadow-sm">
  505. <h3 className="text-sm font-medium text-slate-800 mb-4 flex items-center gap-2">
  506. <Key className="w-4 h-4 text-blue-500" />
  507. {t('changePassword')}
  508. </h3>
  509. <form onSubmit={handleChangePassword} className="space-y-4 max-w-sm">
  510. <div>
  511. <input
  512. type="password"
  513. placeholder={t('currentPassword')}
  514. value={passwordForm.current}
  515. onChange={e => setPasswordForm({ ...passwordForm, current: e.target.value })}
  516. className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none"
  517. required
  518. />
  519. </div>
  520. <div>
  521. <input
  522. type="password"
  523. placeholder={t('newPassword')}
  524. value={passwordForm.new}
  525. onChange={e => setPasswordForm({ ...passwordForm, new: e.target.value })}
  526. className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none"
  527. required
  528. />
  529. </div>
  530. <div>
  531. <input
  532. type="password"
  533. placeholder={t('confirmPassword')}
  534. value={passwordForm.confirm}
  535. onChange={e => setPasswordForm({ ...passwordForm, confirm: e.target.value })}
  536. className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none"
  537. required
  538. />
  539. </div>
  540. {passwordSuccess && <p className="text-xs text-green-600">{passwordSuccess}</p>}
  541. <button
  542. type="submit"
  543. disabled={isLoading}
  544. className="w-full py-2 bg-slate-800 text-white rounded-md text-sm hover:bg-slate-900 transition-colors disabled:opacity-50"
  545. >
  546. {isLoading ? <Loader2 className="w-4 h-4 animate-spin mx-auto" /> : t('confirmChange')}
  547. </button>
  548. </form>
  549. </section>
  550. {/* 语言设置セクション */}
  551. <section className="bg-white p-6 rounded-lg border border-slate-200 shadow-sm">
  552. <h3 className="text-sm font-medium text-slate-800 mb-4 flex items-center gap-2">
  553. <Globe className="w-4 h-4 text-blue-500" />
  554. {t('languageSettings')}
  555. </h3>
  556. <div className="space-y-4 max-w-sm">
  557. <div className="space-y-2">
  558. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">
  559. {t('switchLanguage')}
  560. </label>
  561. <select
  562. value={language}
  563. onChange={(e) => setLanguage(e.target.value as any)}
  564. className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none bg-white"
  565. >
  566. <option value="en">English</option>
  567. <option value="zh">中文 (Chinese)</option>
  568. <option value="ja">日本語 (Japanese)</option>
  569. </select>
  570. </div>
  571. </div>
  572. </section>
  573. </div >
  574. );
  575. const renderUserTab = () => (
  576. <div className="space-y-6 w-full">
  577. <div className="flex justify-between items-center mb-6">
  578. <div>
  579. <h3 className="text-sm font-black text-slate-500 uppercase tracking-[0.15em] mb-1">{t('userList')}</h3>
  580. <p className="text-xs text-slate-400 font-medium">{t('sidebarDesc')}</p>
  581. </div>
  582. <button
  583. onClick={() => setShowAddUser(!showAddUser)}
  584. className="flex items-center gap-2 px-6 py-2.5 bg-indigo-600 text-white text-sm font-bold rounded-2xl hover:bg-indigo-700 shadow-lg shadow-indigo-100 transition-all active:scale-95"
  585. >
  586. <Plus className="w-4 h-4" />
  587. {t('addUser')}
  588. </button>
  589. </div>
  590. {showAddUser && (
  591. <motion.form
  592. initial={{ opacity: 0, y: -10 }}
  593. animate={{ opacity: 1, y: 0 }}
  594. onSubmit={handleCreateUser}
  595. className="bg-white/80 backdrop-blur-md p-8 rounded-3xl border border-slate-200/50 shadow-xl mb-8 space-y-5"
  596. >
  597. <div className="grid grid-cols-1 md:grid-cols-2 gap-5">
  598. <input
  599. type="text"
  600. placeholder={t('usernamePlaceholder')}
  601. value={newUser.username}
  602. onChange={e => setNewUser({ ...newUser, username: e.target.value })}
  603. className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-2xl text-sm font-medium focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500/50 outline-none transition-all"
  604. required
  605. />
  606. <input
  607. type="password"
  608. placeholder={t('passwordPlaceholder')}
  609. value={newUser.password}
  610. onChange={e => setNewUser({ ...newUser, password: e.target.value })}
  611. className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-2xl text-sm font-medium focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500/50 outline-none transition-all"
  612. required
  613. />
  614. </div>
  615. <div className="flex items-center justify-between">
  616. <div className="flex items-center gap-3 bg-slate-50 px-4 py-2 rounded-xl border border-slate-200/50">
  617. <Shield className={`w-4 h-4 ${currentUser?.role === 'SUPER_ADMIN' ? 'text-red-500' : 'text-indigo-500'}`} />
  618. {currentUser?.role === 'SUPER_ADMIN' ? (
  619. <select
  620. className="bg-transparent text-xs font-bold text-slate-600 uppercase tracking-widest outline-none cursor-pointer"
  621. value={newUser.role}
  622. onChange={e => setNewUser({ ...newUser, role: e.target.value })}
  623. >
  624. <option value="TENANT_ADMIN">Tenant Admin</option>
  625. <option value="USER">Regular User</option>
  626. </select>
  627. ) : (
  628. <span className="text-xs font-bold text-slate-600 uppercase tracking-widest">
  629. Creating Regular User
  630. </span>
  631. )}
  632. </div>
  633. <div className="flex gap-3">
  634. <button type="button" onClick={() => setShowAddUser(false)} className="px-5 py-2 text-xs font-bold text-slate-500 hover:text-slate-700">{t('cancel')}</button>
  635. <button type="submit" className="px-8 py-2 bg-slate-900 text-white rounded-xl text-xs font-black uppercase tracking-widest hover:bg-indigo-600 transition-all shadow-lg shadow-slate-100">{t('create')}</button>
  636. </div>
  637. </div>
  638. </motion.form>
  639. )}
  640. {passwordChangeUserData && (
  641. <div className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm flex items-center justify-center z-[100] p-6">
  642. <motion.div
  643. initial={{ scale: 0.95, opacity: 0 }}
  644. animate={{ scale: 1, opacity: 1 }}
  645. className="bg-white rounded-3xl p-10 w-full max-w-md shadow-2xl border border-white/20"
  646. >
  647. <div className="flex items-center justify-between mb-8">
  648. <h3 className="text-xl font-black text-slate-900 tracking-tight">{t('changeUserPassword')}</h3>
  649. <button onClick={() => setPasswordChangeUserData(null)} className="p-2 hover:bg-slate-100 rounded-xl transition-all">
  650. <X size={20} className="text-slate-400" />
  651. </button>
  652. </div>
  653. <form onSubmit={(e) => { e.preventDefault(); handleUserPasswordChange(); }} className="space-y-6">
  654. <div>
  655. <label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 px-1">
  656. {t('newPassword')}
  657. </label>
  658. <input
  659. type="password"
  660. value={passwordChangeUserData.newPassword}
  661. onChange={(e) => setPasswordChangeUserData({ ...passwordChangeUserData, newPassword: e.target.value })}
  662. className="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl text-[14px] font-medium transition-all focus:outline-none focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500/50"
  663. placeholder={t('enterNewPassword')}
  664. required
  665. />
  666. </div>
  667. <div className="flex gap-4 pt-4">
  668. <button type="button" onClick={() => setPasswordChangeUserData(null)} className="flex-1 py-3.5 text-slate-500 font-bold text-sm">{t('cancel')}</button>
  669. <button type="submit" className="flex-1 py-3.5 bg-slate-900 text-white rounded-2xl font-black uppercase tracking-widest text-xs hover:bg-indigo-600 shadow-xl shadow-slate-100 transition-all">{t('confirmChange')}</button>
  670. </div>
  671. </form>
  672. </motion.div>
  673. </div>
  674. )}
  675. {/* Edit Role Modal */}
  676. {roleChangeUserData && (
  677. <div className="fixed inset-0 z-[110] flex items-center justify-center bg-slate-900/40 backdrop-blur-sm p-4">
  678. <motion.div
  679. initial={{ scale: 0.95, opacity: 0 }}
  680. animate={{ scale: 1, opacity: 1 }}
  681. className="bg-white rounded-3xl p-10 w-full max-w-md shadow-2xl border border-white/20"
  682. >
  683. <div className="flex items-center justify-between mb-8">
  684. <h3 className="text-xl font-black text-slate-900 tracking-tight">Edit User Role</h3>
  685. <button onClick={() => setRoleChangeUserData(null)} className="p-2 hover:bg-slate-100 rounded-xl transition-all">
  686. <X size={20} className="text-slate-400" />
  687. </button>
  688. </div>
  689. <form onSubmit={(e) => { e.preventDefault(); handleUserRoleChange(); }} className="space-y-6">
  690. <div>
  691. <label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 px-1">
  692. Target Role
  693. </label>
  694. <select
  695. value={roleChangeUserData.newRole}
  696. onChange={(e) => setRoleChangeUserData({ ...roleChangeUserData, newRole: e.target.value })}
  697. className="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl text-[14px] font-medium transition-all focus:outline-none focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500/50"
  698. >
  699. <option value="TENANT_ADMIN">Tenant Admin</option>
  700. <option value="USER">Regular User</option>
  701. </select>
  702. </div>
  703. <div className="flex gap-4 pt-4">
  704. <button type="button" onClick={() => setRoleChangeUserData(null)} className="flex-1 py-3.5 text-slate-500 font-bold text-sm">{t('cancel')}</button>
  705. <button type="submit" className="flex-1 py-3.5 bg-slate-900 text-white rounded-2xl font-black uppercase tracking-widest text-xs hover:bg-indigo-600 shadow-xl shadow-slate-100 transition-all">{t('confirmChange')}</button>
  706. </div>
  707. </form>
  708. </motion.div>
  709. </div>
  710. )}
  711. <div className="grid grid-cols-1 gap-4 overflow-y-auto pr-2 scrollbar-hide">
  712. <AnimatePresence>
  713. {users.map((user, index) => {
  714. let IconComponent = User;
  715. let iconColors = 'bg-slate-50 text-slate-400';
  716. if (user.role === 'SUPER_ADMIN') {
  717. IconComponent = Shield;
  718. iconColors = 'bg-red-50 text-red-600';
  719. } else if (user.isAdmin || user.role === 'TENANT_ADMIN') {
  720. IconComponent = Shield;
  721. iconColors = 'bg-indigo-50 text-indigo-600';
  722. }
  723. return (
  724. <motion.div
  725. key={user.id}
  726. initial={{ opacity: 0, x: -10 }}
  727. animate={{ opacity: 1, x: 0 }}
  728. transition={{ delay: index * 0.05 }}
  729. className="flex items-center justify-between p-5 bg-white/70 backdrop-blur-md border border-slate-200/50 rounded-2xl shadow-sm hover:shadow-md hover:border-indigo-200/50 transition-all group"
  730. >
  731. <div className="flex items-center gap-4 flex-1 min-w-0">
  732. <div className={`w-12 h-12 rounded-xl flex items-center justify-center transition-all flex-shrink-0 ${iconColors}`}>
  733. <IconComponent size={22} />
  734. </div>
  735. <div className="flex-1 min-w-0">
  736. <div className="flex items-center gap-2 flex-wrap">
  737. <p className="font-black text-slate-900">{user.username}</p>
  738. {(user.role === 'SUPER_ADMIN' || user.isAdmin) && <span className={`text-[9px] font-black px-1.5 py-0.5 rounded-md uppercase tracking-wider ${user.role === 'SUPER_ADMIN' ? 'bg-red-100 text-red-700' : 'bg-indigo-100 text-indigo-700'}`}>{user.role === 'SUPER_ADMIN' ? 'SUPER' : 'ADMIN'}</span>}
  739. </div>
  740. <p className="text-[10px] font-bold text-slate-400 mt-0.5 uppercase tracking-widest">{t('createdAt')}: {new Date(user.createdAt).toLocaleDateString()}</p>
  741. {user.tenantMembers && user.tenantMembers.length > 0 && (
  742. <div className="flex flex-wrap gap-1 mt-1.5">
  743. {user.tenantMembers
  744. .filter((m: any) => m.tenant?.name !== 'Default')
  745. .map((m: any) => (
  746. <span
  747. key={m.tenantId}
  748. className="inline-flex items-center gap-1 px-2 py-0.5 bg-emerald-50 text-emerald-700 text-[9px] font-black rounded-md uppercase tracking-wider border border-emerald-100"
  749. >
  750. <Building2 size={8} />
  751. {m.tenant?.name || m.tenantId}
  752. </span>
  753. ))}
  754. </div>
  755. )}
  756. </div>
  757. </div>
  758. <div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-all">
  759. {user.username !== 'admin' && (
  760. <>
  761. {currentUser?.role === 'SUPER_ADMIN' && user.role !== 'SUPER_ADMIN' && (
  762. <button
  763. onClick={(e) => {
  764. e.preventDefault();
  765. e.stopPropagation();
  766. setRoleChangeUserData({ userId: user.id, newRole: user.role && user.role !== 'USER' ? user.role : (user.isAdmin ? 'TENANT_ADMIN' : 'USER') });
  767. }}
  768. className="p-2.5 rounded-xl text-slate-400 hover:text-indigo-600 hover:bg-indigo-50 transition-all cursor-pointer relative z-10"
  769. title="Edit Role"
  770. >
  771. <Edit2 className="w-4.5 h-4.5" />
  772. </button>
  773. )}
  774. <button
  775. onClick={() => setPasswordChangeUserData({ userId: user.id, newPassword: '' })}
  776. className="p-2.5 rounded-xl text-slate-400 hover:text-indigo-600 hover:bg-indigo-50 transition-all"
  777. title={t('changeUserPassword')}
  778. >
  779. <Key className="w-4.5 h-4.5" />
  780. </button>
  781. {user.id !== currentUser?.id && (
  782. <button
  783. onClick={() => handleDeleteUser(user.id)}
  784. className="p-2.5 rounded-xl text-slate-400 hover:text-red-500 hover:bg-red-50 transition-all"
  785. title={t('deleteUser')}
  786. >
  787. <Trash2 className="w-4.5 h-4.5" />
  788. </button>
  789. )}
  790. </>
  791. )}
  792. </div>
  793. </motion.div>
  794. );
  795. })}
  796. </AnimatePresence>
  797. </div>
  798. </div>
  799. );
  800. const renderTenantsTab = () => (
  801. <div className="w-full space-y-8 animate-in fade-in duration-500">
  802. <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
  803. <div className="p-6 bg-white border border-slate-200 rounded-3xl shadow-sm">
  804. <div className="flex items-center gap-3 mb-2 text-blue-600">
  805. <Building2 size={20} />
  806. <span className="text-xs font-black uppercase tracking-widest">Total Tenants</span>
  807. </div>
  808. <p className="text-3xl font-black text-slate-900">{stats.tenants}</p>
  809. </div>
  810. <div className="p-6 bg-white border border-slate-200 rounded-3xl shadow-sm">
  811. <div className="flex items-center gap-3 mb-2 text-indigo-600">
  812. <User size={20} />
  813. <span className="text-xs font-black uppercase tracking-widest">System Users</span>
  814. </div>
  815. <p className="text-3xl font-black text-slate-900">{stats.users}</p>
  816. </div>
  817. <div className="p-6 bg-emerald-50 border border-emerald-100 rounded-3xl shadow-sm">
  818. <div className="flex items-center gap-3 mb-2 text-emerald-600">
  819. <Sparkles size={20} />
  820. <span className="text-xs font-black uppercase tracking-widest">System Health</span>
  821. </div>
  822. <p className="text-xl font-bold text-emerald-700">Operational</p>
  823. </div>
  824. </div>
  825. <div className="bg-white border border-slate-200 rounded-3xl shadow-xl overflow-hidden">
  826. <div className="p-6 border-b border-slate-100 flex items-center justify-between">
  827. <div>
  828. <h3 className="font-black text-slate-900 text-lg tracking-tight">Organization Management</h3>
  829. <p className="text-xs font-medium text-slate-400">Global tenant list and control</p>
  830. </div>
  831. <button onClick={() => setShowCreateTenant(true)} className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white text-xs font-bold rounded-xl hover:bg-slate-700 transition-all">
  832. <Plus size={16} /> New Tenant
  833. </button>
  834. </div>
  835. <div className="overflow-x-auto">
  836. <table className="w-full text-left">
  837. <thead className="bg-slate-50/50 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-100">
  838. <tr>
  839. <th className="px-6 py-4">Name</th>
  840. <th className="px-6 py-4">Domain</th>
  841. <th className="px-6 py-4">Admin</th>
  842. <th className="px-6 py-4">Members</th>
  843. <th className="px-6 py-4">Features</th>
  844. <th className="px-6 py-4">Created</th>
  845. <th className="px-6 py-4 text-right">Actions</th>
  846. </tr>
  847. </thead>
  848. <tbody className="divide-y divide-slate-100">
  849. {tenants.map(t => (
  850. <tr key={t.id} className="hover:bg-slate-50/50 transition-colors group">
  851. <td className="px-6 py-4 text-sm font-bold text-slate-900">{t.name}</td>
  852. <td className="px-6 py-4 text-xs font-mono text-slate-500">{t.domain || '-'}</td>
  853. <td className="px-6 py-4 text-xs">
  854. {(() => {
  855. // Search admin in tenant members - the source of truth for multi-tenancy
  856. const adminMember = t.members?.find((m: any) => m.role === 'TENANT_ADMIN' || m.role === 'SUPER_ADMIN');
  857. const adminUser = adminMember?.user;
  858. if (adminUser) {
  859. return (
  860. <div className="flex items-center gap-1.5 text-slate-600 font-medium">
  861. <div className="w-5 h-5 rounded-full bg-indigo-50 flex items-center justify-center">
  862. <User className="w-3 h-3 text-indigo-500" />
  863. </div>
  864. {adminUser.username}
  865. {t.name !== 'Default' && (
  866. <button
  867. onClick={() => {
  868. setBindingTenantId(t.id);
  869. setUserSearchQuery('');
  870. fetchUsers();
  871. }}
  872. className="ml-2 p-1 hover:bg-indigo-50 text-slate-400 hover:text-indigo-600 rounded transition-all"
  873. title="Change Admin"
  874. >
  875. <Edit2 size={12} />
  876. </button>
  877. )}
  878. </div>
  879. );
  880. } else {
  881. return (
  882. <div className="flex items-center gap-2">
  883. {t.name !== 'Default' ? (
  884. <button
  885. onClick={() => {
  886. setBindingTenantId(t.id);
  887. setUserSearchQuery('');
  888. fetchUsers();
  889. }}
  890. className="px-3 py-1.5 bg-indigo-50 text-indigo-600 hover:bg-indigo-100 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all"
  891. >
  892. Bind Admin
  893. </button>
  894. ) : (
  895. <span className="text-[10px] text-slate-400 italic">System Restricted</span>
  896. )}
  897. <span className="text-[10px] text-slate-400 italic">None</span>
  898. </div>
  899. );
  900. }
  901. })()}
  902. </td>
  903. <td className="px-6 py-4">
  904. <div className="flex items-center gap-2">
  905. <div className={`p-1.5 rounded-lg ${t.settings_obj?.isNotebookEnabled ? 'bg-indigo-50 text-indigo-600' : 'bg-slate-50 text-slate-400'}`} title="Notebook Feature">
  906. <BookOpen size={14} />
  907. </div>
  908. {t.name !== 'Default' ? (
  909. <button
  910. onClick={() => handleToggleNotebookFeature(t.id, t.settings_obj?.isNotebookEnabled !== false)}
  911. className="text-[10px] font-black uppercase tracking-widest text-indigo-600 hover:underline"
  912. >
  913. {t.settings_obj?.isNotebookEnabled !== false ? 'Enabled' : 'Disabled'}
  914. </button>
  915. ) : (
  916. <span className="text-[10px] font-black uppercase tracking-widest text-slate-400">Fixed</span>
  917. )}
  918. </div>
  919. </td>
  920. <td className="px-6 py-4 text-xs text-slate-400">{new Date(t.createdAt).toLocaleDateString()}</td>
  921. <td className="px-6 py-4 text-xs">
  922. {/* Members count + manage button */}
  923. <div className="flex items-center gap-2">
  924. <span className="font-bold text-slate-700">
  925. {(t.members || []).filter((m: any) => m.role !== 'SUPER_ADMIN').length}
  926. </span>
  927. <span className="text-slate-400">users</span>
  928. {t.name !== 'Default' && (
  929. <button
  930. onClick={() => {
  931. setManagingMembersTenantId(t.id);
  932. setMemberUserSearch('');
  933. fetchTenantMembers(t.id);
  934. fetchUsers();
  935. }}
  936. className="ml-1 px-2.5 py-1 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all"
  937. >
  938. Manage
  939. </button>
  940. )}
  941. </div>
  942. </td>
  943. <td className="px-6 py-4 text-right">
  944. {t.name !== 'Default' && (
  945. <div className="flex justify-end gap-2 opacity-0 group-hover:opacity-100 transition-all">
  946. <button
  947. onClick={() => {
  948. setEditingTenant(t);
  949. setNewTenant({ name: t.name, domain: t.domain || '', adminUserId: '' });
  950. }}
  951. className="p-1.5 hover:bg-slate-100 text-slate-400 hover:text-indigo-600 rounded-lg transition-all"
  952. title="Edit Tenant"
  953. >
  954. <Edit2 size={14} />
  955. </button>
  956. <button
  957. onClick={() => handleDeleteTenant(t.id)}
  958. className="p-1.5 hover:bg-red-50 text-slate-400 hover:text-red-500 rounded-lg transition-all"
  959. title="Delete Tenant"
  960. >
  961. <Trash2 size={14} />
  962. </button>
  963. </div>
  964. )}
  965. </td>
  966. </tr>
  967. ))}
  968. </tbody>
  969. </table>
  970. </div>
  971. </div>
  972. {(showCreateTenant || editingTenant) && (
  973. <div className="fixed inset-0 z-[120] bg-slate-900/40 backdrop-blur-sm flex items-center justify-center p-4">
  974. <motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} className="bg-white rounded-3xl p-8 w-full max-w-md shadow-2xl">
  975. <h3 className="text-xl font-black text-slate-900 mb-6">{editingTenant ? 'Edit Organization' : 'Create New Tenant'}</h3>
  976. <form onSubmit={handleCreateTenant} className="space-y-5">
  977. <input className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-2xl text-sm" placeholder="Tenant Name" value={newTenant.name} onChange={e => setNewTenant({ ...newTenant, name: e.target.value })} required />
  978. <input className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-2xl text-sm" placeholder="Domain (optional)" value={newTenant.domain} onChange={e => setNewTenant({ ...newTenant, domain: e.target.value })} />
  979. {!editingTenant && (
  980. <div className="space-y-2">
  981. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">Assign Initial Admin</label>
  982. <select
  983. className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-2xl text-sm outline-none focus:ring-2 focus:ring-indigo-500/20 transition-all"
  984. value={newTenant.adminUserId}
  985. onChange={e => setNewTenant({ ...newTenant, adminUserId: e.target.value })}
  986. >
  987. <option value="">Select a user (optional)</option>
  988. {users.filter(u => u.role === 'TENANT_ADMIN').map(u => (
  989. <option key={u.id} value={u.id}>{u.username}</option>
  990. ))}
  991. </select>
  992. <p className="text-[10px] text-slate-400 px-1 italic">Selecting a user will promote them to Tenant Admin for this organization.</p>
  993. </div>
  994. )}
  995. <div className="flex gap-4 pt-2">
  996. <button type="button" onClick={() => { setShowCreateTenant(false); setEditingTenant(null); }} className="flex-1 py-3 text-slate-500 font-bold text-sm">Cancel</button>
  997. <button type="submit" className="flex-1 py-3 bg-slate-900 text-white rounded-2xl font-black uppercase tracking-widest text-xs hover:bg-indigo-600">{editingTenant ? 'Save Changes' : 'Create'}</button>
  998. </div>
  999. </form>
  1000. </motion.div>
  1001. </div>
  1002. )}
  1003. {/* Bind Admin Search Modal */}
  1004. <AnimatePresence>
  1005. {bindingTenantId && (
  1006. <div className="fixed inset-0 z-[130] bg-slate-900/60 backdrop-blur-md flex items-center justify-center p-4">
  1007. <motion.div
  1008. initial={{ scale: 0.9, opacity: 0 }}
  1009. animate={{ scale: 1, opacity: 1 }}
  1010. exit={{ scale: 0.9, opacity: 0 }}
  1011. className="bg-white rounded-[2.5rem] p-8 w-full max-w-lg shadow-2xl border border-white/20 overflow-hidden relative"
  1012. >
  1013. <div className="absolute top-0 right-0 p-6">
  1014. <button onClick={() => setBindingTenantId(null)} className="p-2 hover:bg-slate-100 rounded-2xl transition-all">
  1015. <X size={20} className="text-slate-400" />
  1016. </button>
  1017. </div>
  1018. <div className="mb-8">
  1019. <h3 className="text-2xl font-black text-slate-900 tracking-tight mb-2">Bind Tenant Admin</h3>
  1020. <p className="text-sm text-slate-500 font-medium">Search and select a user to manage <span className="text-indigo-600 font-bold">{tenants.find(t => t.id === bindingTenantId)?.name}</span></p>
  1021. </div>
  1022. <div className="relative mb-6">
  1023. <div className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400">
  1024. <Globe size={18} />
  1025. </div>
  1026. <input
  1027. autoFocus
  1028. className="w-full pl-12 pr-4 py-4 bg-slate-50 border border-slate-200 rounded-3xl text-sm font-medium focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500/50 outline-none transition-all"
  1029. placeholder="Search users by name..."
  1030. value={userSearchQuery}
  1031. onChange={e => setUserSearchQuery(e.target.value)}
  1032. />
  1033. </div>
  1034. <div className="max-h-[350px] overflow-y-auto pr-2 flex flex-col gap-3 scrollbar-hide">
  1035. {users
  1036. .filter(u =>
  1037. (u.role === 'TENANT_ADMIN' || u.isAdmin) &&
  1038. u.role !== 'SUPER_ADMIN' &&
  1039. u.username.toLowerCase().includes(userSearchQuery.toLowerCase())
  1040. )
  1041. .map(u => (
  1042. <button
  1043. key={u.id}
  1044. onClick={async () => {
  1045. const success = await handleBindAdmin(bindingTenantId, u.id);
  1046. if (success) setBindingTenantId(null);
  1047. }}
  1048. className="w-full flex items-center justify-between p-4 bg-slate-50/50 hover:bg-indigo-50 border border-slate-200/50 hover:border-indigo-200 rounded-[1.5rem] transition-all group"
  1049. >
  1050. <div className="flex items-center gap-3">
  1051. <div className="w-10 h-10 rounded-xl bg-white flex items-center justify-center text-slate-400 group-hover:text-indigo-600 group-hover:shadow-sm transition-all">
  1052. <User size={18} />
  1053. </div>
  1054. <div className="text-left">
  1055. <p className="text-sm font-black text-slate-900">{u.username}</p>
  1056. <p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{u.role || 'User'}</p>
  1057. </div>
  1058. </div>
  1059. <ChevronRight size={16} className="text-slate-300 group-hover:text-indigo-400 group-hover:translate-x-1 transition-all" />
  1060. </button>
  1061. ))}
  1062. {users.filter(u =>
  1063. (u.role === 'TENANT_ADMIN' || u.isAdmin) &&
  1064. u.role !== 'SUPER_ADMIN' &&
  1065. u.username.toLowerCase().includes(userSearchQuery.toLowerCase())
  1066. ).length === 0 && (
  1067. <div className="py-12 text-center opacity-40">
  1068. <User size={40} className="mx-auto mb-3" />
  1069. <p className="text-xs font-bold uppercase tracking-widest">No unassigned users found</p>
  1070. </div>
  1071. )}
  1072. </div>
  1073. <div className="mt-8 pt-6 border-t border-slate-100 flex justify-end">
  1074. <button onClick={() => setBindingTenantId(null)} className="px-6 py-3 text-sm font-bold text-slate-500 hover:text-slate-700 transition-all">
  1075. Cancel
  1076. </button>
  1077. </div>
  1078. </motion.div>
  1079. </div>
  1080. )}
  1081. </AnimatePresence>
  1082. {/* Manage Members Modal */}
  1083. <AnimatePresence>
  1084. {managingMembersTenantId && (() => {
  1085. const tenant = tenants.find(t => t.id === managingMembersTenantId);
  1086. const memberUserIds = new Set(tenantMembers.map((m: any) => m.userId));
  1087. const availableUsers = users.filter(u =>
  1088. !memberUserIds.has(u.id) &&
  1089. u.role !== 'SUPER_ADMIN' &&
  1090. u.username.toLowerCase().includes(memberUserSearch.toLowerCase())
  1091. );
  1092. return (
  1093. <div className="fixed inset-0 z-[140] bg-slate-900/60 backdrop-blur-md flex items-center justify-center p-4">
  1094. <motion.div
  1095. initial={{ scale: 0.9, opacity: 0 }}
  1096. animate={{ scale: 1, opacity: 1 }}
  1097. exit={{ scale: 0.9, opacity: 0 }}
  1098. className="bg-white rounded-[2.5rem] p-8 w-full max-w-2xl shadow-2xl border border-white/20 overflow-hidden relative max-h-[90vh] flex flex-col"
  1099. >
  1100. <div className="flex items-start justify-between mb-6">
  1101. <div>
  1102. <h3 className="text-2xl font-black text-slate-900 tracking-tight mb-1">Manage Members</h3>
  1103. <p className="text-sm text-slate-500 font-medium">
  1104. Organization: <span className="text-emerald-600 font-bold">{tenant?.name}</span>
  1105. </p>
  1106. </div>
  1107. <button
  1108. onClick={() => { setManagingMembersTenantId(null); setTenantMembers([]); }}
  1109. className="p-2 hover:bg-slate-100 rounded-2xl transition-all"
  1110. >
  1111. <X size={20} className="text-slate-400" />
  1112. </button>
  1113. </div>
  1114. <div className="flex-1 overflow-y-auto flex flex-col gap-6 pr-1 scrollbar-hide">
  1115. {/* Current Members Section */}
  1116. <div>
  1117. <h4 className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-3">
  1118. Current Members ({tenantMembers.length})
  1119. </h4>
  1120. {isMembersLoading ? (
  1121. <div className="flex items-center justify-center py-8 opacity-40">
  1122. <Loader2 size={24} className="animate-spin" />
  1123. </div>
  1124. ) : tenantMembers.length === 0 ? (
  1125. <div className="py-6 text-center bg-slate-50 rounded-2xl opacity-50">
  1126. <User size={28} className="mx-auto mb-2 text-slate-400" />
  1127. <p className="text-xs font-bold uppercase tracking-widest text-slate-400">No members yet</p>
  1128. </div>
  1129. ) : (
  1130. <div className="space-y-2">
  1131. {tenantMembers.map((m: any) => (
  1132. <div key={m.id || m.userId} className="flex items-center justify-between p-4 bg-slate-50 rounded-2xl border border-slate-200/60">
  1133. <div className="flex items-center gap-3">
  1134. <div className="w-9 h-9 rounded-xl bg-white border border-slate-200 flex items-center justify-center">
  1135. <User size={16} className="text-slate-400" />
  1136. </div>
  1137. <div>
  1138. <p className="text-sm font-black text-slate-900">{m.user?.username || m.userId}</p>
  1139. <p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{m.role}</p>
  1140. </div>
  1141. </div>
  1142. <button
  1143. onClick={() => handleRemoveMember(managingMembersTenantId, m.userId)}
  1144. className="p-2 hover:bg-red-50 text-slate-400 hover:text-red-500 rounded-xl transition-all"
  1145. title="Remove member"
  1146. >
  1147. <Trash2 size={14} />
  1148. </button>
  1149. </div>
  1150. ))}
  1151. </div>
  1152. )}
  1153. </div>
  1154. {/* Add Users Section */}
  1155. <div>
  1156. <h4 className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-3">Add Users</h4>
  1157. <div className="relative mb-3">
  1158. <div className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400">
  1159. <User size={16} />
  1160. </div>
  1161. <input
  1162. className="w-full pl-10 pr-4 py-3 bg-slate-50 border border-slate-200 rounded-2xl text-sm font-medium focus:ring-4 focus:ring-emerald-500/10 focus:border-emerald-500/50 outline-none transition-all"
  1163. placeholder="Search users by name..."
  1164. value={memberUserSearch}
  1165. onChange={e => setMemberUserSearch(e.target.value)}
  1166. />
  1167. </div>
  1168. {availableUsers.length === 0 ? (
  1169. <div className="py-6 text-center bg-slate-50 rounded-2xl opacity-50">
  1170. <p className="text-xs font-bold uppercase tracking-widest text-slate-400">
  1171. {memberUserSearch ? 'No matching users' : 'All users already added'}
  1172. </p>
  1173. </div>
  1174. ) : (
  1175. <div className="space-y-2 max-h-[220px] overflow-y-auto scrollbar-hide">
  1176. {availableUsers.map(u => (
  1177. <button
  1178. key={u.id}
  1179. onClick={() => handleAddMember(managingMembersTenantId, u.id)}
  1180. className="w-full flex items-center justify-between p-4 bg-slate-50/50 hover:bg-emerald-50 border border-slate-200/50 hover:border-emerald-200 rounded-2xl transition-all group"
  1181. >
  1182. <div className="flex items-center gap-3">
  1183. <div className="w-9 h-9 rounded-xl bg-white border border-slate-200 flex items-center justify-center group-hover:text-emerald-600 transition-all">
  1184. <User size={16} className="text-slate-400 group-hover:text-emerald-600" />
  1185. </div>
  1186. <div className="text-left">
  1187. <p className="text-sm font-black text-slate-900">{u.username}</p>
  1188. <p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{u.role || 'User'}</p>
  1189. </div>
  1190. </div>
  1191. <div className="flex items-center gap-1.5 px-3 py-1 bg-emerald-50 group-hover:bg-emerald-100 text-emerald-700 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all">
  1192. <Plus size={12} />
  1193. Add
  1194. </div>
  1195. </button>
  1196. ))}
  1197. </div>
  1198. )}
  1199. </div>
  1200. </div>
  1201. <div className="pt-6 border-t border-slate-100 flex justify-end mt-4">
  1202. <button
  1203. onClick={() => { setManagingMembersTenantId(null); setTenantMembers([]); }}
  1204. className="px-6 py-3 text-sm font-bold text-slate-500 hover:text-slate-700 transition-all"
  1205. >
  1206. Done
  1207. </button>
  1208. </div>
  1209. </motion.div>
  1210. </div>
  1211. );
  1212. })()}
  1213. </AnimatePresence>
  1214. </div>
  1215. );
  1216. const renderKnowledgeBaseTab = () => (
  1217. <div className="space-y-8 animate-in slide-in-from-right duration-300 w-full max-w-5xl pb-10">
  1218. {localKbSettings && (
  1219. <>
  1220. {/* Save/Cancel Bar */}
  1221. <div className="flex justify-end gap-3 sticky top-0 z-20 py-4 bg-white/50 backdrop-blur-sm border-b border-slate-100 mb-6">
  1222. <button
  1223. onClick={handleCancelKbSettings}
  1224. disabled={isSavingKbSettings || JSON.stringify(localKbSettings) === JSON.stringify(kbSettings)}
  1225. className="px-6 py-2 text-sm font-bold text-slate-500 hover:text-slate-700 disabled:opacity-30"
  1226. >
  1227. Cancel
  1228. </button>
  1229. <button
  1230. onClick={handleSaveKbSettings}
  1231. disabled={isSavingKbSettings || JSON.stringify(localKbSettings) === JSON.stringify(kbSettings)}
  1232. className="flex items-center gap-2 px-8 py-2 bg-indigo-600 text-white text-sm font-black uppercase tracking-widest rounded-2xl hover:bg-indigo-700 shadow-lg shadow-indigo-100 transition-all active:scale-95 disabled:opacity-50"
  1233. >
  1234. {isSavingKbSettings ? <Loader2 size={16} className="animate-spin" /> : <Save size={16} />}
  1235. Save Changes
  1236. </button>
  1237. </div>
  1238. {/* Model Configuration */}
  1239. <section className="bg-white/80 backdrop-blur-md p-8 rounded-3xl border border-slate-200/50 shadow-sm space-y-6">
  1240. <div className="flex items-center gap-3 text-slate-900 font-black uppercase tracking-widest text-[11px] border-b border-slate-100 pb-4">
  1241. <div className="w-8 h-8 rounded-xl bg-indigo-50 flex items-center justify-center text-indigo-600">
  1242. <Cpu size={16} />
  1243. </div>
  1244. Model Configuration
  1245. </div>
  1246. <div className="grid grid-cols-1 gap-6">
  1247. <div>
  1248. <label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 px-1">Default LLM Model</label>
  1249. <select
  1250. value={localKbSettings.selectedLLMId || ''}
  1251. onChange={(e) => handleUpdateKbSettings('selectedLLMId', e.target.value)}
  1252. className="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl text-sm font-medium outline-none focus:ring-4 focus:ring-indigo-500/10 transition-all cursor-pointer appearance-none"
  1253. >
  1254. <option value="">Select LLM...</option>
  1255. {models.filter(m => m.type === ModelType.LLM).map(m => (
  1256. <option key={m.id} value={m.id}>{m.name} ({m.modelId})</option>
  1257. ))}
  1258. </select>
  1259. </div>
  1260. <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
  1261. <div>
  1262. <label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 px-1">Embedding Model</label>
  1263. <select
  1264. value={localKbSettings.selectedEmbeddingId || ''}
  1265. onChange={(e) => handleUpdateKbSettings('selectedEmbeddingId', e.target.value)}
  1266. className="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl text-sm font-medium outline-none focus:ring-4 focus:ring-indigo-500/10 transition-all"
  1267. >
  1268. <option value="">Select Embedding...</option>
  1269. {models.filter(m => m.type === ModelType.EMBEDDING).map(m => (
  1270. <option key={m.id} value={m.id}>{m.name}</option>
  1271. ))}
  1272. </select>
  1273. </div>
  1274. <div>
  1275. <label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 px-1">Rerank Model</label>
  1276. <select
  1277. value={localKbSettings.selectedRerankId || ''}
  1278. onChange={(e) => handleUpdateKbSettings('selectedRerankId', e.target.value)}
  1279. className="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl text-sm font-medium outline-none focus:ring-4 focus:ring-indigo-500/10 transition-all"
  1280. >
  1281. <option value="">None</option>
  1282. {models.filter(m => m.type === ModelType.RERANK).map(m => (
  1283. <option key={m.id} value={m.id}>{m.name}</option>
  1284. ))}
  1285. </select>
  1286. </div>
  1287. </div>
  1288. </div>
  1289. </section>
  1290. {/* Indexing & Chunking Configuration */}
  1291. <section className="bg-white/80 backdrop-blur-md p-8 rounded-3xl border border-slate-200/50 shadow-sm space-y-6">
  1292. <div className="flex items-center gap-3 text-slate-900 font-black uppercase tracking-widest text-[11px] border-b border-slate-100 pb-4">
  1293. <div className="w-8 h-8 rounded-xl bg-orange-50 flex items-center justify-center text-orange-600">
  1294. <BookOpen size={16} />
  1295. </div>
  1296. Indexing & Chunking Configuration
  1297. </div>
  1298. <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
  1299. <div>
  1300. <div className="flex justify-between mb-3 px-1">
  1301. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Chunk Size (Tokens)</label>
  1302. <span className="text-sm font-black text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded-lg">{localKbSettings.chunkSize || 1000}</span>
  1303. </div>
  1304. <input
  1305. type="range"
  1306. min="100"
  1307. max="8192"
  1308. step="100"
  1309. value={localKbSettings.chunkSize || 1000}
  1310. onChange={(e) => handleUpdateKbSettings('chunkSize', parseInt(e.target.value))}
  1311. className="w-full h-2 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-indigo-600"
  1312. />
  1313. </div>
  1314. <div>
  1315. <div className="flex justify-between mb-3 px-1">
  1316. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Chunk Overlap</label>
  1317. <span className="text-sm font-black text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded-lg">{localKbSettings.chunkOverlap || 100}</span>
  1318. </div>
  1319. <input
  1320. type="range"
  1321. min="0"
  1322. max="2048"
  1323. step="10"
  1324. value={localKbSettings.chunkOverlap || 100}
  1325. onChange={(e) => handleUpdateKbSettings('chunkOverlap', parseInt(e.target.value))}
  1326. className="w-full h-2 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-indigo-600"
  1327. />
  1328. </div>
  1329. </div>
  1330. </section>
  1331. {/* Chat Hyperparameters */}
  1332. <section className="bg-white/80 backdrop-blur-md p-8 rounded-3xl border border-slate-200/50 shadow-sm space-y-6">
  1333. <div className="flex items-center gap-3 text-slate-900 font-black uppercase tracking-widest text-[11px] border-b border-slate-100 pb-4">
  1334. <div className="w-8 h-8 rounded-xl bg-pink-50 flex items-center justify-center text-pink-600">
  1335. <Sparkles size={16} />
  1336. </div>
  1337. Chat Hyperparameters
  1338. </div>
  1339. <div className="space-y-8">
  1340. <div>
  1341. <div className="flex justify-between mb-3 px-1">
  1342. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Temperature</label>
  1343. <span className="text-sm font-black text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded-lg">{localKbSettings.temperature}</span>
  1344. </div>
  1345. <input
  1346. type="range"
  1347. min="0"
  1348. max="1"
  1349. step="0.1"
  1350. value={localKbSettings.temperature || 0.7}
  1351. onChange={(e) => handleUpdateKbSettings('temperature', parseFloat(e.target.value))}
  1352. className="w-full h-2 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-indigo-600"
  1353. />
  1354. <div className="flex justify-between mt-2 px-1 text-[9px] font-bold text-slate-300 uppercase tracking-tighter">
  1355. <span>Precise</span>
  1356. <span>Creative</span>
  1357. </div>
  1358. </div>
  1359. <div>
  1360. <label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 px-1">Max Response Tokens</label>
  1361. <input
  1362. type="number"
  1363. value={localKbSettings.maxTokens || 2000}
  1364. onChange={(e) => handleUpdateKbSettings('maxTokens', parseInt(e.target.value))}
  1365. className="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl text-sm font-medium outline-none focus:ring-4 focus:ring-indigo-500/10 transition-all"
  1366. />
  1367. </div>
  1368. </div>
  1369. </section>
  1370. {/* Retrieval & Search Settings */}
  1371. <section className="bg-white/80 backdrop-blur-md p-8 rounded-3xl border border-slate-200/50 shadow-sm space-y-6">
  1372. <div className="flex items-center gap-3 text-slate-900 font-black uppercase tracking-widest text-[11px] border-b border-slate-100 pb-4">
  1373. <div className="w-8 h-8 rounded-xl bg-emerald-50 flex items-center justify-center text-emerald-600">
  1374. <Database size={16} />
  1375. </div>
  1376. Retrieval & Search Settings
  1377. </div>
  1378. <div className="space-y-8">
  1379. <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
  1380. <div>
  1381. <div className="flex justify-between mb-3 px-1">
  1382. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Top-K Results</label>
  1383. <span className="text-sm font-black text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded-lg">{localKbSettings.topK}</span>
  1384. </div>
  1385. <input
  1386. type="range"
  1387. min="1"
  1388. max="50"
  1389. step="1"
  1390. value={localKbSettings.topK || 10}
  1391. onChange={(e) => handleUpdateKbSettings('topK', parseInt(e.target.value))}
  1392. className="w-full h-2 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-indigo-600"
  1393. />
  1394. </div>
  1395. <div>
  1396. <div className="flex justify-between mb-3 px-1">
  1397. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Similarity Threshold</label>
  1398. <span className="text-sm font-black text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded-lg">{localKbSettings.similarityThreshold}</span>
  1399. </div>
  1400. <input
  1401. type="range"
  1402. min="0"
  1403. max="1"
  1404. step="0.05"
  1405. value={localKbSettings.similarityThreshold || 0.5}
  1406. onChange={(e) => handleUpdateKbSettings('similarityThreshold', parseFloat(e.target.value))}
  1407. className="w-full h-2 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-indigo-600"
  1408. />
  1409. </div>
  1410. </div>
  1411. <div className="space-y-4 pt-4 border-t border-slate-100">
  1412. <div className="flex items-center justify-between p-5 bg-slate-50/50 rounded-2xl border border-slate-200/30 transition-all hover:bg-white hover:border-indigo-100">
  1413. <div>
  1414. <div className="text-sm font-bold text-slate-800">Enable Hybrid Search</div>
  1415. <div className="text-[10px] text-slate-400 font-medium">Combine vector and full-text search results.</div>
  1416. </div>
  1417. <button
  1418. onClick={() => handleUpdateKbSettings('enableFullTextSearch', !localKbSettings.enableFullTextSearch)}
  1419. className={`relative inline-flex h-6 w-11 items-center rounded-full transition-all duration-300 ${localKbSettings.enableFullTextSearch ? 'bg-indigo-600 shadow-md shadow-indigo-100' : 'bg-slate-300'}`}
  1420. >
  1421. <span className={`${localKbSettings.enableFullTextSearch ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`} />
  1422. </button>
  1423. </div>
  1424. {localKbSettings.enableFullTextSearch && (
  1425. <motion.div
  1426. initial={{ opacity: 0, y: -10 }}
  1427. animate={{ opacity: 1, y: 0 }}
  1428. className="p-5 bg-indigo-50/30 rounded-2xl border border-indigo-100/50 space-y-4"
  1429. >
  1430. <div className="flex justify-between mb-2 px-1">
  1431. <label className="text-[10px] font-black text-indigo-400 uppercase tracking-widest">Hybrid Weight (Vector vs Text)</label>
  1432. <span className="text-sm font-black text-indigo-600">{localKbSettings.hybridVectorWeight || 0.5}</span>
  1433. </div>
  1434. <input
  1435. type="range"
  1436. min="0"
  1437. max="1"
  1438. step="0.05"
  1439. value={localKbSettings.hybridVectorWeight || 0.5}
  1440. onChange={(e) => handleUpdateKbSettings('hybridVectorWeight', parseFloat(e.target.value))}
  1441. className="w-full h-2 bg-indigo-100 rounded-lg appearance-none cursor-pointer accent-indigo-600"
  1442. />
  1443. <div className="flex justify-between mt-1 px-1 text-[9px] font-bold text-indigo-300 uppercase">
  1444. <span>Pure Text</span>
  1445. <span>Pure Vector</span>
  1446. </div>
  1447. </motion.div>
  1448. )}
  1449. <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
  1450. <div className="flex items-center justify-between p-5 bg-slate-50/50 rounded-2xl border border-slate-200/30 transition-all hover:bg-white hover:border-indigo-100">
  1451. <div>
  1452. <div className="text-sm font-bold text-slate-800">Enable Query Expansion</div>
  1453. <div className="text-[10px] text-slate-400 font-medium">Rewrites query for better recall.</div>
  1454. </div>
  1455. <button
  1456. onClick={() => handleUpdateKbSettings('enableQueryExpansion', !localKbSettings.enableQueryExpansion)}
  1457. className={`relative inline-flex h-6 w-11 items-center rounded-full transition-all duration-300 ${localKbSettings.enableQueryExpansion ? 'bg-indigo-600 shadow-md shadow-indigo-100' : 'bg-slate-300'}`}
  1458. >
  1459. <span className={`${localKbSettings.enableQueryExpansion ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`} />
  1460. </button>
  1461. </div>
  1462. <div className="flex items-center justify-between p-5 bg-slate-50/50 rounded-2xl border border-slate-200/30 transition-all hover:bg-white hover:border-indigo-100">
  1463. <div>
  1464. <div className="text-sm font-bold text-slate-800">Enable HyDE</div>
  1465. <div className="text-[10px] text-slate-400 font-medium">Hypothetical Document Embeddings.</div>
  1466. </div>
  1467. <button
  1468. onClick={() => handleUpdateKbSettings('enableHyDE', !localKbSettings.enableHyDE)}
  1469. className={`relative inline-flex h-6 w-11 items-center rounded-full transition-all duration-300 ${localKbSettings.enableHyDE ? 'bg-indigo-600 shadow-md shadow-indigo-100' : 'bg-slate-300'}`}
  1470. >
  1471. <span className={`${localKbSettings.enableHyDE ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`} />
  1472. </button>
  1473. </div>
  1474. </div>
  1475. <div className="flex items-center justify-between p-5 bg-slate-50/50 rounded-2xl border border-slate-200/30 transition-all hover:bg-white hover:border-indigo-100">
  1476. <div>
  1477. <div className="text-sm font-bold text-slate-800">Enable Reranking</div>
  1478. <div className="text-[10px] text-slate-400 font-medium">Re-score search results for higher accuracy.</div>
  1479. </div>
  1480. <button
  1481. onClick={() => handleUpdateKbSettings('enableRerank', !localKbSettings.enableRerank)}
  1482. className={`relative inline-flex h-6 w-11 items-center rounded-full transition-all duration-300 ${localKbSettings.enableRerank ? 'bg-indigo-600 shadow-md shadow-indigo-100' : 'bg-slate-300'}`}
  1483. >
  1484. <span className={`${localKbSettings.enableRerank ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`} />
  1485. </button>
  1486. </div>
  1487. {localKbSettings.enableRerank && (
  1488. <motion.div
  1489. initial={{ opacity: 0, y: -10 }}
  1490. animate={{ opacity: 1, y: 0 }}
  1491. className="p-5 bg-indigo-50/30 rounded-2xl border border-indigo-100/50 space-y-4"
  1492. >
  1493. <div className="flex justify-between mb-2 px-1">
  1494. <label className="text-[10px] font-black text-indigo-400 uppercase tracking-widest">Rerank Similarity Threshold</label>
  1495. <span className="text-sm font-black text-indigo-600">{localKbSettings.rerankSimilarityThreshold || 0.5}</span>
  1496. </div>
  1497. <input
  1498. type="range"
  1499. min="0"
  1500. max="1"
  1501. step="0.05"
  1502. value={localKbSettings.rerankSimilarityThreshold || 0.5}
  1503. onChange={(e) => handleUpdateKbSettings('rerankSimilarityThreshold', parseFloat(e.target.value))}
  1504. className="w-full h-2 bg-indigo-100 rounded-lg appearance-none cursor-pointer accent-indigo-600"
  1505. />
  1506. <div className="flex justify-between mt-1 px-1 text-[9px] font-bold text-indigo-300 uppercase">
  1507. <span>Broad</span>
  1508. <span>Strict</span>
  1509. </div>
  1510. </motion.div>
  1511. )}
  1512. </div>
  1513. </div>
  1514. </section>
  1515. </>
  1516. )}
  1517. </div>
  1518. );
  1519. const renderModelTab = () => (
  1520. <div className="w-full space-y-6">
  1521. <div className="flex justify-between items-center mb-6">
  1522. <div>
  1523. <h3 className="text-sm font-black text-slate-500 uppercase tracking-[0.15em] mb-1">{t('mmTitle')}</h3>
  1524. <p className="text-xs text-slate-400 font-medium">{t('sidebarDesc')}</p>
  1525. </div>
  1526. {!editingId && currentUser?.role === 'SUPER_ADMIN' && (
  1527. <button
  1528. onClick={() => { setEditingId('new'); setModelFormData({ type: ModelType.LLM, baseUrl: 'http://localhost:11434/v1', modelId: 'llama3', name: '', dimensions: 1536 }); }}
  1529. className="flex items-center gap-2 px-6 py-2.5 bg-indigo-600 text-white text-sm font-bold rounded-2xl hover:bg-indigo-700 shadow-lg shadow-indigo-100 transition-all active:scale-95"
  1530. >
  1531. <Plus className="w-4 h-4" />
  1532. {t('mmAddBtn')}
  1533. </button>
  1534. )}
  1535. </div>
  1536. {editingId ? (
  1537. <motion.div
  1538. initial={{ opacity: 0, y: 10 }}
  1539. animate={{ opacity: 1, y: 0 }}
  1540. className="bg-white/90 backdrop-blur-md p-10 rounded-3xl border border-slate-200/50 shadow-xl space-y-8"
  1541. >
  1542. <div className="flex items-center gap-4 mb-2">
  1543. <div className="w-12 h-12 rounded-2xl bg-indigo-50 flex items-center justify-center text-indigo-600">
  1544. <Cpu className="w-6 h-6" />
  1545. </div>
  1546. <h3 className="text-xl font-black text-slate-900 tracking-tight">{editingId === 'new' ? t('mmAddBtn') : t('mmEdit')}</h3>
  1547. </div>
  1548. <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
  1549. <div className="space-y-2">
  1550. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">{t('mmFormName')} *</label>
  1551. <input className="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl text-sm font-medium focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500/50 outline-none transition-all" value={modelFormData.name || ''} onChange={e => setModelFormData({ ...modelFormData, name: e.target.value })} disabled={isLoading} />
  1552. </div>
  1553. <div className="space-y-2">
  1554. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">{t('mmFormModelId')} *</label>
  1555. <input className="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl text-sm font-mono focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500/50 outline-none transition-all" value={modelFormData.modelId || ''} onChange={e => setModelFormData({ ...modelFormData, modelId: e.target.value })} disabled={isLoading} />
  1556. </div>
  1557. </div>
  1558. <div className="space-y-2">
  1559. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">{t('mmFormType')} *</label>
  1560. <select className="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl text-sm font-medium focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500/50 outline-none transition-all appearance-none" value={modelFormData.type} onChange={e => setModelFormData({ ...modelFormData, type: e.target.value as ModelType })} disabled={isLoading}>
  1561. <option value={ModelType.LLM}>{t('typeLLM')}</option>
  1562. <option value={ModelType.EMBEDDING}>{t('typeEmbedding')}</option>
  1563. <option value={ModelType.RERANK}>{t('typeRerank')}</option>
  1564. <option value={ModelType.VISION}>{t('typeVision')}</option>
  1565. </select>
  1566. </div>
  1567. <div className="space-y-2">
  1568. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">{t('mmFormBaseUrl')} *</label>
  1569. <input className="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl text-sm font-mono focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500/50 outline-none transition-all" value={modelFormData.baseUrl || ''} onChange={e => setModelFormData({ ...modelFormData, baseUrl: e.target.value })} disabled={isLoading} />
  1570. </div>
  1571. <div className="space-y-2">
  1572. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">{t('mmFormApiKey')}</label>
  1573. <input
  1574. type="password"
  1575. className="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl text-sm font-mono focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500/50 outline-none transition-all"
  1576. value={modelFormData.apiKey || ''}
  1577. onChange={e => setModelFormData({ ...modelFormData, apiKey: e.target.value })}
  1578. disabled={isLoading}
  1579. placeholder={t('mmFormApiKeyPlaceholder')}
  1580. />
  1581. </div>
  1582. {modelFormData.type === ModelType.EMBEDDING && (
  1583. <div className="grid grid-cols-2 gap-6 p-6 bg-slate-50 rounded-3xl border border-slate-200/50">
  1584. <div className="space-y-2">
  1585. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">Max Input</label>
  1586. <input type="number" className="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl text-sm font-bold" value={modelFormData.maxInputTokens || 8191} onChange={e => setModelFormData({ ...modelFormData, maxInputTokens: parseInt(e.target.value) })} />
  1587. </div>
  1588. <div className="space-y-2">
  1589. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">Dimensions</label>
  1590. <input type="number" className="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl text-sm font-bold" value={modelFormData.dimensions || 1536} onChange={e => setModelFormData({ ...modelFormData, dimensions: parseInt(e.target.value) })} />
  1591. </div>
  1592. </div>
  1593. )}
  1594. <div className="flex justify-end gap-3 pt-4">
  1595. <button onClick={() => { setEditingId(null); setError(null); }} className="px-6 py-3 text-sm font-bold text-slate-500 hover:text-slate-700">{t('mmCancel')}</button>
  1596. <button onClick={handleSaveModel} className="px-10 py-3 bg-indigo-600 text-white rounded-2xl font-black uppercase tracking-widest text-xs shadow-xl shadow-indigo-100 transition-all active:scale-95 flex items-center gap-2" disabled={isLoading}>
  1597. {isLoading && <Loader2 className="w-4 h-4 animate-spin" />}
  1598. {t('mmSave')}
  1599. </button>
  1600. </div>
  1601. </motion.div>
  1602. ) : (
  1603. <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-5 overflow-y-auto pr-2 pb-6 scrollbar-hide">
  1604. <AnimatePresence>
  1605. {models.map((model, index) => (
  1606. <motion.div
  1607. key={model.id}
  1608. initial={{ opacity: 0, scale: 0.95 }}
  1609. animate={{ opacity: 1, scale: 1 }}
  1610. transition={{ delay: index * 0.05 }}
  1611. className="bg-white/70 backdrop-blur-md border border-slate-200/50 rounded-[2rem] p-6 flex flex-col justify-between group hover:shadow-xl hover:shadow-indigo-500/5 hover:border-indigo-500/30 transition-all duration-300 relative overflow-hidden"
  1612. >
  1613. {/* Subtle background pattern/glow */}
  1614. <div className="absolute -top-12 -right-12 w-32 h-32 bg-indigo-500/5 rounded-full blur-3xl group-hover:bg-indigo-500/10 transition-all duration-500" />
  1615. <div className="relative z-10">
  1616. <div className="flex items-start justify-between mb-5">
  1617. <div className={`w-14 h-14 rounded-2xl flex items-center justify-center transition-all duration-500 ${model.isEnabled !== false ? 'bg-indigo-600 text-white shadow-lg shadow-indigo-100 rotate-0 group-hover:rotate-6' : 'bg-slate-100 text-slate-400 opacity-60'}`}>
  1618. <Cpu size={26} strokeWidth={2.5} />
  1619. </div>
  1620. <div className="flex gap-1 items-center bg-slate-50 p-1 rounded-xl border border-slate-100/50">
  1621. <button
  1622. onClick={() => handleToggleModel(model)}
  1623. className={`p-1.5 rounded-lg transition-all ${((currentUser?.role === 'SUPER_ADMIN' && model.isEnabled !== false) || (currentUser?.role === 'TENANT_ADMIN' && enabledModelIds.includes(model.id))) ? 'text-indigo-600 bg-white shadow-sm' : 'text-slate-400 hover:text-slate-600'}`}
  1624. title={((currentUser?.role === 'SUPER_ADMIN' && model.isEnabled !== false) || (currentUser?.role === 'TENANT_ADMIN' && enabledModelIds.includes(model.id))) ? t('modelEnabled') : t('modelDisabled')}
  1625. >
  1626. {((currentUser?.role === 'SUPER_ADMIN' && model.isEnabled !== false) || (currentUser?.role === 'TENANT_ADMIN' && enabledModelIds.includes(model.id))) ? <ToggleRight size={24} /> : <ToggleLeft size={24} />}
  1627. </button>
  1628. </div>
  1629. </div>
  1630. <div className="space-y-1 mb-6">
  1631. <div className="flex items-center gap-2.5">
  1632. <h4 className="font-black text-slate-900 text-lg tracking-tight truncate">{model.name}</h4>
  1633. </div>
  1634. <div className="flex items-center gap-2">
  1635. <span className="text-[9px] font-black bg-indigo-50 text-indigo-600 px-2 py-0.5 rounded-lg uppercase tracking-wider border border-indigo-100/50">
  1636. {getTypeLabel(model.type)}
  1637. </span>
  1638. {model.isDefault && (
  1639. <span className="text-[9px] font-black bg-amber-50 text-amber-600 px-2 py-0.5 rounded-lg uppercase tracking-wider border border-amber-100/50 flex items-center gap-1">
  1640. <Sparkles size={8} /> Default
  1641. </span>
  1642. )}
  1643. </div>
  1644. <p className="text-[11px] font-mono text-slate-400 mt-2 truncate bg-slate-50 px-2 py-1 rounded-lg border border-slate-100/50 inline-block max-w-full">
  1645. {model.modelId}
  1646. </p>
  1647. </div>
  1648. {/* Additional info grid */}
  1649. <div className="grid grid-cols-2 gap-3 mb-6">
  1650. {model.type === ModelType.EMBEDDING && (
  1651. <>
  1652. <div className="bg-slate-50/50 p-2.5 rounded-2xl border border-slate-100/50">
  1653. <span className="block text-[8px] font-black text-slate-400 uppercase tracking-widest mb-0.5">Dims</span>
  1654. <span className="text-xs font-bold text-slate-700">{model.dimensions || '-'}</span>
  1655. </div>
  1656. <div className="bg-slate-50/50 p-2.5 rounded-2xl border border-slate-100/50">
  1657. <span className="block text-[8px] font-black text-slate-400 uppercase tracking-widest mb-0.5">Ctx</span>
  1658. <span className="text-xs font-bold text-slate-700">{model.maxInputTokens || '-'}</span>
  1659. </div>
  1660. </>
  1661. )}
  1662. {model.type === ModelType.LLM && (
  1663. <div className="col-span-2 bg-slate-50/50 p-2.5 rounded-2xl border border-slate-100/50 flex items-center justify-between">
  1664. <span className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Base API</span>
  1665. <span className="text-[9px] font-mono font-medium text-slate-600 max-w-[140px] truncate">{model.baseUrl}</span>
  1666. </div>
  1667. )}
  1668. </div>
  1669. </div>
  1670. <div className="flex items-center justify-between pt-4 border-t border-slate-100/50 relative z-10">
  1671. <div className="flex items-center gap-1 text-[10px] font-bold text-slate-400">
  1672. <SettingsIcon size={12} />
  1673. Configured
  1674. </div>
  1675. <div className="flex gap-2">
  1676. {currentUser?.role === 'SUPER_ADMIN' && (
  1677. <>
  1678. <button
  1679. onClick={() => { setEditingId(model.id); setModelFormData({ ...model }); }}
  1680. className="w-9 h-9 flex items-center justify-center rounded-xl bg-slate-50 text-slate-400 hover:text-indigo-600 hover:bg-indigo-50 hover:shadow-sm transition-all"
  1681. >
  1682. <Edit2 size={15} />
  1683. </button>
  1684. <button
  1685. onClick={() => handleDeleteModel(model.id)}
  1686. className="w-9 h-9 flex items-center justify-center rounded-xl bg-slate-50 text-slate-400 hover:text-red-500 hover:bg-red-50 hover:shadow-sm transition-all"
  1687. >
  1688. <Trash2 size={15} />
  1689. </button>
  1690. </>
  1691. )}
  1692. </div>
  1693. </div>
  1694. </motion.div>
  1695. ))}
  1696. </AnimatePresence>
  1697. {models.length === 0 && (
  1698. <div className="bg-white/50 border-2 border-dashed border-slate-200 rounded-3xl p-16 text-center">
  1699. <Cpu className="w-12 h-12 text-slate-200 mx-auto mb-4" />
  1700. <p className="text-slate-400 font-bold uppercase tracking-widest text-xs">{t('mmEmpty')}</p>
  1701. </div>
  1702. )}
  1703. </div>
  1704. )}
  1705. </div>
  1706. );
  1707. return (
  1708. <div className="flex h-full bg-transparent overflow-hidden relative">
  1709. {/* Content Area */}
  1710. <div className="flex-1 flex flex-col min-w-0 h-full overflow-hidden relative z-10">
  1711. <div className="px-8 pt-8 pb-6 flex items-start justify-between shrink-0">
  1712. <div>
  1713. <h1 className="text-2xl font-bold text-slate-900 leading-tight">
  1714. {activeTab === 'general' ? t('generalSettings') : activeTab === 'user' ? t('userManagement') : activeTab === 'model' ? t('modelManagement') : activeTab === 'knowledge_base' ? 'Index Chat Config' : t('navTenants')}
  1715. </h1>
  1716. <p className="text-[15px] text-slate-500 mt-1">
  1717. {activeTab === 'general' ? 'Manage your application preferences.' : activeTab === 'user' ? 'Manage access and accounts.' : activeTab === 'model' ? 'Configure global AI models.' : activeTab === 'knowledge_base' ? 'Technical configuration for indexing and chat parameters.' : 'Global system overview.'}
  1718. </p>
  1719. </div>
  1720. </div>
  1721. <div className="flex-1 overflow-y-auto px-10 pb-10 scrollbar-hide">
  1722. <div className="w-full">
  1723. {error && (
  1724. <motion.div
  1725. initial={{ opacity: 0, y: -10 }}
  1726. animate={{ opacity: 1, y: 0 }}
  1727. className="mb-8 p-4 bg-red-50/80 backdrop-blur-md border border-red-200/50 text-red-700 rounded-2xl flex gap-3 items-center text-sm shadow-sm"
  1728. >
  1729. <div className="w-8 h-8 rounded-full bg-red-100 flex items-center justify-center shrink-0">
  1730. <X className="w-4 h-4 text-red-600" />
  1731. </div>
  1732. <div>
  1733. <span className="font-black uppercase tracking-widest text-[10px] block mb-0.5">{t('errorLabel')}</span>
  1734. {error}
  1735. </div>
  1736. </motion.div>
  1737. )}
  1738. <AnimatePresence mode="wait">
  1739. <motion.div
  1740. key={activeTab}
  1741. initial={{ opacity: 0, x: 20 }}
  1742. animate={{ opacity: 1, x: 0 }}
  1743. exit={{ opacity: 0, x: -20 }}
  1744. transition={{ duration: 0.3 }}
  1745. >
  1746. {activeTab === 'general' && renderGeneralTab()}
  1747. {activeTab === 'user' && renderUserTab()}
  1748. {activeTab === 'model' && (isAdmin || currentUser?.role === 'SUPER_ADMIN') && renderModelTab()}
  1749. {activeTab === 'knowledge_base' && (isAdmin || currentUser?.role === 'SUPER_ADMIN') && renderKnowledgeBaseTab()}
  1750. {activeTab === 'tenants' && currentUser?.role === 'SUPER_ADMIN' && renderTenantsTab()}
  1751. </motion.div>
  1752. </AnimatePresence>
  1753. </div>
  1754. </div>
  1755. </div>
  1756. </div>
  1757. );
  1758. };