anhuiqiang 3 долоо хоног өмнө
parent
commit
4f67169077

+ 4 - 2
server/src/api/api-v1.controller.ts

@@ -92,6 +92,7 @@ export class ApiV1Controller {
                     userSetting?.rerankSimilarityThreshold ?? 0.5, // rerankSimilarityThreshold
                     userSetting?.enableQueryExpansion ?? false, // enableQueryExpansion
                     userSetting?.enableHyDE ?? false,           // enableHyDE
+                    user.tenantId,                             // Passing tenantId correctly
                 );
 
                 for await (const chunk of stream) {
@@ -129,6 +130,7 @@ export class ApiV1Controller {
                     userSetting?.rerankSimilarityThreshold ?? 0.5,
                     userSetting?.enableQueryExpansion ?? false,
                     userSetting?.enableHyDE ?? false,
+                    user.tenantId,                            // Passing tenantId correctly
                 );
 
                 for await (const chunk of chatStream) {
@@ -197,7 +199,7 @@ export class ApiV1Controller {
     @Get('knowledge-bases')
     async listFiles(@Request() req) {
         const user = req.user;
-        const files = await this.knowledgeBaseService.findAll(user.id);
+        const files = await this.knowledgeBaseService.findAll(user.id, user.tenantId);
         return {
             files: files.map((f) => ({
                 id: f.id,
@@ -259,7 +261,7 @@ export class ApiV1Controller {
     @Get('knowledge-bases/:id')
     async getFile(@Request() req, @Param('id') id: string) {
         const user = req.user;
-        const files = await this.knowledgeBaseService.findAll(user.id);
+        const files = await this.knowledgeBaseService.findAll(user.id, user.tenantId);
         const file = files.find((f) => f.id === id);
         if (!file) return { error: 'File not found' };
         return file;

+ 19 - 6
server/src/super-admin/super-admin.controller.ts

@@ -1,27 +1,27 @@
-import { Controller, Get, Post, Put, Body, UseGuards, Param } from '@nestjs/common';
+import { Controller, Get, Post, Put, Body, UseGuards, Param, Delete } from '@nestjs/common';
 import { SuperAdminService } from './super-admin.service';
 import { CombinedAuthGuard } from '../auth/combined-auth.guard';
 import { RolesGuard } from '../auth/roles.guard';
 import { Roles } from '../auth/roles.decorator';
 import { UserRole } from '../user/user.entity';
 
-@Controller('v1/super-admin')
+@Controller('v1/tenants')
 @UseGuards(CombinedAuthGuard, RolesGuard)
 @Roles(UserRole.SUPER_ADMIN)
 export class SuperAdminController {
     constructor(private readonly superAdminService: SuperAdminService) { }
 
-    @Get('tenants')
+    @Get()
     async getTenants() {
         return this.superAdminService.getAllTenants();
     }
 
-    @Post('tenants')
+    @Post()
     async createTenant(@Body() body: { name: string; domain?: string; adminUserId?: string }) {
         return this.superAdminService.createTenant(body.name, body.domain, body.adminUserId);
     }
 
-    @Put('tenants/:tenantId/admin')
+    @Put(':tenantId/admin')
     async bindTenantAdmin(
         @Param('tenantId') tenantId: string,
         @Body() body: { userId: string }
@@ -29,11 +29,24 @@ export class SuperAdminController {
         return this.superAdminService.assignUserToTenant(body.userId, tenantId);
     }
 
-    @Post('tenants/:tenantId/admin/new')
+    @Post(':tenantId/admin/new')
     async createTenantAdmin(
         @Param('tenantId') tenantId: string,
         @Body() body: { username: string; password?: string }
     ) {
         return this.superAdminService.createTenantAdmin(tenantId, body.username, body.password);
     }
+
+    @Put(':tenantId')
+    async updateTenant(
+        @Param('tenantId') tenantId: string,
+        @Body() body: { name?: string; domain?: string }
+    ) {
+        return this.superAdminService.updateTenant(tenantId, body);
+    }
+
+    @Delete(':tenantId')
+    async deleteTenant(@Param('tenantId') tenantId: string) {
+        return this.superAdminService.deleteTenant(tenantId);
+    }
 }

+ 8 - 0
server/src/super-admin/super-admin.service.ts

@@ -44,5 +44,13 @@ export class SuperAdminService {
         };
     }
 
+    async updateTenant(tenantId: string, data: { name?: string; domain?: string }) {
+        return this.tenantService.update(tenantId, data);
+    }
+
+    async deleteTenant(tenantId: string) {
+        return this.tenantService.remove(tenantId);
+    }
+
     // NOTE: Model Management would be added here depending on ModelService functionality
 }

+ 4 - 1
server/src/tenant/tenant.service.ts

@@ -18,7 +18,10 @@ export class TenantService {
     ) { }
 
     async findAll(): Promise<Tenant[]> {
-        return this.tenantRepository.find({ order: { createdAt: 'ASC' } });
+        return this.tenantRepository.find({
+            relations: ['users'],
+            order: { createdAt: 'ASC' }
+        });
     }
 
     async findById(id: string): Promise<Tenant> {

+ 1 - 0
server/src/user/user.controller.ts

@@ -55,6 +55,7 @@ export class UserController {
       username: user.username,
       role: user.role,
       tenantId: user.tenantId,
+      tenantName: user.tenant?.name || 'Default',
       isAdmin: user.isAdmin,
       isNotebookEnabled,
     };

+ 1 - 1
server/src/user/user.service.ts

@@ -106,7 +106,7 @@ export class UserService implements OnModuleInit {
   }
 
   async findOneById(userId: string): Promise<User | null> {
-    return this.usersRepository.findOne({ where: { id: userId } });
+    return this.usersRepository.findOne({ where: { id: userId }, relations: ['tenant'] });
   }
 
   async findByApiKey(apiKeyValue: string): Promise<User | null> {

+ 394 - 94
web/components/views/SettingsView.tsx

@@ -20,7 +20,7 @@ interface SettingsViewProps {
     initialTab?: TabType;
 }
 
-type TabType = 'general' | 'user' | 'model' | 'dashboard' | 'knowledge_base';
+type TabType = 'general' | 'user' | 'model' | 'tenants' | 'knowledge_base';
 
 export const SettingsView: React.FC<SettingsViewProps> = ({
     models,
@@ -75,6 +75,10 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
     const [isSettingsLoading, setIsSettingsLoading] = useState(false);
     const [enabledModelIds, setEnabledModelIds] = useState<string[]>([]);
 
+    // --- Tenant Admin Binding Search State ---
+    const [bindingTenantId, setBindingTenantId] = useState<string | null>(null);
+    const [userSearchQuery, setUserSearchQuery] = useState('');
+
     useEffect(() => {
         if (initialTab) {
             setActiveTab(initialTab);
@@ -89,8 +93,9 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
             fetchSettingsAndGroups();
         } else if (activeTab === 'model' && (isAdmin || currentUser?.role === 'SUPER_ADMIN')) {
             // Model tab initialization
-        } else if (activeTab === 'dashboard' && currentUser?.role === 'SUPER_ADMIN') {
-            fetchDashboardData();
+        } else if (activeTab === 'tenants' && currentUser?.role === 'SUPER_ADMIN') {
+            fetchTenantsData();
+            fetchUsers(); // Ensure users are loaded for admin binding
         } else if (activeTab === 'knowledge_base' && (currentUser?.role === 'TENANT_ADMIN' || currentUser?.role === 'SUPER_ADMIN')) {
             fetchKnowledgeBaseSettings();
         }
@@ -244,20 +249,21 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
         }
     };
 
-    // --- Dashboard State (Migrated) ---
+    // --- Tenants Management State (Migrated) ---
     const [stats, setStats] = useState({ tenants: 0, users: 0 });
     const [tenants, setTenants] = useState<any[]>([]);
     const [tenantAdmins, setTenantAdmins] = useState<any[]>([]);
-    const [isDashboardLoading, setIsDashboardLoading] = useState(false);
+    const [isTenantsLoading, setIsTenantsLoading] = useState(false);
     const [showCreateTenant, setShowCreateTenant] = useState(false);
+    const [editingTenant, setEditingTenant] = useState<any | null>(null);
     const [newTenant, setNewTenant] = useState({ name: '', domain: '', adminUserId: '' });
 
-    const fetchDashboardData = async () => {
-        setIsDashboardLoading(true);
+    const fetchTenantsData = async () => {
+        setIsTenantsLoading(true);
         try {
             const headers = { 'Authorization': `Bearer ${authToken}`, 'x-api-key': authToken || '' };
             const [tenRes, admRes] = await Promise.all([
-                fetch('/api/v1/super-admin/tenants', { headers }),
+                fetch('/api/v1/tenants', { headers }),
                 fetch('/api/v1/admin/users', { headers })
             ]);
             if (tenRes.ok) {
@@ -273,40 +279,80 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
         } catch (e) {
             console.error(e);
         } finally {
-            setIsDashboardLoading(false);
+            setIsTenantsLoading(false);
         }
     };
 
     const handleCreateTenant = async (e: React.FormEvent) => {
         e.preventDefault();
         try {
-            const res = await fetch('/api/v1/super-admin/tenants', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}`, 'x-api-key': authToken || '' },
-                body: JSON.stringify(newTenant)
+            if (editingTenant) {
+                const res = await fetch(`/api/v1/tenants/${editingTenant.id}`, {
+                    method: 'PUT',
+                    headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}`, 'x-api-key': authToken || '' },
+                    body: JSON.stringify({ name: newTenant.name, domain: newTenant.domain })
+                });
+                if (res.ok) {
+                    setEditingTenant(null);
+                    setNewTenant({ name: '', domain: '', adminUserId: '' });
+                    fetchTenantsData();
+                    showSuccess('Tenant updated successfully');
+                } else {
+                    showError('Failed to update tenant');
+                }
+            } else {
+                const res = await fetch('/api/v1/tenants', {
+                    method: 'POST',
+                    headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}`, 'x-api-key': authToken || '' },
+                    body: JSON.stringify(newTenant)
+                });
+                if (res.ok) {
+                    setShowCreateTenant(false);
+                    setNewTenant({ name: '', domain: '', adminUserId: '' });
+                    fetchTenantsData();
+                    showSuccess('Tenant created successfully');
+                } else {
+                    showError('Failed to create tenant');
+                }
+            }
+        } catch (e) {
+            showError('Error processing tenant');
+        }
+    };
+
+    const handleDeleteTenant = async (tenantId: string) => {
+        if (!(await confirm('Are you sure you want to delete this tenant? All associated data will be removed.'))) return;
+        try {
+            const res = await fetch(`/api/v1/tenants/${tenantId}`, {
+                method: 'DELETE',
+                headers: { 'Authorization': `Bearer ${authToken}`, 'x-api-key': authToken || '' }
             });
             if (res.ok) {
-                setShowCreateTenant(false);
-                setNewTenant({ name: '', domain: '', adminUserId: '' });
-                fetchDashboardData();
-                showSuccess('Tenant created successfully');
+                showSuccess('Tenant deleted successfully');
+                fetchTenantsData();
+            } else {
+                showError('Failed to delete tenant');
             }
         } catch (e) {
-            showError('Failed to create tenant');
+            showError('Error deleting tenant');
         }
     };
 
     const handleBindAdmin = async (tenantId: string, userId: string) => {
         if (!userId) return;
         try {
-            const res = await fetch(`/api/v1/super-admin/tenants/${tenantId}/admin`, {
+            const res = await fetch(`/api/v1/tenants/${tenantId}/admin`, {
                 method: 'PUT',
                 headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}`, 'x-api-key': authToken || '' },
                 body: JSON.stringify({ userId })
             });
             if (res.ok) {
                 showSuccess('Admin bound successfully');
-                fetchDashboardData();
+                // Refresh tenants data and users to ensure all states are in sync
+                await Promise.all([
+                    fetchTenantsData(),
+                    fetchUsers()
+                ]);
             } else {
                 showError('Failed to bind admin');
             }
@@ -324,7 +370,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
             });
             if (res.ok) {
                 showSuccess('Feature updated successfully');
-                fetchDashboardData();
+                fetchTenantsData();
             }
         } catch (e) {
             showError('Failed to update feature');
@@ -504,7 +550,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
     );
 
     const renderUserTab = () => (
-        <div className="space-y-6 max-w-4xl">
+        <div className="space-y-6 w-full">
             <div className="flex justify-between items-center mb-6">
                 <div>
                     <h3 className="text-sm font-black text-slate-500 uppercase tracking-[0.15em] mb-1">{t('userList')}</h3>
@@ -722,8 +768,8 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
         </div>
     );
 
-    const renderDashboardTab = () => (
-        <div className="max-w-6xl space-y-8 animate-in fade-in duration-500">
+    const renderTenantsTab = () => (
+        <div className="w-full space-y-8 animate-in fade-in duration-500">
             <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
                 <div className="p-6 bg-white border border-slate-200 rounded-3xl shadow-sm">
                     <div className="flex items-center gap-3 mb-2 text-blue-600">
@@ -767,6 +813,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                                 <th className="px-6 py-4">Admin</th>
                                 <th className="px-6 py-4">Features</th>
                                 <th className="px-6 py-4">Created</th>
+                                <th className="px-6 py-4 text-right">Actions</th>
                             </tr>
                         </thead>
                         <tbody className="divide-y divide-slate-100">
@@ -775,29 +822,49 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                                     <td className="px-6 py-4 text-sm font-bold text-slate-900">{t.name}</td>
                                     <td className="px-6 py-4 text-xs font-mono text-slate-500">{t.domain || '-'}</td>
                                     <td className="px-6 py-4 text-xs">
-                                        {tenantAdmins.filter(u => u.tenantId === t.id).map(u => (
-                                            <div key={u.id} className="flex items-center gap-1.5 text-slate-600 font-medium">
-                                                <div className="w-5 h-5 rounded-full bg-indigo-50 flex items-center justify-center">
-                                                    <User className="w-3 h-3 text-indigo-500" />
-                                                </div>
-                                                {u.username}
-                                            </div>
-                                        ))}
-                                        {tenantAdmins.filter(u => u.tenantId === t.id).length === 0 && (
-                                            <div className="flex items-center gap-2">
-                                                <select
-                                                    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"
-                                                    onChange={(e) => handleBindAdmin(t.id, e.target.value)}
-                                                    defaultValue=""
-                                                >
-                                                    <option value="" disabled>Bind Admin...</option>
-                                                    {users.filter(u => u.role !== 'SUPER_ADMIN' && (!u.tenantId || u.tenantId === 'default')).map(u => (
-                                                        <option key={u.id} value={u.id}>{u.username}</option>
-                                                    ))}
-                                                </select>
-                                                <span className="text-[10px] text-slate-400 italic">None</span>
-                                            </div>
-                                        )}
+                                        {(() => {
+                                            const admin = users.find((u: any) =>
+                                                (u.tenantId === t.id || u.tenant_id === t.id || (t.name === 'Default' && (u.tenantId === 'default' || u.tenant_id === 'default'))) &&
+                                                (u.role === 'TENANT_ADMIN' || u.isAdmin)
+                                            );
+                                            if (admin) {
+                                                return (
+                                                    <div className="flex items-center gap-1.5 text-slate-600 font-medium">
+                                                        <div className="w-5 h-5 rounded-full bg-indigo-50 flex items-center justify-center">
+                                                            <User className="w-3 h-3 text-indigo-500" />
+                                                        </div>
+                                                        {admin.username}
+                                                        <button
+                                                            onClick={() => {
+                                                                setBindingTenantId(t.id);
+                                                                setUserSearchQuery('');
+                                                                fetchUsers();
+                                                            }}
+                                                            className="ml-2 p-1 hover:bg-indigo-50 text-slate-400 hover:text-indigo-600 rounded transition-all"
+                                                            title="Change Admin"
+                                                        >
+                                                            <Edit2 size={12} />
+                                                        </button>
+                                                    </div>
+                                                );
+                                            } else {
+                                                return (
+                                                    <div className="flex items-center gap-2">
+                                                        <button
+                                                            onClick={() => {
+                                                                setBindingTenantId(t.id);
+                                                                setUserSearchQuery('');
+                                                                fetchUsers();
+                                                            }}
+                                                            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"
+                                                        >
+                                                            Bind Admin
+                                                        </button>
+                                                        <span className="text-[10px] text-slate-400 italic">None</span>
+                                                    </div>
+                                                );
+                                            }
+                                        })()}
                                     </td>
                                     <td className="px-6 py-4">
                                         <div className="flex items-center gap-2">
@@ -813,6 +880,27 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                                         </div>
                                     </td>
                                     <td className="px-6 py-4 text-xs text-slate-400">{new Date(t.createdAt).toLocaleDateString()}</td>
+                                    <td className="px-6 py-4 text-right">
+                                        <div className="flex justify-end gap-2 opacity-0 group-hover:opacity-100 transition-all">
+                                            <button
+                                                onClick={() => {
+                                                    setEditingTenant(t);
+                                                    setNewTenant({ name: t.name, domain: t.domain || '', adminUserId: '' });
+                                                }}
+                                                className="p-1.5 hover:bg-slate-100 text-slate-400 hover:text-indigo-600 rounded-lg transition-all"
+                                                title="Edit Tenant"
+                                            >
+                                                <Edit2 size={14} />
+                                            </button>
+                                            <button
+                                                onClick={() => handleDeleteTenant(t.id)}
+                                                className="p-1.5 hover:bg-red-50 text-slate-400 hover:text-red-500 rounded-lg transition-all"
+                                                title="Delete Tenant"
+                                            >
+                                                <Trash2 size={14} />
+                                            </button>
+                                        </div>
+                                    </td>
                                 </tr>
                             ))}
                         </tbody>
@@ -820,41 +908,128 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                 </div>
             </div>
 
-            {showCreateTenant && (
+            {(showCreateTenant || editingTenant) && (
                 <div className="fixed inset-0 z-[120] bg-slate-900/40 backdrop-blur-sm flex items-center justify-center p-4">
                     <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">
-                        <h3 className="text-xl font-black text-slate-900 mb-6">Create New Tenant</h3>
+                        <h3 className="text-xl font-black text-slate-900 mb-6">{editingTenant ? 'Edit Organization' : 'Create New Tenant'}</h3>
                         <form onSubmit={handleCreateTenant} className="space-y-5">
                             <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 />
                             <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 })} />
 
-                            <div className="space-y-2">
-                                <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">Assign Initial Admin</label>
-                                <select
-                                    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"
-                                    value={newTenant.adminUserId}
-                                    onChange={e => setNewTenant({ ...newTenant, adminUserId: e.target.value })}
-                                >
-                                    <option value="">Select a user (optional)</option>
-                                    {users.filter(u => u.role !== 'SUPER_ADMIN').map(u => (
-                                        <option key={u.id} value={u.id}>{u.username} ({u.role})</option>
-                                    ))}
-                                </select>
-                                <p className="text-[10px] text-slate-400 px-1 italic">Selecting a user will promote them to Tenant Admin for this organization.</p>
-                            </div>
+                            {!editingTenant && (
+                                <div className="space-y-2">
+                                    <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">Assign Initial Admin</label>
+                                    <select
+                                        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"
+                                        value={newTenant.adminUserId}
+                                        onChange={e => setNewTenant({ ...newTenant, adminUserId: e.target.value })}
+                                    >
+                                        <option value="">Select a user (optional)</option>
+                                        {users.filter(u => u.role === 'TENANT_ADMIN').map(u => (
+                                            <option key={u.id} value={u.id}>{u.username}</option>
+                                        ))}
+                                    </select>
+                                    <p className="text-[10px] text-slate-400 px-1 italic">Selecting a user will promote them to Tenant Admin for this organization.</p>
+                                </div>
+                            )}
 
                             <div className="flex gap-4 pt-2">
-                                <button type="button" onClick={() => setShowCreateTenant(false)} className="flex-1 py-3 text-slate-500 font-bold text-sm">Cancel</button>
-                                <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>
+                                <button type="button" onClick={() => { setShowCreateTenant(false); setEditingTenant(null); }} className="flex-1 py-3 text-slate-500 font-bold text-sm">Cancel</button>
+                                <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>
                             </div>
                         </form>
                     </motion.div>
                 </div>
             )}
+
+            {/* Bind Admin Search Modal */}
+            <AnimatePresence>
+                {bindingTenantId && (
+                    <div className="fixed inset-0 z-[130] bg-slate-900/60 backdrop-blur-md flex items-center justify-center p-4">
+                        <motion.div
+                            initial={{ scale: 0.9, opacity: 0 }}
+                            animate={{ scale: 1, opacity: 1 }}
+                            exit={{ scale: 0.9, opacity: 0 }}
+                            className="bg-white rounded-[2.5rem] p-8 w-full max-w-lg shadow-2xl border border-white/20 overflow-hidden relative"
+                        >
+                            <div className="absolute top-0 right-0 p-6">
+                                <button onClick={() => setBindingTenantId(null)} className="p-2 hover:bg-slate-100 rounded-2xl transition-all">
+                                    <X size={20} className="text-slate-400" />
+                                </button>
+                            </div>
+
+                            <div className="mb-8">
+                                <h3 className="text-2xl font-black text-slate-900 tracking-tight mb-2">Bind Tenant Admin</h3>
+                                <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>
+                            </div>
+
+                            <div className="relative mb-6">
+                                <div className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400">
+                                    <Globe size={18} />
+                                </div>
+                                <input
+                                    autoFocus
+                                    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"
+                                    placeholder="Search users by name..."
+                                    value={userSearchQuery}
+                                    onChange={e => setUserSearchQuery(e.target.value)}
+                                />
+                            </div>
+
+                            <div className="max-h-[350px] overflow-y-auto pr-2 flex flex-col gap-3 scrollbar-hide">
+                                {users
+                                    .filter(u =>
+                                        (u.role === 'TENANT_ADMIN' || u.isAdmin) &&
+                                        u.role !== 'SUPER_ADMIN' &&
+                                        u.username.toLowerCase().includes(userSearchQuery.toLowerCase())
+                                    )
+                                    .map(u => (
+                                        <button
+                                            key={u.id}
+                                            onClick={() => {
+                                                handleBindAdmin(bindingTenantId, u.id);
+                                                setBindingTenantId(null);
+                                            }}
+                                            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"
+                                        >
+                                            <div className="flex items-center gap-3">
+                                                <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">
+                                                    <User size={18} />
+                                                </div>
+                                                <div className="text-left">
+                                                    <p className="text-sm font-black text-slate-900">{u.username}</p>
+                                                    <p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{u.role || 'User'}</p>
+                                                </div>
+                                            </div>
+                                            <ChevronRight size={16} className="text-slate-300 group-hover:text-indigo-400 group-hover:translate-x-1 transition-all" />
+                                        </button>
+                                    ))}
+
+                                {users.filter(u =>
+                                    (u.role === 'TENANT_ADMIN' || u.isAdmin) &&
+                                    u.role !== 'SUPER_ADMIN' &&
+                                    u.username.toLowerCase().includes(userSearchQuery.toLowerCase())
+                                ).length === 0 && (
+                                        <div className="py-12 text-center opacity-40">
+                                            <User size={40} className="mx-auto mb-3" />
+                                            <p className="text-xs font-bold uppercase tracking-widest">No unassigned users found</p>
+                                        </div>
+                                    )}
+                            </div>
+
+                            <div className="mt-8 pt-6 border-t border-slate-100 flex justify-end">
+                                <button onClick={() => setBindingTenantId(null)} className="px-6 py-3 text-sm font-bold text-slate-500 hover:text-slate-700 transition-all">
+                                    Cancel
+                                </button>
+                            </div>
+                        </motion.div>
+                    </div>
+                )}
+            </AnimatePresence>
         </div>
     );
     const renderKnowledgeBaseTab = () => (
-        <div className="space-y-8 animate-in slide-in-from-right duration-300 max-w-2xl pb-10">
+        <div className="space-y-8 animate-in slide-in-from-right duration-300 w-full max-w-5xl pb-10">
             {kbSettings && (
                 <>
                     {/* Model Configuration */}
@@ -1031,6 +1206,73 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                                         </div>
                                     </motion.div>
                                 )}
+
+                                <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+                                    <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">
+                                        <div>
+                                            <div className="text-sm font-bold text-slate-800">Enable Query Expansion</div>
+                                            <div className="text-[10px] text-slate-400 font-medium">Rewrites query for better recall.</div>
+                                        </div>
+                                        <button
+                                            onClick={() => handleUpdateKbSettings('enableQueryExpansion', !kbSettings.enableQueryExpansion)}
+                                            className={`relative inline-flex h-6 w-11 items-center rounded-full transition-all duration-300 ${kbSettings.enableQueryExpansion ? 'bg-indigo-600 shadow-md shadow-indigo-100' : 'bg-slate-300'}`}
+                                        >
+                                            <span className={`${kbSettings.enableQueryExpansion ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`} />
+                                        </button>
+                                    </div>
+
+                                    <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">
+                                        <div>
+                                            <div className="text-sm font-bold text-slate-800">Enable HyDE</div>
+                                            <div className="text-[10px] text-slate-400 font-medium">Hypothetical Document Embeddings.</div>
+                                        </div>
+                                        <button
+                                            onClick={() => handleUpdateKbSettings('enableHyDE', !kbSettings.enableHyDE)}
+                                            className={`relative inline-flex h-6 w-11 items-center rounded-full transition-all duration-300 ${kbSettings.enableHyDE ? 'bg-indigo-600 shadow-md shadow-indigo-100' : 'bg-slate-300'}`}
+                                        >
+                                            <span className={`${kbSettings.enableHyDE ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`} />
+                                        </button>
+                                    </div>
+                                </div>
+
+                                <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">
+                                    <div>
+                                        <div className="text-sm font-bold text-slate-800">Enable Reranking</div>
+                                        <div className="text-[10px] text-slate-400 font-medium">Re-score search results for higher accuracy.</div>
+                                    </div>
+                                    <button
+                                        onClick={() => handleUpdateKbSettings('enableRerank', !kbSettings.enableRerank)}
+                                        className={`relative inline-flex h-6 w-11 items-center rounded-full transition-all duration-300 ${kbSettings.enableRerank ? 'bg-indigo-600 shadow-md shadow-indigo-100' : 'bg-slate-300'}`}
+                                    >
+                                        <span className={`${kbSettings.enableRerank ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`} />
+                                    </button>
+                                </div>
+
+                                {kbSettings.enableRerank && (
+                                    <motion.div
+                                        initial={{ opacity: 0, y: -10 }}
+                                        animate={{ opacity: 1, y: 0 }}
+                                        className="p-5 bg-indigo-50/30 rounded-2xl border border-indigo-100/50 space-y-4"
+                                    >
+                                        <div className="flex justify-between mb-2 px-1">
+                                            <label className="text-[10px] font-black text-indigo-400 uppercase tracking-widest">Rerank Similarity Threshold</label>
+                                            <span className="text-sm font-black text-indigo-600">{kbSettings.rerankSimilarityThreshold || 0.5}</span>
+                                        </div>
+                                        <input
+                                            type="range"
+                                            min="0"
+                                            max="1"
+                                            step="0.05"
+                                            value={kbSettings.rerankSimilarityThreshold || 0.5}
+                                            onChange={(e) => handleUpdateKbSettings('rerankSimilarityThreshold', parseFloat(e.target.value))}
+                                            className="w-full h-2 bg-indigo-100 rounded-lg appearance-none cursor-pointer accent-indigo-600"
+                                        />
+                                        <div className="flex justify-between mt-1 px-1 text-[9px] font-bold text-indigo-300 uppercase">
+                                            <span>Broad</span>
+                                            <span>Strict</span>
+                                        </div>
+                                    </motion.div>
+                                )}
                             </div>
                         </div>
                     </section>
@@ -1040,7 +1282,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
     );
 
     const renderModelTab = () => (
-        <div className="max-w-4xl space-y-6">
+        <div className="w-full space-y-6">
             <div className="flex justify-between items-center mb-6">
                 <div>
                     <h3 className="text-sm font-black text-slate-500 uppercase tracking-[0.15em] mb-1">{t('mmTitle')}</h3>
@@ -1130,42 +1372,100 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                     </div>
                 </motion.div>
             ) : (
-                <div className="grid grid-cols-1 gap-4 overflow-y-auto pr-2 scrollbar-hide">
+                <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">
                     <AnimatePresence>
                         {models.map((model, index) => (
                             <motion.div
                                 key={model.id}
-                                initial={{ opacity: 0, x: -10 }}
-                                animate={{ opacity: 1, x: 0 }}
+                                initial={{ opacity: 0, scale: 0.95 }}
+                                animate={{ opacity: 1, scale: 1 }}
                                 transition={{ delay: index * 0.05 }}
-                                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"
+                                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"
                             >
-                                <div className="flex gap-4">
-                                    <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'}`}>
-                                        <Cpu size={22} />
+                                {/* Subtle background pattern/glow */}
+                                <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" />
+
+                                <div className="relative z-10">
+                                    <div className="flex items-start justify-between mb-5">
+                                        <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'}`}>
+                                            <Cpu size={26} strokeWidth={2.5} />
+                                        </div>
+                                        <div className="flex gap-1 items-center bg-slate-50 p-1 rounded-xl border border-slate-100/50">
+                                            <button
+                                                onClick={() => handleToggleModel(model)}
+                                                className={`p-1.5 rounded-lg transition-all ${((currentUser?.role === 'SUPER_ADMIN' && model.isEnabled !== false) || (currentUser?.role === 'TENANT_ADMIN' && enabledModelIds.includes(model.id))) ? 'text-indigo-600 bg-white shadow-sm' : 'text-slate-400 hover:text-slate-600'}`}
+                                                title={((currentUser?.role === 'SUPER_ADMIN' && model.isEnabled !== false) || (currentUser?.role === 'TENANT_ADMIN' && enabledModelIds.includes(model.id))) ? t('modelEnabled') : t('modelDisabled')}
+                                            >
+                                                {((currentUser?.role === 'SUPER_ADMIN' && model.isEnabled !== false) || (currentUser?.role === 'TENANT_ADMIN' && enabledModelIds.includes(model.id))) ? <ToggleRight size={24} /> : <ToggleLeft size={24} />}
+                                            </button>
+                                        </div>
                                     </div>
-                                    <div>
+
+                                    <div className="space-y-1 mb-6">
+                                        <div className="flex items-center gap-2.5">
+                                            <h4 className="font-black text-slate-900 text-lg tracking-tight truncate">{model.name}</h4>
+                                        </div>
                                         <div className="flex items-center gap-2">
-                                            <h4 className="font-black text-slate-900">{model.name}</h4>
-                                            <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>
+                                            <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">
+                                                {getTypeLabel(model.type)}
+                                            </span>
+                                            {model.isDefault && (
+                                                <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">
+                                                    <Sparkles size={8} /> Default
+                                                </span>
+                                            )}
                                         </div>
-                                        <p className="text-[10px] font-mono text-slate-400 mt-0.5">{model.modelId}</p>
+                                        <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">
+                                            {model.modelId}
+                                        </p>
+                                    </div>
+
+                                    {/* Additional info grid */}
+                                    <div className="grid grid-cols-2 gap-3 mb-6">
+                                        {model.type === ModelType.EMBEDDING && (
+                                            <>
+                                                <div className="bg-slate-50/50 p-2.5 rounded-2xl border border-slate-100/50">
+                                                    <span className="block text-[8px] font-black text-slate-400 uppercase tracking-widest mb-0.5">Dims</span>
+                                                    <span className="text-xs font-bold text-slate-700">{model.dimensions || '-'}</span>
+                                                </div>
+                                                <div className="bg-slate-50/50 p-2.5 rounded-2xl border border-slate-100/50">
+                                                    <span className="block text-[8px] font-black text-slate-400 uppercase tracking-widest mb-0.5">Ctx</span>
+                                                    <span className="text-xs font-bold text-slate-700">{model.maxInputTokens || '-'}</span>
+                                                </div>
+                                            </>
+                                        )}
+                                        {model.type === ModelType.LLM && (
+                                            <div className="col-span-2 bg-slate-50/50 p-2.5 rounded-2xl border border-slate-100/50 flex items-center justify-between">
+                                                <span className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Base API</span>
+                                                <span className="text-[9px] font-mono font-medium text-slate-600 max-w-[140px] truncate">{model.baseUrl}</span>
+                                            </div>
+                                        )}
                                     </div>
                                 </div>
-                                <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-all">
-                                    <button
-                                        onClick={() => handleToggleModel(model)}
-                                        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'}`}
-                                        title={((currentUser?.role === 'SUPER_ADMIN' && model.isEnabled !== false) || (currentUser?.role === 'TENANT_ADMIN' && enabledModelIds.includes(model.id))) ? t('modelEnabled') : t('modelDisabled')}
-                                    >
-                                        {((currentUser?.role === 'SUPER_ADMIN' && model.isEnabled !== false) || (currentUser?.role === 'TENANT_ADMIN' && enabledModelIds.includes(model.id))) ? <ToggleRight size={24} /> : <ToggleLeft size={24} />}
-                                    </button>
-                                    {currentUser?.role === 'SUPER_ADMIN' && (
-                                        <>
-                                            <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>
-                                            <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>
-                                        </>
-                                    )}
+
+                                <div className="flex items-center justify-between pt-4 border-t border-slate-100/50 relative z-10">
+                                    <div className="flex items-center gap-1 text-[10px] font-bold text-slate-400">
+                                        <SettingsIcon size={12} />
+                                        Configured
+                                    </div>
+                                    <div className="flex gap-2">
+                                        {currentUser?.role === 'SUPER_ADMIN' && (
+                                            <>
+                                                <button
+                                                    onClick={() => { setEditingId(model.id); setModelFormData({ ...model }); }}
+                                                    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"
+                                                >
+                                                    <Edit2 size={15} />
+                                                </button>
+                                                <button
+                                                    onClick={() => handleDeleteModel(model.id)}
+                                                    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"
+                                                >
+                                                    <Trash2 size={15} />
+                                                </button>
+                                            </>
+                                        )}
+                                    </div>
                                 </div>
                             </motion.div>
                         ))}
@@ -1200,7 +1500,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                 </div>
 
                 <div className="flex-1 overflow-y-auto px-10 pb-10 scrollbar-hide">
-                    <div className="max-w-4xl">
+                    <div className="w-full">
                         {error && (
                             <motion.div
                                 initial={{ opacity: 0, y: -10 }}
@@ -1229,7 +1529,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                                 {activeTab === 'user' && renderUserTab()}
                                 {activeTab === 'model' && (isAdmin || currentUser?.role === 'SUPER_ADMIN') && renderModelTab()}
                                 {activeTab === 'knowledge_base' && (isAdmin || currentUser?.role === 'SUPER_ADMIN') && renderKnowledgeBaseTab()}
-                                {activeTab === 'dashboard' && currentUser?.role === 'SUPER_ADMIN' && renderDashboardTab()}
+                                {activeTab === 'tenants' && currentUser?.role === 'SUPER_ADMIN' && renderTenantsTab()}
                             </motion.div>
                         </AnimatePresence>
                     </div>

+ 1 - 1
web/index.tsx

@@ -88,7 +88,7 @@ function App() {
                     <Route path="users" element={<SettingsPage initialTab="user" />} />
                     <Route path="models" element={<SettingsPage initialTab="model" />} />
                     <Route path="kb-settings" element={<SettingsPage initialTab="knowledge_base" />} />
-                    <Route path="dashboard" element={<ProtectedRoute allowedRoles={['SUPER_ADMIN']}><SettingsPage initialTab="dashboard" /></ProtectedRoute>} />
+                    <Route path="tenants" element={<ProtectedRoute allowedRoles={['SUPER_ADMIN']}><SettingsPage initialTab="tenants" /></ProtectedRoute>} />
                   </Route>
 
                   {/* Remove standalone Admin routes as we integrated Dashboard into Settings */}

+ 3 - 3
web/src/components/layouts/WorkspaceLayout.tsx

@@ -155,8 +155,8 @@ export default function WorkspaceLayout() {
                             <SidebarItem
                                 icon={LayoutGrid}
                                 label={t('navTenants')}
-                                path="/dashboard"
-                                isActive={location.pathname.startsWith('/dashboard')}
+                                path="/tenants"
+                                isActive={location.pathname.startsWith('/tenants')}
                                 onClick={handleNavClick}
                             />
                         )}
@@ -176,7 +176,7 @@ export default function WorkspaceLayout() {
                                     {user?.role?.replace('_', ' ') || 'USER'}
                                 </span>
                             </div>
-                            <p className="text-[12px] text-slate-500 truncate">{user?.tenant_name || 'Default Corp'}</p>
+                            <p className="text-[12px] text-slate-500 truncate">{user?.tenant_name || 'Default'}</p>
                         </div>
                     </div>
                     <button

+ 2 - 0
web/src/contexts/AuthContext.tsx

@@ -48,6 +48,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
                         username: data.username,
                         role: (data.role as UserRole) ?? 'USER',
                         tenantId: data.tenantId,
+                        tenant_name: data.tenantName,
                         isNotebookEnabled: data.isNotebookEnabled ?? true,
                     });
                 } else {
@@ -75,6 +76,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
             username: userData.username ?? '',
             role: (userData.role as UserRole) ?? 'USER',
             tenantId: userData.tenantId,
+            tenant_name: (userData as any).tenantName || userData.tenant_name,
             isNotebookEnabled: userData.isNotebookEnabled ?? true,
         });
     };

+ 1 - 1
web/src/pages/workspace/SettingsPage.tsx

@@ -5,7 +5,7 @@ import { ModelConfig, DEFAULT_MODELS } from '../../../types';
 import { modelConfigService } from '../../../services/modelConfigService';
 
 interface SettingsPageProps {
-    initialTab?: 'general' | 'user' | 'model' | 'dashboard' | 'knowledge_base';
+    initialTab?: 'general' | 'user' | 'model' | 'tenants' | 'knowledge_base';
 }
 
 export default function SettingsPage({ initialTab }: SettingsPageProps) {