import { Injectable } from '@nestjs/common'; import { LearningActivityRepository } from './learning-activity.repository'; import { LearningTrendWorkflow } from '../ai/workflows/learning-trend.workflow'; @Injectable() export class LearningActivityService { constructor( private readonly repository: LearningActivityRepository, private readonly trendWorkflow: LearningTrendWorkflow, ) {} async getHeatmap(userId: string) { const activities = await this.repository.findAll(userId); const heatmap: Record = {}; for (const a of activities) { const dateStr = a.activityDate instanceof Date ? a.activityDate.toISOString().split('T')[0] : String(a.activityDate).split('T')[0]; heatmap[dateStr] = a.durationSeconds; } return heatmap; } async getSummary(userId: string) { const activities = await this.repository.findAll(userId); const totalMinutes = Math.round( activities.reduce((s, a) => s + a.durationSeconds, 0) / 60, ); const totalCards = activities.reduce((s, a) => s + a.reviewCount, 0); const activeDays = activities.filter((a) => a.durationSeconds > 0).length; const dailyAverage = activeDays > 0 ? Math.round(totalMinutes / activeDays) : 0; return { totalMinutes, totalCardsReviewed: totalCards, activeDays, dailyAverage }; } async getTrend(userId: string, periodDays: number = 7) { const activities = await this.repository.findAll(userId); const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - periodDays); const recent = activities.filter((a) => new Date(a.activityDate) >= cutoff ); const previousCutoff = new Date(cutoff); previousCutoff.setDate(previousCutoff.getDate() - periodDays); const previous = activities.filter((a) => { const d = new Date(a.activityDate); return d >= previousCutoff && d < cutoff; }); const sum = (items: typeof activities, key: keyof typeof activities[0]) => items.reduce((s, a) => s + (Number(a[key]) || 0), 0); const recentTotalMinutes = Math.round(sum(recent, 'durationSeconds') / 60); const recentSessions = sum(recent, 'sessionsCount'); const recentRecall = sum(recent, 'activeRecallCount'); const recentReview = sum(recent, 'reviewCount'); const recentAiAnalysis = sum(recent, 'aiAnalysisCount'); const recentLoops = sum(recent, 'completedLoopCount'); const recentActiveDays = recent.filter((a) => a.durationSeconds > 0).length; const recentDailyAvg = recentActiveDays > 0 ? Math.round(recentTotalMinutes / recentActiveDays) : 0; const recentActivityLevel = recent.length > 0 ? Math.round(recent.reduce((s, a) => s + a.activityLevel, 0) / recent.length) : 0; const prevTotalMinutes = Math.round(sum(previous, 'durationSeconds') / 60); // Build daily time-series for chart rendering const dailySeries = this.buildDailySeries(recent, periodDays); const trendInput = { userId, periodDays, totalMinutes: recentTotalMinutes, sessionsCount: recentSessions, activeRecallCount: recentRecall, reviewCount: recentReview, aiAnalysisCount: recentAiAnalysis, completedLoopCount: recentLoops, activityLevel: recentActivityLevel, activeDays: recentActiveDays, dailyAverage: recentDailyAvg, previousPeriod: previous.length > 0 ? { totalMinutes: prevTotalMinutes, activeRecallCount: sum(previous, 'activeRecallCount'), reviewCount: sum(previous, 'reviewCount'), activeDays: previous.filter((a) => a.durationSeconds > 0).length, } : undefined, }; // AI analysis with 15s timeout fallback let aiResult: any = {}; try { aiResult = await Promise.race([ this.trendWorkflow.execute(trendInput), new Promise((_, reject) => setTimeout(() => reject(new Error('AI trend analysis timeout')), 15000) ), ]); } catch (err) { aiResult = { periodSummary: 'AI 分析暂不可用', overallScore: 0, overallDirection: 'stable', trends: [], strengths: [], weaknesses: [], recommendations: [], nextFocusAreas: [] }; } return { ...aiResult, dailySeries }; } private buildDailySeries(activities: any[], days: number) { const series: { date: string; value: number; label: string }[] = []; const now = new Date(); for (let i = days - 1; i >= 0; i--) { const d = new Date(now); d.setDate(d.getDate() - i); // Use local date string to avoid UTC timezone shift issues const dateStr = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; const dayActs = activities.filter((a) => { const ad = a.activityDate instanceof Date ? a.activityDate : new Date(a.activityDate); const adStr = `${ad.getFullYear()}-${String(ad.getMonth() + 1).padStart(2, '0')}-${String(ad.getDate()).padStart(2, '0')}`; return adStr === dateStr; }); const minutes = Math.round(dayActs.reduce((s: number, a: any) => s + a.durationSeconds, 0) / 60); series.push({ date: dateStr, value: minutes, label: `${minutes}分钟`, }); } return series; } }