Fix UI bugs: history page fallback, admin page typescript type, cbt page math rendering

This commit is contained in:
2026-07-09 09:47:10 +05:30
parent 8f9bef9744
commit bc444ea737
4 changed files with 66 additions and 39 deletions

View File

@@ -481,17 +481,17 @@ def get_admin_tests(session: Session = Depends(get_session)):
return tests return tests
class ManualQuestionInput(BaseModel): class ManualQuestionInput(BaseModel):
section: str section: str = "General"
text_en: str subject: str
text_hi: Optional[str] = None topic: str
options_en: List[str] subtopic: str
options_hi: Optional[List[str]] = None difficulty: str
correct_answer: str type: str
type: str = "SingleChoice" question_text: str
topic: str = "General" options: Optional[Dict[str, Any]] = None
difficulty: str = "Medium" correct_answer: Dict[str, Any]
marks: int = 4 solution_text: str
negative_marks: int = 1 language: str
class ManualTestCreate(BaseModel): class ManualTestCreate(BaseModel):
title: str title: str
@@ -574,16 +574,17 @@ def create_manual_test(
for q_data in data.questions_json: for q_data in data.questions_json:
q_obj = models.Question( q_obj = models.Question(
text_en=q_data.text_en, subject=q_data.subject,
text_hi=q_data.text_hi,
options_en=q_data.options_en,
options_hi=q_data.options_hi,
correct_answer=q_data.correct_answer,
type=q_data.type,
topic=q_data.topic, topic=q_data.topic,
subtopic=q_data.subtopic,
difficulty=q_data.difficulty, difficulty=q_data.difficulty,
marks=q_data.marks, type=models.QuestionTypeEnum(q_data.type),
negative_marks=q_data.negative_marks question_text=q_data.question_text,
options=q_data.options,
correct_answer=q_data.correct_answer,
solution_text=q_data.solution_text,
language=models.MediumEnum(q_data.language),
created_by=data.admin_id
) )
session.add(q_obj) session.add(q_obj)
session.commit() session.commit()

View File

@@ -153,7 +153,7 @@ export default function AdminTestsPage() {
<label className="text-sm font-medium">Scheduled Start (Optional)</label> <label className="text-sm font-medium">Scheduled Start (Optional)</label>
<DatePicker <DatePicker
selected={scheduledDate} selected={scheduledDate}
onChange={(date) => setScheduledDate(date)} onChange={(date: Date | null) => setScheduledDate(date)}
showTimeSelect showTimeSelect
timeFormat="HH:mm" timeFormat="HH:mm"
timeIntervals={15} timeIntervals={15}
@@ -167,7 +167,7 @@ export default function AdminTestsPage() {
<label className="text-sm font-medium">Scheduled End (Optional)</label> <label className="text-sm font-medium">Scheduled End (Optional)</label>
<DatePicker <DatePicker
selected={endDate} selected={endDate}
onChange={(date) => setEndDate(date)} onChange={(date: Date | null) => setEndDate(date)}
showTimeSelect showTimeSelect
timeFormat="HH:mm" timeFormat="HH:mm"
timeIntervals={15} timeIntervals={15}

View File

@@ -6,6 +6,10 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { ArrowLeft, CheckCircle2, XCircle, Clock, Award } from "lucide-react"; import { ArrowLeft, CheckCircle2, XCircle, Clock, Award } from "lucide-react";
import ReactMarkdown from "react-markdown";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import "katex/dist/katex.min.css";
export default function AttemptHistoryPage({ params }: { params: Promise<{ attemptId: string }> }) { export default function AttemptHistoryPage({ params }: { params: Promise<{ attemptId: string }> }) {
const { attemptId } = use(params); const { attemptId } = use(params);
@@ -123,7 +127,11 @@ export default function AttemptHistoryPage({ params }: { params: Promise<{ attem
{idx + 1} {idx + 1}
</div> </div>
<div> <div>
<h3 className="font-medium text-lg leading-relaxed">{q.question_text || q.text_en || q.text}</h3> <div className="font-medium text-lg leading-relaxed prose prose-sm dark:prose-invert">
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
{q.question_text || q.text_en || q.text || ""}
</ReactMarkdown>
</div>
<div className="flex gap-2 mt-2"> <div className="flex gap-2 mt-2">
<Badge variant="outline" className="text-xs">{q.topic}</Badge> <Badge variant="outline" className="text-xs">{q.topic}</Badge>
<Badge variant="outline" className="text-xs">{q.difficulty}</Badge> <Badge variant="outline" className="text-xs">{q.difficulty}</Badge>
@@ -140,28 +148,39 @@ export default function AttemptHistoryPage({ params }: { params: Promise<{ attem
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6 ml-12"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6 ml-12">
<div className="space-y-2"> <div className="space-y-2">
<p className="text-sm font-bold text-muted-foreground uppercase">Your Answer</p> <p className="text-sm font-bold text-muted-foreground uppercase">Your Answer</p>
<div className={`p-3 rounded-lg border ${ans.is_correct ? 'bg-green-500/10 border-green-500/30 text-green-500' : 'bg-red-500/10 border-red-500/30 text-red-500'}`}> <div className={`p-3 rounded-lg border prose prose-sm dark:prose-invert ${ans.is_correct ? 'bg-green-500/10 border-green-500/30 text-green-500' : 'bg-red-500/10 border-red-500/30 text-red-500'}`}>
{ans.is_subjective || q.type === 'Subjective' || q.type === 'Numerical' <ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
? (ans.selected_answer || 'Not Attempted') {ans.is_subjective || q.type === 'Subjective' || q.type === 'Numerical'
: (q.options && q.options[ans.selected_answer] ? `${ans.selected_answer}) ${q.options[ans.selected_answer]}` : ans.selected_answer || 'Not Attempted')} ? (ans.selected_answer || 'Not Attempted')
: (q.options && q.options[ans.selected_answer] ? `${ans.selected_answer}) ${q.options[ans.selected_answer]}` : ans.selected_answer || 'Not Attempted')}
</ReactMarkdown>
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<p className="text-sm font-bold text-muted-foreground uppercase">Correct Answer</p> <p className="text-sm font-bold text-muted-foreground uppercase">Correct Answer</p>
<div className="p-3 rounded-lg border bg-green-500/10 border-green-500/30 text-green-500"> <div className="p-3 rounded-lg border bg-green-500/10 border-green-500/30 text-green-500 prose prose-sm dark:prose-invert">
{ans.is_subjective || q.type === 'Subjective' || q.type === 'Numerical' <ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
? (q.correct_answer?.value || q.correct_answer?.correct || 'Subjective text') {ans.is_subjective || q.type === 'Subjective' || q.type === 'Numerical'
: (q.options && q.options[q.correct_answer?.correct || q.correct_answer?.value] ? (q.correct_answer?.value || q.correct_answer?.correct || q.correct_answer?.selected || 'Subjective text')
? `${q.correct_answer?.correct || q.correct_answer?.value}) ${q.options[q.correct_answer?.correct || q.correct_answer?.value]}` : (q.options && q.options[q.correct_answer?.correct || q.correct_answer?.value || q.correct_answer?.selected]
: (q.correct_answer?.correct || q.correct_answer?.value || 'N/A'))} ? `${q.correct_answer?.correct || q.correct_answer?.value || q.correct_answer?.selected}) ${q.options[q.correct_answer?.correct || q.correct_answer?.value || q.correct_answer?.selected]}`
: (q.correct_answer?.correct || q.correct_answer?.value || q.correct_answer?.selected || 'N/A'))}
</ReactMarkdown>
</div> </div>
</div> </div>
</div> </div>
{q.correct_answer?.explanation && ( {(q.solution_text || q.correct_answer?.explanation) && (
<div className="mt-6 ml-12 p-4 bg-secondary/30 rounded-xl border border-border"> <div className="mt-6 ml-12 p-4 bg-secondary/30 rounded-xl border border-border">
<p className="text-sm font-bold mb-2">Explanation:</p> <p className="text-sm font-bold mb-2 flex items-center gap-2">
<p className="text-muted-foreground">{q.correct_answer.explanation}</p> <Award className="w-4 h-4 text-primary" />
Detailed Solution:
</p>
<div className="text-muted-foreground prose prose-sm dark:prose-invert max-w-none">
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
{q.solution_text || q.correct_answer?.explanation || ""}
</ReactMarkdown>
</div>
</div> </div>
)} )}
</CardContent> </CardContent>

View File

@@ -126,9 +126,16 @@ export default function CBTPage() {
<Card key={exam.id} className={`hover:border-[#00F0FF]/50 hover:shadow-[0_0_15px_-5px_#00F0FF] transition-all group ${isAttempted ? 'border-primary/20' : ''} ${isLocked ? 'opacity-80' : ''}`}> <Card key={exam.id} className={`hover:border-[#00F0FF]/50 hover:shadow-[0_0_15px_-5px_#00F0FF] transition-all group ${isAttempted ? 'border-primary/20' : ''} ${isLocked ? 'opacity-80' : ''}`}>
<CardContent className="p-6"> <CardContent className="p-6">
<div className="flex justify-between items-start mb-4"> <div className="flex justify-between items-start mb-4">
<Badge variant="outline" className="bg-[#00F0FF]/10 text-[#00F0FF] border-[#00F0FF]/20"> <div className="flex gap-2 flex-wrap">
{exam.type} Test <Badge variant="outline" className="bg-[#00F0FF]/10 text-[#00F0FF] border-[#00F0FF]/20">
</Badge> {exam.type} Test
</Badge>
{exam.exam_pattern && (
<Badge variant="outline" className="bg-primary/20 text-primary border-primary/30 uppercase tracking-wider text-[10px]">
{exam.exam_pattern}
</Badge>
)}
</div>
{isUpcoming ? ( {isUpcoming ? (
<Badge variant="secondary" className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20"> <Badge variant="secondary" className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20">
Upcoming Upcoming
@@ -211,7 +218,7 @@ export default function CBTPage() {
<div className="font-bold text-base md:text-lg text-[#00F0FF]">{Math.round(attempt.accuracy)}%</div> <div className="font-bold text-base md:text-lg text-[#00F0FF]">{Math.round(attempt.accuracy)}%</div>
</div> </div>
<Link href={`/dashboard/cbt/history/${attempt.id}`}> <Link href={`/dashboard/cbt/history/${attempt.id}`}>
<Button variant="outline" className="rounded-xl border-border hover:bg-secondary"> <Button variant="outline" className="rounded-xl border-border hover:bg-secondary hover:text-foreground">
View Solutions View Solutions
</Button> </Button>
</Link> </Link>