feat: enhance textbook reader with anchor text support and improve knowledge point management
This commit is contained in:
@@ -2,7 +2,7 @@ import "server-only";
|
||||
|
||||
import { randomInt } from "node:crypto"
|
||||
import { cache } from "react"
|
||||
import { and, asc, desc, eq, inArray, sql, type SQL } from "drizzle-orm"
|
||||
import { and, asc, desc, eq, inArray, or, sql, type SQL } from "drizzle-orm"
|
||||
import { createId } from "@paralleldrive/cuid2"
|
||||
|
||||
import { db } from "@/shared/db"
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
homeworkAssignments,
|
||||
homeworkSubmissions,
|
||||
schools,
|
||||
subjects,
|
||||
exams,
|
||||
users,
|
||||
} from "@/shared/db/schema"
|
||||
import { DEFAULT_CLASS_SUBJECTS } from "./types"
|
||||
@@ -169,7 +171,35 @@ export const getTeacherClasses = cache(async (params?: { teacherId?: string }):
|
||||
}))
|
||||
|
||||
list.sort(compareClassLike)
|
||||
return list
|
||||
|
||||
// Fetch recent assignments for trends and schedule
|
||||
const listWithTrends = await Promise.all(
|
||||
list.map(async (c) => {
|
||||
const [insights, schedule] = await Promise.all([
|
||||
getClassHomeworkInsights({ classId: c.id, teacherId, limit: 7 }),
|
||||
getClassSchedule({ classId: c.id, teacherId }),
|
||||
])
|
||||
|
||||
const recentAssignments = insights
|
||||
? insights.assignments.map((a) => ({
|
||||
id: a.assignmentId,
|
||||
title: a.title,
|
||||
status: a.status,
|
||||
subject: a.subject,
|
||||
isActive: a.isActive,
|
||||
isOverdue: a.isOverdue,
|
||||
dueAt: a.dueAt ? new Date(a.dueAt) : null,
|
||||
submittedCount: a.submittedCount,
|
||||
targetCount: a.targetCount,
|
||||
avgScore: a.scoreStats.avg,
|
||||
medianScore: a.scoreStats.median,
|
||||
}))
|
||||
: []
|
||||
return { ...c, recentAssignments, schedule }
|
||||
})
|
||||
)
|
||||
|
||||
return listWithTrends
|
||||
})
|
||||
|
||||
export const getTeacherOptions = cache(async (): Promise<TeacherOption[]> => {
|
||||
@@ -752,11 +782,22 @@ export const getClassHomeworkInsights = cache(
|
||||
}
|
||||
|
||||
const limit = typeof params.limit === "number" && params.limit > 0 ? params.limit : 50
|
||||
const assignments = await db.query.homeworkAssignments.findMany({
|
||||
where: and(inArray(homeworkAssignments.id, assignmentIds), eq(homeworkAssignments.creatorId, teacherId)),
|
||||
orderBy: [desc(homeworkAssignments.createdAt)],
|
||||
limit,
|
||||
})
|
||||
const assignments = await db
|
||||
.select({
|
||||
id: homeworkAssignments.id,
|
||||
title: homeworkAssignments.title,
|
||||
status: homeworkAssignments.status,
|
||||
createdAt: homeworkAssignments.createdAt,
|
||||
dueAt: homeworkAssignments.dueAt,
|
||||
subjectId: exams.subjectId,
|
||||
subjectName: subjects.name
|
||||
})
|
||||
.from(homeworkAssignments)
|
||||
.innerJoin(exams, eq(homeworkAssignments.sourceExamId, exams.id))
|
||||
.leftJoin(subjects, eq(exams.subjectId, subjects.id))
|
||||
.where(and(inArray(homeworkAssignments.id, assignmentIds), eq(homeworkAssignments.creatorId, teacherId)))
|
||||
.orderBy(desc(homeworkAssignments.createdAt))
|
||||
.limit(limit)
|
||||
|
||||
const usedAssignmentIds = assignments.map((a) => a.id)
|
||||
if (usedAssignmentIds.length === 0) {
|
||||
@@ -845,6 +886,7 @@ export const getClassHomeworkInsights = cache(
|
||||
assignmentId: a.id,
|
||||
title: a.title,
|
||||
status: (a.status as string) ?? "draft",
|
||||
subject: a.subjectName,
|
||||
createdAt: a.createdAt.toISOString(),
|
||||
dueAt: a.dueAt ? a.dueAt.toISOString() : null,
|
||||
isActive: dueMs === null || dueMs >= nowMs,
|
||||
@@ -1694,3 +1736,104 @@ export async function deleteClassScheduleItem(scheduleId: string): Promise<void>
|
||||
|
||||
await db.delete(classSchedule).where(eq(classSchedule.id, id))
|
||||
}
|
||||
|
||||
export const getStudentsSubjectScores = cache(
|
||||
async (studentIds: string[]): Promise<Map<string, Record<string, number | null>>> => {
|
||||
if (studentIds.length === 0) return new Map()
|
||||
|
||||
// 1. Find assignments targeted at these students
|
||||
const assignmentTargets = await db
|
||||
.select({ assignmentId: homeworkAssignmentTargets.assignmentId })
|
||||
.from(homeworkAssignmentTargets)
|
||||
.where(inArray(homeworkAssignmentTargets.studentId, studentIds))
|
||||
|
||||
const assignmentIds = Array.from(new Set(assignmentTargets.map(t => t.assignmentId)))
|
||||
if (assignmentIds.length === 0) return new Map()
|
||||
|
||||
// 2. Get assignment details including subject from linked exam
|
||||
const assignments = await db
|
||||
.select({
|
||||
id: homeworkAssignments.id,
|
||||
createdAt: homeworkAssignments.createdAt,
|
||||
subjectId: exams.subjectId,
|
||||
subjectName: subjects.name
|
||||
})
|
||||
.from(homeworkAssignments)
|
||||
.innerJoin(exams, eq(homeworkAssignments.sourceExamId, exams.id))
|
||||
.leftJoin(subjects, eq(exams.subjectId, subjects.id))
|
||||
.where(and(
|
||||
inArray(homeworkAssignments.id, assignmentIds),
|
||||
eq(homeworkAssignments.status, "published")
|
||||
))
|
||||
.orderBy(desc(homeworkAssignments.createdAt))
|
||||
|
||||
// 3. Filter subjects (exclude PE, Music, Art)
|
||||
const excludeSubjects = ["体育", "音乐", "美术"]
|
||||
const subjectAssignments = new Map<string, string>() // subject -> assignmentId (latest)
|
||||
|
||||
for (const a of assignments) {
|
||||
if (!a.subjectName) continue
|
||||
if (excludeSubjects.includes(a.subjectName)) continue
|
||||
if (!subjectAssignments.has(a.subjectName)) {
|
||||
subjectAssignments.set(a.subjectName, a.id)
|
||||
}
|
||||
}
|
||||
|
||||
const targetAssignmentIds = Array.from(subjectAssignments.values())
|
||||
if (targetAssignmentIds.length === 0) return new Map()
|
||||
|
||||
// 4. Get submissions for these assignments
|
||||
const submissions = await db
|
||||
.select({
|
||||
studentId: homeworkSubmissions.studentId,
|
||||
assignmentId: homeworkSubmissions.assignmentId,
|
||||
score: homeworkSubmissions.score,
|
||||
createdAt: homeworkSubmissions.createdAt,
|
||||
})
|
||||
.from(homeworkSubmissions)
|
||||
.where(inArray(homeworkSubmissions.assignmentId, targetAssignmentIds))
|
||||
.orderBy(desc(homeworkSubmissions.createdAt))
|
||||
|
||||
// 5. Map back to subject scores per student
|
||||
const studentScores = new Map<string, Record<string, number | null>>()
|
||||
|
||||
// Create reverse map for assignment -> subject
|
||||
const assignmentSubjectMap = new Map<string, string>()
|
||||
for (const [subject, id] of subjectAssignments.entries()) {
|
||||
assignmentSubjectMap.set(id, subject)
|
||||
}
|
||||
|
||||
for (const s of submissions) {
|
||||
const subject = assignmentSubjectMap.get(s.assignmentId)
|
||||
if (!subject) continue
|
||||
|
||||
if (!studentScores.has(s.studentId)) {
|
||||
studentScores.set(s.studentId, {})
|
||||
}
|
||||
|
||||
const scores = studentScores.get(s.studentId)!
|
||||
// Only set if not already set (since we ordered by desc createdAt, first one is latest)
|
||||
if (scores[subject] === undefined) {
|
||||
scores[subject] = s.score
|
||||
}
|
||||
}
|
||||
|
||||
return studentScores
|
||||
}
|
||||
)
|
||||
|
||||
export const getClassStudentSubjectScoresV2 = cache(
|
||||
async (classId: string): Promise<Map<string, Record<string, number | null>>> => {
|
||||
// 1. Get student IDs in the class
|
||||
const enrollments = await db
|
||||
.select({ studentId: classEnrollments.studentId })
|
||||
.from(classEnrollments)
|
||||
.where(and(
|
||||
eq(classEnrollments.classId, classId),
|
||||
eq(classEnrollments.status, "active")
|
||||
))
|
||||
|
||||
const studentIds = enrollments.map(e => e.studentId)
|
||||
return getStudentsSubjectScores(studentIds)
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user