feat: Add comprehensive peer assessment functionality including new data models, API endpoints, database migrations, and dedicated UI components for Studio and Experience applications.
This commit is contained in:
@@ -15,6 +15,7 @@ import VideoMarkerBlock from "@/components/blocks/VideoMarkerBlock";
|
||||
import AudioResponseBlock from "@/components/blocks/AudioResponseBlock";
|
||||
import HotspotBlock from "@/components/blocks/HotspotBlock";
|
||||
import MemoryBlock from "@/components/blocks/MemoryBlock";
|
||||
import PeerReviewBlock from "@/components/blocks/PeerReviewBlock";
|
||||
import Modal from "@/components/Modal";
|
||||
import {
|
||||
Save,
|
||||
@@ -186,6 +187,7 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
|
||||
...(type === 'audio-response' && { prompt: "Ask a question for the student to record their answer...", keywords: [], timeLimit: 60 }),
|
||||
...(type === 'hotspot' && { imageUrl: "", description: "Find the following items...", hotspots: [] }),
|
||||
...(type === 'memory-match' && { pairs: [{ id: "1", left: "Term A", right: "Match A" }] }),
|
||||
...(type === 'peer-review' && { prompt: "Submit your work below.", reviewCriteria: "Evaluate based on clarity and completeness." }),
|
||||
};
|
||||
setBlocks([...blocks, newBlock]);
|
||||
};
|
||||
@@ -647,6 +649,16 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
|
||||
onChange={(updates) => updateBlock(block.id, updates)}
|
||||
/>
|
||||
)}
|
||||
{block.type === 'peer-review' && (
|
||||
<PeerReviewBlock
|
||||
id={block.id}
|
||||
title={block.title}
|
||||
prompt={block.prompt || ""}
|
||||
reviewCriteria={block.reviewCriteria}
|
||||
editMode={editMode}
|
||||
onChange={(updates) => updateBlock(block.id, updates)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -740,6 +752,13 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
|
||||
<span className="text-2xl group-hover:scale-110 transition-transform">🧠</span>
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400">Memory</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => addBlock('peer-review')}
|
||||
className="flex flex-col items-center gap-2 p-6 glass hover:border-purple-500/50 transition-all group w-32"
|
||||
>
|
||||
<span className="text-2xl group-hover:scale-110 transition-transform">👥</span>
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400">Peer Rev</span>
|
||||
</button>
|
||||
|
||||
<div className="w-px h-12 bg-white/5"></div>
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
interface PeerReviewBlockProps {
|
||||
id: string;
|
||||
title?: string;
|
||||
prompt: string;
|
||||
reviewCriteria?: string;
|
||||
editMode: boolean;
|
||||
onChange: (updates: { title?: string; prompt?: string; reviewCriteria?: string }) => void;
|
||||
}
|
||||
|
||||
export default function PeerReviewBlock({ id, title, prompt, reviewCriteria, editMode, onChange }: PeerReviewBlockProps) {
|
||||
return (
|
||||
<div className="space-y-8" id={id}>
|
||||
<div className="space-y-2">
|
||||
{editMode ? (
|
||||
<div className="space-y-2 p-6 glass border-white/5 bg-white/5 mb-4">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Activity Title (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title || ""}
|
||||
onChange={(e) => onChange({ title: e.target.value })}
|
||||
placeholder="e.g. Final Project Submission..."
|
||||
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm font-bold focus:border-blue-500/50 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<h3 className="text-xl font-bold border-l-4 border-purple-500 pl-4 py-1 tracking-tight text-white flex items-center gap-2">
|
||||
<span>👥</span> {title || "Peer Assessment"}
|
||||
</h3>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editMode ? (
|
||||
<div className="space-y-6">
|
||||
<div className="p-6 glass border-white/5 space-y-4">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Assignment Instructions</label>
|
||||
<textarea
|
||||
value={prompt}
|
||||
onChange={(e) => onChange({ prompt: e.target.value })}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl p-4 min-h-[100px] text-lg font-medium focus:outline-none focus:border-blue-500/50 transition-all"
|
||||
placeholder="Describe what the student needs to submit (e.g. 'Write a 500-word essay about...')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-6 glass border-white/5 space-y-4">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Review Criteria (Rubric)</label>
|
||||
<textarea
|
||||
value={reviewCriteria || ""}
|
||||
onChange={(e) => onChange({ reviewCriteria: e.target.value })}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl p-4 min-h-[100px] text-sm font-medium focus:outline-none focus:border-blue-500/50 transition-all"
|
||||
placeholder="Guide the reviewer on how to evaluate the submission..."
|
||||
/>
|
||||
<p className="text-[10px] text-gray-500 uppercase tracking-wider">Instructions for the student who will review this work.</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 glass border-white/5 rounded-3xl space-y-8">
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-xs font-black uppercase tracking-widest text-gray-500">Instructions</h4>
|
||||
<p className="text-lg text-gray-200 leading-relaxed whitespace-pre-wrap">{prompt || "Submit your work below."}</p>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-white/5 rounded-2xl border border-white/10">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-full bg-purple-500/20 flex items-center justify-center text-purple-400">
|
||||
📤
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="font-bold text-sm">Student Submission Area</h5>
|
||||
<p className="text-xs text-gray-500">Students will see a text area here to submit their work.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-32 bg-black/20 rounded-xl border border-white/5 flex items-center justify-center text-gray-600 text-sm italic">
|
||||
[Submission Interface Preview]
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{reviewCriteria && (
|
||||
<div className="space-y-2 border-t border-white/5 pt-6">
|
||||
<h4 className="text-xs font-black uppercase tracking-widest text-gray-500">Review Criteria</h4>
|
||||
<p className="text-sm text-gray-400 whitespace-pre-wrap">{reviewCriteria}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -57,7 +57,7 @@ export interface QuizQuestion {
|
||||
|
||||
export interface Block {
|
||||
id: string;
|
||||
type: 'description' | 'media' | 'quiz' | 'fill-in-the-blanks' | 'matching' | 'ordering' | 'short-answer' | 'document' | 'video_marker' | 'audio-response' | 'memory-match' | 'hotspot';
|
||||
type: 'description' | 'media' | 'quiz' | 'fill-in-the-blanks' | 'matching' | 'ordering' | 'short-answer' | 'document' | 'video_marker' | 'audio-response' | 'memory-match' | 'hotspot' | 'peer-review';
|
||||
title?: string;
|
||||
content?: string;
|
||||
url?: string;
|
||||
@@ -87,6 +87,7 @@ export interface Block {
|
||||
label: string;
|
||||
}[];
|
||||
imageUrl?: string;
|
||||
reviewCriteria?: string;
|
||||
}
|
||||
|
||||
export interface Lesson {
|
||||
@@ -297,9 +298,28 @@ export interface StudentGradeReport {
|
||||
email: string;
|
||||
progress: number;
|
||||
average_score: number | null;
|
||||
average_score: number | null;
|
||||
last_active_at: string | null;
|
||||
}
|
||||
|
||||
export interface CourseSubmission {
|
||||
id: string;
|
||||
user_id: string;
|
||||
course_id: string;
|
||||
lesson_id: string;
|
||||
content: string;
|
||||
submitted_at: string;
|
||||
}
|
||||
|
||||
export interface PeerReview {
|
||||
id: string;
|
||||
submission_id: string;
|
||||
reviewer_id: string;
|
||||
score: number;
|
||||
feedback: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
const getToken = () => typeof window !== 'undefined' ? localStorage.getItem('studio_token') : null;
|
||||
const getSelectedOrgId = () => typeof window !== 'undefined' ? localStorage.getItem('studio_selected_org_id') : null;
|
||||
|
||||
@@ -504,10 +524,15 @@ export const lmsApi = {
|
||||
addMember: (cohortId: string, userId: string): Promise<UserCohort> => apiFetch(`/cohorts/${cohortId}/members`, { method: 'POST', body: JSON.stringify({ user_id: userId }) }, true),
|
||||
removeMember: (cohortId: string, userId: string): Promise<void> => apiFetch(`/cohorts/${cohortId}/members/${userId}`, { method: 'DELETE' }, true),
|
||||
getMembers: (id: string): Promise<string[]> => apiFetch(`/cohorts/${id}/members`, {}, true),
|
||||
getCourseGrades: (id: string, cohortId?: string): Promise<StudentGradeReport[]> => {
|
||||
const query = cohortId ? `?cohort_id=${cohortId}` : '';
|
||||
return apiFetch(`/courses/${id}/grades${query}`, {}, true);
|
||||
},
|
||||
const query = cohortId ? `?cohort_id=${cohortId}` : '';
|
||||
return apiFetch(`/courses/${id}/grades${query}`, {}, true);
|
||||
},
|
||||
// Peer Assessment
|
||||
submitAssignment: (courseId: string, lessonId: string, content: string): Promise<CourseSubmission> => apiFetch(`/courses/${courseId}/lessons/${lessonId}/submit`, { method: 'POST', body: JSON.stringify({ content }) }, true),
|
||||
getPeerReviewAssignment: (courseId: string, lessonId: string): Promise<CourseSubmission | null> => apiFetch(`/courses/${courseId}/lessons/${lessonId}/peer-review`, {}, true),
|
||||
submitPeerReview: (courseId: string, lessonId: string, submissionId: string, score: number, feedback: string): Promise<PeerReview> => apiFetch(`/courses/${courseId}/lessons/${lessonId}/peer-review`, { method: 'POST', body: JSON.stringify({ submission_id: submissionId, score, feedback }) }, true),
|
||||
getMySubmissionFeedback: (courseId: string, lessonId: string): Promise<PeerReview[]> => apiFetch(`/courses/${courseId}/lessons/${lessonId}/feedback`, {}, true),
|
||||
|
||||
};
|
||||
|
||||
export interface BackgroundTask {
|
||||
|
||||
Reference in New Issue
Block a user