Files
BoostAI/Backend/internal/assignmentgen/personalization_weakness.go
2026-05-25 17:05:06 +01:00

172 lines
4.2 KiB
Go

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
}