anhuiqiang 1 тиждень тому
батько
коміт
c83f9eda43

+ 60 - 0
server/pdf_to_images.py

@@ -0,0 +1,60 @@
+import fitz  # PyMuPDF
+import sys
+import os
+import json
+
+def convert_pdf_to_images(pdf_path, output_dir, zoom=2.0, quality=85):
+    """
+    Converts PDF pages to images.
+    zoom: 2.0 means 200% scaling (approx 144 DPI if original is 72 DPI)
+    """
+    try:
+        if not os.path.exists(output_dir):
+            os.makedirs(output_dir)
+
+        doc = fitz.open(pdf_path)
+        images = []
+        
+        # Matrix for scaling (DPI control)
+        mat = fitz.Matrix(zoom, zoom)
+        
+        for i in range(len(doc)):
+            page = doc.load_page(i)
+            pix = page.get_pixmap(matrix=mat, colorspace=fitz.csRGB)
+            
+            output_path = os.path.join(output_dir, f"page-{i+1}.jpg")
+            # In newer PyMuPDF, save() doesn't take quality. Use tobytes instead.
+            img_bytes = pix.tobytes("jpg", jpg_quality=quality)
+            with open(output_path, "wb") as f:
+                f.write(img_bytes)
+            
+            images.append({
+                "path": output_path,
+                "pageIndex": i + 1,
+                "size": os.path.getsize(output_path)
+            })
+            
+        doc.close()
+        return {
+            "success": True,
+            "images": images,
+            "totalPages": len(images)
+        }
+    except Exception as e:
+        return {
+            "success": False,
+            "error": str(e)
+        }
+
+if __name__ == "__main__":
+    if len(sys.argv) < 3:
+        print(json.dumps({"success": False, "error": "Usage: python pdf_to_images.py <pdf_path> <output_dir> [zoom] [quality]"}))
+        sys.exit(1)
+        
+    pdf_path = sys.argv[1]
+    output_dir = sys.argv[2]
+    zoom = float(sys.argv[3]) if len(sys.argv) > 3 else 2.0
+    quality = int(sys.argv[4]) if len(sys.argv) > 4 else 85
+    
+    result = convert_pdf_to_images(pdf_path, output_dir, zoom, quality)
+    print(json.dumps(result))

+ 8 - 0
server/src/app.module.ts

@@ -28,6 +28,7 @@ import { SearchHistoryModule } from './search-history/search-history.module';
 import { NoteModule } from './note/note.module';
 import { PodcastModule } from './podcasts/podcast.module';
 import { ImportTaskModule } from './import-task/import-task.module';
+import { AssessmentModule } from './assessment/assessment.module';
 import { I18nMiddleware } from './i18n/i18n.middleware';
 import { TenantMiddleware } from './tenant/tenant.middleware';
 import { User } from './user/user.entity';
@@ -41,6 +42,9 @@ import { Note } from './note/note.entity';
 import { NoteCategory } from './note/note-category.entity';
 import { PodcastEpisode } from './podcasts/entities/podcast-episode.entity';
 import { ImportTask } from './import-task/import-task.entity';
+import { AssessmentSession } from './assessment/entities/assessment-session.entity';
+import { AssessmentQuestion } from './assessment/entities/assessment-question.entity';
+import { AssessmentAnswer } from './assessment/entities/assessment-answer.entity';
 import { Tenant } from './tenant/tenant.entity';
 import { TenantSetting } from './tenant/tenant-setting.entity';
 import { ApiKey } from './auth/entities/api-key.entity';
@@ -78,6 +82,9 @@ import { AdminModule } from './admin/admin.module';
           NoteCategory,
           PodcastEpisode,
           ImportTask,
+          AssessmentSession,
+          AssessmentQuestion,
+          AssessmentAnswer,
           Tenant,
           TenantSetting,
           TenantMember,
@@ -106,6 +113,7 @@ import { AdminModule } from './admin/admin.module';
     UploadModule,
     ChatModule,
     ImportTaskModule,
+    AssessmentModule,
     SuperAdminModule,
     AdminModule,
   ],

