SettingsView.tsx 122 KB

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