anhuiqiang 7 hours ago
parent
commit
e92649b612

+ 2 - 0
.gitignore

@@ -57,3 +57,5 @@ server/models_status.json
 libreoffice-server/__pycache__/main.cpython-312.pyc
 nul
 .sisyphus/
+server/build_error.txt
+server/ts_errors.txt

+ 0 - 0
server/metadata.db


+ 3 - 3
server/src/assessment/assessment.service.ts

@@ -422,10 +422,10 @@ export class AssessmentService {
           const isZh = (session.language || 'en') === 'zh';
           const isJa = session.language === 'ja';
           const initialMsg = isZh
-            ? '现在生成评估问题。'
+            ? '现在生成评估问题。请务必使用中文。'
             : isJa
-              ? '今すぐアセスメント問題を生成してください。'
-              : 'Generate the assessment questions now.';
+              ? '今すぐアセスメント問題を生成してください。必ず日本語で回答してください。'
+              : 'Generate the assessment questions now. Please strictly respond in English.';
 
           this.logger.log(
             `[startSessionStream] Starting stream for session ${sessionId}`,

+ 13 - 11
server/src/assessment/graph/nodes/analyzer.node.ts

@@ -39,11 +39,12 @@ export const reportAnalyzerNode = async (
 请审查以下评估结果,并为员工提供一份严谨的掌握程度报告。
 
 重要提示:
-1. 你必须使用以下语言生成报告:中文 (Simplified Chinese)。
-2. 报告的第一行必须严格遵守此格式:"LEVEL: [Novice/Proficient/Advanced/Expert]"。
-3. 必须保持客观。如果用户没有提供有效的回答或得分为 0,你必须将其识别为 'Novice',并明确指出他们尚未证明其掌握程度。
-4. 不要虚构或幻想优点(如“潜力”或“好奇心”),如果用户明确表示“不知道”或未提供实质内容。
-5. 专注于对话记录中已证明的事实。
+1. **你必须使用以下语言生成报告:中文 (Simplified Chinese)**。
+2. **严禁夹杂日文**。即使对话记录中包含日文,报告内容也必须全中文。
+3. 报告的第一行必须严格遵守此格式:"LEVEL: [Novice/Proficient/Advanced/Expert]"。
+4. 必须保持客观。如果用户没有提供有效的回答或得分为 0,你必须将其识别为 'Novice',并明确指出他们尚未证明其掌握程度。
+5. 不要虚构或幻想优点(如“潜力”或“好奇心”),如果用户明确表示“不知道”或未提供实质内容。
+6. 专注于对话记录中已证明的事实。
 
 问题与得分:
 ${scoreSummary}
@@ -64,11 +65,12 @@ ${messages
 以下の評価結果をレビューし、従業員に対して厳格な習熟度レポートを提供してください。
 
 重要事項:
-1. レポートは次の言語で生成してください:日本語。
-2. レポートの最初の行は、必ず次の形式に従ってください:"LEVEL: [Novice/Proficient/Advanced/Expert]"。
-3. 客観的であること。ユーザーが有効な回答を提供しなかった場合、またはスコアが 0 の場合、'Novice' と判定し、習熟度が証明されていないことを明示してください。
-4. ユーザーが「わからない」と言ったり、内容を提供しなかった場合に、長所(「ポテンシャル」や「好奇心」など)を捏造しないでください。
-5. 会話ログで証明された事実に集中してください。
+1. **レポートは必ず次の言語で生成してください:日本語**。
+2. **中国語を混ぜないでください**。会話ログに中国語が含まれていても、レポートの内容はすべて日本語で記述してください。
+3. レポートの最初の行は, 必ず次の形式に従ってください:"LEVEL: [Novice/Proficient/Advanced/Expert]"。
+4. 客観的であること。ユーザーが有効な回答を提供しなかった場合、またはスコアが 0 の場合、'Novice' と判定し、習熟度が証明されていないことを明示してください。
+5. ユーザーが「わからない」と言ったり、内容を提供しなかった場合に、長所(「ポテンシャル」や「好奇心」など)を捏造しないでください。
+6. 会話ログで証明された事実に集中してください。
 
 質問とスコア:
 ${scoreSummary}
@@ -89,7 +91,7 @@ ${messages
 Review the following assessment results and provide a rigorous mastery report for the employee.
 
 IMPORTANT: 
-1. You MUST generate the report in English.
+1. **You MUST generate the report strictly in English.**
 2. START the report with exactly this format: "LEVEL: [Novice/Proficient/Advanced/Expert]" on the first line.
 3. Be OBJECTIVE. If the user provided no valid answers or scores are 0, you MUST identify them as 'Novice' and explicitly state they have NOT demonstrated mastery.
 4. DO NOT invent or hallucinate strengths (like 'potential' or 'curiosity') if the user explicitly said "I don't know" or provided no content.

+ 39 - 7
server/src/assessment/graph/nodes/generator.node.ts

@@ -33,7 +33,11 @@ export const questionGeneratorNode = async (
   const style = state.style || 'technical';
   const difficultyText = state.difficultyDistribution
     ? JSON.stringify(state.difficultyDistribution)
-    : '随机分布 (Random distribution)';
+    : isZh
+      ? '随机分布'
+      : isJa
+        ? 'ランダム分布'
+        : 'Random distribution';
   const keywords = state.keywords || [];
   const hasKeywords = keywords.length > 0;
   const keywordText = hasKeywords ? keywords.join(', ') : '';
@@ -79,6 +83,9 @@ export const questionGeneratorNode = async (
 
   const systemPromptZh = `你是一位专业的知识评估专家。请根据提供的知识库片段生成 1 个唯一的测试题目。
 
+### 强制性语言规则:
+**必须使用中文 (Simplified Chinese) 进行回复**。即使知识库内容是英文或其他语言,问题(question_text)和关键点(key_points)也必须使用中文。
+
 ### 强制性多样性规则:
 ${rulesZh}
 
@@ -101,16 +108,34 @@ ${hasKeywords ? `目标关键词:${keywordText}\n` : ''}出题风格:${style
 
   const systemPromptJa = `あなたは専門的なアセスメントエキスパートです。提供されたナレッジベースに基づいて、ユニークな問題を 1 つ作成してください。
 
+### 言語ルール(最重要):
+**必ず日本語で作成してください**。提供されたナレッジベースが英語や中国語、その他の言語であっても、質問文(question_text)およびキーポイント(key_points)は必ず日本語で回答してください。中国語が混ざらないように厳格に注意してください。
+
 ### 多様性ルール:
 ${rulesJa}
 
 ### 作成済み問題リスト:
 ${existingQuestionsText || 'なし'}
 
-JSON 形式で 1 つ返してください。`;
+### 任務:
+${hasKeywords ? `目標キーワード:${keywordText}\n` : ''}出題スタイル:${style}
+難易度:${difficultyText}
+
+以下のJSON配列形式で問題を1つ返してください:
+[
+  {
+    "question_text": "...",
+    "key_points": ["ポイント1", "ポイント2"],
+    "difficulty": "...",
+    "basis": "[n] 引用箇所..."
+  }
+]`;
 
   const systemPromptEn = `You are an expert examiner. Generate 1 UNIQUE question based on the provided context.
 
+### Language Rule:
+**You MUST generate the question and key points in English.**
+
 ### Diversity Rules:
 ${rulesEn}
 
@@ -125,10 +150,10 @@ Return 1 question as a JSON array.`;
       ? systemPromptJa
       : systemPromptEn;
   const humanMsg = isZh
-    ? `基于以下内容生成题目:\n\n${knowledgeBaseContent}`
+    ? `请使用中文基于以下内容生成题目:\n\n${knowledgeBaseContent}`
     : isJa
-      ? `以下の内容に基づいて作成してください:\n\n${knowledgeBaseContent}`
-      : `Generate question based on:\n\n${knowledgeBaseContent}`;
+      ? `以下の内容に基づいて、必ず日本語でアセスメント問題を作成してください:\n\n${knowledgeBaseContent}`
+      : `Generate evaluation question in English based on:\n\n${knowledgeBaseContent}`;
 
   try {
     const response = await model.invoke([
@@ -137,10 +162,17 @@ Return 1 question as a JSON array.`;
     ]);
 
     try {
-      const newQuestions = safeParseJson(response.content as string);
-      if (!newQuestions || !Array.isArray(newQuestions)) {
+      let newQuestions = safeParseJson<any>(response.content as string);
+      
+      if (!newQuestions) {
+        console.error('[GeneratorNode] Failed to parse JSON. Raw content:', response.content);
         throw new Error('Invalid JSON format from AI');
       }
+
+      // Handle both array and single object
+      if (!Array.isArray(newQuestions)) {
+        newQuestions = [newQuestions];
+      }
       const mappedNewQuestions = newQuestions.map((q: any) => ({
         id: (existingQuestions.length + 1).toString(),
         questionText: q.question_text,

+ 14 - 8
server/src/assessment/graph/nodes/grader.node.ts

@@ -71,8 +71,8 @@ export const graderNode = async (
 请根据以下问题和关键点对用户的回答进行评分。
 
 重要提示:
-1. 你必须使用以下语言提供反馈:中文 (Simplified Chinese)。
-2. 如果用户的回答或知识库内容涉及其他语言,请确保你的反馈和解释依然使用中文。
+1. **你必须使用以下语言提供反馈:中文 (Simplified Chinese)**
+2. 即使用户的回答或知识库内容涉及其他语言,请确保你的反馈和解释依然严格使用中文。不要夹杂日文。
 
 问题:${currentQuestion.questionText}
 预期的关键点:${currentQuestion.keyPoints.join(', ')}
@@ -98,8 +98,8 @@ export const graderNode = async (
 以下の質問とキーポイントに基づいて、ユーザーの回答を採点してください。
 
 重要事項:
-1. フィードバックは次の言語で提供してください:日本語。
-2. ユーザーの回答やナレッジベースの内容に他の言語が含まれている場合でも、フィードバックと説明は必ず日本語で行ってください。
+1. **フィードバックは必ず次の言語で提供してください:日本語**
+2. ユーザーの回答やナレッジベースの内容に他の言語(中国語や英語など)が含まれている場合でも、フィードバックと説明は必ず日本語のみで行ってください。中国語が混ざらないよう厳格に注意してください。
 
 質問:${currentQuestion.questionText}
 期待されるキーポイント:${currentQuestion.keyPoints.join(', ')}
@@ -125,8 +125,8 @@ JSON 形式で回答してください:
 Grade the user's answer based on the following question and key points.
 
 IMPORTANT: 
-1. You MUST provide the feedback in English.
-2. If the user's answer or knowledge base content references other languages, ensure your feedback and explanation remain in English.
+1. **You MUST provide the feedback in English.**
+2. If the user's answer or knowledge base content references other languages, ensure your feedback and explanation remain strictly in English.
 
 QUESTION: ${currentQuestion.questionText}
 EXPECTED KEY POINTS: ${currentQuestion.keyPoints.join(', ')}
@@ -165,8 +165,9 @@ Format your response as JSON:
   ]);
 
   try {
-    const result = safeParseJson(response.content as string);
+    const result = safeParseJson<any>(response.content as string);
     if (!result) {
+      console.error('[GraderNode] Failed to parse JSON. Raw content:', response.content);
       throw new Error('Invalid JSON format from AI');
     }
     console.log('[GraderNode] AI Grade Result:', result);
@@ -196,7 +197,12 @@ Format your response as JSON:
         normalizedContent.includes('不会') ||
         normalizedContent.includes("don't know") ||
         normalizedContent.includes('no idea') ||
-        normalizedContent.includes('不知'));
+        normalizedContent.includes('不知') ||
+        normalizedContent.includes('わかりません') ||
+        normalizedContent.includes('わからん') ||
+        normalizedContent.includes('知らない') ||
+        normalizedContent.includes('不明') ||
+        normalizedContent.includes('わからない'));
 
     if (currentFollowUpCount >= 1 || result.score >= 8 || saysIDontKnow) {
       shouldFollowUp = false;

+ 2 - 0
server/src/auth/auth.module.ts

@@ -7,10 +7,12 @@ import { JwtModule } from '@nestjs/jwt';
 import { ConfigModule, ConfigService } from '@nestjs/config';
 import { LocalStrategy } from './local.strategy';
 import { JwtStrategy } from './jwt.strategy';
+import { TenantModule } from '../tenant/tenant.module';
 
 @Module({
   imports: [
     UserModule,
+    TenantModule,
     PassportModule,
     JwtModule.registerAsync({
       imports: [ConfigModule],

+ 15 - 1
server/src/auth/combined-auth.guard.ts

@@ -12,6 +12,7 @@ import { lastValueFrom, Observable } from 'rxjs';
 import { IS_PUBLIC_KEY } from './public.decorator';
 import { tenantStore } from '../tenant/tenant.store';
 import { UserRole } from '../user/user-role.enum';
+import { TenantService } from '../tenant/tenant.service';
 import * as fs from 'fs';
 import * as path from 'path';
 
@@ -30,6 +31,7 @@ export class CombinedAuthGuard implements CanActivate {
   constructor(
     private reflector: Reflector,
     private userService: UserService,
+    private tenantService: TenantService,
   ) {
     // Create a JWT guard instance
     const JwtGuardClass = AuthGuard('jwt');
@@ -81,10 +83,15 @@ export class CombinedAuthGuard implements CanActivate {
           }
         }
 
+        const role = await this.tenantService.getUserRole(
+          user.id,
+          activeTenantId,
+        );
+
         request.user = {
           id: user.id,
           username: user.username,
-          role: user.isAdmin ? UserRole.SUPER_ADMIN : UserRole.USER,
+          role,
           tenantId: activeTenantId,
         };
         request.tenantId = activeTenantId;
@@ -133,6 +140,13 @@ export class CombinedAuthGuard implements CanActivate {
           }
         }
 
+        // Fetch the role for the active tenant
+        const role = await this.tenantService.getUserRole(
+          user.id,
+          user.tenantId,
+        );
+        user.role = role;
+
         request.tenantId = user.tenantId;
 
         // Update tenant context store

+ 11 - 5
server/src/auth/jwt.strategy.ts

@@ -5,12 +5,14 @@ import { ConfigService } from '@nestjs/config';
 import { UserService } from '../user/user.service';
 import { SafeUser } from '../user/dto/user-safe.dto';
 import { UserRole } from '../user/user-role.enum';
+import { TenantService } from '../tenant/tenant.service';
 
 @Injectable()
 export class JwtStrategy extends PassportStrategy(Strategy) {
   constructor(
     private configService: ConfigService,
     private userService: UserService,
+    private tenantService: TenantService,
   ) {
     super({
       jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
@@ -35,14 +37,18 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
       // In a multi-tenant setup, the tenantId in the payload is the "default" or "last active" one.
       // But it can be overridden by the x-tenant-id header in the guard.
       // Map the backend isAdmin flag to the global UserRole
-      const computedRole = result.isAdmin
-        ? UserRole.SUPER_ADMIN
-        : UserRole.USER;
+      const activeTenantId = payload.tenantId || result.tenantId;
+
+      // Fetch the actual role for this tenant from the database
+      const role = await this.tenantService.getUserRole(
+        result.id,
+        activeTenantId,
+      );
 
       return {
         ...result,
-        role: payload.role || computedRole,
-        tenantId: payload.tenantId || result.tenantId,
+        role,
+        tenantId: activeTenantId,
       } as SafeUser;
     }
     return null;

+ 8 - 2
server/src/auth/local.strategy.ts

@@ -5,12 +5,14 @@ import { AuthService } from './auth.service';
 import { SafeUser } from '../user/dto/user-safe.dto'; // Import SafeUser
 import { I18nService } from '../i18n/i18n.service';
 import { UserRole } from '../user/user-role.enum';
+import { TenantService } from '../tenant/tenant.service';
 
 @Injectable()
 export class LocalStrategy extends PassportStrategy(Strategy) {
   constructor(
     private authService: AuthService,
     private i18nService: I18nService,
+    private tenantService: TenantService,
   ) {
     super({ usernameField: 'username' });
   }
@@ -22,10 +24,14 @@ export class LocalStrategy extends PassportStrategy(Strategy) {
         this.i18nService.getMessage('incorrectCredentials'),
       );
     }
-    const { password: userPassword, ...result } = user; // Destructure to remove password
+    const { password: userPassword, ...result } = user;
+
+    // Fetch the actual role for the user's primary tenant
+    const role = await this.tenantService.getUserRole(user.id, user.tenantId);
+
     return {
       ...result,
-      role: user.isAdmin ? UserRole.SUPER_ADMIN : UserRole.USER,
+      role,
     } as SafeUser;
   }
 }

+ 29 - 7
server/src/common/json-utils.ts

@@ -1,26 +1,48 @@
 /**
- * Safely parses JSON from a string, handling markdown code blocks if present.
+ * Safely parses JSON from a string, handling markdown code blocks and leading/trailing text.
  */
 export function safeParseJson<T = any>(text: string): T | null {
-  if (!text) return null as any;
+  if (!text) return null;
 
   let jsonStr = text.trim();
 
-  // Remove markdown code blocks if they exist
+  // 1. Try to extract JSON from markdown code blocks if they exist
   // Matches ```json ... ``` or just ``` ... ```
   const codeBlockRegex = /```(?:json)?\s*([\s\S]*?)\s*```/i;
   const match = jsonStr.match(codeBlockRegex);
 
   if (match && match[1]) {
     jsonStr = match[1].trim();
+  } else {
+    // 2. If no markdown block, try to find the start and end of JSON characters
+    // This handles cases where the AI adds introductory or concluding text outside the block
+    const firstOpenBrace = jsonStr.indexOf('{');
+    const firstOpenBracket = jsonStr.indexOf('[');
+    
+    let startIndex = -1;
+    if (firstOpenBrace !== -1 && (firstOpenBracket === -1 || firstOpenBrace < firstOpenBracket)) {
+      startIndex = firstOpenBrace;
+    } else if (firstOpenBracket !== -1) {
+      startIndex = firstOpenBracket;
+    }
+
+    if (startIndex !== -1) {
+      const lastCloseBrace = jsonStr.lastIndexOf('}');
+      const lastCloseBracket = jsonStr.lastIndexOf(']');
+      const endIndex = Math.max(lastCloseBrace, lastCloseBracket);
+      
+      if (endIndex !== -1 && endIndex > startIndex) {
+        jsonStr = jsonStr.substring(startIndex, endIndex + 1);
+      }
+    }
   }
 
   try {
     return JSON.parse(jsonStr) as T;
   } catch (error) {
-    console.error('Failed to parse JSON:', error);
-    console.error('Original text:', text);
-    console.error('Extracted string:', jsonStr);
-    throw error;
+    console.error('[safeParseJson] Failed to parse JSON:', error);
+    console.error('[safeParseJson] Original text:', text);
+    console.error('[safeParseJson] Extracted string:', jsonStr);
+    return null;
   }
 }

+ 2 - 2
server/src/knowledge-base/knowledge-base.service.ts

@@ -437,8 +437,8 @@ export class KnowledgeBaseService {
     );
 
     try {
-      // Get all files and delete them one by one
-      const files = await this.kbRepository.find();
+      // Get all files for the specific tenant and delete them one by one
+      const files = await this.kbRepository.find({ where: { tenantId } });
 
       for (const file of files) {
         await this.deleteFile(file.id, userId, tenantId);

+ 47 - 0
server/src/migrations/1773210000000-DropTenantFromNotes.ts

@@ -0,0 +1,47 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class DropTenantFromNotes1773210000000 implements MigrationInterface {
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    // Log tables to help debug
+    const tables = await queryRunner.getTables();
+    console.log('Database tables found:', tables.map(t => t.name).join(', '));
+
+    const noteTables = ['note', 'notes'];
+    const noteCategoryTables = ['note_category', 'note_categories'];
+    const tenantColumns = ['tenant_id', 'tenantId', 'tenantid'];
+
+    // 1. Drop from notes tables
+    for (const table of noteTables) {
+      for (const col of tenantColumns) {
+        try {
+          await queryRunner.query(`ALTER TABLE "${table}" DROP COLUMN "${col}"`);
+          console.log(`Successfully dropped ${col} from ${table}`);
+        } catch (e) {
+          // Ignore - column or table might not exist
+        }
+      }
+    }
+
+    // 2. Drop from note_categories tables
+    for (const table of noteCategoryTables) {
+      for (const col of tenantColumns) {
+        try {
+          await queryRunner.query(`ALTER TABLE "${table}" DROP COLUMN "${col}"`);
+          console.log(`Successfully dropped ${col} from ${table}`);
+        } catch (e) {
+          // Ignore
+        }
+      }
+    }
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    // To reverse, we would add the column back.
+    try {
+      await queryRunner.query('ALTER TABLE "notes" ADD COLUMN "tenant_id" varchar');
+      await queryRunner.query('ALTER TABLE "note_categories" ADD COLUMN "tenant_id" varchar');
+    } catch (e) {
+      // Ignore
+    }
+  }
+}

+ 2 - 1
server/src/tenant/tenant.module.ts

@@ -1,4 +1,4 @@
-import { Module } from '@nestjs/common';
+import { Global, Module } from '@nestjs/common';
 import { TypeOrmModule } from '@nestjs/typeorm';
 import { Tenant } from './tenant.entity';
 import { TenantSetting } from './tenant-setting.entity';
@@ -7,6 +7,7 @@ import { TenantController } from './tenant.controller';
 import { TenantService } from './tenant.service';
 import { TenantEntitySubscriber } from './tenant-entity.subscriber';
 
+@Global()
 @Module({
   imports: [TypeOrmModule.forFeature([Tenant, TenantSetting, TenantMember])],
   providers: [TenantService, TenantEntitySubscriber],

+ 29 - 0
server/src/tenant/tenant.service.ts

@@ -289,4 +289,33 @@ export class TenantService {
 
     return false;
   }
+
+  /**
+   * Determine the user's role in a specific tenant.
+   * Checks for global SUPER_ADMIN status first.
+   */
+  async getUserRole(userId: string, tenantId: string): Promise<UserRole> {
+    // 1. Check if user is a global SUPER_ADMIN
+    const user = await this.tenantRepository.query(
+      'SELECT isAdmin FROM users WHERE id = ?',
+      [userId],
+    );
+
+    if (user && user[0] && user[0].isAdmin) {
+      return UserRole.SUPER_ADMIN;
+    }
+
+    // 2. Check for tenant-specific role
+    const membership = await this.tenantMemberRepository.findOneBy({
+      userId,
+      tenantId,
+    });
+
+    if (membership) {
+      return membership.role;
+    }
+
+    // Default to USER
+    return UserRole.USER;
+  }
 }

+ 1 - 1
web/components/views/ChatView.tsx

@@ -329,7 +329,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
                     </div>
 
                     <div className='flex items-center gap-3 flex-shrink-0'>
-                        {/* 历史记录按钮 */}
+                        {/* History button */}
                         <button
                             onClick={handleShowHistory}
                             className="flex items-center gap-2 px-4 py-2 bg-white border border-slate-200 text-slate-700 hover:text-blue-600 hover:bg-slate-50 rounded-lg font-semibold text-sm transition-all shadow-sm"

+ 3 - 3
web/services/assessmentService.ts

@@ -29,11 +29,11 @@ export interface AssessmentState {
 }
 
 export class AssessmentService {
-    async startSession(knowledgeBaseId: string, language: string = 'zh', templateId?: string): Promise<AssessmentSession> {
+    async startSession(knowledgeBaseId: string, language: string, templateId?: string): Promise<AssessmentSession> {
         const { data } = await apiClient.post<AssessmentSession>('/assessment/start', { knowledgeBaseId, language, templateId });
         return data;
     }
-    async submitAnswer(sessionId: string, answer: string, language: string = 'zh'): Promise<AssessmentState> {
+    async submitAnswer(sessionId: string, answer: string, language: string): Promise<AssessmentState> {
         const { data } = await apiClient.post<AssessmentState>(`/assessment/${sessionId}/answer`, { answer, language });
         return data;
     }
@@ -59,7 +59,7 @@ export class AssessmentService {
         yield* this.parseStream(response);
     }
 
-    async *submitAnswerStream(sessionId: string, answer: string, language: string = 'zh', templateId?: string): AsyncIterableIterator<any> {
+    async *submitAnswerStream(sessionId: string, answer: string, language: string, templateId?: string): AsyncIterableIterator<any> {
         const query = new URLSearchParams({ answer, language, ...(templateId && { templateId }) }).toString();
         const response = await apiClient.request(`/assessment/${sessionId}/answer-stream?${query}`, {
             method: 'GET',

+ 4 - 2
web/src/pages/workspace/KnowledgePage.tsx

@@ -5,7 +5,7 @@ import { modelConfigService } from '../../../services/modelConfigService';
 import { ModelConfig } from '../../../types';
 
 export default function KnowledgePage() {
-    const { apiKey, user, logout } = useAuth();
+    const { apiKey, user, logout, activeTenant } = useAuth();
     const [modelConfigs, setModelConfigs] = useState<ModelConfig[]>([]);
 
     useEffect(() => {
@@ -21,13 +21,15 @@ export default function KnowledgePage() {
         fetchModels();
     }, [apiKey]);
 
+    const isAdmin = user?.role === 'SUPER_ADMIN' || activeTenant?.role === 'TENANT_ADMIN' || activeTenant?.role === 'SUPER_ADMIN';
+
     return (
         <KnowledgeBaseView
             authToken={apiKey || ''}
             onLogout={logout}
             onNavigate={() => { }}
             modelConfigs={modelConfigs}
-            isAdmin={user?.role === 'TENANT_ADMIN' || user?.role === 'SUPER_ADMIN'}
+            isAdmin={isAdmin}
         />
     );
 }

+ 4 - 2
web/src/pages/workspace/MemosPage.tsx

@@ -3,12 +3,14 @@ import { useAuth } from '../../contexts/AuthContext';
 import { MemosView } from '../../../components/views/MemosView';
 
 export default function MemosPage() {
-    const { apiKey, user } = useAuth();
+    const { apiKey, user, activeTenant } = useAuth();
+
+    const isAdmin = user?.role === 'SUPER_ADMIN' || activeTenant?.role === 'TENANT_ADMIN' || activeTenant?.role === 'SUPER_ADMIN';
 
     return (
         <MemosView
             authToken={apiKey}
-            isAdmin={user?.role === 'TENANT_ADMIN' || user?.role === 'SUPER_ADMIN'}
+            isAdmin={isAdmin}
         />
     );
 }

+ 4 - 2
web/src/pages/workspace/NotebooksPage.tsx

@@ -3,14 +3,16 @@ import { useAuth } from '../../contexts/AuthContext';
 import { NotebooksView } from '../../../components/views/NotebooksView';
 
 const NotebooksPage: React.FC = () => {
-    const { apiKey, user } = useAuth()
+    const { apiKey, user, activeTenant } = useAuth()
+
+    const isAdmin = user?.role === 'SUPER_ADMIN' || activeTenant?.role === 'TENANT_ADMIN' || activeTenant?.role === 'SUPER_ADMIN';
 
     return (
         <div className="flex flex-col h-full">
             <NotebooksView
                 authToken={apiKey}
                 onChatWithContext={() => { }}
-                isAdmin={user?.role === 'TENANT_ADMIN' || user?.role === 'SUPER_ADMIN'}
+                isAdmin={isAdmin}
             />
         </div>
     )

+ 8 - 0
web/utils/translations.ts

@@ -2268,6 +2268,7 @@ export const translations = {
     importSuccess: "インポート完了: 成功 $1, 失敗 $2",
     importFailed: "ユーザーのインポートに失敗しました",
     exportFailed: "ユーザーのエクスポートに失敗しました",
+    navChat: "チャット",
     navCoach: "コーチ",
     navKnowledge: "ナレッジベース",
     navKnowledgeGroups: "ナレッジグループ",
@@ -2276,6 +2277,13 @@ export const translations = {
     navAssessment: "アセスメント",
     navPlugin: "プラグイン",
     navCrawler: "リソース取得",
+    navGlobal: "グローバル",
+    navSystemModels: "システムモデル",
+    navTenantManagement: "テナント管理",
+    navUsersTeam: "ユーザーとチーム",
+    navTenantSettings: "テナント設定",
+    adminConsole: "管理コンソール",
+    globalDashboard: "グローバルダッシュボード",
     expandMenu: "メニューを展開",
     switchLanguage: "言語を切り替える",