+ 6 - 0
server/src/knowledge-base/knowledge-base.controller.ts

@@ -44,6 +44,12 @@ export class KnowledgeBaseController {
     return this.knowledgeBaseService.findAll(req.user.id, req.user.tenantId);
   }
 
+  @Get('stats')
+  @UseGuards(CombinedAuthGuard)
+  async getStats(@Request() req): Promise<{ total: number, uncategorized: number }> {
+    return this.knowledgeBaseService.getStats(req.user.id, req.user.tenantId);
+  }
+
   @Delete('clear')
   @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
   async clearAll(@Request() req): Promise<{ message: string }> {

+ 35 - 0
server/src/knowledge-base/knowledge-base.service.ts

@@ -153,6 +153,41 @@ export class KnowledgeBaseService {
     });
   }
 
+  async getStats(userId: string, tenantId?: string): Promise<{ total: number, uncategorized: number }> {
+    const where: any = {};
+    if (tenantId) {
+      where.tenantId = tenantId;
+    } else {
+      where.userId = userId;
+    }
+
+    // Get total count
+    const total = await this.kbRepository.count({ where });
+
+    // Get uncategorized count (files with no groups)
+    // We need to use query builder to check for empty groups relation
+    const uncategorizedQuery = this.kbRepository
+      .createQueryBuilder('kb')
+      .leftJoin('kb.groups', 'groups');
+
+    // Apply where conditions
+    if (tenantId) {
+      uncategorizedQuery.where('kb.tenantId = :tenantId', { tenantId });
+    } else {
+      uncategorizedQuery.where('kb.userId = :userId', { userId });
+    }
+
+    // Count files where groups array is empty
+    const uncategorizedCount = await uncategorizedQuery
+      .andWhere('groups.id IS NULL')
+      .getCount();
+
+    return {
+      total,
+      uncategorized: uncategorizedCount,
+    };
+  }
+
   async searchKnowledge(userId: string, tenantId: string, query: string, topK: number = 5) {
     try {
       // Generate simulation vector using default dimensions from environment variable

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
server/tsconfig.build.tsbuildinfo


+ 47 - 35
web/components/views/AgentsView.tsx

@@ -1,7 +1,9 @@
 import React from 'react';
 import { useLanguage } from '../../contexts/LanguageContext';
+import { useNavigate } from 'react-router-dom';
 import { Search, Plus, MoreHorizontal, MessageSquare } from 'lucide-react';
 import { motion, AnimatePresence } from 'framer-motion';
+import { cn } from '../../src/utils/cn';
 
 // Mock data based on the provided design
 interface AgentMock {
@@ -12,76 +14,70 @@ interface AgentMock {
     updatedAt: string;
     iconEmoji: string;
     iconBgClass: string;
+    path?: string;
 }
 
 const mockAgents: AgentMock[] = [
     {
-        id: '1',
+        id: 'assessment',
+        name: 'assessmentTitle',
+        description: 'assessmentDesc',
+        status: 'running',
+        updatedAt: 'agent1Time',
+        iconEmoji: '📋',
+        iconBgClass: 'bg-blue-50',
+        path: '/assessment'
+    },
+    {
+        id: 'data-analyst',
         name: 'agent1Name',
         description: 'agent1Desc',
         status: 'running',
         updatedAt: 'agent1Time',
-        iconEmoji: '🧑‍💻',
-        iconBgClass: 'bg-indigo-50'
+        iconEmoji: '📊',
+        iconBgClass: 'bg-emerald-50'
     },
     {
-        id: '2',
+        id: 'code-review',
         name: 'agent2Name',
         description: 'agent2Desc',
         status: 'running',
         updatedAt: 'agent2Time',
         iconEmoji: '💻',
-        iconBgClass: 'bg-green-50'
+        iconBgClass: 'bg-indigo-50'
     },
     {
-        id: '3',
+        id: 'paper-polisher',
         name: 'agent3Name',
         description: 'agent3Desc',
-        status: 'running',
+        status: 'stopped',
         updatedAt: 'agent3Time',
-        iconEmoji: '📐',
-        iconBgClass: 'bg-blue-50'
+        iconEmoji: '✍️',
+        iconBgClass: 'bg-amber-50'
     },
     {
-        id: '4',
+        id: 'legal-consultant',
         name: 'agent4Name',
         description: 'agent4Desc',
-        status: 'stopped',
+        status: 'running',
         updatedAt: 'agent4Time',
-        iconEmoji: '🧪',
-        iconBgClass: 'bg-slate-100'
+        iconEmoji: '⚖️',
+        iconBgClass: 'bg-rose-50'
     },
     {
-        id: '5',
+        id: 'market-researcher',
         name: 'agent5Name',
         description: 'agent5Desc',
         status: 'running',
         updatedAt: 'agent5Time',
-        iconEmoji: '📊',
-        iconBgClass: 'bg-purple-50'
-    },
-    {
-        id: '6',
-        name: 'agent6Name',
-        description: 'agent6Desc',
-        status: 'running',
-        updatedAt: 'agent6Time',
-        iconEmoji: '⚙️',
-        iconBgClass: 'bg-orange-50'
-    },
-    {
-        id: '7',
-        name: 'agent7Name',
-        description: 'agent7Desc',
-        status: 'running',
-        updatedAt: 'agent7Time',
         iconEmoji: '📈',
-        iconBgClass: 'bg-red-50'
+        iconBgClass: 'bg-cyan-50'
     }
 ];
 
 export const AgentsView: React.FC = () => {
     const { t } = useLanguage();
+    const navigate = useNavigate();
 
     return (
         <div className="flex flex-col h-full bg-[#f4f7fb] overflow-hidden">
@@ -119,7 +115,15 @@ export const AgentsView: React.FC = () => {
                                 layout
                                 initial={{ opacity: 0, y: 10 }}
                                 animate={{ opacity: 1, y: 0 }}
-                                className="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 hover:shadow-md transition-all group flex flex-col h-[220px]"
+                                className={cn(
+                                    "bg-white rounded-2xl p-6 shadow-sm border border-slate-100 hover:shadow-md transition-all group flex flex-col h-[220px]",
+                                    agent.path && "cursor-pointer hover:border-blue-200"
+                                )}
+                                onClick={() => {
+                                    if (agent.path) {
+                                        navigate(agent.path);
+                                    }
+                                }}
                             >
                                 {/* Top layer */}
                                 <div className="flex items-center justify-between mb-4">
@@ -159,7 +163,15 @@ export const AgentsView: React.FC = () => {
                                     <span className="text-[12px] font-medium text-slate-400">
                                         {t('updatedAtPrefix')}{t(agent.updatedAt as any)}
                                     </span>
-                                    <button className="flex items-center justify-center gap-1.5 px-3 py-1.5 text-blue-600 bg-blue-50 hover:bg-blue-100 rounded-lg transition-colors">
+                                    <button
+                                        onClick={(e) => {
+                                            e.stopPropagation();
+                                            if (agent.path) {
+                                                navigate(agent.path);
+                                            }
+                                        }}
+                                        className="flex items-center justify-center gap-1.5 px-3 py-1.5 text-blue-600 bg-blue-50 hover:bg-blue-100 rounded-lg transition-colors"
+                                    >
                                         <MessageSquare size={14} className="text-blue-500" />
                                         <span className="text-[13px] font-bold">{t('btnChat')}</span>
                                     </button>

+ 8 - 2
web/components/views/KnowledgeBaseView.tsx

@@ -229,6 +229,7 @@ export const KnowledgeBaseView: React.FC<KnowledgeBaseViewProps> = (props) => {
     const [groups, setGroups] = useState<KnowledgeGroup[]>([])
     const flatGroups = useMemo(() => flattenGroups(groups), [groups])
     const [settings, setSettings] = useState<AppSettings>(DEFAULT_SETTINGS)
+    const [globalStats, setGlobalStats] = useState<{ total: number, uncategorized: number }>({ total: 0, uncategorized: 0 })
     const [isLoadingSettings, setIsLoadingSettings] = useState(true)
     const [isLoadingFiles, setIsLoadingFiles] = useState(true)
 
@@ -246,7 +247,7 @@ export const KnowledgeBaseView: React.FC<KnowledgeBaseViewProps> = (props) => {
     const pageSize = 12
     const [chunkDrawer, setChunkDrawer] = useState<{ isOpen: boolean; fileId: string; fileName: string } | null>(null)
 
-    const [isAutoRefreshEnabled] = useState(true)
+    const [isAutoRefreshEnabled] = useState(false)
     const [autoRefreshInterval] = useState<number>(5000)
 
     // Sidebar State
@@ -282,7 +283,6 @@ export const KnowledgeBaseView: React.FC<KnowledgeBaseViewProps> = (props) => {
                 groupId: selectedSidebarFilter.type === 'group' ? selectedSidebarFilter.groupId : undefined
             })
             setFiles(result.items)
-            setTotalFiles(result.total)
         } catch (error) {
             console.error('Failed to fetch files:', error)
         } finally {
@@ -318,6 +318,12 @@ export const KnowledgeBaseView: React.FC<KnowledgeBaseViewProps> = (props) => {
         }
     }, [authToken, fetchAndSetSettings, fetchAndSetGroups, fetchAndSetStats])
 
+    useEffect(() => {
+        if (authToken) {
+            fetchAndSetFiles()
+        }
+    }, [fetchAndSetFiles])
+
     useEffect(() => {
         if (shouldOpenModal && pendingFiles.length > 0) {
             setIsIndexingModalOpen(true);

+ 4 - 2
web/components/views/SettingsView.tsx

@@ -1283,7 +1283,9 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                                                             <User size={18} />
                                                         </div>
                                                         <div className="min-w-0">
-                                                            <p className="text-sm font-black text-slate-900 truncate">{m.user?.username || m.userId}</p>
+                                                            <p className="text-sm font-black text-slate-900 truncate">
+                                                                {m.user?.displayName || m.user?.username || m.userId}
+                                                            </p>
                                                             <select
                                                                 value={m.role}
                                                                 onChange={(e) => handleUpdateMemberRole(activeTenant.id, m.userId, e.target.value)}
@@ -1372,7 +1374,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                                                                 <User size={14} />
                                                             </div>
                                                             <span className={`text-[13px] font-bold truncate ${isAlreadyMember ? 'text-slate-400' : 'text-slate-700'}`}>
-                                                                {u.username}
+                                                                {u.displayName || u.username}
                                                             </span>
                                                         </div>
                                                         {isAlreadyMember ? (

+ 2 - 0
web/index.tsx

@@ -12,6 +12,7 @@ import WorkspaceLayout from './src/components/layouts/WorkspaceLayout';
 // Lazy-loaded page components
 const ChatPage = lazy(() => import('./src/pages/workspace/ChatPage'));
 const AgentsPage = lazy(() => import('./src/pages/workspace/AgentsPage'));
+const AssessmentPage = lazy(() => import('./src/pages/workspace/AssessmentPage'));
 const PluginsPage = lazy(() => import('./src/pages/workspace/PluginsPage'));
 const KnowledgePage = lazy(() => import('./src/pages/workspace/KnowledgePage'));
 const NotebooksPage = lazy(() => import('./src/pages/workspace/NotebooksPage'));
@@ -84,6 +85,7 @@ function App() {
                     <Route index element={<OverviewPage />} />
                     <Route path="chat" element={<ChatPage />} />
                     <Route path="agents" element={<AgentsPage />} />
+                    <Route path="assessment" element={<AssessmentPage />} />
                     <Route path="plugins" element={<PluginsPage />} />
                     <Route path="notebook" element={<MemosPage />} />
                     <Route path="knowledge/*" element={<KnowledgePage />} />

+ 3 - 22
web/services/apiClient.ts

@@ -13,18 +13,14 @@ class ApiClient {
   }
 
   private getAuthHeaders(): Record<string, string> {
-    // V2 API key auth (primary)
     const apiKey = localStorage.getItem('kb_api_key');
     const activeTenantId = localStorage.getItem('kb_active_tenant_id');
-    // Legacy JWT token (fallback, kept for compatibility during transition)
     const token = localStorage.getItem('authToken') || localStorage.getItem('token');
     const language = localStorage.getItem('userLanguage') || 'ja';
-    return {
+
+    const headers: Record<string, string> = {
       'Content-Type': 'application/json',
       'x-user-language': language,
-      ...(apiKey && { 'x-api-key': apiKey }),
-      ...(activeTenantId && { 'x-tenant-id': activeTenantId }),
-      ...(token && { Authorization: `Bearer ${token}` }),
     };
 
     if (apiKey) {
@@ -37,25 +33,10 @@ class ApiClient {
       headers['Authorization'] = `Bearer ${token}`;
     }
 
-    // Final fail-safe: Ensure no header is 'undefined' string
-    Object.keys(headers).forEach(key => {
-        if (headers[key]?.toLowerCase().includes('undefined')) {
-            delete headers[key];
-        }
-    });
-
-    if (activeTenantId && isValid(activeTenantId)) {
+    if (activeTenantId && activeTenantId !== 'undefined' && activeTenantId !== 'null') {
       headers['x-tenant-id'] = activeTenantId;
     }
 
-    // DEBUG: Only log first few chars
-    console.log('[ApiClient] Auth Headers:', {
-        hasApiKey: !!headers['x-api-key'],
-        hasAuth: !!headers['Authorization'],
-        authPreview: headers['Authorization']?.substring(0, 20),
-        tenantId: headers['x-tenant-id']
-    });
-
     return headers;
   }
 

+ 8 - 1
web/services/chunkConfigService.ts

@@ -1,4 +1,4 @@
-// Chunk configuration service - Used to fetch and validate chunk configuration limits
+import { apiClient } from './apiClient';
 
 export interface ChunkConfigLimits {
   maxChunkSize: number;        // Max chunk size (tokens)
@@ -27,4 +27,11 @@ export const chunkConfigService = {
       `Vector Dimensions: ${limits.modelInfo.expectedDimensions}`,
     ].join(' | ');
   },
+
+  async getLimits(embeddingModelId: string, token?: string): Promise<ChunkConfigLimits> {
+    const response = await apiClient.get<ChunkConfigLimits>(
+      `/knowledge-bases/chunk-config/limits?embeddingModelId=${embeddingModelId}`
+    );
+    return response.data;
+  },
 };

+ 81 - 97
web/src/components/layouts/WorkspaceLayout.tsx

@@ -15,48 +15,14 @@ import {
     Building2,
     ChevronRight,
     Bot,
-    Blocks
+    Blocks,
+    ClipboardCheck
 } from 'lucide-react';
 import { motion, AnimatePresence } from 'framer-motion';
 import { useAuth } from '../../contexts/AuthContext';
 import { useLanguage } from '../../../contexts/LanguageContext';
 import { cn } from '../../utils/cn';
 
-interface TenantTreeNode {
-    tenantId: string;
-    role: string;
-    tenant: {
-        id: string;
-        name: string;
-        domain?: string;
-        parentId?: string;
-    };
-    children?: TenantTreeNode[];
-}
-
-const buildTenantTree = (memberships: any[]): TenantTreeNode[] => {
-    const map = new Map<string, TenantTreeNode>();
-    const roots: TenantTreeNode[] = [];
-
-    memberships.forEach(m => {
-        map.set(m.tenantId, { ...m, children: [] });
-    });
-
-    memberships.forEach(m => {
-        const node = map.get(m.tenantId)!;
-        const parentId = (m.tenant as any)?.parentId;
-        if (parentId && map.has(parentId)) {
-            const parent = map.get(parentId)!;
-            parent.children = parent.children || [];
-            parent.children.push(node);
-        } else {
-            roots.push(node);
-        }
-    });
-
-    return roots;
-};
-
 interface SidebarItemProps {
     icon: React.ElementType;
     label: string;
@@ -103,6 +69,57 @@ const WorkspaceLayout: React.FC = () => {
         navigate(path);
     };
 
+    const buildTenantTree = (memberships: typeof availableTenants) => {
+        const map = new Map<string, any>();
+        const roots: any[] = [];
+
+        memberships.forEach(m => {
+            map.set(m.tenantId, { ...m, children: [] });
+        });
+
+        memberships.forEach(m => {
+            const node = map.get(m.tenantId)!;
+            const parentId = m.tenant.parentId;
+            if (parentId && map.has(parentId)) {
+                const parent = map.get(parentId)!;
+                parent.children.push(node);
+            } else {
+                roots.push(node);
+            }
+        });
+
+        return roots;
+    };
+
+    const renderTenantItem = (membership: any, depth = 0) => (
+        <React.Fragment key={membership.tenantId}>
+            <button
+                onClick={() => {
+                    switchTenant(membership.tenantId);
+                    setShowTenantMenu(false);
+                }}
+                className={cn(
+                    "w-full text-left px-4 py-3 rounded-xl flex items-center justify-between group transition-colors",
+                    activeTenant?.tenantId === membership.tenantId
+                        ? "bg-blue-50 text-blue-700"
+                        : "hover:bg-slate-50 text-slate-600"
+                )}
+                style={{ paddingLeft: `${depth * 1.5 + 1}rem` }}
+            >
+                <div className="flex flex-col min-w-0">
+                    <span className="text-sm font-bold truncate">{membership.tenant.name}</span>
+                    <span className="text-[10px] font-medium opacity-60 uppercase tracking-tight">{membership.role}</span>
+                </div>
+                {activeTenant?.tenantId === membership.tenantId && (
+                    <div className="w-1.5 h-1.5 bg-blue-600 rounded-full shrink-0" />
+                )}
+            </button>
+            {membership.children?.map((child: any) => renderTenantItem(child, depth + 1))}
+        </React.Fragment>
+    );
+
+    const tenantTree = buildTenantTree(availableTenants);
+
     return (
         <div className="flex h-screen bg-[#FCFDFF] text-slate-900 font-sans selection:bg-blue-100 selection:text-blue-900">
 
@@ -131,7 +148,6 @@ const WorkspaceLayout: React.FC = () => {
                             isActive={location.pathname.startsWith('/chat')}
                             onClick={handleNavClick}
                         />
-{/* 
                         <SidebarItem
                             icon={Bot}
                             label={t('navAgent')}
@@ -139,14 +155,14 @@ const WorkspaceLayout: React.FC = () => {
                             isActive={location.pathname.startsWith('/agents')}
                             onClick={handleNavClick}
                         />
-                        <SidebarItem
+                        {/* <SidebarItem
                             icon={Blocks}
                             label={t('navPlugin')}
                             path="/plugins"
                             isActive={location.pathname.startsWith('/plugins')}
                             onClick={handleNavClick}
-                        />
-                        */}
+                        /> */}
+
                         <SidebarItem
                             icon={Database}
                             label={t('navKnowledge')}
@@ -154,7 +170,6 @@ const WorkspaceLayout: React.FC = () => {
                             isActive={location.pathname === '/knowledge' || location.pathname.startsWith('/knowledge/')}
                             onClick={handleNavClick}
                         />
-                        {/* 
                         {(activeTenant?.features?.isNotebookEnabled ?? true) && (
                             <SidebarItem
                                 icon={BookOpen}
@@ -164,7 +179,6 @@ const WorkspaceLayout: React.FC = () => {
                                 onClick={handleNavClick}
                             />
                         )}
-                        */}
                     </div>
 
                     <div className="space-y-0.5">
@@ -177,6 +191,16 @@ const WorkspaceLayout: React.FC = () => {
                         />
                     </div>
                 </nav>
+
+                <div className="p-4 border-t border-slate-100/80 mt-auto">
+                    <button
+                        onClick={logout}
+                        className="flex items-center gap-2 px-3 py-2 text-[13px] font-medium text-red-500 hover:bg-red-50 w-full rounded-lg transition-colors"
+                    >
+                        <LogOut size={14} />
+                        {t('logout')}
+                    </button>
+                </div>
             </aside>
 
             {/* Main Content Area */}
@@ -215,49 +239,8 @@ const WorkspaceLayout: React.FC = () => {
                                             <div className="p-3 border-b border-slate-100">
                                                 <span className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-2">{t('navTenants')}</span>
                                             </div>
-                                            <div className="max-h-80 overflow-y-auto p-1">
-                                                {(() => {
-                                                    const tree = buildTenantTree(availableTenants);
-                                                    const renderTenantItem = (node: TenantTreeNode, depth: number = 0) => {
-                                                        const isSelected = activeTenant?.tenantId === node.tenantId;
-                                                        return (
-                                                            <React.Fragment key={node.tenantId}>
-                                                                <button
-                                                                    onClick={() => {
-                                                                        switchTenant(node.tenantId);
-                                                                        setShowTenantMenu(false);
-                                                                    }}
-                                                                    className={cn(
-                                                                        "w-full text-left px-4 py-2.5 rounded-xl flex items-center justify-between group transition-colors",
-                                                                        isSelected
-                                                                            ? "bg-blue-50 text-blue-700"
-                                                                            : "hover:bg-slate-50 text-slate-600"
-                                                                    )}
-                                                                    style={{ marginLeft: `${depth * 12}px`, width: `calc(100% - ${depth * 12}px)` }}
-                                                                >
-                                                                    <div className="flex items-center gap-2 min-w-0">
-                                                                        {depth > 0 && (
-                                                                            <ChevronRight size={12} className="text-slate-300 shrink-0" />
-                                                                        )}
-                                                                        <div className="flex flex-col min-w-0">
-                                                                            <span className="text-sm font-bold truncate">{node.tenant.name}</span>
-                                                                            <span className="text-[9px] font-black opacity-40 uppercase tracking-tighter leading-none">{node.role?.replace('_', ' ')}</span>
-                                                                        </div>
-                                                                    </div>
-                                                                    {isSelected && (
-                                                                        <div className="w-1.5 h-1.5 bg-blue-600 rounded-full shrink-0 ml-2" />
-                                                                    )}
-                                                                </button>
-                                                                {node.children && node.children.length > 0 && (
-                                                                    <div className="mt-0.5 mb-1">
-                                                                        {node.children.map(child => renderTenantItem(child, depth + 1))}
-                                                                    </div>
-                                                                )}
-                                                            </React.Fragment>
-                                                        );
-                                                    };
-                                                    return tree.map(root => renderTenantItem(root));
-                                                })()}
+                                            <div className="max-h-64 overflow-y-auto p-1">
+                                                {tenantTree.map(membership => renderTenantItem(membership))}
                                             </div>
                                         </motion.div>
                                     </>
@@ -265,20 +248,21 @@ const WorkspaceLayout: React.FC = () => {
                             </AnimatePresence>
                         </div>
                     </div>
-                    <div className="flex items-center gap-2">
-                        <div className="flex items-center gap-2 px-3 py-1.5 bg-slate-50 rounded-lg border border-slate-200/50">
-                            <div className="flex items-center justify-center w-8 h-8 text-sm font-bold text-white bg-indigo-600 rounded-full shrink-0 shadow-sm">
-                                {user?.username?.[0]?.toUpperCase() || 'A'}
+                    
+                    <div className="flex items-center gap-4">
+                        <div className="flex items-center gap-3 px-3 py-1.5 bg-slate-50/50 border border-slate-100 rounded-2xl">
+                            <div className="flex items-center justify-center w-8 h-8 text-xs font-bold text-white bg-indigo-600 rounded-full shrink-0 shadow-sm">
+                                {user?.displayName?.[0]?.toUpperCase() || user?.username?.[0]?.toUpperCase() || 'A'}
+                            </div>
+                            <div className="flex flex-col">
+                                <p className="text-sm font-bold text-slate-900 truncate max-w-[120px]">
+                                    {user?.displayName || user?.username}
+                                </p>
+                                <span className="text-[9px] font-black text-blue-600 uppercase tracking-tighter">
+                                    {activeTenant?.role?.replace('_', ' ') || user?.role?.replace('_', ' ') || 'USER'}
+                                </span>
                             </div>
-                            <p className="text-sm font-semibold text-slate-900 truncate">{user?.displayName || user?.username}</p>
                         </div>
-                        <button
-                            onClick={logout}
-                            className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
-                            title={t('logout')}
-                        >
-                            <LogOut size={18} />
-                        </button>
                     </div>
                 </header>
 

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

@@ -22,6 +22,7 @@ export interface TenantMembership {
         id: string;
         name: string;
         domain?: string;
+        parentId?: string | null;
     };
     features?: {
         isNotebookEnabled: boolean;
@@ -55,6 +56,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
             });
             if (res.ok) {
                 const tenants: TenantMembership[] = await res.json();
+                console.log('[AuthContext] Fetched tenants:', tenants);
                 const filteredTenants = tenants.filter(t => t.tenant?.name !== 'Default');
                 setAvailableTenants(filteredTenants);
 
@@ -83,6 +85,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
                 });
                 if (res.ok) {
                     const data = await res.json();
+                    console.log('[AuthContext] Restored user:', data);
                     setUser({
                         id: data.id,
                         username: data.username,

+ 10 - 1
web/utils/translations.ts

@@ -22,7 +22,7 @@ export const translations = {
     confirmTitle: "确认操作",
     defaultTenant: "默认租户",
     selectOrganization: "选择组织",
-    confirmDeleteGroup: "Confirm要删除分组 \"$1\" 吗?",
+    confirmDeleteGroup: "确认要删除分组 \"$1\" 吗?",
 
     sidebarTitle: "索引与聊天配置",
     backToWorkspace: "返回工作台",
@@ -405,6 +405,7 @@ export const translations = {
     navKnowledgeGroups: "知识组",
     navNotebook: "笔记本",
     navAgent: "智能体",
+    navAssessment: "评测",
     navPlugin: "插件",
     notebookDesc: "记录您的个人想法和研究笔记。",
     newNote: "新建笔记",
@@ -749,6 +750,8 @@ export const translations = {
     btnChat: "开始对话",
 
     // Agent Mock Data
+    assessmentTitle: "人才评测",
+    assessmentDesc: "专业的评估工具,助您发现和发展人才潜力。",
     agent1Name: "数据分析专家",
     agent1Desc: "精通 SQL 和数据可视化,能够从复杂数据中提取洞察。",
     agent2Name: "代码审查助手",
@@ -1303,6 +1306,7 @@ export const translations = {
     navKnowledgeGroups: "Knowledge Groups",
     navNotebook: "Notebook",
     navAgent: "Agents",
+    navAssessment: "Assessment",
     navPlugin: "Plugins",
     notebookDesc: "Capture your personal thoughts and research notes.",
     newNote: "New Note",
@@ -1579,6 +1583,8 @@ export const translations = {
     btnChat: "Start Chat",
 
     // Agent Mock Data
+    assessmentTitle: "Talent Assessment",
+    assessmentDesc: "Professional assessment tool to help you discover and develop talent potential.",
     agent1Name: "Data Analyst Pro",
     agent1Desc: "Expert in SQL and data visualization, capable of extracting insights from complex data.",
     agent2Name: "Code Review Assistant",
@@ -2091,6 +2097,7 @@ export const translations = {
     navKnowledgeGroups: "ナレッジグループ",
     navNotebook: "ノートブック",
     navAgent: "エージェント",
+    navAssessment: "アセスメント",
     navPlugin: "プラグイン",
     navCrawler: "リソース取得",
     expandMenu: "メニューを展開",
@@ -2402,6 +2409,8 @@ export const translations = {
     btnChat: "会話を開始",
 
     // Agent Mock Data
+    assessmentTitle: "人材アセスメント",
+    assessmentDesc: "才能の可能性を発見し、育成するための専門的なアセスメントツールです。",
     agent1Name: "データ分析エキスパート",
     agent1Desc: "SQL とデータ視覚化に精通し、複雑なデータから洞察を抽出できます。",
     agent2Name: "コードレビュー助手",

Деякі файли не було показано, через те що забагато файлів було змінено