SettingsView.tsx 124 KB

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