From 29bac74b5866ae37f1adbed0fba3202f21f4ec32 Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 19 May 2026 16:57:45 +0800 Subject: [PATCH] M3: console.log -> Logger + UI redesign (QuestionBank) + S7/A9/A10/A11/U11 bug fixes + #1/#2/#3/#4 enhancements + i18n for QuestionBank pages --- server/src/api/api.service.ts | 6 +- server/src/assessment/assessment.service.ts | 184 ++++--- .../src/assessment/dto/create-template.dto.ts | 12 + .../services/question-bank.service.ts | 2 +- server/src/auth/combined-auth.guard.ts | 7 +- server/src/chat/chat.controller.ts | 47 +- server/src/chat/chat.service.ts | 58 +-- server/src/common/json-utils.ts | 10 +- .../knowledge-group.controller.ts | 5 +- .../knowledge-group.service.ts | 11 +- server/src/note/note.service.ts | 8 +- server/src/ocr/ocr.controller.ts | 11 +- server/src/user/user.service.ts | 7 +- .../views/AssessmentTemplateManager.tsx | 65 ++- web/components/views/AssessmentView.tsx | 41 +- .../views/QuestionBankDetailView.tsx | 435 +++++++++++------ web/components/views/QuestionBankView.tsx | 454 +++++++++++------- web/services/assessmentService.ts | 3 + web/types.ts | 6 + web/utils/translations.ts | 210 ++++++++ 20 files changed, 1081 insertions(+), 501 deletions(-) diff --git a/server/src/api/api.service.ts b/server/src/api/api.service.ts index f1ca4e0..5609c06 100644 --- a/server/src/api/api.service.ts +++ b/server/src/api/api.service.ts @@ -1,10 +1,12 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { ChatOpenAI } from '@langchain/openai'; import { ModelConfig } from '../types'; import { I18nService } from '../i18n/i18n.service'; @Injectable() export class ApiService { + private readonly logger = new Logger(ApiService.name); + constructor(private i18nService: I18nService) {} // Simple health check method @@ -23,7 +25,7 @@ export class ApiService { const response = await llm.invoke(prompt); return response.content.toString(); } catch (error) { - console.error('LangChain call failed:', error); + this.logger.error('LangChain call failed:', error); if (error.message?.includes('401')) { throw new Error(this.i18nService.getMessage('invalidApiKey')); } diff --git a/server/src/assessment/assessment.service.ts b/server/src/assessment/assessment.service.ts index e7be0a6..95cc9f9 100644 --- a/server/src/assessment/assessment.service.ts +++ b/server/src/assessment/assessment.service.ts @@ -561,7 +561,9 @@ private async getModel(tenantId: string): Promise { `[startSession] Session ${savedSession.id} created and saved`, ); - this.cleanupOldSessions(userId); + // cleanupOldSessions permanently destroys data - disabled to preserve history. + // Admins can use batch-delete endpoint for manual cleanup. + // this.cleanupOldSessions(userId); return savedSession; } @@ -777,6 +779,33 @@ const initialState: Partial = { }); if (!session) throw new NotFoundException('Session not found'); + if (session.status === AssessmentStatus.IN_PROGRESS) { + const now = new Date(); + const startTime = session.startedAt ? new Date(session.startedAt) : now; + const questionStartTime = session.currentQuestionStartedAt ? new Date(session.currentQuestionStartedAt) : now; + const totalElapsed = Math.floor((now.getTime() - startTime.getTime()) / 1000); + const questionElapsed = Math.floor((now.getTime() - questionStartTime.getTime()) / 1000); + + if (totalElapsed >= session.totalTimeLimit || questionElapsed >= session.perQuestionTimeLimit) { + session.status = AssessmentStatus.COMPLETED; + session.finalReport = totalElapsed >= session.totalTimeLimit + ? '评测总时间已用尽,评估已自动结束' + : '单题答题时间已用尽,评估已自动结束'; + if (session.finalScore === null || session.finalScore === undefined) { + session.finalScore = 0; + } + await this.sessionRepository.save(session); + this.logger.log(`[submitAnswer] Session ${sessionId} auto-ended due to timeout`); + return { + assessmentSessionId: sessionId, + status: 'COMPLETED', + timeout: true, + finalScore: session.finalScore, + finalReport: session.finalReport, + }; + } + } + const model = await this.getModel(session.tenantId); await this.ensureGraphState(sessionId, session); const content = await this.getSessionContent(session); @@ -844,18 +873,18 @@ const initialState: Partial = { const scores = finalResult.scores as Record; const questions = finalResult.questions || []; const weightConfig = session.templateJson?.weightConfig || { prompt: 50, other: 50 }; - const passingScore = session.templateJson?.passingScore || 90; + const passingScore = (session.templateJson?.passingScore ?? 90) / 10; - if (questions.length > 0 && Object.keys(scores).length > 0) { - const { finalScore, dimensionScores, radarData } = this.calculateScores( - questions, - scores, - weightConfig, - ); - session.finalScore = finalScore; - (session as any).dimensionScores = dimensionScores; - (session as any).radarData = radarData; - (session as any).passed = finalScore >= passingScore; + if (questions.length > 0 && Object.keys(scores).length > 0) { + const { finalScore, dimensionScores, radarData } = this.calculateScores( + questions, + scores, + weightConfig, + ); + session.finalScore = finalScore; + (session as any).dimensionScores = dimensionScores; + (session as any).radarData = radarData; + (session as any).passed = finalScore >= passingScore; } } @@ -918,6 +947,35 @@ const initialState: Partial = { return; } + if (session.status === AssessmentStatus.IN_PROGRESS) { + const now = new Date(); + const startTime = session.startedAt ? new Date(session.startedAt) : now; + const questionStartTime = session.currentQuestionStartedAt ? new Date(session.currentQuestionStartedAt) : now; + const totalElapsed = Math.floor((now.getTime() - startTime.getTime()) / 1000); + const questionElapsed = Math.floor((now.getTime() - questionStartTime.getTime()) / 1000); + + if (totalElapsed >= session.totalTimeLimit || questionElapsed >= session.perQuestionTimeLimit) { + session.status = AssessmentStatus.COMPLETED; + session.finalReport = totalElapsed >= session.totalTimeLimit + ? '评测总时间已用尽,评估已自动结束' + : '单题答题时间已用尽,评估已自动结束'; + if (session.finalScore === null || session.finalScore === undefined) { + session.finalScore = 0; + } + await this.sessionRepository.save(session); + this.logger.log(`[submitAnswerStream] Session ${sessionId} auto-ended due to timeout`); + observer.next({ + assessmentSessionId: sessionId, + status: 'COMPLETED', + timeout: true, + finalScore: session.finalScore, + finalReport: session.finalReport, + }); + observer.complete(); + return; + } + } + const model = await this.getModel(session.tenantId); const content = await this.getSessionContent(session); await this.ensureGraphState(sessionId, session); @@ -1037,7 +1095,7 @@ const initialState: Partial = { const scores = finalData.scores; const questions = finalData.questions || []; const weightConfig = session.templateJson?.weightConfig || { prompt: 50, other: 50 }; - const passingScore = session.templateJson?.passingScore || 90; + const passingScore = (session.templateJson?.passingScore ?? 90) / 10; if (questions.length > 0 && Object.keys(scores).length > 0) { const { finalScore, dimensionScores, radarData } = this.calculateScores( @@ -1049,6 +1107,7 @@ const initialState: Partial = { (session as any).dimensionScores = dimensionScores; (session as any).radarData = radarData; (session as any).passed = finalScore >= passingScore; + this.logger.log( `[DimensionScoring] Session ${sessionId} Final Score: ${finalScore}, Passed: ${finalScore >= passingScore}`, ); @@ -1188,55 +1247,46 @@ const initialState: Partial = { const historicalMessages = this.hydrateMessages(session.messages); const existingQuestions = session.questions_json || []; const hasQuestionsFromBank = existingQuestions.length > 0; + const scoresRecord: Record = {}; + if (session.feedbackHistory) { + for (const fh of session.feedbackHistory) { + if (fh.score && fh.questionId) scoresRecord[fh.questionId] = fh.score; + } + } + + const recoveredState: any = { + assessmentSessionId: sessionId, + knowledgeBaseId: + session.knowledgeBaseId || session.knowledgeGroupId || '', + messages: historicalMessages, + feedbackHistory: this.hydrateMessages( + session.feedbackHistory || [], + ), + questions: existingQuestions, + currentQuestionIndex: session.currentQuestionIndex || 0, + followUpCount: session.followUpCount || 0, + shouldFollowUp: false, + scores: scoresRecord, + questionCount: session.templateJson?.questionCount || 5, + difficultyDistribution: + session.templateJson?.difficultyDistribution, + style: session.templateJson?.style, + keywords: session.templateJson?.keywords, + language: session.language || 'zh', + report: session.finalReport || undefined, + }; if (hasQuestionsFromBank) { this.logger.log( `[ensureGraphState] Using ${existingQuestions.length} questions from question bank`, ); - await this.graph.updateState( - { configurable: { thread_id: sessionId } }, - { - assessmentSessionId: sessionId, - knowledgeBaseId: - session.knowledgeBaseId || session.knowledgeGroupId || '', - messages: historicalMessages, - feedbackHistory: this.hydrateMessages( - session.feedbackHistory || [], - ), - questions: existingQuestions, - currentQuestionIndex: session.currentQuestionIndex || 0, - followUpCount: session.followUpCount || 0, - questionCount: session.templateJson?.questionCount || 5, - difficultyDistribution: - session.templateJson?.difficultyDistribution, - style: session.templateJson?.style, - keywords: session.templateJson?.keywords, - }, - 'grader', - ); - } else { - await this.graph.updateState( - { configurable: { thread_id: sessionId } }, - { - assessmentSessionId: sessionId, - knowledgeBaseId: - session.knowledgeBaseId || session.knowledgeGroupId || '', - messages: historicalMessages, - feedbackHistory: this.hydrateMessages( - session.feedbackHistory || [], - ), - questions: session.questions_json || [], - currentQuestionIndex: session.currentQuestionIndex || 0, - followUpCount: session.followUpCount || 0, - questionCount: session.templateJson?.questionCount || 5, - difficultyDistribution: - session.templateJson?.difficultyDistribution, - style: session.templateJson?.style, - keywords: session.templateJson?.keywords, - }, - 'grader', - ); } + + await this.graph.updateState( + { configurable: { thread_id: sessionId } }, + recoveredState, + 'interviewer', + ); } else { this.logger.log(`Initializing new state for session ${sessionId}`); const content = await this.getSessionContent(session); @@ -1350,7 +1400,7 @@ const initialState: Partial = { } if (session.status !== AssessmentStatus.COMPLETED) { - throw new Error('Session not completed'); + throw new BadRequestException('Session not completed yet'); } const existing = await this.certificateRepository.findOne({ @@ -1474,19 +1524,15 @@ const initialState: Partial = { const sessions = await qb.take(100).getMany(); - const dimensionScores: Record = { - PROMPT: [], - LLM: [], - IDE: [], - DEV_PATTERN: [], - WORK_CAPABILITY: [], - }; + const dimensionScores: Record = {}; for (const session of sessions) { - const messages = session.messages || []; - for (const msg of messages) { - if (msg.dimension && msg.score !== undefined) { - dimensionScores[msg.dimension]?.push(msg.score); + const scores = (session as any).dimensionScores || {}; + for (const [dim, score] of Object.entries(scores)) { + if (dimensionScores[dim]) { + dimensionScores[dim].push(score as number); + } else { + dimensionScores[dim] = [score as number]; } } } @@ -1570,7 +1616,7 @@ const initialState: Partial = { } session.finalScore = newScore; - const passingScore = session.templateJson?.passingScore || 90; + const passingScore = (session.templateJson?.passingScore ?? 90) / 10; (session as any).passed = newScore >= passingScore; session.reviewedBy = reviewerId; session.reviewedAt = new Date(); diff --git a/server/src/assessment/dto/create-template.dto.ts b/server/src/assessment/dto/create-template.dto.ts index 8523cd0..a273d3a 100644 --- a/server/src/assessment/dto/create-template.dto.ts +++ b/server/src/assessment/dto/create-template.dto.ts @@ -97,4 +97,16 @@ export class CreateTemplateDto { @Max(100) @IsOptional() passingScore?: number; + + @IsInt() + @Min(60) + @Max(7200) + @IsOptional() + totalTimeLimit?: number; + + @IsInt() + @Min(30) + @Max(1800) + @IsOptional() + perQuestionTimeLimit?: number; } diff --git a/server/src/assessment/services/question-bank.service.ts b/server/src/assessment/services/question-bank.service.ts index 2af89d9..6b0e9d4 100644 --- a/server/src/assessment/services/question-bank.service.ts +++ b/server/src/assessment/services/question-bank.service.ts @@ -122,7 +122,7 @@ export class QuestionBankService { page?: number, limit?: number, ): Promise<{ data: QuestionBank[]; total: number } | QuestionBank[]> { - console.log('[QuestionBank findAll] userId:', userId, 'tenantId:', tenantId); + this.logger.log('[QuestionBank findAll] userId: ' + userId + ', tenantId: ' + tenantId); const queryBuilder = this.bankRepository .createQueryBuilder('bank') .leftJoinAndSelect('bank.template', 'template'); diff --git a/server/src/auth/combined-auth.guard.ts b/server/src/auth/combined-auth.guard.ts index a842499..6903e7f 100644 --- a/server/src/auth/combined-auth.guard.ts +++ b/server/src/auth/combined-auth.guard.ts @@ -3,6 +3,7 @@ import { CanActivate, ExecutionContext, UnauthorizedException, + Logger, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; @@ -25,6 +26,8 @@ import * as path from 'path'; */ @Injectable() export class CombinedAuthGuard implements CanActivate { + private readonly logger = new Logger(CombinedAuthGuard.name); + // We extend AuthGuard('jwt') functionality by composition private jwtGuard: ReturnType; @@ -55,7 +58,7 @@ export class CombinedAuthGuard implements CanActivate { return true; } - console.log( + this.logger.log( `[CombinedAuthGuard] Checking auth for route: ${request.method} ${request.url}`, ); @@ -160,7 +163,7 @@ export class CombinedAuthGuard implements CanActivate { } return false; } catch (e) { - console.error(`[CombinedAuthGuard] JWT Auth Error:`, e); + this.logger.error('[CombinedAuthGuard] JWT Auth Error: ' + e); throw e instanceof UnauthorizedException ? e : new UnauthorizedException('Authentication required'); diff --git a/server/src/chat/chat.controller.ts b/server/src/chat/chat.controller.ts index f102d24..df20f35 100644 --- a/server/src/chat/chat.controller.ts +++ b/server/src/chat/chat.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, + Logger, Post, Request, Res, @@ -36,6 +37,8 @@ class StreamChatDto { @Controller('chat') @UseGuards(CombinedAuthGuard) export class ChatController { + private readonly logger = new Logger(ChatController.name); + constructor( private chatService: ChatService, private modelConfigService: ModelConfigService, @@ -49,7 +52,7 @@ export class ChatController { @Res() res: Response, ) { try { - console.log('Full Request Body:', JSON.stringify(body, null, 2)); + this.logger.log('Full Request Body:', JSON.stringify(body, null, 2)); const { message, history = [], @@ -71,22 +74,22 @@ export class ChatController { } = body; const userId = req.user.id; - console.log('=== Chat Debug Info ==='); - console.log('User ID:', userId); - console.log('Message:', message); - console.log('User Language:', userLanguage); - console.log('Selected Embedding ID:', selectedEmbeddingId); - console.log('Selected LLM ID:', selectedLLMId); - console.log('Selected Groups:', selectedGroups); - console.log('Selected Files:', selectedFiles); - console.log('History ID:', historyId); - console.log('Temperature:', temperature); - console.log('Max Tokens:', maxTokens); - console.log('Top K:', topK); - console.log('Similarity Threshold:', similarityThreshold); - console.log('Rerank Similarity Threshold:', rerankSimilarityThreshold); - console.log('Query Expansion:', enableQueryExpansion); - console.log('HyDE:', enableHyDE); + this.logger.log('=== Chat Debug Info ==='); + this.logger.log('User ID:', userId); + this.logger.log('Message:', message); + this.logger.log('User Language:', userLanguage); + this.logger.log('Selected Embedding ID:', selectedEmbeddingId); + this.logger.log('Selected LLM ID:', selectedLLMId); + this.logger.log('Selected Groups:', selectedGroups); + this.logger.log('Selected Files:', selectedFiles); + this.logger.log('History ID:', historyId); + this.logger.log('Temperature:', temperature); + this.logger.log('Max Tokens:', maxTokens); + this.logger.log('Top K:', topK); + this.logger.log('Similarity Threshold:', similarityThreshold); + this.logger.log('Rerank Similarity Threshold:', rerankSimilarityThreshold); + this.logger.log('Query Expansion:', enableQueryExpansion); + this.logger.log('HyDE:', enableHyDE); const role = req.user.role; const tenantId = req.user.tenantId; @@ -105,14 +108,14 @@ export class ChatController { if (selectedLLMId) { // Find specifically selected model llmModel = await this.modelConfigService.findOne(selectedLLMId); - console.log('使用选中的LLM模型:', llmModel.name); + this.logger.log('使用选中的LLM模型:', llmModel.name); } else { // Use organization's default LLM from Index Chat Config (strict) llmModel = await this.modelConfigService.findDefaultByType( tenantId, ModelType.LLM, ); - console.log( + this.logger.log( '最终使用的LLM模型 (默认):', llmModel ? llmModel.name : '无', ); @@ -162,7 +165,7 @@ export class ChatController { res.write('data: [DONE]\n\n'); res.end(); } catch (error) { - console.error('Stream chat error:', error); + this.logger.error('Stream chat error:', error); try { res.write( `data: ${JSON.stringify({ type: 'error', data: error.message || 'Server Error' })}\n\n`, @@ -170,7 +173,7 @@ export class ChatController { res.write('data: [DONE]\n\n'); res.end(); } catch (writeError) { - console.error('Failed to write error response:', writeError); + this.logger.error('Failed to write error response:', writeError); } } } @@ -220,7 +223,7 @@ export class ChatController { res.write('data: [DONE]\n\n'); res.end(); } catch (error) { - console.error('Stream assist error:', error); + this.logger.error('Stream assist error:', error); res.write( `data: ${JSON.stringify({ type: 'error', data: error.message || 'Server Error' })}\n\n`, ); diff --git a/server/src/chat/chat.service.ts b/server/src/chat/chat.service.ts index fa606be..a740f03 100644 --- a/server/src/chat/chat.service.ts +++ b/server/src/chat/chat.service.ts @@ -71,30 +71,30 @@ export class ChatService { enableHyDE?: boolean, // New tenantId?: string, // New: tenant isolation ): AsyncGenerator<{ type: 'content' | 'sources' | 'historyId'; data: any }> { - console.log('=== ChatService.streamChat ==='); - console.log('User ID:', userId); - console.log('User language:', userLanguage); - console.log('Selected embedding model ID:', selectedEmbeddingId); - console.log('Selected groups:', selectedGroups); - console.log('Selected files:', selectedFiles); - console.log('History ID:', historyId); - console.log('Temperature:', temperature); - console.log('Max Tokens:', maxTokens); - console.log('Top K:', topK); - console.log('Similarity threshold:', similarityThreshold); - console.log('Rerank threshold:', rerankSimilarityThreshold); - console.log('Query expansion:', enableQueryExpansion); - console.log('HyDE:', enableHyDE); - console.log('Model configuration:', { + this.logger.log('=== ChatService.streamChat ==='); + this.logger.log('User ID:', userId); + this.logger.log('User language:', userLanguage); + this.logger.log('Selected embedding model ID:', selectedEmbeddingId); + this.logger.log('Selected groups:', selectedGroups); + this.logger.log('Selected files:', selectedFiles); + this.logger.log('History ID:', historyId); + this.logger.log('Temperature:', temperature); + this.logger.log('Max Tokens:', maxTokens); + this.logger.log('Top K:', topK); + this.logger.log('Similarity threshold:', similarityThreshold); + this.logger.log('Rerank threshold:', rerankSimilarityThreshold); + this.logger.log('Query expansion:', enableQueryExpansion); + this.logger.log('HyDE:', enableHyDE); + this.logger.log('Model configuration:', { name: modelConfig.name, modelId: modelConfig.modelId, baseUrl: modelConfig.baseUrl, }); - console.log( + this.logger.log( 'API Key prefix:', modelConfig.apiKey?.substring(0, 10) + '...', ); - console.log('API Key length:', modelConfig.apiKey?.length); + this.logger.log('API Key length:', modelConfig.apiKey?.length); // Get current language setting (keeping LANGUAGE_CONFIG for backward compatibility, now uses i18n service) // Use actual language based on user settings @@ -113,7 +113,7 @@ export class ChatService { selectedGroups, ); currentHistoryId = searchHistory.id; - console.log( + this.logger.log( this.i18nService.getMessage( 'creatingHistory', effectiveUserLanguage, @@ -143,7 +143,7 @@ export class ChatService { ); } - console.log( + this.logger.log( this.i18nService.getMessage( 'usingEmbeddingModel', effectiveUserLanguage, @@ -156,7 +156,7 @@ export class ChatService { ); // 2. Search using user's query directly - console.log( + this.logger.log( this.i18nService.getMessage('startingSearch', effectiveUserLanguage), ); yield { @@ -204,7 +204,7 @@ export class ChatService { // HybridSearch returns ES hit structure, but RagSearchResult is normalized // BuildContext expects {fileName, content}. RagSearchResult has these searchResults = ragResults; - console.log( + this.logger.log( this.i18nService.getMessage( 'searchResultsCount', effectiveUserLanguage, @@ -274,7 +274,7 @@ export class ChatService { }; } } catch (searchError) { - console.error( + this.logger.error( this.i18nService.getMessage( 'searchFailedLog', effectiveUserLanguage, @@ -461,14 +461,14 @@ ${instruction}`; try { // Join keywords into search string const combinedQuery = keywords.join(' '); - console.log( + this.logger.log( this.i18nService.getMessage('searchString', userLanguage) + combinedQuery, ); // Check if embedding model ID is provided if (!embeddingModelId) { - console.log( + this.logger.log( this.i18nService.getMessage( 'embeddingModelIdNotProvided', userLanguage, @@ -478,7 +478,7 @@ ${instruction}`; } // Use actual embedding vector - console.log( + this.logger.log( this.i18nService.getMessage('generatingEmbeddings', userLanguage), ); const queryEmbedding = await this.embeddingService.getEmbeddings( @@ -486,7 +486,7 @@ ${instruction}`; embeddingModelId, ); const queryVector = queryEmbedding[0]; - console.log( + this.logger.log( this.i18nService.getMessage('embeddingsGenerated', userLanguage) + this.i18nService.getMessage('dimensions', userLanguage) + ':', @@ -494,7 +494,7 @@ ${instruction}`; ); // Hybrid search - console.log( + this.logger.log( this.i18nService.getMessage('performingHybridSearch', userLanguage), ); const results = await this.elasticsearchService.hybridSearch( @@ -507,7 +507,7 @@ ${instruction}`; explicitFileIds, // Pass explicit file IDs tenantId, // Pass tenant ID ); - console.log( + this.logger.log( this.i18nService.getMessage('esSearchCompleted', userLanguage) + this.i18nService.getMessage('resultsCount', userLanguage) + ':', @@ -516,7 +516,7 @@ ${instruction}`; return results.slice(0, 10); } catch (error) { - console.error( + this.logger.error( this.i18nService.getMessage('hybridSearchFailed', userLanguage) + ':', error, ); diff --git a/server/src/common/json-utils.ts b/server/src/common/json-utils.ts index 7aca000..d7f68ff 100644 --- a/server/src/common/json-utils.ts +++ b/server/src/common/json-utils.ts @@ -1,3 +1,7 @@ +import { Logger } from '@nestjs/common'; + +const logger = new Logger('JsonUtils'); + /** * Safely parses JSON from a string, handling markdown code blocks and leading/trailing text. */ @@ -40,9 +44,9 @@ export function safeParseJson(text: string): T | null { try { return JSON.parse(jsonStr) as T; } catch (error) { - console.error('[safeParseJson] Failed to parse JSON:', error); - console.error('[safeParseJson] Original text:', text); - console.error('[safeParseJson] Extracted string:', jsonStr); + logger.error('[safeParseJson] Failed to parse JSON:', error); + logger.error('[safeParseJson] Original text:', text); + logger.error('[safeParseJson] Extracted string:', jsonStr); return null; } } diff --git a/server/src/knowledge-group/knowledge-group.controller.ts b/server/src/knowledge-group/knowledge-group.controller.ts index 49d5aa0..691d813 100644 --- a/server/src/knowledge-group/knowledge-group.controller.ts +++ b/server/src/knowledge-group/knowledge-group.controller.ts @@ -9,6 +9,7 @@ import { UseGuards, Request, Query, + Logger, } from '@nestjs/common'; import { CombinedAuthGuard } from '../auth/combined-auth.guard'; import { RolesGuard } from '../auth/roles.guard'; @@ -24,6 +25,8 @@ import { I18nService } from '../i18n/i18n.service'; @Controller('knowledge-groups') @UseGuards(CombinedAuthGuard, RolesGuard) export class KnowledgeGroupController { + private readonly logger = new Logger(KnowledgeGroupController.name); + constructor( private readonly groupService: KnowledgeGroupService, private readonly i18nService: I18nService, @@ -43,7 +46,7 @@ export class KnowledgeGroupController { @Post() @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN) async create(@Body() createGroupDto: CreateGroupDto, @Request() req) { - console.log('[KnowledgeGroup] create called, user:', req.user); + this.logger.log('[KnowledgeGroup] create called, user: ' + JSON.stringify(req.user)); return await this.groupService.create( req.user.id, req.user.tenantId, diff --git a/server/src/knowledge-group/knowledge-group.service.ts b/server/src/knowledge-group/knowledge-group.service.ts index 73df963..29d727b 100644 --- a/server/src/knowledge-group/knowledge-group.service.ts +++ b/server/src/knowledge-group/knowledge-group.service.ts @@ -1,5 +1,6 @@ import { Injectable, + Logger, NotFoundException, ForbiddenException, Inject, @@ -47,6 +48,8 @@ export interface PaginatedGroups { @Injectable() export class KnowledgeGroupService { + private readonly logger = new Logger(KnowledgeGroupService.name); + constructor( @InjectRepository(KnowledgeGroup) private groupRepository: Repository, @@ -62,7 +65,7 @@ export class KnowledgeGroupService { userId: string, tenantId: string, ): Promise { - console.log('[KnowledgeGroup findAll] userId:', userId, 'tenantId:', tenantId); + this.logger.log('[KnowledgeGroup findAll] userId: ' + userId + ', tenantId: ' + tenantId); // Return all groups for the tenant with file counts const queryBuilder = this.groupRepository .createQueryBuilder('group') @@ -147,7 +150,7 @@ export class KnowledgeGroupService { tenantId: string, createGroupDto: CreateGroupDto, ): Promise { - console.log('[KnowledgeGroup create] userId:', userId, 'tenantId:', tenantId); + this.logger.log('[KnowledgeGroup create] userId: ' + userId + ', tenantId: ' + tenantId); const group = this.groupRepository.create({ ...createGroupDto, parentId: createGroupDto.parentId ?? null, @@ -155,7 +158,7 @@ export class KnowledgeGroupService { }); const saved = await this.groupRepository.save(group); - console.log('[KnowledgeGroup create] saved group tenantId:', saved.tenantId); + this.logger.log('[KnowledgeGroup create] saved group tenantId: ' + saved.tenantId); return saved; } @@ -229,7 +232,7 @@ export class KnowledgeGroupService { ); } } catch (error) { - console.error( + this.logger.error( `Failed to delete file ${file.id} when deleting group ${id}`, error, ); diff --git a/server/src/note/note.service.ts b/server/src/note/note.service.ts index bfe6f8e..98e49fb 100644 --- a/server/src/note/note.service.ts +++ b/server/src/note/note.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Note } from './note.entity'; @@ -11,6 +11,8 @@ import { I18nService } from '../i18n/i18n.service'; @Injectable() export class NoteService { + private readonly logger = new Logger(NoteService.name); + // Directory will be created dynamically per user private getScreenshotsDir(userId: string) { return path.join(process.cwd(), 'uploads', 'notes-screenshots', userId); @@ -153,7 +155,7 @@ export class NoteService { } // Optional: Add logging to help debug permission issues - console.log(`User ${userId} attempting to add note to group ${groupId}`); + this.logger.log('User ' + userId + ' attempting to add note to group ' + groupId); } if (categoryId === '') { @@ -176,7 +178,7 @@ export class NoteService { screenshot.buffer, ); } catch (error) { - console.error('OCR extraction failed:', error); + this.logger.error('OCR extraction failed:', error); // Continue without OCR text if extraction fails } diff --git a/server/src/ocr/ocr.controller.ts b/server/src/ocr/ocr.controller.ts index b2454ec..126d058 100644 --- a/server/src/ocr/ocr.controller.ts +++ b/server/src/ocr/ocr.controller.ts @@ -1,5 +1,6 @@ import { Controller, + Logger, Post, UseGuards, UseInterceptors, @@ -14,6 +15,8 @@ import { I18nService } from '../i18n/i18n.service'; @UseGuards(CombinedAuthGuard) @UseGuards(CombinedAuthGuard) export class OcrController { + private readonly logger = new Logger(OcrController.name); + constructor( private readonly ocrService: OcrService, private readonly i18n: I18nService, @@ -22,14 +25,14 @@ export class OcrController { @Post('recognize') @UseInterceptors(FileInterceptor('image')) async recognizeText(@UploadedFile() image: Express.Multer.File) { - console.log('OCR recognition endpoint called'); + this.logger.log('OCR recognition endpoint called'); if (!image) { - console.error('No image uploaded'); + this.logger.error('No image uploaded'); throw new Error(this.i18n.getMessage('noImageUploaded')); } - console.log(`Received image. Size: ${image.size} bytes`); + this.logger.log('Received image. Size: ' + image.size + ' bytes'); const text = await this.ocrService.extractTextFromImage(image.buffer); - console.log(`OCR extraction completed. Text length: ${text.length}`); + this.logger.log('OCR extraction completed. Text length: ' + text.length); return { text }; } } diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index 20db0b9..58dd25c 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -171,7 +171,7 @@ export class UserService implements OnModuleInit { } const hashedPassword = await bcrypt.hash(password, 10); - console.log( + this.logger.log( `[UserService] Creating user: ${username}, isAdmin: ${isAdmin}`, ); const user = await this.usersRepository.save({ @@ -403,10 +403,7 @@ export class UserService implements OnModuleInit { role: UserRole.SUPER_ADMIN, }); - console.log('\n=== Admin account created ==='); - console.log('Username: admin'); - console.log('Password:', randomPassword); - console.log('========================================\n'); + this.logger.log('Admin account created (username: admin, password: ' + randomPassword + ')'); } } } diff --git a/web/components/views/AssessmentTemplateManager.tsx b/web/components/views/AssessmentTemplateManager.tsx index 76998ae..d18ebdd 100644 --- a/web/components/views/AssessmentTemplateManager.tsx +++ b/web/components/views/AssessmentTemplateManager.tsx @@ -29,6 +29,9 @@ export const AssessmentTemplateManager: React.FC = () => { difficultyDistribution: 'Basic: 30%, Intermediate: 40%, Advanced: 30%', style: 'Professional', knowledgeGroupId: '', + passingScore: 6, + totalTimeLimit: 1800, + perQuestionTimeLimit: 300, }); const [copiedId, setCopiedId] = useState(null); const [dimensions, setDimensions] = useState([]); @@ -73,6 +76,9 @@ export const AssessmentTemplateManager: React.FC = () => { : (template.difficultyDistribution || ''), style: template.style || 'Professional', knowledgeGroupId: template.knowledgeGroupId || '', + passingScore: template.passingScore ?? 6, + totalTimeLimit: template.totalTimeLimit ?? 1800, + perQuestionTimeLimit: template.perQuestionTimeLimit ?? 300, }); setDimensions(template.dimensions || []); } else { @@ -85,6 +91,9 @@ export const AssessmentTemplateManager: React.FC = () => { difficultyDistribution: '{"Basic": 3, "Intermediate": 4, "Advanced": 3}', style: 'Professional', knowledgeGroupId: '', + passingScore: 6, + totalTimeLimit: 1800, + perQuestionTimeLimit: 300, }); setDimensions([]); } @@ -115,6 +124,9 @@ export const AssessmentTemplateManager: React.FC = () => { style: formData.style, knowledgeGroupId: formData.knowledgeGroupId || undefined, dimensions: dimensions.length > 0 ? dimensions : undefined, + passingScore: formData.passingScore, + totalTimeLimit: formData.totalTimeLimit, + perQuestionTimeLimit: formData.perQuestionTimeLimit, }; if (editingTemplate) { @@ -400,17 +412,48 @@ export const AssessmentTemplateManager: React.FC = () => { -
- - setFormData({ ...formData, style: e.target.value })} - /> -
+
+ + setFormData({ ...formData, style: e.target.value })} + /> +
+ +
+ + setFormData({ ...formData, passingScore: parseFloat(e.target.value) || 0 })} + /> +
+
+ + setFormData({ ...formData, totalTimeLimit: parseInt(e.target.value) || 1800 })} + /> +
+
+ + setFormData({ ...formData, perQuestionTimeLimit: parseInt(e.target.value) || 300 })} + /> +
diff --git a/web/components/views/AssessmentView.tsx b/web/components/views/AssessmentView.tsx index e2fe274..0939a41 100644 --- a/web/components/views/AssessmentView.tsx +++ b/web/components/views/AssessmentView.tsx @@ -51,6 +51,7 @@ export const AssessmentView: React.FC = ({ const [templates, setTemplates] = useState([]); const [selectedTemplate, setSelectedTemplate] = useState(null); const [timeCheck, setTimeCheck] = useState<{ totalTimeRemaining: number; questionTimeRemaining: number; isTotalTimeout: boolean; isQuestionTimeout: boolean } | null>(null); + const isTimedOut = timeCheck?.isTotalTimeout || timeCheck?.isQuestionTimeout; const messagesEndRef = useRef(null); @@ -137,7 +138,11 @@ export const AssessmentView: React.FC = ({ setState(histState); setSession(histSession); } catch (err: any) { - setError(err.message || 'Failed to load historical assessment'); + if (histSession.status === 'IN_PROGRESS') { + setError(t('cannotResumeInProgress')); + } else { + setError(err.message || 'Failed to load historical assessment'); + } } finally { setIsLoading(false); setLoadingHistoryId(null); @@ -184,7 +189,7 @@ export const AssessmentView: React.FC = ({ ...prev, ...event.data, messages: event.data.messages - ? [...prevMessages, ...event.data.messages.filter((m: any) => !prevMessages.some((pm: any) => pm.content === m.content && pm.role === m.role))] + ? [...prevMessages, ...event.data.messages.filter((m: any) => !prevMessages.some((pm: any) => (m.id && pm.id === m.id) || (pm.content === m.content && pm.role === m.role)))] : prevMessages, feedbackHistory: event.data.feedbackHistory ? [...(prev.feedbackHistory || []), ...event.data.feedbackHistory.filter((fh: any) => !(prev.feedbackHistory || []).some((pfh: any) => pfh.content === fh.content))] @@ -227,7 +232,7 @@ export const AssessmentView: React.FC = ({ }; const handleSubmitAnswer = async () => { - if (!session || !inputValue.trim() || isLoading) return; + if (!session || !inputValue.trim() || isLoading || isTimedOut) return; const answer = inputValue.trim(); setInputValue(''); @@ -252,7 +257,7 @@ export const AssessmentView: React.FC = ({ if (!prev) return event.data; const prevMessages = prev.messages || []; const mergedMessages = event.data.messages - ? [...prevMessages, ...event.data.messages.filter((m: any) => !prevMessages.some((pm: any) => pm.content === m.content && pm.role === m.role))] + ? [...prevMessages, ...event.data.messages.filter((m: any) => !prevMessages.some((pm: any) => (m.id && pm.id === m.id) || (pm.content === m.content && pm.role === m.role)))] : prevMessages; return { @@ -428,7 +433,7 @@ export const AssessmentView: React.FC = ({ {/* Assessment History Sidebar */} {history.length > 0 && ( -
+

{t('recentAssessments')} @@ -576,26 +581,32 @@ export const AssessmentView: React.FC = ({

+ {isTimedOut && ( +
+ {t('timeLimitExceeded')} +
+ )}