=test_update_homework_tests_and_work_log
Some checks failed
CI / build-deploy (push) Has been cancelled

This commit is contained in:
SpecialX
2026-03-19 13:16:49 +08:00
parent eb08c0ab68
commit 99f116cb64
70 changed files with 7470 additions and 20220 deletions

View File

@@ -3,14 +3,7 @@ import Link from "next/link"
import { EmptyState } from "@/shared/components/ui/empty-state"
import { Badge } from "@/shared/components/ui/badge"
import { Button } from "@/shared/components/ui/button"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/components/ui/table"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import { formatDate } from "@/shared/lib/utils"
import { getDemoStudentUser, getStudentHomeworkAssignments } from "@/modules/homework/data-access"
import { Inbox } from "lucide-react"
@@ -43,18 +36,14 @@ const getActionVariant = (status: string): "default" | "secondary" | "outline" =
return "default"
}
const isAnswered = (status: string) => status === "submitted" || status === "graded"
export default async function StudentAssignmentsPage() {
const student = await getDemoStudentUser()
if (!student) {
return (
<div className="h-full flex-1 flex-col space-y-8 p-8 md:flex">
<div className="flex items-center justify-between space-y-2">
<div>
<h2 className="text-2xl font-bold tracking-tight">Assignments</h2>
<p className="text-muted-foreground">Your homework assignments.</p>
</div>
</div>
<EmptyState title="No user found" description="Create a student user to see assignments." icon={Inbox} />
</div>
)
@@ -62,63 +51,115 @@ export default async function StudentAssignmentsPage() {
const assignments = await getStudentHomeworkAssignments(student.id)
const hasAssignments = assignments.length > 0
const assignmentsBySubject = assignments.reduce((acc, assignment) => {
const subject = assignment.subjectName?.trim() || "Other"
const existing = acc.get(subject)
if (existing) {
existing.push(assignment)
} else {
acc.set(subject, [assignment])
}
return acc
}, new Map<string, typeof assignments>())
const subjectEntries = Array.from(assignmentsBySubject.entries()).sort((a, b) => a[0].localeCompare(b[0]))
return (
<div className="h-full flex-1 flex-col space-y-8 p-8 md:flex">
<div className="flex items-center justify-between space-y-2">
<div>
<h2 className="text-2xl font-bold tracking-tight">Assignments</h2>
<p className="text-muted-foreground">Your homework assignments.</p>
</div>
<Button asChild variant="outline">
<Link href="/student/dashboard">Back</Link>
</Button>
</div>
{!hasAssignments ? (
<EmptyState title="No assignments" description="You have no assigned homework right now." icon={Inbox} />
) : (
<div className="rounded-md border bg-card">
<Table>
<TableHeader>
<TableRow>
<TableHead>Title</TableHead>
<TableHead>Status</TableHead>
<TableHead>Due</TableHead>
<TableHead>Attempts</TableHead>
<TableHead>Score</TableHead>
<TableHead className="text-right">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{assignments.map((a) => (
<TableRow key={a.id}>
<TableCell className="font-medium">
<Link href={`/student/learning/assignments/${a.id}`} className="hover:underline">
{a.title}
</Link>
</TableCell>
<TableCell>
<Badge variant={getStatusVariant(a.progressStatus)} className="capitalize">
{getStatusLabel(a.progressStatus)}
</Badge>
</TableCell>
<TableCell>{a.dueAt ? formatDate(a.dueAt) : "-"}</TableCell>
<TableCell className="tabular-nums text-muted-foreground">
{a.attemptsUsed}/{a.maxAttempts}
</TableCell>
<TableCell className="tabular-nums">{a.latestScore ?? "-"}</TableCell>
<TableCell className="text-right">
<Button asChild size="sm" variant={getActionVariant(a.progressStatus)}>
<Link href={`/student/learning/assignments/${a.id}`}>
{getActionLabel(a.progressStatus)}
</Link>
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div className="space-y-6">
{subjectEntries.map(([subject, items]) => {
const answeredItems = items.filter((a) => isAnswered(a.progressStatus))
const unansweredItems = items.filter((a) => !isAnswered(a.progressStatus))
return (
<div key={subject} className="space-y-3">
<div className="text-sm font-semibold text-muted-foreground">{subject}</div>
{unansweredItems.length > 0 && (
<div className="space-y-3">
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground"></div>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{unansweredItems.map((a) => (
<Card key={a.id} className="flex h-full flex-col overflow-hidden transition-all hover:shadow-md">
<CardHeader className="gap-2 pb-3">
<div className="flex items-start justify-between gap-3">
<CardTitle className="text-base">
<Link href={`/student/learning/assignments/${a.id}`} className="hover:underline">
{a.title}
</Link>
</CardTitle>
<Badge variant={getStatusVariant(a.progressStatus)} className="capitalize">
{getStatusLabel(a.progressStatus)}
</Badge>
</div>
<div className="text-xs text-muted-foreground">
<span>Due {a.dueAt ? formatDate(a.dueAt) : "-"}</span>
<span className="px-2"></span>
<span>
Attempts {a.attemptsUsed}/{a.maxAttempts}
</span>
</div>
</CardHeader>
<CardContent className="mt-auto flex items-center justify-between">
<div className="text-sm">
<div className="text-muted-foreground">Score</div>
<div className="font-medium tabular-nums">{a.latestScore ?? "-"}</div>
</div>
<Button asChild size="sm" variant={getActionVariant(a.progressStatus)}>
<Link href={`/student/learning/assignments/${a.id}`}>
{getActionLabel(a.progressStatus)}
</Link>
</Button>
</CardContent>
</Card>
))}
</div>
</div>
)}
{answeredItems.length > 0 && (
<div className="space-y-3">
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground"></div>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{answeredItems.map((a) => (
<Card key={a.id} className="flex h-full flex-col overflow-hidden transition-all hover:shadow-md">
<CardHeader className="gap-2 pb-3">
<div className="flex items-start justify-between gap-3">
<CardTitle className="text-base">
<Link href={`/student/learning/assignments/${a.id}`} className="hover:underline">
{a.title}
</Link>
</CardTitle>
<Badge variant={getStatusVariant(a.progressStatus)} className="capitalize">
{getStatusLabel(a.progressStatus)}
</Badge>
</div>
<div className="text-xs text-muted-foreground">
<span>Due {a.dueAt ? formatDate(a.dueAt) : "-"}</span>
<span className="px-2"></span>
<span>
Attempts {a.attemptsUsed}/{a.maxAttempts}
</span>
</div>
</CardHeader>
<CardContent className="mt-auto flex items-center justify-between">
<div className="text-sm">
<div className="text-muted-foreground">Score</div>
<div className="font-medium tabular-nums">{a.latestScore ?? "-"}</div>
</div>
<Button asChild size="sm" variant={getActionVariant(a.progressStatus)}>
<Link href={`/student/learning/assignments/${a.id}`}>
{getActionLabel(a.progressStatus)}
</Link>
</Button>
</CardContent>
</Card>
))}
</div>
</div>
)}
</div>
)})}
</div>
)}
</div>

View File

@@ -3,7 +3,7 @@ import { ExamForm } from "@/modules/exams/components/exam-form"
export default function CreateExamPage() {
return (
<div className="flex justify-center items-center min-h-[calc(100vh-160px)] p-8 max-w-[1200px] mx-auto">
<div className="flex w-full justify-center items-center min-h-[calc(100vh-160px)] p-8 max-w-[1200px] mx-auto">
<ExamForm />
</div>
)

View File

@@ -0,0 +1,28 @@
import { NextResponse } from "next/server"
import { auth } from "@/auth"
import { createAiChatCompletion, getAiErrorMessage, parseAiChatPayload } from "@/shared/lib/ai"
export const dynamic = "force-dynamic"
const getStatusFromError = (message: string) => {
if (message === "Invalid payload" || message === "Messages are required") return 400
if (message === "AI API key missing") return 500
if (message === "Empty response") return 502
return 502
}
export async function POST(req: Request) {
const session = await auth()
const userId = String(session?.user?.id ?? "").trim()
if (!userId) return NextResponse.json({ success: false, message: "Unauthorized" }, { status: 401 })
try {
const body = await req.json().catch(() => null)
const input = parseAiChatPayload(body)
const result = await createAiChatCompletion(input)
return NextResponse.json({ success: true, content: result.content, usage: result.usage })
} catch (e) {
const message = getAiErrorMessage(e)
return NextResponse.json({ success: false, message }, { status: getStatusFromError(message) })
}
}

View File

@@ -172,7 +172,7 @@
@apply border-border;
}
body {
@apply bg-background text-foreground;
@apply bg-background text-foreground h-screen overflow-hidden;
font-feature-settings: "rlig" 1, "calt" 1;
}
}

View File

@@ -20,6 +20,7 @@ export default function RootLayout({
<html lang="en" suppressHydrationWarning>
<body
className={`antialiased`}
suppressHydrationWarning
>
<ThemeProvider
attribute="class"