SettingsView.tsx 73 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240
  1. import React, { useState, useEffect } from 'react';
  2. import { ModelConfig, ModelType, AppSettings, KnowledgeGroup } from '../../types';
  3. import { useLanguage } from '../../contexts/LanguageContext';
  4. import { X, Plus, Trash2, Edit2, Save, Cpu, Box, Loader2, User, Shield, Key, LogOut, Globe, Settings as SettingsIcon, ToggleLeft, ToggleRight, Database, Sparkles, ChevronRight, Lock, Building2, BookOpen } from 'lucide-react';
  5. import { motion, AnimatePresence } from 'framer-motion';
  6. import { userService } from '../../services/userService';
  7. // import { settingsService } from '../../services/settingsService';
  8. import { userSettingService } from '../../services/userSettingService';
  9. import { knowledgeGroupService } from '../../services/knowledgeGroupService';
  10. import { useConfirm } from '../../contexts/ConfirmContext';
  11. import { useToast } from '../../contexts/ToastContext';
  12. interface SettingsViewProps {
  13. // Model Props
  14. models: ModelConfig[];
  15. authToken: string | null;
  16. onUpdateModels: (action: 'create' | 'update' | 'delete', model: ModelConfig) => Promise<void>;
  17. isAdmin?: boolean; // Added isAdmin prop
  18. currentUser?: any; // Added current user prop
  19. initialTab?: TabType;
  20. }
  21. type TabType = 'general' | 'user' | 'model' | 'dashboard' | 'knowledge_base';
  22. export const SettingsView: React.FC<SettingsViewProps> = ({
  23. models,
  24. authToken,
  25. onUpdateModels,
  26. isAdmin = false,
  27. currentUser,
  28. initialTab = 'general',
  29. }) => {
  30. const { t, language, setLanguage } = useLanguage();
  31. const { confirm } = useConfirm();
  32. const { showError, showSuccess } = useToast();
  33. const [activeTab, setActiveTab] = useState<TabType>('general');
  34. const [isLoading, setIsLoading] = useState(false);
  35. const [error, setError] = useState<string | null>(null);
  36. // --- Model Manager State ---
  37. const [editingId, setEditingId] = useState<string | null>(null);
  38. const [modelFormData, setModelFormData] = useState<Partial<ModelConfig>>({
  39. type: ModelType.LLM,
  40. baseUrl: 'http://localhost:11434/v1',
  41. modelId: 'llama3',
  42. name: '',
  43. dimensions: 1536,
  44. apiKey: '',
  45. maxInputTokens: 8191,
  46. maxBatchSize: 2048
  47. });
  48. // --- User Management State ---
  49. interface UserType {
  50. id: string;
  51. username: string;
  52. isAdmin: boolean;
  53. role?: string;
  54. tenantId?: string;
  55. createdAt: string;
  56. }
  57. const [users, setUsers] = useState<UserType[]>([]);
  58. const [isUserLoading, setIsUserLoading] = useState(false);
  59. const [showAddUser, setShowAddUser] = useState(false);
  60. const [newUser, setNewUser] = useState({ username: '', password: '', role: 'USER' });
  61. const [userSuccess, setUserSuccess] = useState('');
  62. // --- Change Password State ---
  63. const [passwordForm, setPasswordForm] = useState({ current: '', new: '', confirm: '' });
  64. const [passwordSuccess, setPasswordSuccess] = useState('');
  65. // --- App Settings State ---
  66. const [appSettings, setAppSettings] = useState<AppSettings | null>(null);
  67. const [knowledgeGroups, setKnowledgeGroups] = useState<KnowledgeGroup[]>([]);
  68. const [isSettingsLoading, setIsSettingsLoading] = useState(false);
  69. const [enabledModelIds, setEnabledModelIds] = useState<string[]>([]);
  70. useEffect(() => {
  71. if (initialTab) {
  72. setActiveTab(initialTab);
  73. }
  74. }, [initialTab]);
  75. // ユーザー一覧の取得(ユーザータブがアクティブな場合)
  76. useEffect(() => {
  77. if (activeTab === 'user') {
  78. fetchUsers();
  79. } else if (activeTab === 'general') {
  80. fetchSettingsAndGroups();
  81. } else if (activeTab === 'model' && (isAdmin || currentUser?.role === 'SUPER_ADMIN')) {
  82. // Model tab initialization
  83. } else if (activeTab === 'dashboard' && currentUser?.role === 'SUPER_ADMIN') {
  84. fetchDashboardData();
  85. } else if (activeTab === 'knowledge_base' && (currentUser?.role === 'TENANT_ADMIN' || currentUser?.role === 'SUPER_ADMIN')) {
  86. fetchKnowledgeBaseSettings();
  87. }
  88. }, [activeTab, currentUser]);
  89. const [kbSettings, setKbSettings] = useState<any>(null);
  90. const fetchKnowledgeBaseSettings = async () => {
  91. if (!authToken) return;
  92. setIsLoading(true);
  93. try {
  94. const res = await fetch('/api/v1/admin/settings', {
  95. headers: { 'Authorization': `Bearer ${authToken}` }
  96. });
  97. if (res.ok) {
  98. const data = await res.json();
  99. setKbSettings(data);
  100. }
  101. } catch (error) {
  102. console.error(error);
  103. } finally {
  104. setIsLoading(false);
  105. }
  106. };
  107. const handleUpdateKbSettings = async (key: string, value: any) => {
  108. if (!authToken) return;
  109. const newSettings = { ...kbSettings, [key]: value };
  110. try {
  111. const res = await fetch('/api/v1/admin/settings', {
  112. method: 'PUT',
  113. headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
  114. body: JSON.stringify(newSettings)
  115. });
  116. if (res.ok) {
  117. setKbSettings(newSettings);
  118. showSuccess('Knowledge Base settings updated');
  119. }
  120. } catch (error) {
  121. showError('Failed to update settings');
  122. }
  123. };
  124. const fetchSettingsAndGroups = async () => {
  125. if (!authToken) return;
  126. setIsSettingsLoading(true);
  127. try {
  128. const [settings, groups] = await Promise.all([
  129. userSettingService.get(authToken),
  130. knowledgeGroupService.getGroups()
  131. ]);
  132. setAppSettings(settings);
  133. setKnowledgeGroups(groups);
  134. } catch (error) {
  135. console.error('Failed to fetch settings or groups:', error);
  136. } finally {
  137. setIsSettingsLoading(false);
  138. }
  139. };
  140. // --- 一般タブのハンドラー ---
  141. const handleChangePassword = async (e: React.FormEvent) => {
  142. e.preventDefault();
  143. setError(null);
  144. setPasswordSuccess('');
  145. if (passwordForm.new !== passwordForm.confirm) {
  146. setError(t('passwordMismatch'));
  147. return;
  148. }
  149. if (passwordForm.new.length < 6) {
  150. setError(t('newPasswordMinLength'));
  151. return;
  152. }
  153. setIsLoading(true);
  154. try {
  155. await userService.changePassword(passwordForm.current, passwordForm.new);
  156. setPasswordSuccess(t('passwordChangeSuccess'));
  157. setPasswordForm({ current: '', new: '', confirm: '' });
  158. } catch (err: any) {
  159. setError(err.message || t('passwordChangeFailed'));
  160. } finally {
  161. setIsLoading(false);
  162. }
  163. };
  164. // --- ユーザータブのハンドラー ---
  165. const fetchUsers = async () => {
  166. setIsUserLoading(true);
  167. try {
  168. const userList = await userService.getUsers();
  169. setUsers(userList);
  170. } catch (error: any) {
  171. setError(error.message || t('getUserListFailed'));
  172. } finally {
  173. setIsUserLoading(false);
  174. }
  175. };
  176. const handleCreateUser = async (e: React.FormEvent) => {
  177. e.preventDefault();
  178. setError('');
  179. setUserSuccess('');
  180. if (newUser.password.length < 6) {
  181. setError(t('passwordMinLength'));
  182. return;
  183. }
  184. try {
  185. await userService.createUser(newUser.username, newUser.password, newUser.role);
  186. setUserSuccess(t('userCreatedSuccess'));
  187. setNewUser({ username: '', password: '', role: 'USER' });
  188. setShowAddUser(false);
  189. fetchUsers();
  190. } catch (error: any) {
  191. setError(error.message || t('createUserFailed'));
  192. }
  193. };
  194. const [passwordChangeUserData, setPasswordChangeUserData] = useState<{ userId: string, newPassword: string } | null>(null);
  195. // --- Edit Role State ---
  196. const [roleChangeUserData, setRoleChangeUserData] = useState<{ userId: string, newRole: string } | null>(null);
  197. const handleToggleUserAdmin = async (userId: string, newAdminStatus: boolean) => {
  198. try {
  199. await userService.updateUser(userId, newAdminStatus);
  200. // ユーザーリストを再取得
  201. fetchUsers();
  202. setUserSuccess(newAdminStatus ? t('userPromotedToAdmin') : t('userDemotedFromAdmin'));
  203. } catch (error: any) {
  204. setError(error.message || t('updateUserFailed'));
  205. }
  206. };
  207. const handleUserPasswordChange = async () => {
  208. if (!passwordChangeUserData || !passwordChangeUserData.newPassword) return;
  209. try {
  210. // Update user password
  211. await userService.updateUserInfo(passwordChangeUserData.userId, { password: passwordChangeUserData.newPassword });
  212. setUserSuccess(t('passwordChangeSuccess'));
  213. setPasswordChangeUserData(null);
  214. fetchUsers(); // Refresh the user list
  215. } catch (error: any) {
  216. setError(error.message || t('passwordChangeFailed'));
  217. }
  218. };
  219. // --- Dashboard State (Migrated) ---
  220. const [stats, setStats] = useState({ tenants: 0, users: 0 });
  221. const [tenants, setTenants] = useState<any[]>([]);
  222. const [tenantAdmins, setTenantAdmins] = useState<any[]>([]);
  223. const [isDashboardLoading, setIsDashboardLoading] = useState(false);
  224. const [showCreateTenant, setShowCreateTenant] = useState(false);
  225. const [newTenant, setNewTenant] = useState({ name: '', domain: '', adminUserId: '' });
  226. const fetchDashboardData = async () => {
  227. setIsDashboardLoading(true);
  228. try {
  229. const headers = { 'Authorization': `Bearer ${authToken}`, 'x-api-key': authToken || '' };
  230. const [tenRes, admRes] = await Promise.all([
  231. fetch('/api/v1/super-admin/tenants', { headers }),
  232. fetch('/api/v1/admin/users', { headers })
  233. ]);
  234. if (tenRes.ok) {
  235. const data = await tenRes.json();
  236. setTenants(data);
  237. setStats(s => ({ ...s, tenants: data.length }));
  238. }
  239. if (admRes.ok) {
  240. const data = await admRes.json();
  241. setTenantAdmins(data.filter((u: any) => u.role === 'TENANT_ADMIN' || u.role === 'SUPER_ADMIN'));
  242. setStats(s => ({ ...s, users: data.length }));
  243. }
  244. } catch (e) {
  245. console.error(e);
  246. } finally {
  247. setIsDashboardLoading(false);
  248. }
  249. };
  250. const handleCreateTenant = async (e: React.FormEvent) => {
  251. e.preventDefault();
  252. try {
  253. const res = await fetch('/api/v1/super-admin/tenants', {
  254. method: 'POST',
  255. headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}`, 'x-api-key': authToken || '' },
  256. body: JSON.stringify(newTenant)
  257. });
  258. if (res.ok) {
  259. setShowCreateTenant(false);
  260. setNewTenant({ name: '', domain: '', adminUserId: '' });
  261. fetchDashboardData();
  262. showSuccess('Tenant created successfully');
  263. }
  264. } catch (e) {
  265. showError('Failed to create tenant');
  266. }
  267. };
  268. const handleBindAdmin = async (tenantId: string, userId: string) => {
  269. if (!userId) return;
  270. try {
  271. const res = await fetch(`/api/v1/super-admin/tenants/${tenantId}/admin`, {
  272. method: 'PUT',
  273. headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}`, 'x-api-key': authToken || '' },
  274. body: JSON.stringify({ userId })
  275. });
  276. if (res.ok) {
  277. showSuccess('Admin bound successfully');
  278. fetchDashboardData();
  279. } else {
  280. showError('Failed to bind admin');
  281. }
  282. } catch (e) {
  283. showError('Error binding admin');
  284. }
  285. };
  286. const handleToggleNotebookFeature = async (tenantId: string, currentEnabled: boolean) => {
  287. try {
  288. const res = await fetch(`/api/tenants/${tenantId}/settings`, {
  289. method: 'PUT',
  290. headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
  291. body: JSON.stringify({ isNotebookEnabled: !currentEnabled })
  292. });
  293. if (res.ok) {
  294. showSuccess('Feature updated successfully');
  295. fetchDashboardData();
  296. }
  297. } catch (e) {
  298. showError('Failed to update feature');
  299. }
  300. };
  301. const handleUserRoleChange = async () => {
  302. if (!roleChangeUserData) return;
  303. try {
  304. await userService.updateUserInfo(roleChangeUserData.userId, { role: roleChangeUserData.newRole });
  305. showSuccess('Role updated successfully.');
  306. setRoleChangeUserData(null);
  307. fetchUsers();
  308. } catch (error: any) {
  309. showError(error.response?.data?.message || 'Failed to update user role.');
  310. }
  311. };
  312. const handleDeleteUser = async (userId: string) => {
  313. if (!confirm(t('confirmDeleteUser') || "Are you sure you want to delete this user?")) return;
  314. try {
  315. await userService.deleteUser(userId);
  316. showSuccess(t('userDeletedSuccessfully'));
  317. fetchUsers();
  318. } catch (error: any) {
  319. showError(error.message || t('deleteUserFailed'));
  320. }
  321. };
  322. // --- モデルタブのハンドラー ---
  323. const handleSaveModel = async () => {
  324. if (!authToken) {
  325. setError(t('mmErrorNotAuthenticated'));
  326. return;
  327. }
  328. setError(null);
  329. if (!modelFormData.name?.trim()) { setError(t('mmErrorNameRequired')); return; }
  330. if (!modelFormData.modelId?.trim()) { setError(t('mmErrorModelIdRequired')); return; }
  331. if (!modelFormData.baseUrl?.trim()) { setError(t('mmErrorBaseUrlRequired')); return; }
  332. setIsLoading(true);
  333. try {
  334. const saveData = { ...modelFormData } as ModelConfig;
  335. await onUpdateModels(editingId === 'new' ? 'create' : 'update', saveData);
  336. setEditingId(null);
  337. } catch (err: any) {
  338. setError(err.message || t('errorGeneric'));
  339. } finally {
  340. setIsLoading(false);
  341. }
  342. };
  343. const handleToggleModel = async (model: ModelConfig) => {
  344. if (currentUser?.role === 'TENANT_ADMIN') {
  345. const newEnabledIds = enabledModelIds.includes(model.id)
  346. ? enabledModelIds.filter(id => id !== model.id)
  347. : [...enabledModelIds, model.id];
  348. try {
  349. await fetch('/api/v1/admin/settings', {
  350. method: 'PUT',
  351. headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
  352. body: JSON.stringify({ enabledModelIds: newEnabledIds })
  353. });
  354. setEnabledModelIds(newEnabledIds);
  355. showSuccess('Settings updated successfully');
  356. } catch (error) {
  357. console.error(error);
  358. showError(t('errorGeneric'));
  359. }
  360. return;
  361. }
  362. try {
  363. await onUpdateModels('update', { ...model, isEnabled: !model.isEnabled });
  364. } catch (error) {
  365. console.error('Failed to toggle model:', error);
  366. }
  367. };
  368. const handleDeleteModel = async (id: string) => {
  369. if (await confirm(t('confirmClear'))) {
  370. await onUpdateModels('delete', { id } as ModelConfig);
  371. }
  372. };
  373. const getTypeLabel = (type: ModelType) => {
  374. switch (type) {
  375. case ModelType.LLM: return t('typeLLM');
  376. case ModelType.EMBEDDING: return t('typeEmbedding');
  377. case ModelType.RERANK: return t('typeRerank');
  378. }
  379. };
  380. // --- レンダリング関数 ---
  381. const renderGeneralTab = () => (
  382. <div className="space-y-8 animate-in slide-in-from-right duration-300 max-w-2xl">
  383. {/* パスワード変更セクション */}
  384. <section className="bg-white p-6 rounded-lg border border-slate-200 shadow-sm">
  385. <h3 className="text-sm font-medium text-slate-800 mb-4 flex items-center gap-2">
  386. <Key className="w-4 h-4 text-blue-500" />
  387. {t('changePassword')}
  388. </h3>
  389. <form onSubmit={handleChangePassword} className="space-y-4 max-w-sm">
  390. <div>
  391. <input
  392. type="password"
  393. placeholder={t('currentPassword')}
  394. value={passwordForm.current}
  395. onChange={e => setPasswordForm({ ...passwordForm, current: e.target.value })}
  396. className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none"
  397. required
  398. />
  399. </div>
  400. <div>
  401. <input
  402. type="password"
  403. placeholder={t('newPassword')}
  404. value={passwordForm.new}
  405. onChange={e => setPasswordForm({ ...passwordForm, new: e.target.value })}
  406. className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none"
  407. required
  408. />
  409. </div>
  410. <div>
  411. <input
  412. type="password"
  413. placeholder={t('confirmPassword')}
  414. value={passwordForm.confirm}
  415. onChange={e => setPasswordForm({ ...passwordForm, confirm: e.target.value })}
  416. className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none"
  417. required
  418. />
  419. </div>
  420. {passwordSuccess && <p className="text-xs text-green-600">{passwordSuccess}</p>}
  421. <button
  422. type="submit"
  423. disabled={isLoading}
  424. className="w-full py-2 bg-slate-800 text-white rounded-md text-sm hover:bg-slate-900 transition-colors disabled:opacity-50"
  425. >
  426. {isLoading ? <Loader2 className="w-4 h-4 animate-spin mx-auto" /> : t('confirmChange')}
  427. </button>
  428. </form>
  429. </section>
  430. {/* 语言设置セクション */}
  431. <section className="bg-white p-6 rounded-lg border border-slate-200 shadow-sm">
  432. <h3 className="text-sm font-medium text-slate-800 mb-4 flex items-center gap-2">
  433. <Globe className="w-4 h-4 text-blue-500" />
  434. {t('languageSettings')}
  435. </h3>
  436. <div className="space-y-4 max-w-sm">
  437. <div className="space-y-2">
  438. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">
  439. {t('switchLanguage')}
  440. </label>
  441. <select
  442. value={language}
  443. onChange={(e) => setLanguage(e.target.value as any)}
  444. 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"
  445. >
  446. <option value="en">English</option>
  447. <option value="zh">中文 (Chinese)</option>
  448. <option value="ja">日本語 (Japanese)</option>
  449. </select>
  450. </div>
  451. </div>
  452. </section>
  453. </div >
  454. );
  455. const renderUserTab = () => (
  456. <div className="space-y-6 max-w-4xl">
  457. <div className="flex justify-between items-center mb-6">
  458. <div>
  459. <h3 className="text-sm font-black text-slate-500 uppercase tracking-[0.15em] mb-1">{t('userList')}</h3>
  460. <p className="text-xs text-slate-400 font-medium">{t('sidebarDesc')}</p>
  461. </div>
  462. <button
  463. onClick={() => setShowAddUser(!showAddUser)}
  464. 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"
  465. >
  466. <Plus className="w-4 h-4" />
  467. {t('addUser')}
  468. </button>
  469. </div>
  470. {showAddUser && (
  471. <motion.form
  472. initial={{ opacity: 0, y: -10 }}
  473. animate={{ opacity: 1, y: 0 }}
  474. onSubmit={handleCreateUser}
  475. className="bg-white/80 backdrop-blur-md p-8 rounded-3xl border border-slate-200/50 shadow-xl mb-8 space-y-5"
  476. >
  477. <div className="grid grid-cols-1 md:grid-cols-2 gap-5">
  478. <input
  479. type="text"
  480. placeholder={t('usernamePlaceholder')}
  481. value={newUser.username}
  482. onChange={e => setNewUser({ ...newUser, username: e.target.value })}
  483. 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"
  484. required
  485. />
  486. <input
  487. type="password"
  488. placeholder={t('passwordPlaceholder')}
  489. value={newUser.password}
  490. onChange={e => setNewUser({ ...newUser, password: e.target.value })}
  491. 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"
  492. required
  493. />
  494. </div>
  495. <div className="flex items-center justify-between">
  496. <div className="flex items-center gap-3 bg-slate-50 px-4 py-2 rounded-xl border border-slate-200/50">
  497. <Shield className={`w-4 h-4 ${currentUser?.role === 'SUPER_ADMIN' ? 'text-red-500' : 'text-indigo-500'}`} />
  498. {currentUser?.role === 'SUPER_ADMIN' ? (
  499. <select
  500. className="bg-transparent text-xs font-bold text-slate-600 uppercase tracking-widest outline-none cursor-pointer"
  501. value={newUser.role}
  502. onChange={e => setNewUser({ ...newUser, role: e.target.value })}
  503. >
  504. <option value="TENANT_ADMIN">Tenant Admin</option>
  505. <option value="USER">Regular User</option>
  506. </select>
  507. ) : (
  508. <span className="text-xs font-bold text-slate-600 uppercase tracking-widest">
  509. Creating Regular User
  510. </span>
  511. )}
  512. </div>
  513. <div className="flex gap-3">
  514. <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>
  515. <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>
  516. </div>
  517. </div>
  518. </motion.form>
  519. )}
  520. {passwordChangeUserData && (
  521. <div className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm flex items-center justify-center z-[100] p-6">
  522. <motion.div
  523. initial={{ scale: 0.95, opacity: 0 }}
  524. animate={{ scale: 1, opacity: 1 }}
  525. className="bg-white rounded-3xl p-10 w-full max-w-md shadow-2xl border border-white/20"
  526. >
  527. <div className="flex items-center justify-between mb-8">
  528. <h3 className="text-xl font-black text-slate-900 tracking-tight">{t('changeUserPassword')}</h3>
  529. <button onClick={() => setPasswordChangeUserData(null)} className="p-2 hover:bg-slate-100 rounded-xl transition-all">
  530. <X size={20} className="text-slate-400" />
  531. </button>
  532. </div>
  533. <form onSubmit={(e) => { e.preventDefault(); handleUserPasswordChange(); }} className="space-y-6">
  534. <div>
  535. <label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 px-1">
  536. {t('newPassword')}
  537. </label>
  538. <input
  539. type="password"
  540. value={passwordChangeUserData.newPassword}
  541. onChange={(e) => setPasswordChangeUserData({ ...passwordChangeUserData, newPassword: e.target.value })}
  542. 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"
  543. placeholder={t('enterNewPassword')}
  544. required
  545. />
  546. </div>
  547. <div className="flex gap-4 pt-4">
  548. <button type="button" onClick={() => setPasswordChangeUserData(null)} className="flex-1 py-3.5 text-slate-500 font-bold text-sm">{t('cancel')}</button>
  549. <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>
  550. </div>
  551. </form>
  552. </motion.div>
  553. </div>
  554. )}
  555. {/* Edit Role Modal */}
  556. {roleChangeUserData && (
  557. <div className="fixed inset-0 z-[110] flex items-center justify-center bg-slate-900/40 backdrop-blur-sm p-4">
  558. <motion.div
  559. initial={{ scale: 0.95, opacity: 0 }}
  560. animate={{ scale: 1, opacity: 1 }}
  561. className="bg-white rounded-3xl p-10 w-full max-w-md shadow-2xl border border-white/20"
  562. >
  563. <div className="flex items-center justify-between mb-8">
  564. <h3 className="text-xl font-black text-slate-900 tracking-tight">Edit User Role</h3>
  565. <button onClick={() => setRoleChangeUserData(null)} className="p-2 hover:bg-slate-100 rounded-xl transition-all">
  566. <X size={20} className="text-slate-400" />
  567. </button>
  568. </div>
  569. <form onSubmit={(e) => { e.preventDefault(); handleUserRoleChange(); }} className="space-y-6">
  570. <div>
  571. <label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 px-1">
  572. Target Role
  573. </label>
  574. <select
  575. value={roleChangeUserData.newRole}
  576. onChange={(e) => setRoleChangeUserData({ ...roleChangeUserData, newRole: e.target.value })}
  577. 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"
  578. >
  579. <option value="TENANT_ADMIN">Tenant Admin</option>
  580. <option value="USER">Regular User</option>
  581. </select>
  582. </div>
  583. <div className="flex gap-4 pt-4">
  584. <button type="button" onClick={() => setRoleChangeUserData(null)} className="flex-1 py-3.5 text-slate-500 font-bold text-sm">{t('cancel')}</button>
  585. <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>
  586. </div>
  587. </form>
  588. </motion.div>
  589. </div>
  590. )}
  591. <div className="grid grid-cols-1 gap-4 overflow-y-auto pr-2 scrollbar-hide">
  592. <AnimatePresence>
  593. {users.map((user, index) => {
  594. let IconComponent = User;
  595. let iconColors = 'bg-slate-50 text-slate-400';
  596. if (user.role === 'SUPER_ADMIN') {
  597. IconComponent = Shield;
  598. iconColors = 'bg-red-50 text-red-600';
  599. } else if (user.isAdmin || user.role === 'TENANT_ADMIN') {
  600. IconComponent = Shield;
  601. iconColors = 'bg-indigo-50 text-indigo-600';
  602. }
  603. return (
  604. <motion.div
  605. key={user.id}
  606. initial={{ opacity: 0, x: -10 }}
  607. animate={{ opacity: 1, x: 0 }}
  608. transition={{ delay: index * 0.05 }}
  609. 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"
  610. >
  611. <div className="flex items-center gap-4">
  612. <div className={`w-12 h-12 rounded-xl flex items-center justify-center transition-all ${iconColors}`}>
  613. <IconComponent size={22} />
  614. </div>
  615. <div>
  616. <div className="flex items-center gap-2">
  617. <p className="font-black text-slate-900">{user.username}</p>
  618. {(user.role === 'SUPER_ADMIN' || user.isAdmin) && <span className={`text-[9px] font-black px-1.5 py-0.5 rounded-md uppercase tracking-wider ${user.role === 'SUPER_ADMIN' ? 'bg-red-100 text-red-700' : 'bg-indigo-100 text-indigo-700'}`}>{user.role === 'SUPER_ADMIN' ? 'SUPER' : 'ADMIN'}</span>} </div>
  619. <p className="text-[10px] font-bold text-slate-400 mt-0.5 uppercase tracking-widest">{t('createdAt')}: {new Date(user.createdAt).toLocaleDateString()}</p>
  620. </div>
  621. </div>
  622. <div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-all">
  623. {user.username !== 'admin' && (
  624. <>
  625. {currentUser?.role === 'SUPER_ADMIN' && user.role !== 'SUPER_ADMIN' && (
  626. <button
  627. onClick={(e) => {
  628. e.preventDefault();
  629. e.stopPropagation();
  630. setRoleChangeUserData({ userId: user.id, newRole: user.role && user.role !== 'USER' ? user.role : (user.isAdmin ? 'TENANT_ADMIN' : 'USER') });
  631. }}
  632. className="p-2.5 rounded-xl text-slate-400 hover:text-indigo-600 hover:bg-indigo-50 transition-all cursor-pointer relative z-10"
  633. title="Edit Role"
  634. >
  635. <Edit2 className="w-4.5 h-4.5" />
  636. </button>
  637. )}
  638. <button
  639. onClick={() => setPasswordChangeUserData({ userId: user.id, newPassword: '' })}
  640. className="p-2.5 rounded-xl text-slate-400 hover:text-indigo-600 hover:bg-indigo-50 transition-all"
  641. title={t('changeUserPassword')}
  642. >
  643. <Key className="w-4.5 h-4.5" />
  644. </button>
  645. {user.id !== currentUser?.id && (
  646. <button
  647. onClick={() => handleDeleteUser(user.id)}
  648. className="p-2.5 rounded-xl text-slate-400 hover:text-red-500 hover:bg-red-50 transition-all"
  649. title={t('deleteUser')}
  650. >
  651. <Trash2 className="w-4.5 h-4.5" />
  652. </button>
  653. )}
  654. </>
  655. )}
  656. </div>
  657. </motion.div>
  658. );
  659. })}
  660. </AnimatePresence>
  661. </div>
  662. </div>
  663. );
  664. const renderDashboardTab = () => (
  665. <div className="max-w-6xl space-y-8 animate-in fade-in duration-500">
  666. <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
  667. <div className="p-6 bg-white border border-slate-200 rounded-3xl shadow-sm">
  668. <div className="flex items-center gap-3 mb-2 text-blue-600">
  669. <Building2 size={20} />
  670. <span className="text-xs font-black uppercase tracking-widest">Total Tenants</span>
  671. </div>
  672. <p className="text-3xl font-black text-slate-900">{stats.tenants}</p>
  673. </div>
  674. <div className="p-6 bg-white border border-slate-200 rounded-3xl shadow-sm">
  675. <div className="flex items-center gap-3 mb-2 text-indigo-600">
  676. <User size={20} />
  677. <span className="text-xs font-black uppercase tracking-widest">System Users</span>
  678. </div>
  679. <p className="text-3xl font-black text-slate-900">{stats.users}</p>
  680. </div>
  681. <div className="p-6 bg-emerald-50 border border-emerald-100 rounded-3xl shadow-sm">
  682. <div className="flex items-center gap-3 mb-2 text-emerald-600">
  683. <Sparkles size={20} />
  684. <span className="text-xs font-black uppercase tracking-widest">System Health</span>
  685. </div>
  686. <p className="text-xl font-bold text-emerald-700">Operational</p>
  687. </div>
  688. </div>
  689. <div className="bg-white border border-slate-200 rounded-3xl shadow-xl overflow-hidden">
  690. <div className="p-6 border-b border-slate-100 flex items-center justify-between">
  691. <div>
  692. <h3 className="font-black text-slate-900 text-lg tracking-tight">Organization Management</h3>
  693. <p className="text-xs font-medium text-slate-400">Global tenant list and control</p>
  694. </div>
  695. <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">
  696. <Plus size={16} /> New Tenant
  697. </button>
  698. </div>
  699. <div className="overflow-x-auto">
  700. <table className="w-full text-left">
  701. <thead className="bg-slate-50/50 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-100">
  702. <tr>
  703. <th className="px-6 py-4">Name</th>
  704. <th className="px-6 py-4">Domain</th>
  705. <th className="px-6 py-4">Admin</th>
  706. <th className="px-6 py-4">Features</th>
  707. <th className="px-6 py-4">Created</th>
  708. </tr>
  709. </thead>
  710. <tbody className="divide-y divide-slate-100">
  711. {tenants.map(t => (
  712. <tr key={t.id} className="hover:bg-slate-50/50 transition-colors group">
  713. <td className="px-6 py-4 text-sm font-bold text-slate-900">{t.name}</td>
  714. <td className="px-6 py-4 text-xs font-mono text-slate-500">{t.domain || '-'}</td>
  715. <td className="px-6 py-4 text-xs">
  716. {tenantAdmins.filter(u => u.tenantId === t.id).map(u => (
  717. <div key={u.id} className="flex items-center gap-1.5 text-slate-600 font-medium">
  718. <div className="w-5 h-5 rounded-full bg-indigo-50 flex items-center justify-center">
  719. <User className="w-3 h-3 text-indigo-500" />
  720. </div>
  721. {u.username}
  722. </div>
  723. ))}
  724. {tenantAdmins.filter(u => u.tenantId === t.id).length === 0 && (
  725. <div className="flex items-center gap-2">
  726. <select
  727. className="text-[10px] bg-slate-50 border border-slate-200 rounded-lg px-2 py-1 outline-none focus:ring-1 focus:ring-indigo-500/30 transition-all"
  728. onChange={(e) => handleBindAdmin(t.id, e.target.value)}
  729. defaultValue=""
  730. >
  731. <option value="" disabled>Bind Admin...</option>
  732. {users.filter(u => u.role !== 'SUPER_ADMIN' && (!u.tenantId || u.tenantId === 'default')).map(u => (
  733. <option key={u.id} value={u.id}>{u.username}</option>
  734. ))}
  735. </select>
  736. <span className="text-[10px] text-slate-400 italic">None</span>
  737. </div>
  738. )}
  739. </td>
  740. <td className="px-6 py-4">
  741. <div className="flex items-center gap-2">
  742. <div className={`p-1.5 rounded-lg ${t.settings_obj?.isNotebookEnabled ? 'bg-indigo-50 text-indigo-600' : 'bg-slate-50 text-slate-400'}`} title="Notebook Feature">
  743. <BookOpen size={14} />
  744. </div>
  745. <button
  746. onClick={() => handleToggleNotebookFeature(t.id, t.settings_obj?.isNotebookEnabled !== false)}
  747. className="text-[10px] font-black uppercase tracking-widest text-indigo-600 hover:underline"
  748. >
  749. {t.settings_obj?.isNotebookEnabled !== false ? 'Enabled' : 'Disabled'}
  750. </button>
  751. </div>
  752. </td>
  753. <td className="px-6 py-4 text-xs text-slate-400">{new Date(t.createdAt).toLocaleDateString()}</td>
  754. </tr>
  755. ))}
  756. </tbody>
  757. </table>
  758. </div>
  759. </div>
  760. {showCreateTenant && (
  761. <div className="fixed inset-0 z-[120] bg-slate-900/40 backdrop-blur-sm flex items-center justify-center p-4">
  762. <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">
  763. <h3 className="text-xl font-black text-slate-900 mb-6">Create New Tenant</h3>
  764. <form onSubmit={handleCreateTenant} className="space-y-5">
  765. <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 />
  766. <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 })} />
  767. <div className="space-y-2">
  768. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">Assign Initial Admin</label>
  769. <select
  770. 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"
  771. value={newTenant.adminUserId}
  772. onChange={e => setNewTenant({ ...newTenant, adminUserId: e.target.value })}
  773. >
  774. <option value="">Select a user (optional)</option>
  775. {users.filter(u => u.role !== 'SUPER_ADMIN').map(u => (
  776. <option key={u.id} value={u.id}>{u.username} ({u.role})</option>
  777. ))}
  778. </select>
  779. <p className="text-[10px] text-slate-400 px-1 italic">Selecting a user will promote them to Tenant Admin for this organization.</p>
  780. </div>
  781. <div className="flex gap-4 pt-2">
  782. <button type="button" onClick={() => setShowCreateTenant(false)} className="flex-1 py-3 text-slate-500 font-bold text-sm">Cancel</button>
  783. <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">Create</button>
  784. </div>
  785. </form>
  786. </motion.div>
  787. </div>
  788. )}
  789. </div>
  790. );
  791. const renderKnowledgeBaseTab = () => (
  792. <div className="space-y-8 animate-in slide-in-from-right duration-300 max-w-2xl pb-10">
  793. {kbSettings && (
  794. <>
  795. {/* Model Configuration */}
  796. <section className="bg-white/80 backdrop-blur-md p-8 rounded-3xl border border-slate-200/50 shadow-sm space-y-6">
  797. <div className="flex items-center gap-3 text-slate-900 font-black uppercase tracking-widest text-[11px] border-b border-slate-100 pb-4">
  798. <div className="w-8 h-8 rounded-xl bg-indigo-50 flex items-center justify-center text-indigo-600">
  799. <Cpu size={16} />
  800. </div>
  801. Model Configuration
  802. </div>
  803. <div className="grid grid-cols-1 gap-6">
  804. <div>
  805. <label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 px-1">Default LLM Model</label>
  806. <select
  807. value={kbSettings.selectedLLMId || ''}
  808. onChange={(e) => handleUpdateKbSettings('selectedLLMId', e.target.value)}
  809. 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"
  810. >
  811. <option value="">Select LLM...</option>
  812. {models.filter(m => m.type === ModelType.LLM).map(m => (
  813. <option key={m.id} value={m.id}>{m.name} ({m.modelId})</option>
  814. ))}
  815. </select>
  816. </div>
  817. <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
  818. <div>
  819. <label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 px-1">Embedding Model</label>
  820. <select
  821. value={kbSettings.selectedEmbeddingId || ''}
  822. onChange={(e) => handleUpdateKbSettings('selectedEmbeddingId', e.target.value)}
  823. 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"
  824. >
  825. <option value="">Select Embedding...</option>
  826. {models.filter(m => m.type === ModelType.EMBEDDING).map(m => (
  827. <option key={m.id} value={m.id}>{m.name}</option>
  828. ))}
  829. </select>
  830. </div>
  831. <div>
  832. <label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 px-1">Rerank Model</label>
  833. <select
  834. value={kbSettings.selectedRerankId || ''}
  835. onChange={(e) => handleUpdateKbSettings('selectedRerankId', e.target.value)}
  836. 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"
  837. >
  838. <option value="">None</option>
  839. {models.filter(m => m.type === ModelType.RERANK).map(m => (
  840. <option key={m.id} value={m.id}>{m.name}</option>
  841. ))}
  842. </select>
  843. </div>
  844. </div>
  845. </div>
  846. </section>
  847. {/* Chat Hyperparameters */}
  848. <section className="bg-white/80 backdrop-blur-md p-8 rounded-3xl border border-slate-200/50 shadow-sm space-y-6">
  849. <div className="flex items-center gap-3 text-slate-900 font-black uppercase tracking-widest text-[11px] border-b border-slate-100 pb-4">
  850. <div className="w-8 h-8 rounded-xl bg-pink-50 flex items-center justify-center text-pink-600">
  851. <Sparkles size={16} />
  852. </div>
  853. Chat Hyperparameters
  854. </div>
  855. <div className="space-y-8">
  856. <div>
  857. <div className="flex justify-between mb-3 px-1">
  858. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Temperature</label>
  859. <span className="text-sm font-black text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded-lg">{kbSettings.temperature}</span>
  860. </div>
  861. <input
  862. type="range"
  863. min="0"
  864. max="1"
  865. step="0.1"
  866. value={kbSettings.temperature || 0.7}
  867. onChange={(e) => handleUpdateKbSettings('temperature', parseFloat(e.target.value))}
  868. className="w-full h-2 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-indigo-600"
  869. />
  870. <div className="flex justify-between mt-2 px-1 text-[9px] font-bold text-slate-300 uppercase tracking-tighter">
  871. <span>Precise</span>
  872. <span>Creative</span>
  873. </div>
  874. </div>
  875. <div>
  876. <label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 px-1">Max Response Tokens</label>
  877. <input
  878. type="number"
  879. value={kbSettings.maxTokens || 2000}
  880. onChange={(e) => handleUpdateKbSettings('maxTokens', parseInt(e.target.value))}
  881. 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"
  882. />
  883. </div>
  884. </div>
  885. </section>
  886. {/* Retrieval & Search Settings */}
  887. <section className="bg-white/80 backdrop-blur-md p-8 rounded-3xl border border-slate-200/50 shadow-sm space-y-6">
  888. <div className="flex items-center gap-3 text-slate-900 font-black uppercase tracking-widest text-[11px] border-b border-slate-100 pb-4">
  889. <div className="w-8 h-8 rounded-xl bg-emerald-50 flex items-center justify-center text-emerald-600">
  890. <Database size={16} />
  891. </div>
  892. Retrieval & Search Settings
  893. </div>
  894. <div className="space-y-8">
  895. <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
  896. <div>
  897. <div className="flex justify-between mb-3 px-1">
  898. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Top-K Results</label>
  899. <span className="text-sm font-black text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded-lg">{kbSettings.topK}</span>
  900. </div>
  901. <input
  902. type="range"
  903. min="1"
  904. max="50"
  905. step="1"
  906. value={kbSettings.topK || 10}
  907. onChange={(e) => handleUpdateKbSettings('topK', parseInt(e.target.value))}
  908. className="w-full h-2 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-indigo-600"
  909. />
  910. </div>
  911. <div>
  912. <div className="flex justify-between mb-3 px-1">
  913. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Similarity Threshold</label>
  914. <span className="text-sm font-black text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded-lg">{kbSettings.similarityThreshold}</span>
  915. </div>
  916. <input
  917. type="range"
  918. min="0"
  919. max="1"
  920. step="0.05"
  921. value={kbSettings.similarityThreshold || 0.5}
  922. onChange={(e) => handleUpdateKbSettings('similarityThreshold', parseFloat(e.target.value))}
  923. className="w-full h-2 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-indigo-600"
  924. />
  925. </div>
  926. </div>
  927. <div className="space-y-4 pt-4 border-t border-slate-100">
  928. <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">
  929. <div>
  930. <div className="text-sm font-bold text-slate-800">Enable Hybrid Search</div>
  931. <div className="text-[10px] text-slate-400 font-medium">Combine vector and full-text search results.</div>
  932. </div>
  933. <button
  934. onClick={() => handleUpdateKbSettings('enableFullTextSearch', !kbSettings.enableFullTextSearch)}
  935. className={`relative inline-flex h-6 w-11 items-center rounded-full transition-all duration-300 ${kbSettings.enableFullTextSearch ? 'bg-indigo-600 shadow-md shadow-indigo-100' : 'bg-slate-300'}`}
  936. >
  937. <span className={`${kbSettings.enableFullTextSearch ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`} />
  938. </button>
  939. </div>
  940. {kbSettings.enableFullTextSearch && (
  941. <motion.div
  942. initial={{ opacity: 0, y: -10 }}
  943. animate={{ opacity: 1, y: 0 }}
  944. className="p-5 bg-indigo-50/30 rounded-2xl border border-indigo-100/50 space-y-4"
  945. >
  946. <div className="flex justify-between mb-2 px-1">
  947. <label className="text-[10px] font-black text-indigo-400 uppercase tracking-widest">Hybrid Weight (Vector vs Text)</label>
  948. <span className="text-sm font-black text-indigo-600">{kbSettings.hybridVectorWeight || 0.5}</span>
  949. </div>
  950. <input
  951. type="range"
  952. min="0"
  953. max="1"
  954. step="0.05"
  955. value={kbSettings.hybridVectorWeight || 0.5}
  956. onChange={(e) => handleUpdateKbSettings('hybridVectorWeight', parseFloat(e.target.value))}
  957. className="w-full h-2 bg-indigo-100 rounded-lg appearance-none cursor-pointer accent-indigo-600"
  958. />
  959. <div className="flex justify-between mt-1 px-1 text-[9px] font-bold text-indigo-300 uppercase">
  960. <span>Pure Text</span>
  961. <span>Pure Vector</span>
  962. </div>
  963. </motion.div>
  964. )}
  965. </div>
  966. </div>
  967. </section>
  968. </>
  969. )}
  970. </div>
  971. );
  972. const renderModelTab = () => (
  973. <div className="max-w-4xl space-y-6">
  974. <div className="flex justify-between items-center mb-6">
  975. <div>
  976. <h3 className="text-sm font-black text-slate-500 uppercase tracking-[0.15em] mb-1">{t('mmTitle')}</h3>
  977. <p className="text-xs text-slate-400 font-medium">{t('sidebarDesc')}</p>
  978. </div>
  979. {!editingId && currentUser?.role === 'SUPER_ADMIN' && (
  980. <button
  981. onClick={() => { setEditingId('new'); setModelFormData({ type: ModelType.LLM, baseUrl: 'http://localhost:11434/v1', modelId: 'llama3', name: '', dimensions: 1536 }); }}
  982. 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"
  983. >
  984. <Plus className="w-4 h-4" />
  985. {t('mmAddBtn')}
  986. </button>
  987. )}
  988. </div>
  989. {editingId ? (
  990. <motion.div
  991. initial={{ opacity: 0, y: 10 }}
  992. animate={{ opacity: 1, y: 0 }}
  993. className="bg-white/90 backdrop-blur-md p-10 rounded-3xl border border-slate-200/50 shadow-xl space-y-8"
  994. >
  995. <div className="flex items-center gap-4 mb-2">
  996. <div className="w-12 h-12 rounded-2xl bg-indigo-50 flex items-center justify-center text-indigo-600">
  997. <Cpu className="w-6 h-6" />
  998. </div>
  999. <h3 className="text-xl font-black text-slate-900 tracking-tight">{editingId === 'new' ? t('mmAddBtn') : t('mmEdit')}</h3>
  1000. </div>
  1001. <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
  1002. <div className="space-y-2">
  1003. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">{t('mmFormName')} *</label>
  1004. <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} />
  1005. </div>
  1006. <div className="space-y-2">
  1007. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">{t('mmFormModelId')} *</label>
  1008. <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} />
  1009. </div>
  1010. </div>
  1011. <div className="space-y-2">
  1012. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">{t('mmFormType')} *</label>
  1013. <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}>
  1014. <option value={ModelType.LLM}>{t('typeLLM')}</option>
  1015. <option value={ModelType.EMBEDDING}>{t('typeEmbedding')}</option>
  1016. <option value={ModelType.RERANK}>{t('typeRerank')}</option>
  1017. <option value={ModelType.VISION}>{t('typeVision')}</option>
  1018. </select>
  1019. </div>
  1020. <div className="space-y-2">
  1021. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">{t('mmFormBaseUrl')} *</label>
  1022. <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} />
  1023. </div>
  1024. <div className="space-y-2">
  1025. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">{t('mmFormApiKey')}</label>
  1026. <input
  1027. type="password"
  1028. 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"
  1029. value={modelFormData.apiKey || ''}
  1030. onChange={e => setModelFormData({ ...modelFormData, apiKey: e.target.value })}
  1031. disabled={isLoading}
  1032. placeholder={t('mmFormApiKeyPlaceholder')}
  1033. />
  1034. </div>
  1035. {modelFormData.type === ModelType.EMBEDDING && (
  1036. <div className="grid grid-cols-2 gap-6 p-6 bg-slate-50 rounded-3xl border border-slate-200/50">
  1037. <div className="space-y-2">
  1038. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">Max Input</label>
  1039. <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) })} />
  1040. </div>
  1041. <div className="space-y-2">
  1042. <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">Dimensions</label>
  1043. <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) })} />
  1044. </div>
  1045. </div>
  1046. )}
  1047. <div className="flex justify-end gap-3 pt-4">
  1048. <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>
  1049. <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}>
  1050. {isLoading && <Loader2 className="w-4 h-4 animate-spin" />}
  1051. {t('mmSave')}
  1052. </button>
  1053. </div>
  1054. </motion.div>
  1055. ) : (
  1056. <div className="grid grid-cols-1 gap-4 overflow-y-auto pr-2 scrollbar-hide">
  1057. <AnimatePresence>
  1058. {models.map((model, index) => (
  1059. <motion.div
  1060. key={model.id}
  1061. initial={{ opacity: 0, x: -10 }}
  1062. animate={{ opacity: 1, x: 0 }}
  1063. transition={{ delay: index * 0.05 }}
  1064. className="bg-white/70 backdrop-blur-md border border-slate-200/50 rounded-2xl p-5 flex justify-between items-center group hover:shadow-md hover:border-emerald-200/50 transition-all"
  1065. >
  1066. <div className="flex gap-4">
  1067. <div className={`w-12 h-12 rounded-xl flex items-center justify-center transition-all ${model.isEnabled !== false ? 'bg-emerald-50 text-emerald-600' : 'bg-slate-50 text-slate-300'}`}>
  1068. <Cpu size={22} />
  1069. </div>
  1070. <div>
  1071. <div className="flex items-center gap-2">
  1072. <h4 className="font-black text-slate-900">{model.name}</h4>
  1073. <span className="text-[9px] font-black bg-slate-100 text-slate-500 px-1.5 py-0.5 rounded-md uppercase tracking-wider">{getTypeLabel(model.type)}</span>
  1074. </div>
  1075. <p className="text-[10px] font-mono text-slate-400 mt-0.5">{model.modelId}</p>
  1076. </div>
  1077. </div>
  1078. <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-all">
  1079. <button
  1080. onClick={() => handleToggleModel(model)}
  1081. className={`p-2.5 rounded-xl transition-all ${((currentUser?.role === 'SUPER_ADMIN' && model.isEnabled !== false) || (currentUser?.role === 'TENANT_ADMIN' && enabledModelIds.includes(model.id))) ? 'text-emerald-500 hover:bg-emerald-50' : 'text-slate-300 hover:bg-slate-100'}`}
  1082. title={((currentUser?.role === 'SUPER_ADMIN' && model.isEnabled !== false) || (currentUser?.role === 'TENANT_ADMIN' && enabledModelIds.includes(model.id))) ? t('modelEnabled') : t('modelDisabled')}
  1083. >
  1084. {((currentUser?.role === 'SUPER_ADMIN' && model.isEnabled !== false) || (currentUser?.role === 'TENANT_ADMIN' && enabledModelIds.includes(model.id))) ? <ToggleRight size={24} /> : <ToggleLeft size={24} />}
  1085. </button>
  1086. {currentUser?.role === 'SUPER_ADMIN' && (
  1087. <>
  1088. <button onClick={() => { setEditingId(model.id); setModelFormData({ ...model }); }} className="p-2.5 rounded-xl text-slate-400 hover:text-indigo-600 hover:bg-indigo-50 transition-all"><Edit2 size={18} /></button>
  1089. <button onClick={() => handleDeleteModel(model.id)} className="p-2.5 rounded-xl text-slate-400 hover:text-red-500 hover:bg-red-50 transition-all"><Trash2 size={18} /></button>
  1090. </>
  1091. )}
  1092. </div>
  1093. </motion.div>
  1094. ))}
  1095. </AnimatePresence>
  1096. {models.length === 0 && (
  1097. <div className="bg-white/50 border-2 border-dashed border-slate-200 rounded-3xl p-16 text-center">
  1098. <Cpu className="w-12 h-12 text-slate-200 mx-auto mb-4" />
  1099. <p className="text-slate-400 font-bold uppercase tracking-widest text-xs">{t('mmEmpty')}</p>
  1100. </div>
  1101. )}
  1102. </div>
  1103. )}
  1104. </div>
  1105. );
  1106. return (
  1107. <div className="flex h-full bg-transparent overflow-hidden relative">
  1108. {/* Content Area */}
  1109. <div className="flex-1 flex flex-col min-w-0 h-full overflow-hidden relative z-10">
  1110. <div className="px-8 pt-8 pb-6 flex items-start justify-between shrink-0">
  1111. <div>
  1112. <h1 className="text-2xl font-bold text-slate-900 leading-tight">
  1113. {activeTab === 'general' ? t('generalSettings') : activeTab === 'user' ? t('userManagement') : activeTab === 'model' ? t('modelManagement') : activeTab === 'knowledge_base' ? 'Index Chat Config' : t('navTenants')}
  1114. </h1>
  1115. <p className="text-[15px] text-slate-500 mt-1">
  1116. {activeTab === 'general' ? 'Manage your application preferences.' : activeTab === 'user' ? 'Manage access and accounts.' : activeTab === 'model' ? 'Configure global AI models.' : activeTab === 'knowledge_base' ? 'Technical configuration for indexing and chat parameters.' : 'Global system overview.'}
  1117. </p>
  1118. </div>
  1119. </div>
  1120. <div className="flex-1 overflow-y-auto px-10 pb-10 scrollbar-hide">
  1121. <div className="max-w-4xl">
  1122. {error && (
  1123. <motion.div
  1124. initial={{ opacity: 0, y: -10 }}
  1125. animate={{ opacity: 1, y: 0 }}
  1126. 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"
  1127. >
  1128. <div className="w-8 h-8 rounded-full bg-red-100 flex items-center justify-center shrink-0">
  1129. <X className="w-4 h-4 text-red-600" />
  1130. </div>
  1131. <div>
  1132. <span className="font-black uppercase tracking-widest text-[10px] block mb-0.5">{t('errorLabel')}</span>
  1133. {error}
  1134. </div>
  1135. </motion.div>
  1136. )}
  1137. <AnimatePresence mode="wait">
  1138. <motion.div
  1139. key={activeTab}
  1140. initial={{ opacity: 0, x: 20 }}
  1141. animate={{ opacity: 1, x: 0 }}
  1142. exit={{ opacity: 0, x: -20 }}
  1143. transition={{ duration: 0.3 }}
  1144. >
  1145. {activeTab === 'general' && renderGeneralTab()}
  1146. {activeTab === 'user' && renderUserTab()}
  1147. {activeTab === 'model' && (isAdmin || currentUser?.role === 'SUPER_ADMIN') && renderModelTab()}
  1148. {activeTab === 'knowledge_base' && (isAdmin || currentUser?.role === 'SUPER_ADMIN') && renderKnowledgeBaseTab()}
  1149. {activeTab === 'dashboard' && currentUser?.role === 'SUPER_ADMIN' && renderDashboardTab()}
  1150. </motion.div>
  1151. </AnimatePresence>
  1152. </div>
  1153. </div>
  1154. </div>
  1155. </div>
  1156. );
  1157. };