172 lines
4.2 KiB
Go
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
|
|
}
|