package assignmentgen import ( "math" "sort" "strings" "boostai-backend/internal/sqlc" "github.com/jackc/pgx/v5/pgtype" ) func buildWeaknessSummary(rows []sqlc.ListStudentPlanningPerformanceRow) WeaknessSummary { topicTotals := make(map[sqlc.QuestionTopic]float64) topicCounts := make(map[sqlc.QuestionTopic]int) tagStats := make(map[string]*tagWeaknessStats) recentIssues := make([]string, 0, 5) seenIssues := make(map[string]struct{}) for _, row := range rows { score := planningScore(row.IsCorrect.Bool, numericFloat64OrZero(row.ReviewUnderstandingScore)) accumulateTopicScore(topicTotals, topicCounts, row, score) accumulateTagStats(tagStats, row, score) recentIssues = appendRecentIssue(recentIssues, seenIssues, row.ReviewIssueReason.String) if len(recentIssues) >= 5 { break } } return WeaknessSummary{ TopicScores: buildTopicScores(topicTotals, topicCounts), WeakTags: collectWeakTags(tagStats), RecentIssues: recentIssues, } } type tagWeaknessStats struct { total float64 count int flaggedCount int } type scoredTag struct { tag string score float64 flaggedCount int } func accumulateTopicScore( topicTotals map[sqlc.QuestionTopic]float64, topicCounts map[sqlc.QuestionTopic]int, row sqlc.ListStudentPlanningPerformanceRow, score float64, ) { if !row.Topic.Valid { return } topicTotals[row.Topic.QuestionTopic] += score topicCounts[row.Topic.QuestionTopic]++ } func accumulateTagStats(tagStats map[string]*tagWeaknessStats, row sqlc.ListStudentPlanningPerformanceRow, score float64) { for _, rawTag := range row.QuestionTags { tag := strings.TrimSpace(strings.ToLower(rawTag)) if tag == "" { continue } stats, ok := tagStats[tag] if !ok { stats = &tagWeaknessStats{} tagStats[tag] = stats } stats.total += score stats.count++ if row.ReviewNeedsAttention { stats.flaggedCount++ } } } func appendRecentIssue(recentIssues []string, seenIssues map[string]struct{}, rawIssue string) []string { issue := strings.TrimSpace(rawIssue) if issue == "" { return recentIssues } if _, exists := seenIssues[issue]; exists { return recentIssues } seenIssues[issue] = struct{}{} return append(recentIssues, issue) } func buildTopicScores(topicTotals map[sqlc.QuestionTopic]float64, topicCounts map[sqlc.QuestionTopic]int) map[sqlc.QuestionTopic]float64 { topicScores := make(map[sqlc.QuestionTopic]float64, len(topicTotals)) for topic, total := range topicTotals { count := topicCounts[topic] if count == 0 { continue } topicScores[topic] = roundToOneDecimal((total / float64(count)) * 100) } return topicScores } func collectWeakTags(tagStats map[string]*tagWeaknessStats) []string { if len(tagStats) == 0 { return nil } weakCandidates := make([]scoredTag, 0, len(tagStats)) for tag, stats := range tagStats { if stats == nil || stats.count == 0 { continue } average := (stats.total / float64(stats.count)) * 100 if average >= 70 && stats.flaggedCount == 0 { continue } weakCandidates = append(weakCandidates, scoredTag{ tag: tag, score: roundToOneDecimal(average), flaggedCount: stats.flaggedCount, }) } sort.SliceStable(weakCandidates, func(i, j int) bool { if weakCandidates[i].score == weakCandidates[j].score { if weakCandidates[i].flaggedCount == weakCandidates[j].flaggedCount { return weakCandidates[i].tag < weakCandidates[j].tag } return weakCandidates[i].flaggedCount > weakCandidates[j].flaggedCount } return weakCandidates[i].score < weakCandidates[j].score }) limit := 6 if len(weakCandidates) < limit { limit = len(weakCandidates) } weakTags := make([]string, 0, limit) for idx := 0; idx < limit; idx++ { weakTags = append(weakTags, weakCandidates[idx].tag) } return weakTags } func planningScore(isCorrect bool, understandingScore float64) float64 { correctness := 0.0 if isCorrect { correctness = 1.0 } return (correctness + understandingScore) / 2 } func roundToOneDecimal(value float64) float64 { return math.Round(value*10) / 10 } func numericFloat64OrZero(value pgtype.Numeric) float64 { floatValue, err := value.Float64Value() if err != nil || !floatValue.Valid { return 0 } return floatValue.Float64 }