SettingsView.tsx 94 KB

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