refactor: update UI components and pages with a refreshed visual design and improved styling.
This commit is contained in:
@@ -199,68 +199,71 @@ export default function RubricEditor({ rubricId, courseId, onClose, onSaved }: R
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-[#1a1d23] rounded-3xl border border-white/10 overflow-hidden shadow-2xl flex flex-col max-h-[90vh]">
|
||||
<div className="bg-white dark:bg-[#1a1d23] rounded-[2.5rem] border border-slate-200 dark:border-white/10 overflow-hidden shadow-2xl flex flex-col max-h-[90vh]">
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-white/10 flex items-center justify-between bg-white/[0.02]">
|
||||
<div className="p-8 border-b border-slate-100 dark:border-white/10 flex items-center justify-between bg-slate-50/50 dark:bg-white/[0.02]">
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
value={rubric.name}
|
||||
onChange={(e) => handleUpdateRubric(e.target.value, rubric.description || "")}
|
||||
placeholder="Rubric Name"
|
||||
className="bg-transparent text-2xl font-bold text-white focus:outline-none focus:ring-1 focus:ring-blue-500/50 rounded px-2 w-full"
|
||||
className="bg-transparent text-3xl font-black text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-blue-500/30 rounded px-2 w-full uppercase tracking-tighter"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={rubric.description || ""}
|
||||
onChange={(e) => handleUpdateRubric(rubric.name, e.target.value)}
|
||||
placeholder="Add a description..."
|
||||
className="bg-transparent text-sm text-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500/50 rounded px-2 mt-1 w-full"
|
||||
className="bg-transparent text-sm font-bold text-slate-400 dark:text-gray-500 focus:outline-none focus:ring-1 focus:ring-blue-500/30 rounded px-2 mt-2 w-full italic"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 ml-4">
|
||||
<div className="text-right mr-4">
|
||||
<span className="text-xs text-gray-500 uppercase tracking-widest block font-semibold">Total Points</span>
|
||||
<span className="text-2xl font-bold text-blue-400">{rubric.total_points}</span>
|
||||
<div className="flex items-center gap-6 ml-4">
|
||||
<div className="text-right">
|
||||
<span className="text-[10px] text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] block font-black mb-1">Total Points</span>
|
||||
<span className="text-3xl font-black text-blue-600 dark:text-blue-400 tracking-tighter">{rubric.total_points}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-white/10 rounded-full text-gray-400 hover:text-white transition-colors"
|
||||
className="w-12 h-12 flex items-center justify-center hover:bg-red-50 dark:hover:bg-white/10 rounded-full text-slate-400 hover:text-red-500 transition-all active:scale-95 border border-transparent hover:border-red-100"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
<X className="w-8 h-8" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-8 space-y-12">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-lg font-semibold flex items-center gap-2 text-gray-200">
|
||||
Criteria
|
||||
<span className="text-xs font-normal text-gray-500 bg-white/5 px-2 py-0.5 rounded-full">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h3 className="text-xl font-black flex items-center gap-3 text-slate-900 dark:text-gray-200 uppercase tracking-tight">
|
||||
<div className="w-10 h-10 rounded-xl bg-blue-50 dark:bg-blue-500/10 border border-blue-100 dark:border-blue-500/20 flex items-center justify-center text-blue-600 dark:text-blue-400 shadow-sm">
|
||||
<Plus size={20} />
|
||||
</div>
|
||||
Evaluation Criteria
|
||||
<span className="text-[10px] font-black text-slate-400 dark:text-gray-500 bg-slate-100 dark:bg-white/5 border border-slate-200 dark:border-white/10 px-3 py-1 rounded-full uppercase tracking-widest ml-2">
|
||||
{rubric.criteria.length} items
|
||||
</span>
|
||||
</h3>
|
||||
<button
|
||||
onClick={handleAddCriterion}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-600/10 text-blue-400 border border-blue-500/30 rounded-xl hover:bg-blue-600 hover:text-white transition-all transform active:scale-95"
|
||||
className="flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-2xl font-black text-[10px] uppercase tracking-[0.2em] transition-all shadow-xl shadow-blue-500/20 active:scale-95"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Criterion
|
||||
ADD NEW CRITERION
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-8 pb-10">
|
||||
{rubric.criteria.length === 0 ? (
|
||||
<div className="text-center py-12 border-2 border-dashed border-white/5 rounded-3xl bg-white/[0.01]">
|
||||
<p className="text-gray-500 italic">No criteria added yet. Add your first evaluation criterion.</p>
|
||||
<div className="text-center py-20 border-2 border-dashed border-slate-200 dark:border-white/5 rounded-[3rem] bg-slate-50/50 dark:bg-white/[0.01]">
|
||||
<p className="text-slate-400 dark:text-gray-500 italic font-medium">Define your first grading criterion to start building the rubric.</p>
|
||||
</div>
|
||||
) : (
|
||||
rubric.criteria.map((item, idx) => (
|
||||
<div key={item.id} className="group bg-white/[0.03] border border-white/10 rounded-3xl p-6 hover:border-blue-500/30 transition-all">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="mt-2 text-gray-600 group-hover:text-blue-500/50 transition-colors">
|
||||
<GripVertical className="w-5 h-5 cursor-grab active:cursor-grabbing" />
|
||||
<div key={item.id} className="group bg-slate-50/30 dark:bg-white/[0.03] border border-slate-200 dark:border-white/10 rounded-[2.5rem] p-10 hover:border-blue-500/30 transition-all shadow-sm hover:shadow-md">
|
||||
<div className="flex items-start gap-6">
|
||||
<div className="mt-2 text-slate-300 dark:text-gray-600 group-hover:text-blue-500 transition-colors">
|
||||
<GripVertical className="w-6 h-6 cursor-grab active:cursor-grabbing" />
|
||||
</div>
|
||||
<div className="flex-1 space-y-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
@@ -269,29 +272,29 @@ export default function RubricEditor({ rubricId, courseId, onClose, onSaved }: R
|
||||
type="text"
|
||||
value={item.name}
|
||||
onChange={(e) => handleUpdateCriterion(item.id, { name: e.target.value })}
|
||||
className="bg-transparent text-lg font-bold text-gray-100 focus:outline-none focus:border-b border-blue-500/50 w-full"
|
||||
className="bg-transparent text-xl font-black text-slate-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500/20 rounded-lg px-2 w-full uppercase tracking-tight"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={item.description || ""}
|
||||
onChange={(e) => handleUpdateCriterion(item.id, { description: e.target.value })}
|
||||
placeholder="Description of what to evaluate..."
|
||||
className="bg-transparent text-sm text-gray-500 focus:outline-none w-full mt-1"
|
||||
placeholder="Focus area (e.g. Critical Thinking, Technical Accuracy...)"
|
||||
className="bg-transparent text-sm font-medium text-slate-500 dark:text-gray-500 focus:outline-none w-full mt-2 italic px-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex flex-col items-end">
|
||||
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-bold mb-1">Max Points</label>
|
||||
<label className="text-[9px] text-slate-400 dark:text-gray-500 uppercase tracking-widest font-black mb-1 mr-1">Points</label>
|
||||
<input
|
||||
type="number"
|
||||
value={item.max_points}
|
||||
onChange={(e) => handleUpdateCriterion(item.id, { max_points: parseInt(e.target.value) || 0 })}
|
||||
className="bg-white/5 border border-white/10 rounded-lg px-2 py-1 w-16 text-center text-blue-400 font-bold focus:outline-none focus:border-blue-500"
|
||||
className="bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-xl px-3 py-2 w-20 text-center text-blue-600 dark:text-blue-400 font-black text-lg focus:outline-none focus:ring-2 focus:ring-blue-500/30 transition-all shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDeleteCriterion(item.id)}
|
||||
className="p-2 text-red-500/50 hover:text-red-500 hover:bg-red-500/10 rounded-lg transition-all"
|
||||
className="p-3 bg-red-50 dark:bg-red-500/10 text-red-500 hover:bg-red-500 hover:text-white rounded-xl transition-all shadow-sm active:scale-95"
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
@@ -299,45 +302,45 @@ export default function RubricEditor({ rubricId, courseId, onClose, onSaved }: R
|
||||
</div>
|
||||
|
||||
{/* Levels */}
|
||||
<div className="pl-4 border-l-2 border-white/5 space-y-4 pt-2">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="text-xs font-bold text-gray-500 uppercase tracking-widest">Performance Levels</h4>
|
||||
<div className="pl-8 border-l-4 border-slate-100 dark:border-white/5 space-y-6 pt-4 mt-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h4 className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Achievement Levels</h4>
|
||||
<button
|
||||
onClick={() => handleAddLevel(item.id)}
|
||||
className="text-xs text-blue-400 hover:text-blue-300 flex items-center gap-1 font-semibold"
|
||||
className="text-[10px] font-black uppercase tracking-widest text-blue-600 hover:text-blue-500 flex items-center gap-2 px-3 py-1 bg-blue-50 dark:bg-blue-500/10 rounded-lg border border-blue-100 dark:border-blue-500/20 transition-all"
|
||||
>
|
||||
<Plus className="w-3 h-3" /> Add Level
|
||||
<Plus size={12} /> ADD LEVEL
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
|
||||
{item.levels.map(level => (
|
||||
<div key={level.id} className="bg-white/5 border border-white/10 rounded-2xl p-4 flex flex-col gap-2 relative group/level hover:bg-white/[0.08] transition-all">
|
||||
<div key={level.id} className="bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[1.5rem] p-6 flex flex-col gap-4 relative group/level hover:border-blue-500/30 transition-all shadow-sm">
|
||||
<button
|
||||
onClick={() => handleDeleteLevel(level.id, item.id)}
|
||||
className="absolute top-2 right-2 p-1 text-gray-600 hover:text-red-500 opacity-0 group-level-hover:opacity-100 transition-all"
|
||||
className="absolute top-4 right-4 p-2 bg-red-50 dark:bg-red-500/10 text-red-500 rounded-lg opacity-0 group-hover/level:opacity-100 transition-all active:scale-90"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
value={level.name}
|
||||
onChange={(e) => handleUpdateLevel(level.id, item.id, { name: e.target.value })}
|
||||
placeholder="Level Name (e.g. Excellent)"
|
||||
className="bg-transparent text-sm font-semibold text-gray-200 focus:outline-none"
|
||||
placeholder="Level (e.g. Master)"
|
||||
className="bg-transparent text-sm font-black text-slate-900 dark:text-gray-200 focus:outline-none uppercase tracking-tight"
|
||||
/>
|
||||
<textarea
|
||||
value={level.description || ""}
|
||||
onChange={(e) => handleUpdateLevel(level.id, item.id, { description: e.target.value })}
|
||||
placeholder="Criteria description..."
|
||||
className="bg-transparent text-xs text-gray-500 focus:outline-none resize-none h-12"
|
||||
placeholder="Grade description..."
|
||||
className="bg-transparent text-xs font-medium text-slate-500 dark:text-gray-500 focus:outline-none resize-none h-20 leading-relaxed italic"
|
||||
/>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-[10px] text-gray-600 uppercase font-bold">Points:</span>
|
||||
<div className="flex items-center justify-between mt-2 pt-4 border-t border-slate-50 dark:border-white/5">
|
||||
<span className="text-[9px] text-slate-400 dark:text-gray-600 uppercase font-black tracking-widest">Points Value</span>
|
||||
<input
|
||||
type="number"
|
||||
value={level.points}
|
||||
onChange={(e) => handleUpdateLevel(level.id, item.id, { points: parseInt(e.target.value) || 0 })}
|
||||
className="bg-white/5 border border-white/10 rounded px-1.5 w-12 text-xs text-center text-blue-400 focus:outline-none"
|
||||
className="bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-lg px-2 py-1 w-14 text-xs font-black text-center text-blue-600 dark:text-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-500/30 shadow-inner"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -353,22 +356,22 @@ export default function RubricEditor({ rubricId, courseId, onClose, onSaved }: R
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-6 border-t border-white/10 bg-white/[0.02] flex justify-end gap-4">
|
||||
<div className="p-8 border-t border-slate-100 dark:border-white/10 bg-slate-50 dark:bg-white/[0.02] flex justify-end gap-5">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-6 py-2 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all font-medium"
|
||||
className="px-8 py-3 rounded-2xl text-[10px] font-black uppercase tracking-[0.2em] text-slate-500 hover:text-slate-900 dark:text-gray-400 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-white/5 transition-all"
|
||||
>
|
||||
Close
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (onSaved) onSaved();
|
||||
onClose();
|
||||
}}
|
||||
className="px-8 py-2 bg-blue-600 text-white rounded-xl font-bold hover:bg-blue-500 shadow-lg shadow-blue-500/20 active:scale-95 transition-all flex items-center gap-2"
|
||||
className="px-10 py-3 bg-blue-600 text-white rounded-2xl font-black text-[10px] uppercase tracking-[0.3em] hover:bg-blue-500 shadow-xl shadow-blue-500/20 active:scale-95 transition-all flex items-center gap-3"
|
||||
>
|
||||
<Check className="w-5 h-5" />
|
||||
Done
|
||||
Save & Finish
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function RubricList({ courseId, onEdit }: RubricListProps) {
|
||||
placeholder="Search rubrics..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-2xl pl-11 pr-4 py-3 focus:outline-none focus:border-blue-500/50 transition-all text-gray-100"
|
||||
className="w-full bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl pl-11 pr-4 py-4 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/30 transition-all text-slate-900 dark:text-gray-100 placeholder:text-slate-300 shadow-inner font-medium"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -109,14 +109,14 @@ export default function RubricList({ courseId, onEdit }: RubricListProps) {
|
||||
value={newRubricName}
|
||||
onChange={(e) => setNewRubricName(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
|
||||
className="flex-1 md:w-64 bg-white/5 border border-white/10 rounded-xl px-4 py-2 focus:outline-none focus:border-blue-500/50 transition-all"
|
||||
className="flex-1 md:w-64 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl px-5 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/30 transition-all text-slate-900 dark:text-white placeholder:text-slate-300 shadow-inner font-medium"
|
||||
/>
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
disabled={creating || !newRubricName}
|
||||
className="bg-blue-600 hover:bg-blue-500 disabled:bg-gray-700 disabled:text-gray-500 text-white font-bold px-6 py-2 rounded-xl transition-all shadow-lg shadow-blue-500/20 active:scale-95 flex items-center gap-2"
|
||||
className="bg-blue-600 hover:bg-blue-500 disabled:bg-slate-100 disabled:text-slate-300 text-white font-black text-xs uppercase tracking-[0.2em] px-8 py-3 rounded-2xl transition-all shadow-xl shadow-blue-500/20 active:scale-95 flex items-center gap-2"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<Plus className="w-5 h-5" />
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
@@ -124,26 +124,26 @@ export default function RubricList({ courseId, onEdit }: RubricListProps) {
|
||||
|
||||
{/* Rubrics Grid */}
|
||||
{filteredRubrics.length === 0 ? (
|
||||
<div className="bg-white/5 border border-white/10 rounded-3xl p-16 text-center">
|
||||
<div className="w-16 h-16 bg-blue-500/10 rounded-2xl flex items-center justify-center mx-auto mb-6">
|
||||
<FileText className="w-8 h-8 text-blue-400" />
|
||||
<div className="bg-slate-50 dark:bg-white/5 border border-dashed border-slate-200 dark:border-white/10 rounded-[3rem] p-24 text-center">
|
||||
<div className="w-20 h-20 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/5 rounded-2xl flex items-center justify-center mx-auto mb-8 shadow-sm">
|
||||
<FileText className="w-10 h-10 text-slate-300 dark:text-blue-400" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-gray-200 mb-2">No rubrics found</h3>
|
||||
<p className="text-gray-500 max-w-md mx-auto">
|
||||
Create a rubric to start using advanced grading criteria in your course lessons.
|
||||
<h3 className="text-2xl font-black text-slate-900 dark:text-white mb-3 uppercase tracking-tight">No rubrics created</h3>
|
||||
<p className="text-slate-500 dark:text-gray-500 max-w-sm mx-auto font-medium">
|
||||
Standardize your evaluation process by creating detailed rubrics for your course assignments.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{filteredRubrics.map((rubric) => (
|
||||
<div
|
||||
key={rubric.id}
|
||||
className="group bg-white/5 border border-white/10 rounded-3xl p-6 hover:border-blue-500/50 hover:bg-white/[0.08] transition-all duration-300 flex flex-col justify-between"
|
||||
className="group bg-white dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-[2.5rem] p-10 hover:border-blue-500/30 hover:shadow-xl transition-all duration-300 flex flex-col justify-between shadow-sm"
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-400">
|
||||
<FileText className="w-6 h-6" />
|
||||
<div className="flex items-start justify-between mb-6">
|
||||
<div className="w-14 h-14 rounded-2xl bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-100 dark:border-indigo-500/20 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shadow-sm transition-transform group-hover:scale-110">
|
||||
<FileText className="w-8 h-8" />
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
@@ -155,32 +155,32 @@ export default function RubricList({ courseId, onEdit }: RubricListProps) {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(rubric.id)}
|
||||
className="p-2 text-gray-500 hover:text-red-400 hover:bg-red-400/10 rounded-lg transition-all"
|
||||
className="p-3 text-slate-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-xl transition-all shadow-sm active:scale-90"
|
||||
title="Delete Rubric"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-bold text-gray-100 group-hover:text-blue-400 transition-colors mb-2">
|
||||
<h3 className="text-2xl font-black text-slate-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors mb-2 uppercase tracking-tight">
|
||||
{rubric.name}
|
||||
</h3>
|
||||
<p className="text-gray-500 text-sm line-clamp-2 min-h-[2.5rem]">
|
||||
{rubric.description || "No description provided."}
|
||||
<p className="text-slate-500 dark:text-gray-500 text-sm line-clamp-2 min-h-[3rem] font-medium leading-relaxed">
|
||||
{rubric.description || "Establish consistent grading standards with custom criteria."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<div className="mt-12 flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] text-gray-500 uppercase tracking-widest font-bold mb-1">Total Points</span>
|
||||
<span className="text-xl font-black text-white">{rubric.total_points}</span>
|
||||
<span className="text-[10px] text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] font-black mb-1">Total Points</span>
|
||||
<span className="text-3xl font-black text-slate-900 dark:text-white tracking-tighter">{rubric.total_points}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onEdit(rubric.id)}
|
||||
className="px-4 py-2 bg-white/5 border border-white/10 rounded-xl text-sm font-semibold hover:bg-blue-600 hover:text-white hover:border-blue-600 transition-all active:scale-95"
|
||||
className="px-6 py-3 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[1.25rem] text-[10px] font-black uppercase tracking-widest text-slate-600 dark:text-white hover:bg-blue-600 hover:text-white hover:border-blue-600 transition-all active:scale-95 shadow-sm"
|
||||
>
|
||||
Manage Criteria
|
||||
EDIT CRITERIA
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,18 +25,19 @@ export default function AudioResponseBlock({
|
||||
<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>
|
||||
<div className="space-y-4 p-8 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2rem] mb-6 shadow-inner relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-1 h-full bg-purple-500/40"></div>
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Activity Title (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title || ""}
|
||||
onChange={(e) => onChange({ title: e.target.value })}
|
||||
placeholder="e.g. Speaking Practice, Pronunciation Test..."
|
||||
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"
|
||||
className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-3 text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-purple-500/10 focus:border-purple-500 transition-all outline-none shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<h3 className="text-xl font-bold border-l-4 border-purple-500 pl-4 py-1 tracking-tight text-white">
|
||||
<h3 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase border-l-4 border-purple-600 pl-6 py-1">
|
||||
{title || "Audio Response"}
|
||||
</h3>
|
||||
)}
|
||||
@@ -44,39 +45,40 @@ export default function AudioResponseBlock({
|
||||
|
||||
{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 flex items-center gap-2">
|
||||
<Mic className="w-4 h-4" />
|
||||
Question Prompt
|
||||
<div className="p-8 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[2.5rem] space-y-4 shadow-sm relative overflow-hidden group/prompt">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-purple-500/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2"></div>
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] flex items-center gap-3">
|
||||
<Mic className="w-4 h-4 text-purple-600" />
|
||||
Phonetic Inquiry (Prompt)
|
||||
</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-purple-500/50 transition-all"
|
||||
placeholder="What question should the student answer? (e.g. 'Describe your daily routine in English')"
|
||||
className="w-full bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-2xl p-6 min-h-[120px] text-xl font-black uppercase tracking-tight text-slate-800 dark:text-white focus:outline-none focus:ring-4 focus:ring-purple-500/10 transition-all shadow-inner placeholder:opacity-20"
|
||||
placeholder="WHAT IS THE ENIGMA TO BE SPOKEN?..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-6 glass border-white/5 space-y-4">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest flex items-center gap-2">
|
||||
<Tag className="w-4 h-4" />
|
||||
Expected Keywords (Optional)
|
||||
<div className="p-8 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[2.5rem] space-y-4 shadow-sm">
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] flex items-center gap-3">
|
||||
<Tag className="w-4 h-4 text-purple-600" />
|
||||
Lexical Anchors (Expected Keywords)
|
||||
</label>
|
||||
<textarea
|
||||
value={keywords.join("\n")}
|
||||
onChange={(e) => onChange({ keywords: e.target.value.split("\n").filter(k => k.trim() !== "") })}
|
||||
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-purple-500/50 transition-all"
|
||||
placeholder="breakfast morning routine work"
|
||||
className="w-full bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-2xl p-6 min-h-[100px] text-sm font-bold text-slate-700 dark:text-gray-300 focus:outline-none focus:ring-4 focus:ring-purple-500/10 transition-all shadow-inner"
|
||||
placeholder="Keyword Alpha Keyword Beta..."
|
||||
/>
|
||||
<p className="text-[10px] text-gray-500 uppercase tracking-wider">
|
||||
One keyword per line. Used for automatic evaluation of speech content.
|
||||
<p className="text-[9px] text-slate-400 dark:text-gray-600 uppercase font-black italic pl-1 italic">
|
||||
Singular entry per line. Syntactic scan for automatic validation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-6 glass border-white/5 space-y-4">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
Time Limit (Optional)
|
||||
<div className="p-8 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[2.5rem] space-y-4 shadow-sm">
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] flex items-center gap-3">
|
||||
<Clock className="w-4 h-4 text-purple-600" />
|
||||
Temporal Ceiling (Seconds)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
@@ -85,46 +87,51 @@ export default function AudioResponseBlock({
|
||||
placeholder="60"
|
||||
min="10"
|
||||
max="300"
|
||||
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm font-medium focus:border-purple-500/50 focus:outline-none"
|
||||
className="w-full bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-2xl px-6 py-4 text-sm font-black text-slate-800 dark:text-white focus:ring-4 focus:ring-purple-500/10 transition-all outline-none"
|
||||
/>
|
||||
<p className="text-[10px] text-gray-500 uppercase tracking-wider">
|
||||
Maximum recording time in seconds (10-300). Leave empty for no limit.
|
||||
<p className="text-[9px] text-slate-400 dark:text-gray-600 uppercase font-black italic pl-1 italic">
|
||||
Maximum temporal window (10-300). Null for unconstrained stream.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 glass border-white/5 rounded-3xl space-y-8">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-3 bg-purple-500/20 rounded-xl">
|
||||
<Mic className="w-6 h-6 text-purple-400" />
|
||||
<div className="p-10 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[3rem] space-y-10 shadow-sm relative overflow-hidden group/audioplay">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-purple-500/5 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2 group-hover/audioplay:bg-purple-500/10 transition-colors"></div>
|
||||
|
||||
<div className="flex items-start gap-8 relative z-10">
|
||||
<div className="p-5 bg-purple-100 dark:bg-purple-500/10 rounded-2xl shadow-inner border border-purple-200 dark:border-purple-500/20">
|
||||
<Mic className="w-8 h-8 text-purple-700 dark:text-purple-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-xl font-bold text-gray-100 mb-2">{prompt || "Record your audio response:"}</p>
|
||||
{keywords.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
<span className="text-xs text-gray-500 uppercase tracking-wider">Expected topics:</span>
|
||||
{keywords.slice(0, 5).map((kw, i) => (
|
||||
<span key={i} className="px-2 py-1 bg-purple-500/10 border border-purple-500/20 rounded text-xs text-purple-300">
|
||||
{kw}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{timeLimit && (
|
||||
<p className="text-xs text-gray-500 mt-2 flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
Maximum {timeLimit} seconds
|
||||
</p>
|
||||
)}
|
||||
<div className="flex-1 space-y-4">
|
||||
<p className="text-2xl font-black text-slate-800 dark:text-gray-100 tracking-tight uppercase italic leading-tight">{prompt || "Awaiting phonetic capture..."}</p>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-6">
|
||||
{keywords.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{keywords.slice(0, 5).map((kw, i) => (
|
||||
<span key={i} className="px-3 py-1 bg-slate-50 dark:bg-purple-500/5 border border-slate-100 dark:border-purple-500/20 rounded-lg text-[9px] font-black uppercase text-purple-600 dark:text-purple-400 tracking-widest shadow-sm">
|
||||
{kw}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{timeLimit && (
|
||||
<p className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 flex items-center gap-2">
|
||||
<Clock className="w-3 h-3 text-purple-500" />
|
||||
Temporal Window: {timeLimit}s
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-purple-500/5 border-2 border-dashed border-purple-500/20 rounded-2xl text-center">
|
||||
<p className="text-sm text-gray-400">
|
||||
🎤 Audio recording will be available in the Experience player
|
||||
<div className="p-10 bg-purple-50/50 dark:bg-purple-500/5 border-2 border-dashed border-purple-200 dark:border-purple-500/20 rounded-[2.5rem] text-center space-y-3 relative z-10 group-hover/audioplay:border-purple-400 transition-colors duration-500">
|
||||
<div className="w-16 h-1 w-1 bg-purple-200 dark:bg-purple-500/20 mx-auto rounded-full mb-4"></div>
|
||||
<p className="text-[10px] font-black text-purple-900 dark:text-purple-400 uppercase tracking-[0.3em]">
|
||||
Phonetic Engine Interface
|
||||
</p>
|
||||
<p className="text-xs text-gray-600 mt-2">
|
||||
Students will use their microphone to record their response
|
||||
<p className="text-[9px] text-slate-400 dark:text-gray-600 uppercase font-black italic">
|
||||
Capture node will manifest within the Experience stream
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -79,139 +79,146 @@ export default function DescriptionBlock({ id, title, content, editMode, courseI
|
||||
{/* Block Header */}
|
||||
<div className="space-y-2">
|
||||
{editMode ? (
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Activity Title (Optional)</label>
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Activity Title (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title || ""}
|
||||
onChange={(e) => onChange({ title: e.target.value })}
|
||||
placeholder="e.g. Introduction, Context..."
|
||||
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"
|
||||
placeholder="e.g. Fundamental Concepts, Learning Objectives..."
|
||||
className="w-full bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl px-5 py-3 text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all outline-none"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
title && <h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white">{title}</h3>
|
||||
title && <h3 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase border-l-4 border-blue-600 pl-6 py-1">{title}</h3>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editMode ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Instructional Content</label>
|
||||
<div className="h-4 w-px bg-white/10 mx-2" />
|
||||
{/* Toolbar */}
|
||||
{!showPreview && (
|
||||
<div className="flex items-center gap-1">
|
||||
<button onClick={() => insertMarkdown("**", "**")} className="p-1.5 hover:bg-white/10 rounded-md transition-colors text-gray-400 hover:text-white" title="Bold"><Bold size={14} /></button>
|
||||
<button onClick={() => insertMarkdown("*", "*")} className="p-1.5 hover:bg-white/10 rounded-md transition-colors text-gray-400 hover:text-white" title="Italic"><Italic size={14} /></button>
|
||||
<button onClick={() => insertMarkdown("[", "](url)")} className="p-1.5 hover:bg-white/10 rounded-md transition-colors text-gray-400 hover:text-white" title="Link"><LinkIcon size={14} /></button>
|
||||
<div className="w-px h-3 bg-white/10 mx-1" />
|
||||
<button
|
||||
onClick={() => { setPickerType("image"); setIsAssetPickerOpen(true); }}
|
||||
className="p-1.5 hover:bg-white/10 rounded-md transition-colors text-blue-400 hover:text-blue-300"
|
||||
title="Insert Image"
|
||||
>
|
||||
<ImageIcon size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setPickerType("file"); setIsAssetPickerOpen(true); }}
|
||||
className="p-1.5 hover:bg-white/10 rounded-md transition-colors text-purple-400 hover:text-purple-300"
|
||||
title="Insert File Link"
|
||||
>
|
||||
<FileText size={14} />
|
||||
</button>
|
||||
<div className="w-px h-3 bg-white/10 mx-1" />
|
||||
<button
|
||||
onClick={handleReviewText}
|
||||
disabled={isReviewing}
|
||||
className={`p-1.5 rounded-md transition-all flex items-center gap-1.5 text-xs font-bold ${isReviewing ? 'bg-indigo-500/20 text-indigo-300 animate-pulse' : 'hover:bg-indigo-500/20 text-indigo-400 hover:text-indigo-300'}`}
|
||||
title="AI Suggest Improvements"
|
||||
>
|
||||
<Sparkles size={14} className={isReviewing ? 'animate-spin' : ''} />
|
||||
{isReviewing ? 'Analyzing...' : 'AI Suggest'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex bg-white/5 rounded-lg p-1 border border-white/5">
|
||||
<button
|
||||
onClick={() => setShowPreview(false)}
|
||||
className={`px-3 py-1 flex items-center gap-2 text-[10px] uppercase font-black tracking-widest rounded-md transition-all ${!showPreview ? "bg-blue-500 text-white shadow-lg" : "text-gray-500 hover:text-white"}`}
|
||||
>
|
||||
<PenLine size={12} /> Write
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowPreview(true)}
|
||||
className={`px-3 py-1 flex items-center gap-2 text-[10px] uppercase font-black tracking-widest rounded-md transition-all ${showPreview ? "bg-blue-500 text-white shadow-lg" : "text-gray-500 hover:text-white"}`}
|
||||
>
|
||||
<Eye size={12} /> Preview
|
||||
</button>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Instructional Canvas</label>
|
||||
<div className="h-4 w-px bg-slate-200 dark:bg-white/10 mx-2" />
|
||||
{/* Toolbar */}
|
||||
{!showPreview && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button onClick={() => insertMarkdown("**", "**")} className="p-2 hover:bg-slate-100 dark:hover:bg-white/10 rounded-xl transition-all text-slate-500 dark:text-gray-400 hover:text-slate-900 dark:hover:text-white shadow-sm border border-transparent hover:border-slate-200" title="Bold"><Bold size={16} /></button>
|
||||
<button onClick={() => insertMarkdown("*", "*")} className="p-2 hover:bg-slate-100 dark:hover:bg-white/10 rounded-xl transition-all text-slate-500 dark:text-gray-400 hover:text-slate-900 dark:hover:text-white shadow-sm border border-transparent hover:border-slate-200" title="Italic"><Italic size={16} /></button>
|
||||
<button onClick={() => insertMarkdown("[", "](url)")} className="p-2 hover:bg-slate-100 dark:hover:bg-white/10 rounded-xl transition-all text-slate-500 dark:text-gray-400 hover:text-slate-900 dark:hover:text-white shadow-sm border border-transparent hover:border-slate-200" title="Link"><LinkIcon size={16} /></button>
|
||||
<div className="w-px h-4 bg-slate-200 dark:bg-white/10 mx-2" />
|
||||
<button
|
||||
onClick={() => { setPickerType("image"); setIsAssetPickerOpen(true); }}
|
||||
className="p-2 hover:bg-blue-50 dark:hover:bg-blue-500/10 rounded-xl transition-all text-blue-600 dark:text-blue-400 hover:text-blue-700 shadow-sm border border-transparent hover:border-blue-200"
|
||||
title="Insert Image"
|
||||
>
|
||||
<ImageIcon size={16} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setPickerType("file"); setIsAssetPickerOpen(true); }}
|
||||
className="p-2 hover:bg-purple-50 dark:hover:bg-purple-500/10 rounded-xl transition-all text-purple-600 dark:text-purple-400 hover:text-purple-700 shadow-sm border border-transparent hover:border-purple-200"
|
||||
title="Insert File Link"
|
||||
>
|
||||
<FileText size={16} />
|
||||
</button>
|
||||
<div className="w-px h-4 bg-slate-200 dark:bg-white/10 mx-2" />
|
||||
<button
|
||||
onClick={handleReviewText}
|
||||
disabled={isReviewing}
|
||||
className={`px-4 py-2 rounded-xl transition-all flex items-center gap-2 text-[10px] font-black uppercase tracking-widest border ${isReviewing ? 'bg-indigo-50 dark:bg-indigo-500/20 text-indigo-400 border-indigo-200 animate-pulse shadow-inner' : 'bg-indigo-50 dark:bg-indigo-500/5 text-indigo-600 dark:text-indigo-400 hover:bg-indigo-600 hover:text-white border-indigo-100 hover:border-indigo-600 shadow-sm active:scale-95'}`}
|
||||
title="AI Suggest Improvements"
|
||||
>
|
||||
<Sparkles size={14} className={isReviewing ? 'animate-spin' : ''} />
|
||||
{isReviewing ? 'Synthesizing...' : 'AI Refine'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex bg-slate-100 dark:bg-white/5 rounded-2xl p-1.5 border border-slate-200 dark:border-white/5 shadow-inner">
|
||||
<button
|
||||
onClick={() => setShowPreview(false)}
|
||||
className={`px-5 py-2 flex items-center gap-2 text-[10px] uppercase font-black tracking-widest rounded-xl transition-all ${!showPreview ? "bg-white dark:bg-indigo-600 text-slate-900 dark:text-white shadow-md" : "text-slate-400 hover:text-slate-600"}`}
|
||||
>
|
||||
<PenLine size={12} /> Composition
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowPreview(true)}
|
||||
className={`px-5 py-2 flex items-center gap-2 text-[10px] uppercase font-black tracking-widest rounded-xl transition-all ${showPreview ? "bg-white dark:bg-indigo-600 text-slate-900 dark:text-white shadow-md" : "text-slate-400 hover:text-slate-600"}`}
|
||||
>
|
||||
<Eye size={12} /> Rendering
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showPreview ? (
|
||||
<div className="min-h-[200px] p-8 rounded-2xl glass border-white/5 bg-white/5">
|
||||
<div className="prose prose-invert max-w-none prose-p:text-gray-300 prose-p:leading-relaxed prose-p:text-lg prose-headings:text-white prose-a:text-blue-400 prose-img:rounded-xl">
|
||||
<ReactMarkdown urlTransform={getImageUrl}>{content || "Nothing to preview..."}</ReactMarkdown>
|
||||
<div className="min-h-[250px] p-10 rounded-[2.5rem] bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 shadow-sm overflow-hidden relative">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-500/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2"></div>
|
||||
<div className="prose dark:prose-invert max-w-none prose-p:text-slate-600 dark:prose-p:text-gray-300 prose-p:leading-relaxed prose-p:text-lg prose-headings:text-slate-900 dark:prose-headings:text-white prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-img:rounded-3xl prose-img:shadow-2xl relative z-10">
|
||||
<ReactMarkdown urlTransform={getImageUrl}>{content || "Draft empty. Initiate composition above..."}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<div className="relative group/canvas">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={content}
|
||||
onChange={(e) => onChange({ content: e.target.value })}
|
||||
placeholder="Explain the activity (Markdown supported)..."
|
||||
className="w-full h-80 bg-white/5 border border-white/10 rounded-xl p-6 text-lg tracking-tight focus:border-blue-500/50 focus:outline-none transition-all resize-none shadow-inner custom-scrollbar"
|
||||
placeholder="Unfold your vision. Markdown architectural syntax supported..."
|
||||
className="w-full h-[400px] bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-[2rem] p-10 text-lg tracking-tight focus:ring-8 focus:ring-blue-500/5 focus:border-blue-500/50 focus:outline-none transition-all resize-none shadow-inner custom-scrollbar relative z-10 font-medium leading-relaxed"
|
||||
/>
|
||||
<div className="absolute bottom-4 right-4 text-[10px] text-gray-600 font-bold uppercase tracking-widest pointer-events-none">
|
||||
Markdown Mode
|
||||
<div className="absolute bottom-6 right-8 text-[10px] text-slate-300 dark:text-gray-600 font-black uppercase tracking-[0.3em] pointer-events-none z-20 group-hover/canvas:text-blue-400/50 transition-colors">
|
||||
Structural Markdown Mode
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{suggestion && (
|
||||
<div className="bg-indigo-500/10 border border-indigo-500/30 rounded-2xl p-6 space-y-4 animate-in fade-in slide-in-from-top-4 duration-500 shadow-xl shadow-indigo-500/5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Wand2 size={16} className="text-indigo-400" />
|
||||
<span className="text-xs font-black uppercase tracking-widest text-indigo-300">AI Teacher Suggestions</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setSuggestion(null)}
|
||||
className="p-1.5 hover:bg-white/10 rounded-lg text-gray-400 transition-colors"
|
||||
>
|
||||
<CloseIcon size={14} />
|
||||
</button>
|
||||
<div className="bg-white dark:bg-indigo-500/10 border border-indigo-200 dark:border-indigo-500/30 rounded-[2.5rem] p-10 space-y-8 animate-in fade-in slide-in-from-top-6 duration-700 shadow-2xl relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-indigo-500/5 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2"></div>
|
||||
<div className="flex items-center justify-between relative z-10">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-2xl bg-indigo-600 text-white flex items-center justify-center shadow-lg shadow-indigo-500/30">
|
||||
<Wand2 size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase">Neural Refinement Suggestions</h4>
|
||||
<p className="text-[10px] text-slate-400 dark:text-gray-400 mt-1 uppercase tracking-[0.2em] font-black">AI-optimized pedagogical structure</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSuggestion(null)}
|
||||
className="w-10 h-10 flex items-center justify-center hover:bg-slate-100 dark:hover:bg-white/10 rounded-full text-slate-400 transition-colors"
|
||||
>
|
||||
<CloseIcon size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-widest pl-1">Improved Version</span>
|
||||
<div className="p-4 bg-black/40 rounded-xl border border-white/5 text-sm text-gray-300 leading-relaxed italic">
|
||||
{suggestion.suggestion}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-10 relative z-10">
|
||||
<div className="space-y-4">
|
||||
<span className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Proposed Architecture</span>
|
||||
<div className="p-8 bg-slate-50 dark:bg-black/40 rounded-[2rem] border border-slate-200 dark:border-white/5 text-sm text-slate-600 dark:text-gray-300 leading-relaxed italic shadow-inner">
|
||||
"{suggestion.suggestion}"
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-widest pl-1">Key Changes</span>
|
||||
<div className="text-xs text-gray-400 leading-relaxed pl-1 whitespace-pre-line">
|
||||
<div className="space-y-6">
|
||||
<span className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Pedagogical Insights</span>
|
||||
<div className="text-sm text-slate-500 dark:text-gray-400 font-medium leading-relaxed pl-1 whitespace-pre-line border-l-2 border-indigo-100 pl-6 py-2">
|
||||
{suggestion.comments}
|
||||
</div>
|
||||
<div className="pt-4 flex gap-3">
|
||||
<div className="pt-6 flex gap-4">
|
||||
<button
|
||||
onClick={applySuggestion}
|
||||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold rounded-lg transition-all shadow-lg shadow-indigo-500/20 flex items-center gap-2 group active:scale-95"
|
||||
className="px-8 py-4 bg-indigo-600 hover:bg-indigo-500 text-white text-[10px] font-black uppercase tracking-[0.2em] rounded-2xl transition-all shadow-xl shadow-indigo-500/30 flex items-center gap-3 active:scale-95"
|
||||
>
|
||||
<Check size={14} className="group-hover:scale-110 transition-transform" />
|
||||
Apply Changes
|
||||
<Check size={16} />
|
||||
Update Syllabus
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSuggestion(null)}
|
||||
className="px-4 py-2 bg-white/5 hover:bg-white/10 text-gray-400 hover:text-white text-xs font-bold rounded-lg border border-white/10 transition-all active:scale-95"
|
||||
className="px-8 py-4 bg-white dark:bg-white/5 hover:bg-slate-50 text-slate-400 dark:text-gray-400 hover:text-slate-900 text-[10px] font-black uppercase tracking-[0.2em] rounded-2xl border border-slate-200 dark:border-white/10 transition-all active:scale-95 shadow-sm"
|
||||
>
|
||||
Dismiss
|
||||
Discard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -220,8 +227,8 @@ export default function DescriptionBlock({ id, title, content, editMode, courseI
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="prose prose-invert max-w-none prose-p:text-gray-300 prose-p:leading-relaxed prose-p:text-lg prose-headings:text-white prose-a:text-blue-400 prose-img:rounded-xl">
|
||||
<ReactMarkdown urlTransform={getImageUrl}>{content || "No description provided."}</ReactMarkdown>
|
||||
<div className="prose dark:prose-invert max-w-none prose-p:text-slate-600 dark:prose-p:text-gray-300 prose-p:leading-relaxed prose-p:text-lg prose-headings:text-slate-900 dark:prose-headings:text-white prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-img:rounded-3xl prose-img:shadow-xl">
|
||||
<ReactMarkdown urlTransform={getImageUrl}>{content || "Conceptual flow not established yet."}</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -21,30 +21,32 @@ export default function DocumentBlock({ title, url, editMode, onChange }: Docume
|
||||
{/* Block Header */}
|
||||
<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">Document Activity Title</label>
|
||||
<div className="space-y-4 p-8 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2rem] mb-6 shadow-inner relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-1 h-full bg-indigo-500/40"></div>
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Document Activity Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title || ""}
|
||||
onChange={(e) => onChange({ title: e.target.value })}
|
||||
placeholder="e.g. Course Syllabus, Reading Guide..."
|
||||
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"
|
||||
className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-3 text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500 transition-all outline-none shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
title && <h3 className="text-xl font-bold border-l-4 border-indigo-500 pl-4 py-1 tracking-tight text-white">{title}</h3>
|
||||
title && <h3 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase border-l-4 border-indigo-600 pl-6 py-1">{title}</h3>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editMode && (
|
||||
<div className="p-6 glass border-blue-500/10 mb-8 bg-blue-500/5">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest mb-4 block">Upload Document (PDF, DOCX, PPTX)</label>
|
||||
<div className="p-10 bg-white dark:bg-white/5 border border-indigo-500/10 dark:border-indigo-500/20 mb-10 rounded-[2.5rem] shadow-xl shadow-indigo-500/5 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-indigo-500/5 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2"></div>
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest mb-6 block pl-1 relative z-10">Asset Pipeline (PDF, DOCX, PPTX)</label>
|
||||
<FileUpload
|
||||
currentUrl={url}
|
||||
onUploadComplete={(newUrl) => onChange({ url: newUrl })}
|
||||
/>
|
||||
<p className="text-[10px] text-gray-500 uppercase leading-relaxed mt-4 px-2">
|
||||
Supported formats: PDF (can be previewed), DOCX, PPTX (download only).
|
||||
<p className="text-[9px] text-slate-400 dark:text-gray-500 uppercase font-black italic mt-6 px-2 leading-relaxed relative z-10">
|
||||
Standards: PDF (Native Rendering), DOCX/PPTX (Direct Retrieval).
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -54,51 +56,54 @@ export default function DocumentBlock({ title, url, editMode, onChange }: Docume
|
||||
{url ? (
|
||||
<div className="space-y-4">
|
||||
{isPdf ? (
|
||||
<div className="glass rounded-2xl overflow-hidden border-white/5 bg-white/5 aspect-[4/3] w-full">
|
||||
<iframe
|
||||
src={`${displayUrl}#toolbar=0`}
|
||||
className="w-full h-full border-none"
|
||||
title={title || "Document Preview"}
|
||||
/>
|
||||
<div className="p-4 border-t border-white/5 flex items-center justify-between bg-black/40">
|
||||
<span className="text-xs font-bold text-gray-400 uppercase tracking-widest flex items-center gap-2">
|
||||
<Eye size={14} className="text-indigo-400" /> PDF Preview
|
||||
<div className="bg-white dark:bg-white/5 rounded-[2.5rem] overflow-hidden border border-slate-100 dark:border-white/10 shadow-2xl relative">
|
||||
<div className="aspect-[4/3] w-full bg-slate-100 dark:bg-black/20">
|
||||
<iframe
|
||||
src={`${displayUrl}#toolbar=0`}
|
||||
className="w-full h-full border-none"
|
||||
title={title || "Document Preview"}
|
||||
/>
|
||||
</div>
|
||||
<div className="p-6 border-t border-slate-100 dark:border-white/10 flex items-center justify-between bg-slate-50 dark:bg-black/40">
|
||||
<span className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] flex items-center gap-3">
|
||||
<Eye size={16} className="text-indigo-600 dark:text-indigo-400" /> Neural Vision Enabled
|
||||
</span>
|
||||
<a
|
||||
href={displayUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 px-4 py-2 bg-indigo-500/20 hover:bg-indigo-500/40 text-indigo-400 rounded-lg text-xs font-black uppercase tracking-widest transition-all"
|
||||
className="flex items-center gap-3 px-6 py-3 bg-indigo-600 text-white rounded-xl text-[10px] font-black uppercase tracking-widest hover:scale-[1.05] active:scale-95 transition-all shadow-lg shadow-indigo-500/30"
|
||||
>
|
||||
<Download size={14} /> Full Screen / Download
|
||||
<Download size={16} /> Asset Extraction
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="glass p-12 rounded-3xl border border-white/5 bg-white/5 flex flex-col items-center text-center gap-6">
|
||||
<div className="w-20 h-20 rounded-2xl bg-indigo-500/10 flex items-center justify-center text-indigo-400">
|
||||
<FileText size={40} />
|
||||
<div className="bg-white dark:bg-white/5 p-16 rounded-[3rem] border border-slate-100 dark:border-white/10 shadow-sm flex flex-col items-center text-center gap-8 relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-indigo-500/40 to-transparent"></div>
|
||||
<div className="w-24 h-24 rounded-[2rem] bg-indigo-50 dark:bg-indigo-500/10 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shadow-inner">
|
||||
<FileText size={48} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-bold text-white mb-2">Non-PDF Document</p>
|
||||
<p className="text-sm text-gray-500 max-w-sm mx-auto uppercase tracking-widest font-black leading-relaxed">
|
||||
This file cannot be previewed directly. Please download it to read.
|
||||
<div className="space-y-3">
|
||||
<p className="text-2xl font-black text-slate-900 dark:text-white uppercase tracking-tight italic">Legacy Container</p>
|
||||
<p className="text-[10px] text-slate-400 dark:text-gray-500 max-w-xs mx-auto uppercase tracking-[0.2em] font-black leading-loose">
|
||||
Source material requires local decompression for full synaptic ingestion.
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href={displayUrl}
|
||||
download
|
||||
className="flex items-center gap-3 px-8 py-4 bg-indigo-600 hover:bg-indigo-500 text-white rounded-2xl font-black uppercase tracking-widest transition-all shadow-xl shadow-indigo-500/20 active:scale-95"
|
||||
className="flex items-center gap-4 px-10 py-5 bg-indigo-600 hover:bg-indigo-500 text-white rounded-[2rem] font-black text-[10px] uppercase tracking-[0.3em] transition-all shadow-2xl shadow-indigo-500/40 active:scale-95 group"
|
||||
>
|
||||
<Download size={20} /> Download File
|
||||
<Download size={20} className="group-hover:translate-y-1 transition-transform" /> Trigger Download
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="glass border-dashed border-white/10 p-12 rounded-3xl flex flex-col items-center gap-4 text-gray-500">
|
||||
<FileText size={48} className="opacity-20" />
|
||||
<p className="font-bold uppercase tracking-widest text-xs">No file selected</p>
|
||||
<div className="bg-slate-50 dark:bg-black/20 border-2 border-dashed border-slate-200 dark:border-white/10 p-20 rounded-[3rem] flex flex-col items-center gap-6 text-slate-300 dark:text-gray-700">
|
||||
<FileText size={64} className="opacity-10" />
|
||||
<p className="font-black uppercase tracking-[0.3em] text-[10px]">No Neural Data Linked</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -50,42 +50,45 @@ export default function FillInTheBlanksBlock({ id, title, content, editMode, onC
|
||||
<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>
|
||||
<div className="space-y-4 p-8 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2rem] mb-6 shadow-inner relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-1 h-full bg-blue-500/40"></div>
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Activity Title (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title || ""}
|
||||
onChange={(e) => onChange({ title: e.target.value })}
|
||||
placeholder="e.g. Fill the gaps, Quote..."
|
||||
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"
|
||||
className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-3 text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all outline-none shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white">
|
||||
<h3 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase border-l-4 border-blue-600 pl-6 py-1">
|
||||
{title || "Fill in the Blanks"}
|
||||
</h3>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editMode ? (
|
||||
<div className="space-y-4">
|
||||
<div className="p-6 glass border-white/5 space-y-4">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Text with blanks (use [[answer]])</label>
|
||||
<div className="space-y-8">
|
||||
<div className="p-10 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2.5rem] space-y-4 shadow-sm relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-500/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2"></div>
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Semantic Matrix (use [[answer]])</label>
|
||||
<textarea
|
||||
value={content}
|
||||
onChange={(e) => onChange({ content: e.target.value })}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl p-4 min-h-[150px] text-lg font-medium focus:outline-none focus:border-blue-500/50 transition-all"
|
||||
className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl p-6 min-h-[150px] text-lg font-black tracking-tight text-slate-800 dark:text-white focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all placeholder:text-slate-300 resize-none shadow-inner"
|
||||
placeholder="Example: The [[capital]] of France is [[Paris]]."
|
||||
/>
|
||||
<p className="text-[10px] text-gray-500 uppercase tracking-wider">Tip: Surround any word with double brackets to create a blank.</p>
|
||||
<p className="text-[9px] text-slate-400 dark:text-gray-500 uppercase font-black italic pl-1 leading-relaxed">Neural Protocol: Encapsulate target markers within double square brackets.</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 glass border-white/5 rounded-3xl space-y-8">
|
||||
<div className="text-lg leading-loose text-gray-100">
|
||||
<div className="p-10 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[3rem] space-y-10 shadow-sm hover:shadow-xl transition-all duration-700 relative overflow-hidden group/fbitem">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-indigo-500/5 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2 group-hover/fbitem:bg-indigo-500/10 transition-colors"></div>
|
||||
<div className="text-2xl font-black text-slate-800 dark:text-gray-100 uppercase tracking-tight leading-loose relative z-10">
|
||||
{parsed.parts.map((part, i) => (
|
||||
part.type === 'text' ? (
|
||||
<span key={i}>{part.value}</span>
|
||||
<span key={i} className="mx-0.5">{part.value}</span>
|
||||
) : (
|
||||
<input
|
||||
key={i}
|
||||
@@ -97,12 +100,12 @@ export default function FillInTheBlanksBlock({ id, title, content, editMode, onC
|
||||
setUserAnswers(newAnswers);
|
||||
}}
|
||||
disabled={submitted}
|
||||
className={`mx-1 px-2 py-0 border-b-2 bg-transparent transition-all focus:outline-none text-center rounded-t-sm ${submitted
|
||||
? (isCorrect(part.index!) ? "border-green-500 text-green-400 bg-green-500/10" : "border-red-500 text-red-100 bg-red-500/10")
|
||||
: "border-blue-500/30 focus:border-blue-500 text-blue-400 focus:bg-blue-500/5"
|
||||
className={`mx-2 px-4 py-1 border-b-4 bg-slate-50 dark:bg-black/20 transition-all focus:outline-none text-center rounded-t-xl font-black uppercase tracking-widest placeholder:text-slate-300 ${submitted
|
||||
? (isCorrect(part.index!) ? "border-green-500 text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-500/10" : "border-red-500 text-red-700 dark:text-red-100 bg-red-50 dark:bg-red-500/10")
|
||||
: "border-blue-500/40 focus:border-blue-600 text-blue-600 dark:text-blue-400 focus:bg-blue-500/5"
|
||||
}`}
|
||||
style={{ width: `${Math.max((part.answer?.length || 5) * 12, 60)}px` }}
|
||||
placeholder="..."
|
||||
style={{ width: `${Math.max((part.answer?.length || 5) * 16, 120)}px` }}
|
||||
placeholder="?"
|
||||
/>
|
||||
)
|
||||
))}
|
||||
@@ -111,18 +114,18 @@ export default function FillInTheBlanksBlock({ id, title, content, editMode, onC
|
||||
{!submitted && parsed.answers.length > 0 && (
|
||||
<button
|
||||
onClick={() => setSubmitted(true)}
|
||||
className="btn-premium w-full py-4 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20"
|
||||
className="w-full py-6 bg-blue-600 text-white font-black text-[10px] uppercase tracking-[0.3em] shadow-2xl shadow-blue-500/40 hover:scale-[1.02] active:scale-[0.98] transition-all rounded-[2rem] flex items-center justify-center gap-4 relative z-10"
|
||||
>
|
||||
Validate Answers
|
||||
Execute Linguistic Validation
|
||||
</button>
|
||||
)}
|
||||
|
||||
{submitted && (
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="w-full py-4 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-xl border-white/5"
|
||||
className="w-full py-6 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-blue-600 dark:text-blue-400 font-black text-[10px] uppercase tracking-[0.3em] hover:bg-slate-50 transition-all rounded-[2rem] active:scale-[0.98] shadow-sm relative z-10"
|
||||
>
|
||||
Try Again
|
||||
Reset Semantic Chain
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -75,26 +75,29 @@ export default function HotspotBlock({
|
||||
|
||||
if (!editMode) {
|
||||
return (
|
||||
<div className="space-y-4" id={id}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-amber-500/20 text-amber-500">
|
||||
<Search size={20} />
|
||||
<div className="space-y-6" id={id}>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 rounded-2xl bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-500 shadow-inner">
|
||||
<Search size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-white transition-colors">{title || "Image Hunt"}</h3>
|
||||
<p className="text-xs text-gray-500 uppercase tracking-widest font-black">{description || "Find the hidden spots!"}</p>
|
||||
<h3 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase transition-colors">{title || "Visual Scrutiny"}</h3>
|
||||
<p className="text-[10px] text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] font-black">{description || "Identify the hidden visual nodes"}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative aspect-video rounded-2xl overflow-hidden border border-white/5 bg-black/40">
|
||||
<div className="relative aspect-video rounded-[3rem] overflow-hidden border border-slate-100 dark:border-white/5 bg-slate-50 dark:bg-black/40 shadow-xl group/hsview">
|
||||
{imageUrl ? (
|
||||
<Image src={getImageUrl(imageUrl)} alt={title || ""} fill unoptimized className="object-cover opacity-50" />
|
||||
<Image src={getImageUrl(imageUrl)} alt={title || ""} fill unoptimized className="object-cover opacity-60 grayscale group-hover/hsview:grayscale-0 transition-all duration-700" />
|
||||
) : (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-gray-600 italic text-sm">No image provided.</div>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center text-slate-300 dark:text-gray-700 gap-4">
|
||||
<ImageIcon size={48} className="opacity-20" />
|
||||
<p className="text-[9px] font-black uppercase tracking-widest italic">Temporal image stream offline.</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute inset-0 flex items-center justify-center backdrop-blur-[2px]">
|
||||
<div className="text-center space-y-2">
|
||||
<Crosshair className="w-12 h-12 text-white/20 mx-auto" />
|
||||
<p className="text-xs font-bold text-white/40 uppercase tracking-widest">Interactive Game Preview (Switch to Student View to Play)</p>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center backdrop-blur-[4px] bg-slate-900/10 dark:bg-black/40 p-8 text-center group-hover/hsview:backdrop-blur-0 transition-all duration-700">
|
||||
<div className="space-y-4 max-w-sm">
|
||||
<Crosshair className="w-16 h-16 text-white/40 mx-auto animate-pulse" />
|
||||
<p className="text-[10px] font-black text-white uppercase tracking-[0.3em] leading-relaxed">Structural Game Synchronization Pending<br /><span className="text-white/40 italic">(Activate Simulation Mode to Initialize)</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,46 +107,50 @@ export default function HotspotBlock({
|
||||
|
||||
return (
|
||||
<div className="space-y-6" id={id}>
|
||||
<div className="p-6 glass border-white/5 bg-white/5 space-y-6 rounded-3xl">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500">Game Title</label>
|
||||
<div className="p-10 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 space-y-10 rounded-[3rem] shadow-sm relative overflow-hidden group/hseditor shadow-xl shadow-slate-200/50 dark:shadow-none">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-amber-500/5 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2 group-hover/hseditor:bg-amber-500/10 transition-colors"></div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-10 relative z-10">
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 pl-1">Operational Protocol (Title)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title || ""}
|
||||
onChange={(e) => onChange({ title: e.target.value })}
|
||||
placeholder="e.g. Parts of the Body..."
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2 text-sm font-bold focus:border-amber-500/50 focus:outline-none"
|
||||
className="w-full bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-2xl px-6 py-4 text-sm font-black uppercase tracking-tight text-slate-800 dark:text-white focus:ring-4 focus:ring-amber-500/10 focus:border-amber-500 transition-all outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500">Student Instructions</label>
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 pl-1">Tactical Instructions</label>
|
||||
<input
|
||||
type="text"
|
||||
value={description || ""}
|
||||
onChange={(e) => onChange({ description: e.target.value })}
|
||||
placeholder="e.g. Find and click on the following items..."
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2 text-sm focus:border-amber-500/50 focus:outline-none"
|
||||
className="w-full bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-2xl px-6 py-4 text-sm font-bold text-slate-600 dark:text-gray-300 focus:ring-4 focus:ring-amber-500/10 focus:border-amber-500 transition-all outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500">Game Image</label>
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 pl-1">Visual Baseline (Main Image)</label>
|
||||
{!imageUrl ? (
|
||||
<button
|
||||
onClick={() => setIsAssetPickerOpen(true)}
|
||||
className="w-full aspect-video rounded-2xl border-2 border-dashed border-white/10 hover:border-amber-500/50 hover:bg-amber-500/5 transition-all flex flex-col items-center justify-center gap-2 group"
|
||||
className="w-full aspect-video rounded-[2rem] border-2 border-dashed border-slate-200 dark:border-white/10 hover:border-amber-500/50 hover:bg-white dark:hover:bg-amber-500/5 transition-all flex flex-col items-center justify-center gap-4 group/imgup"
|
||||
>
|
||||
<ImageIcon className="text-gray-600 group-hover:text-amber-500 transition-colors" size={32} />
|
||||
<span className="text-xs font-bold text-gray-500 uppercase tracking-widest group-hover:text-amber-300">Choose Image</span>
|
||||
<div className="p-4 rounded-2xl bg-slate-50 dark:bg-white/5 group-hover/imgup:bg-amber-500/10 transition-colors">
|
||||
<ImageIcon className="text-slate-300 dark:text-gray-600 group-hover/imgup:text-amber-600 transition-colors" size={32} />
|
||||
</div>
|
||||
<span className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest group-hover/imgup:text-amber-700">Initialize Visual Stream</span>
|
||||
</button>
|
||||
) : (
|
||||
<div className="relative aspect-video rounded-2xl overflow-hidden group">
|
||||
<Image src={getImageUrl(imageUrl)} alt="Hotspot base" fill unoptimized className="object-cover" />
|
||||
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-3">
|
||||
<button onClick={() => setIsAssetPickerOpen(true)} className="p-2 bg-white/10 hover:bg-white/20 rounded-lg text-white transition-all"><ImageIcon size={18} /></button>
|
||||
<button onClick={() => onChange({ imageUrl: undefined })} className="p-2 bg-red-500/20 hover:bg-red-500/40 rounded-lg text-red-400 transition-all"><Trash2 size={18} /></button>
|
||||
<div className="relative aspect-video rounded-[2rem] overflow-hidden border border-slate-100 dark:border-white/10 group/imgpreview shadow-inner">
|
||||
<Image src={getImageUrl(imageUrl)} alt="Hotspot base" fill unoptimized className="object-cover select-none" />
|
||||
<div className="absolute inset-0 bg-slate-900/60 opacity-0 group-hover/imgpreview:opacity-100 transition-all duration-500 flex items-center justify-center gap-4 backdrop-blur-sm">
|
||||
<button onClick={() => setIsAssetPickerOpen(true)} className="p-4 bg-white/10 hover:bg-white hover:text-slate-900 rounded-2xl text-white transition-all transform hover:scale-110"><ImageIcon size={20} /></button>
|
||||
<button onClick={() => onChange({ imageUrl: undefined })} className="p-4 bg-red-500/20 hover:bg-red-500 rounded-2xl text-white transition-all transform hover:scale-110"><Trash2 size={20} /></button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -151,24 +158,24 @@ export default function HotspotBlock({
|
||||
</div>
|
||||
|
||||
{imageUrl && (
|
||||
<div className="space-y-4 pt-4 border-t border-white/5">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500">Define Hotspots (Click on the image below)</label>
|
||||
<span className="text-[10px] font-bold text-amber-500 bg-amber-500/10 px-2 py-1 rounded uppercase tracking-widest">{hotspots.length} Defined</span>
|
||||
<div className="space-y-8 pt-10 border-t border-slate-50 dark:border-white/5">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 italic">Structural Node Mapping (Click Image Area)</label>
|
||||
<span className="text-[9px] font-black text-amber-700 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10 px-4 py-2 rounded-xl border border-amber-100 dark:border-amber-500/20 shadow-inner uppercase tracking-widest">{hotspots.length} Nodes Synchronized</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-10">
|
||||
<div className="lg:col-span-2">
|
||||
<div
|
||||
ref={containerRef}
|
||||
onClick={handleImageClick}
|
||||
className="relative aspect-video rounded-2xl overflow-hidden border-2 border-white/10 cursor-crosshair shadow-2xl"
|
||||
className="relative aspect-video rounded-[2.5rem] overflow-hidden border-2 border-slate-100 dark:border-white/10 cursor-crosshair bg-slate-50 dark:bg-black group/mpro shadow-2xl transition-all duration-500 hover:shadow-amber-500/10"
|
||||
>
|
||||
<Image src={getImageUrl(imageUrl)} alt="Define Hotspots" fill unoptimized className="object-cover select-none" />
|
||||
{hotspots.map((h) => (
|
||||
<Image src={getImageUrl(imageUrl)} alt="Define Hotspots" fill unoptimized className="object-cover select-none group-hover/mpro:opacity-80 transition-opacity" />
|
||||
{hotspots.map((h, hidx) => (
|
||||
<div
|
||||
key={h.id}
|
||||
className="absolute group/pin"
|
||||
className="absolute group/pin z-20"
|
||||
style={{
|
||||
left: `${h.x}%`,
|
||||
top: `${h.y}%`,
|
||||
@@ -176,19 +183,21 @@ export default function HotspotBlock({
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="bg-amber-500/30 border-2 border-amber-400 rounded-full flex items-center justify-center relative transition-transform hover:scale-110"
|
||||
className="bg-amber-500/20 border-2 border-amber-500 dark:border-amber-400 rounded-full flex items-center justify-center relative transition-all duration-500 hover:scale-125 hover:bg-amber-500/40"
|
||||
style={{
|
||||
width: `${h.radius * 2}vw`,
|
||||
height: `${h.radius * 2}vw`,
|
||||
maxWidth: '100px',
|
||||
maxHeight: '100px'
|
||||
width: `${h.radius * 2.5}vw`,
|
||||
height: `${h.radius * 2.5}vw`,
|
||||
maxWidth: '120px',
|
||||
maxHeight: '120px',
|
||||
minWidth: '40px',
|
||||
minHeight: '40px'
|
||||
}}
|
||||
>
|
||||
<div className="bg-amber-500 rounded-full p-1 text-black shadow-lg">
|
||||
<MapPin size={12} strokeWidth={3} />
|
||||
<div className="bg-amber-600 dark:bg-amber-500 rounded-full p-2 text-white shadow-2xl ring-4 ring-amber-500/20">
|
||||
<Crosshair size={14} strokeWidth={3} className="group-hover/pin:rotate-90 transition-transform duration-500" />
|
||||
</div>
|
||||
<div className="absolute top-full mt-2 left-1/2 -translate-x-1/2 px-2 py-1 bg-black/80 rounded text-[10px] font-bold text-white whitespace-nowrap opacity-0 group-hover/pin:opacity-100 transition-opacity">
|
||||
{h.label}
|
||||
<div className="absolute top-full mt-3 left-1/2 -translate-x-1/2 px-4 py-2 bg-slate-900 dark:bg-white text-white dark:text-slate-900 rounded-xl text-[10px] font-black uppercase tracking-tight whitespace-nowrap opacity-0 group-hover/pin:opacity-100 transition-all transform translate-y-2 group-hover/pin:translate-y-0 shadow-2xl z-50">
|
||||
NODE #{hidx + 1}: {h.label}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,38 +205,48 @@ export default function HotspotBlock({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 max-h-[400px] overflow-y-auto custom-scrollbar pr-2">
|
||||
<div className="space-y-4 max-h-[500px] overflow-y-auto custom-scrollbar pr-3">
|
||||
{hotspots.length === 0 ? (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center p-6 border-2 border-dashed border-white/5 rounded-2xl">
|
||||
<Plus className="text-gray-700 mb-2" size={24} />
|
||||
<p className="text-xs text-gray-600 font-bold uppercase tracking-widest">Click on the image to add hotspots</p>
|
||||
<div className="h-full flex flex-col items-center justify-center text-center p-12 bg-slate-50 dark:bg-black/20 border-2 border-dashed border-slate-200 dark:border-white/10 rounded-[2.5rem] space-y-4">
|
||||
<div className="p-4 bg-white dark:bg-white/5 rounded-2xl shadow-inner italic">
|
||||
<Plus className="text-slate-300 dark:text-gray-700" size={32} />
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-400 dark:text-gray-500 font-black uppercase tracking-widest">Execute visual contact on the map area to generate nodes.</p>
|
||||
</div>
|
||||
) : (
|
||||
hotspots.map((h, idx) => (
|
||||
<div key={h.id} className="p-4 bg-white/5 border border-white/10 rounded-xl space-y-3 animate-in fade-in slide-in-from-right-4 duration-300">
|
||||
<div key={h.id} className="p-6 bg-slate-50 dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[1.5rem] space-y-5 animate-in fade-in slide-in-from-right-4 duration-500 group/hscard hover:border-amber-500/30 transition-all shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] font-black text-amber-500 uppercase tracking-widest">Hotspot #{idx + 1}</span>
|
||||
<button onClick={() => removeHotspot(idx)} className="p-1 hover:bg-red-500/20 text-red-500 rounded transition-colors"><Trash2 size={12} /></button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="text"
|
||||
value={h.label}
|
||||
onChange={(e) => updateHotspot(idx, { label: e.target.value })}
|
||||
placeholder="Item name..."
|
||||
className="w-full bg-black/40 border border-white/10 rounded-lg px-3 py-1.5 text-xs font-bold focus:border-amber-500/50 focus:outline-none"
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="text-[9px] font-black uppercase tracking-widest text-gray-600">Radius</label>
|
||||
<span className="w-6 h-6 rounded-lg bg-amber-500 flex items-center justify-center text-[10px] font-black text-white shadow-lg">#{idx + 1}</span>
|
||||
<span className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] italic">Structural Node</span>
|
||||
</div>
|
||||
<button onClick={() => removeHotspot(idx)} className="p-2 bg-red-50 dark:bg-red-500/5 hover:bg-red-500 hover:text-white rounded-xl text-red-500 transition-all active:scale-90"><Trash2 size={14} /></button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[9px] font-black uppercase text-slate-400 dark:text-gray-600 pl-1">Identification Label</label>
|
||||
<input
|
||||
type="text"
|
||||
value={h.label}
|
||||
onChange={(e) => updateHotspot(idx, { label: e.target.value })}
|
||||
placeholder="Item name..."
|
||||
className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-xl px-4 py-2 text-xs font-black uppercase tracking-tight text-slate-800 dark:text-white focus:ring-4 focus:ring-amber-500/10 outline-none transition-all shadow-inner"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between pl-1">
|
||||
<label className="text-[9px] font-black uppercase text-slate-400 dark:text-gray-600">Influence Radius</label>
|
||||
<span className="text-[10px] font-black text-amber-600 dark:text-amber-400 font-mono">{h.radius}%</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="2"
|
||||
max="15"
|
||||
value={h.radius}
|
||||
onChange={(e) => updateHotspot(idx, { radius: parseInt(e.target.value) })}
|
||||
className="flex-1 accent-amber-500 h-1 bg-white/10 rounded-lg appearance-none cursor-pointer"
|
||||
className="w-full h-1.5 bg-slate-200 dark:bg-white/10 rounded-lg appearance-none cursor-pointer accent-amber-500 transition-all"
|
||||
/>
|
||||
<span className="text-[10px] font-bold text-gray-400 w-6">{h.radius}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -43,27 +43,28 @@ export default function MatchingBlock({ id, title, pairs, editMode, onChange }:
|
||||
<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>
|
||||
<div className="space-y-4 p-8 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2rem] mb-6 shadow-inner relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-1 h-full bg-blue-500/40"></div>
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Activity Title (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title || ""}
|
||||
onChange={(e) => onChange({ title: e.target.value })}
|
||||
placeholder="e.g. Match the concepts..."
|
||||
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"
|
||||
className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-3 text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all outline-none shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white">
|
||||
<h3 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase border-l-4 border-blue-600 pl-6 py-1">
|
||||
{title || "Concept Matching"}
|
||||
</h3>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editMode ? (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-6">
|
||||
{pairs.map((pair, idx) => (
|
||||
<div key={idx} className="flex gap-4 items-center animate-in slide-in-from-left-4 duration-300">
|
||||
<div key={idx} className="flex gap-4 items-center bg-white dark:bg-white/5 p-4 rounded-[2rem] border border-slate-100 dark:border-white/10 shadow-sm animate-in slide-in-from-left-4 duration-300 group/pair">
|
||||
<input
|
||||
value={pair.left}
|
||||
onChange={(e) => {
|
||||
@@ -71,10 +72,10 @@ export default function MatchingBlock({ id, title, pairs, editMode, onChange }:
|
||||
newPairs[idx].left = e.target.value;
|
||||
onChange({ pairs: newPairs });
|
||||
}}
|
||||
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-sm focus:border-blue-500/50 focus:outline-none"
|
||||
placeholder="Term A"
|
||||
className="flex-1 bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-xl px-5 py-3 text-sm font-bold text-slate-700 dark:text-gray-200 focus:ring-4 focus:ring-blue-500/10 transition-all outline-none shadow-inner"
|
||||
placeholder="Synaptic Origin"
|
||||
/>
|
||||
<span className="text-gray-500 font-bold">↔</span>
|
||||
<div className="w-10 h-10 rounded-full bg-blue-50 dark:bg-blue-500/10 flex items-center justify-center text-blue-600 font-black shadow-inner">↔</div>
|
||||
<input
|
||||
value={pair.right}
|
||||
onChange={(e) => {
|
||||
@@ -82,47 +83,50 @@ export default function MatchingBlock({ id, title, pairs, editMode, onChange }:
|
||||
newPairs[idx].right = e.target.value;
|
||||
onChange({ pairs: newPairs });
|
||||
}}
|
||||
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-sm focus:border-blue-500/50 focus:outline-none"
|
||||
placeholder="Definition B"
|
||||
className="flex-1 bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-xl px-5 py-3 text-sm font-bold text-slate-700 dark:text-gray-200 focus:ring-4 focus:ring-blue-500/10 transition-all outline-none shadow-inner"
|
||||
placeholder="Semantic Destination"
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
const newPairs = pairs.filter((_, i) => i !== idx);
|
||||
onChange({ pairs: newPairs });
|
||||
}}
|
||||
className="p-2 text-gray-500 hover:text-red-400"
|
||||
className="p-3 bg-red-50 dark:bg-red-500/5 hover:bg-red-500 hover:text-white rounded-xl text-red-500 transition-all active:scale-90 opacity-0 group-hover/pair:opacity-100"
|
||||
>
|
||||
×
|
||||
<span className="text-xl">×</span>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
onClick={() => onChange({ pairs: [...pairs, { left: "", right: "" }] })}
|
||||
className="w-full py-4 border-dashed border-2 border-white/10 text-gray-400 hover:text-white hover:border-blue-500/30 transition-all font-bold text-xs uppercase tracking-widest rounded-xl"
|
||||
className="w-full py-6 bg-slate-50 dark:bg-black/20 border-2 border-dashed border-slate-200 dark:border-white/10 text-slate-400 dark:text-gray-600 hover:text-blue-600 dark:hover:text-blue-400 hover:border-blue-500/40 hover:bg-white dark:hover:bg-white/5 transition-all font-black text-[10px] uppercase tracking-[0.3em] rounded-[2rem] shadow-inner"
|
||||
>
|
||||
+ Add Pair
|
||||
+ Integrate New Synaptic Pair
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 p-8 glass border-white/5 rounded-3xl relative">
|
||||
<div className="space-y-4">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-4 block">Term</label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 p-10 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[3rem] shadow-sm relative overflow-hidden group/match">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-blue-500/5 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2 group-hover/match:bg-blue-500/10 transition-colors"></div>
|
||||
|
||||
<div className="space-y-6 relative z-10">
|
||||
<label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 mb-6 block pl-1 italic">Synaptic Origin (Term)</label>
|
||||
{pairs.map((pair, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => !submitted && setSelectedLeft(i)}
|
||||
className={`w-full p-4 rounded-xl border text-left text-sm font-bold transition-all ${selectedLeft === i ? "border-blue-500 bg-blue-500/10 text-white shadow-lg" :
|
||||
matches[i] !== undefined ? "border-blue-500/20 bg-blue-500/5 text-blue-400" :
|
||||
"border-white/5 bg-white/5 text-gray-200 hover:border-white/20"
|
||||
className={`w-full p-6 h-20 rounded-[1.5rem] border-2 text-left text-sm font-black uppercase tracking-tight transition-all duration-500 flex items-center shadow-sm ${selectedLeft === i ? "border-blue-600 bg-blue-600 text-white shadow-2xl shadow-blue-500/40 -translate-y-1 scale-[1.02]" :
|
||||
matches[i] !== undefined ? "border-blue-500/20 bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 ring-4 ring-blue-500/5 scale-[0.98] opacity-60" :
|
||||
"border-slate-100 dark:border-white/10 bg-slate-50 dark:bg-black/20 text-slate-700 dark:text-gray-300 hover:border-blue-400 hover:bg-white dark:hover:bg-white/5"
|
||||
}`}
|
||||
>
|
||||
{pair.left}
|
||||
<span className="truncate">{pair.left}</span>
|
||||
{matches[i] !== undefined && <span className="ml-auto text-xs opacity-40">✓</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-4 block">Definition</label>
|
||||
<div className="space-y-6 relative z-10">
|
||||
<label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 mb-6 block pl-1 italic">Semantic Target (Definition)</label>
|
||||
{shuffledRight.map((item, i) => {
|
||||
const matchedLeftIdx = Object.keys(matches).find(k => matches[parseInt(k)] === item.originalIdx);
|
||||
const isCorrect = submitted && matchedLeftIdx !== undefined && parseInt(matchedLeftIdx) === item.originalIdx;
|
||||
@@ -133,38 +137,38 @@ export default function MatchingBlock({ id, title, pairs, editMode, onChange }:
|
||||
key={i}
|
||||
disabled={selectedLeft === null || submitted}
|
||||
onClick={() => handleMatch(selectedLeft!, item.originalIdx)}
|
||||
className={`w-full p-4 rounded-xl border text-left text-sm font-bold transition-all ${selectedLeft !== null && matchedLeftIdx === undefined ? "hover:border-blue-500/50 hover:bg-white/5" : ""
|
||||
} ${isCorrect ? "border-green-500 bg-green-500/20 text-green-400" :
|
||||
isWrong ? "border-red-500 bg-red-500/20 text-red-100" :
|
||||
matchedLeftIdx !== undefined ? "border-blue-500/30 bg-blue-500/5 text-blue-400" :
|
||||
"border-white/5 bg-white/5 text-gray-200"
|
||||
className={`w-full p-6 min-h-20 rounded-[1.5rem] border-2 text-left text-[11px] font-bold uppercase tracking-wider leading-relaxed transition-all duration-500 shadow-sm ${selectedLeft !== null && matchedLeftIdx === undefined ? "hover:border-blue-500/50 hover:bg-white dark:hover:bg-white/5 hover:-translate-y-0.5" : ""
|
||||
} ${isCorrect ? "border-green-500 bg-green-50 dark:bg-green-500/10 text-green-700 dark:text-green-400 shadow-xl shadow-green-500/20" :
|
||||
isWrong ? "border-red-500 bg-red-50 dark:bg-red-500/10 text-red-700 dark:text-red-100 shadow-xl shadow-red-500/20" :
|
||||
matchedLeftIdx !== undefined ? "border-blue-500/20 bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 ring-4 ring-blue-500/5 scale-[0.98] opacity-60" :
|
||||
"border-slate-100 dark:border-white/10 bg-slate-50 dark:bg-black/20 text-slate-700 dark:text-gray-300"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{item.value}</span>
|
||||
{isCorrect && <span>✅</span>}
|
||||
{isWrong && <span>❌</span>}
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="flex-1">{item.value}</span>
|
||||
{isCorrect && <span className="text-xl rotate-12 transition-transform duration-700 group-hover/match:rotate-0">✅</span>}
|
||||
{isWrong && <span className="text-xl animate-pulse">❌</span>}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2 pt-8 border-t border-white/5">
|
||||
<div className="md:col-span-2 pt-10 border-t border-slate-50 dark:border-white/5 relative z-10">
|
||||
{!submitted && Object.keys(matches).length === pairs.length && (
|
||||
<button
|
||||
onClick={() => setSubmitted(true)}
|
||||
className="btn-premium w-full py-4 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20"
|
||||
className="w-full py-6 bg-blue-600 text-white font-black text-[10px] uppercase tracking-[0.3em] shadow-2xl shadow-blue-500/40 hover:scale-[1.02] active:scale-[0.98] transition-all rounded-[2rem] flex items-center justify-center gap-4 animate-in fade-in zoom-in duration-500"
|
||||
>
|
||||
Validate Matching
|
||||
Reconcile Semantic Network
|
||||
</button>
|
||||
)}
|
||||
{submitted && (
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="w-full py-4 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-xl border-white/5"
|
||||
className="w-full py-6 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-blue-600 dark:text-blue-400 font-black text-[10px] uppercase tracking-[0.3em] hover:bg-slate-50 transition-all rounded-[2rem] active:scale-[0.98] shadow-sm"
|
||||
>
|
||||
Try Again
|
||||
Reset Synaptic Grid
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -64,100 +64,104 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
|
||||
{/* Block Header */}
|
||||
<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>
|
||||
<div className="space-y-4 p-8 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2rem] mb-6 shadow-inner relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-1 h-full bg-blue-500/40"></div>
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Activity Title (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title || ""}
|
||||
onChange={(e) => onChange({ title: e.target.value })}
|
||||
placeholder="e.g. Explainer Video, Audio Guide..."
|
||||
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"
|
||||
placeholder="e.g. Masterclass Stream, Acoustic Analysis..."
|
||||
className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-3 text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all outline-none shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
title && <h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white">{title}</h3>
|
||||
title && <h3 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase border-l-4 border-blue-600 pl-6 py-1">{title}</h3>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editMode && (
|
||||
<div className="space-y-6 p-6 glass border-blue-500/10 mb-8 bg-blue-500/5">
|
||||
<div className="flex items-center gap-4 mb-2">
|
||||
<div className="space-y-8 p-10 bg-white dark:bg-white/5 border border-blue-500/10 dark:border-blue-500/20 mb-10 rounded-[2.5rem] shadow-xl shadow-blue-500/5 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-blue-500/5 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2"></div>
|
||||
<div className="flex items-center gap-4 relative z-10">
|
||||
<button
|
||||
onClick={() => setSourceType("url")}
|
||||
className={`px-4 py-2 text-[10px] uppercase font-black tracking-widest rounded-lg transition-all ${sourceType === "url" ? "bg-blue-500 text-white shadow-lg" : "text-gray-500 hover:text-white"}`}
|
||||
className={`px-6 py-2 text-[10px] uppercase font-black tracking-[0.2em] rounded-xl transition-all border ${sourceType === "url" ? "bg-blue-600 text-white border-blue-600 shadow-lg shadow-blue-500/30" : "bg-slate-50 dark:bg-white/5 text-slate-400 dark:text-gray-500 border-slate-100 hover:border-slate-200"}`}
|
||||
>
|
||||
External URL
|
||||
External Stream
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSourceType("upload")}
|
||||
className={`px-4 py-2 text-[10px] uppercase font-black tracking-widest rounded-lg transition-all ${sourceType === "upload" ? "bg-blue-500 text-white shadow-lg" : "text-gray-500 hover:text-white"}`}
|
||||
className={`px-6 py-2 text-[10px] uppercase font-black tracking-[0.2em] rounded-xl transition-all border ${sourceType === "upload" ? "bg-blue-600 text-white border-blue-600 shadow-lg shadow-blue-500/30" : "bg-slate-50 dark:bg-white/5 text-slate-400 dark:text-gray-500 border-slate-100 hover:border-slate-200"}`}
|
||||
>
|
||||
Upload File
|
||||
Direct Asset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{sourceType === "url" ? (
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Media URL</label>
|
||||
<input
|
||||
type="text"
|
||||
value={url.startsWith("/") ? "" : url}
|
||||
onChange={(e) => onChange({ url: e.target.value })}
|
||||
placeholder="YouTube, Vimeo or static link"
|
||||
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm focus:border-blue-500/50 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">File Manager</label>
|
||||
<FileUpload
|
||||
currentUrl={url.startsWith("/") ? url : undefined}
|
||||
onUploadComplete={(newUrl) => onChange({ url: newUrl })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 relative z-10">
|
||||
{sourceType === "url" ? (
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Media Source Locator</label>
|
||||
<input
|
||||
type="text"
|
||||
value={url.startsWith("/") ? "" : url}
|
||||
onChange={(e) => onChange({ url: e.target.value })}
|
||||
placeholder="YouTube, Vimeo or static link"
|
||||
className="w-full bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-4 text-sm font-bold focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all outline-none shadow-inner"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Asset Pipeline</label>
|
||||
<FileUpload
|
||||
currentUrl={url.startsWith("/") ? url : undefined}
|
||||
onUploadComplete={(newUrl) => onChange({ url: newUrl })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Playback Limit (0 = Unlimited)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={maxPlays}
|
||||
onChange={(e) => onChange({ config: { ...config, maxPlays: parseInt(e.target.value) || 0 } })}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm focus:border-blue-500/50 focus:outline-none h-11"
|
||||
/>
|
||||
<p className="text-[10px] text-gray-500 uppercase leading-relaxed mt-2">Prevent content fatigue by limiting how many times a student can watch/listen.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Additional Options</label>
|
||||
<div className="flex items-center gap-3 bg-white/5 border border-white/10 rounded-lg px-4 py-2 h-11">
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Play Capacity (0 = Inf)</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`show-transcript-${title}`} // Unique ID
|
||||
checked={config.show_transcript !== false} // Default to true
|
||||
onChange={(e) => onChange({ config: { ...config, show_transcript: e.target.checked } })}
|
||||
className="w-4 h-4 rounded border-gray-600 text-blue-600 focus:ring-blue-500 bg-gray-700"
|
||||
type="number"
|
||||
value={maxPlays}
|
||||
onChange={(e) => onChange({ config: { ...config, maxPlays: parseInt(e.target.value) || 0 } })}
|
||||
className="w-full bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-4 text-sm font-bold focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all outline-none shadow-inner h-14"
|
||||
/>
|
||||
<label htmlFor={`show-transcript-${title}`} className="text-sm text-gray-300 font-medium select-none cursor-pointer">
|
||||
Show Interactive Transcript
|
||||
</label>
|
||||
<p className="text-[9px] text-slate-400 dark:text-gray-500 uppercase font-black italic pl-1 leading-relaxed">Throttle cognitive load by limiting session repetition.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Modalities</label>
|
||||
<div className="flex items-center gap-4 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl px-6 h-14 shadow-inner">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`show-transcript-${title}`} // Unique ID
|
||||
checked={config.show_transcript !== false} // Default to true
|
||||
onChange={(e) => onChange({ config: { ...config, show_transcript: e.target.checked } })}
|
||||
className="w-6 h-6 rounded-lg border-2 border-slate-200 dark:border-white/10 appearance-none checked:bg-blue-600 checked:border-blue-600 transition-all cursor-pointer shadow-sm"
|
||||
/>
|
||||
<label htmlFor={`show-transcript-${title}`} className="text-sm text-slate-600 dark:text-gray-300 font-black uppercase tracking-tight select-none cursor-pointer">
|
||||
Interactive Subtitles
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-[9px] text-slate-400 dark:text-gray-500 uppercase font-black italic pl-1 leading-relaxed">Enable for accessibility; disable for auditory tests.</p>
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-500 uppercase leading-relaxed mt-2">Uncheck to hide transcription text (e.g. for listening tests).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Markers Editor */}
|
||||
<div className="space-y-4 pt-6 border-t border-white/10">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest block">Interactive Questions (Timestamps)</label>
|
||||
<div className="space-y-6 pt-10 border-t border-slate-100 dark:border-white/10 relative z-10">
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] block pl-1">Neural Interactivity Markers (Timestamps)</label>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-4">
|
||||
{(config.markers || []).map((marker, idx) => (
|
||||
<div key={idx} className="bg-white/5 p-4 rounded-lg border border-white/5 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-mono bg-blue-500/20 text-blue-400 px-2 py-1 rounded">
|
||||
<div key={idx} className="bg-slate-50 dark:bg-black/40 p-8 rounded-[2rem] border border-slate-200 dark:border-white/5 space-y-6 shadow-inner group/marker">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="px-5 py-3 rounded-2xl bg-blue-600 text-white text-xs font-black italic shadow-lg shadow-blue-500/30">
|
||||
{Math.floor(marker.timestamp / 60)}:{String(marker.timestamp % 60).padStart(2, '0')}
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
value={marker.question}
|
||||
onChange={(e) => {
|
||||
@@ -165,7 +169,8 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
|
||||
newMarkers[idx].question = e.target.value;
|
||||
onChange({ config: { ...config, markers: newMarkers } });
|
||||
}}
|
||||
className="text-sm bg-transparent border-b border-white/10 flex-1 focus:border-blue-500 outline-none"
|
||||
placeholder="Question for this timestamp..."
|
||||
className="flex-1 bg-transparent border-b border-slate-200 dark:border-white/10 focus:border-blue-500 outline-none text-sm font-bold text-slate-700 dark:text-white py-2"
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -187,17 +192,17 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
|
||||
newMarkers.splice(idx, 1);
|
||||
onChange({ config: { ...config, markers: newMarkers } });
|
||||
}}
|
||||
className="text-red-400 hover:text-red-300 p-1"
|
||||
className="text-slate-400 hover:text-red-500 p-2 transition-colors"
|
||||
>
|
||||
×
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-x"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Options Management */}
|
||||
<div className="pl-14 space-y-2">
|
||||
<label className="text-[10px] font-bold text-gray-500 uppercase">Options</label>
|
||||
<div className="pl-24 space-y-3">
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest">Response Vectors</label>
|
||||
{marker.options.map((opt, optIdx) => (
|
||||
<div key={optIdx} className="flex items-center gap-2">
|
||||
<div key={optIdx} className="flex items-center gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
name={`correct-${idx}`}
|
||||
@@ -207,7 +212,7 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
|
||||
newMarkers[idx].correctIndex = optIdx;
|
||||
onChange({ config: { ...config, markers: newMarkers } });
|
||||
}}
|
||||
className="accent-green-500"
|
||||
className="w-5 h-5 accent-green-500 cursor-pointer"
|
||||
/>
|
||||
<input
|
||||
value={opt}
|
||||
@@ -216,7 +221,7 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
|
||||
newMarkers[idx].options[optIdx] = e.target.value;
|
||||
onChange({ config: { ...config, markers: newMarkers } });
|
||||
}}
|
||||
className={`text-xs bg-transparent border border-white/10 rounded px-2 py-1 flex-1 ${marker.correctIndex === optIdx ? 'text-green-400 border-green-500/30' : ''}`}
|
||||
className={`flex-1 bg-white dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-xl px-4 py-2 text-sm font-medium focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all outline-none shadow-sm ${marker.correctIndex === optIdx ? 'text-green-600 dark:text-green-400 border-green-500/30' : 'text-slate-700 dark:text-gray-300'}`}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -227,9 +232,9 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
|
||||
}
|
||||
onChange({ config: { ...config, markers: newMarkers } });
|
||||
}}
|
||||
className="text-gray-600 hover:text-red-400 px-2"
|
||||
className="text-slate-400 hover:text-red-500 p-2 transition-colors"
|
||||
>
|
||||
×
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-x"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
@@ -239,27 +244,27 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
|
||||
newMarkers[idx].options.push(`Option ${newMarkers[idx].options.length + 1}`);
|
||||
onChange({ config: { ...config, markers: newMarkers } });
|
||||
}}
|
||||
className="text-[10px] text-blue-400 hover:text-blue-300 uppercase font-bold tracking-widest mt-1"
|
||||
className="text-[10px] text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 uppercase font-black tracking-widest mt-2 px-4 py-2 rounded-xl bg-blue-500/10 hover:bg-blue-500/20 transition-colors"
|
||||
>
|
||||
+ Add Option
|
||||
+ Add Response Vector
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
<div className="grid grid-cols-4 gap-4 p-6 bg-slate-100 dark:bg-white/5 rounded-2xl border border-slate-200 dark:border-white/10 shadow-inner">
|
||||
<input
|
||||
code-type="number"
|
||||
type="number"
|
||||
placeholder="Sec"
|
||||
id="new-marker-time"
|
||||
className="col-span-1 bg-white/5 border border-white/10 rounded px-3 py-2 text-sm"
|
||||
className="col-span-1 bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-xl px-4 py-2 text-sm font-medium focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all outline-none shadow-sm"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Question?"
|
||||
id="new-marker-question"
|
||||
className="col-span-2 bg-white/5 border border-white/10 rounded px-3 py-2 text-sm"
|
||||
className="col-span-2 bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-xl px-4 py-2 text-sm font-medium focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all outline-none shadow-sm"
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -281,12 +286,12 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
|
||||
questionInput.value = "";
|
||||
}
|
||||
}}
|
||||
className="col-span-1 bg-blue-500 hover:bg-blue-600 text-white rounded text-xs font-bold uppercase"
|
||||
className="col-span-1 bg-blue-600 hover:bg-blue-700 text-white rounded-xl text-xs font-black uppercase tracking-widest shadow-lg shadow-blue-500/30 transition-all"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-500 uppercase leading-relaxed">
|
||||
<p className="text-[10px] text-slate-400 dark:text-gray-500 uppercase font-black italic leading-relaxed pl-1">
|
||||
Questions will pause the video at the specified second. Only simple Yes/No questions supported currently.
|
||||
</p>
|
||||
</div>
|
||||
@@ -305,10 +310,13 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
|
||||
/>
|
||||
|
||||
{!editMode && maxPlays > 0 && (
|
||||
<div className="mt-4 flex items-center justify-between px-4 py-2 glass bg-white/5 border-white/5 rounded-lg">
|
||||
<span className="text-xs text-gray-500 uppercase font-medium">Plays Remaining</span>
|
||||
<span className={`text-sm font-bold ${maxPlays - localPlays <= 1 ? 'text-orange-400' : 'text-blue-400'}`}>
|
||||
{Math.max(0, maxPlays - localPlays)} / {maxPlays}
|
||||
<div className="mt-6 flex items-center justify-between px-8 py-4 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[2rem] shadow-sm">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></div>
|
||||
<span className="text-[10px] text-slate-400 dark:text-gray-500 uppercase font-black tracking-widest">Cognitive Reserve Remaining</span>
|
||||
</div>
|
||||
<span className={`text-sm font-black italic ${maxPlays - localPlays <= 1 ? 'text-orange-500' : 'text-blue-600 dark:text-blue-400'}`}>
|
||||
{Math.max(0, maxPlays - localPlays)} / {maxPlays} Access Vectors
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -39,25 +39,26 @@ export default function MemoryBlock({ id, title, pairs = [], editMode, onChange
|
||||
|
||||
if (!editMode) {
|
||||
return (
|
||||
<div className="space-y-4" id={id}>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-white shadow-lg shadow-indigo-500/20">
|
||||
<Brain size={24} />
|
||||
<div className="space-y-8" id={id}>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="w-16 h-16 rounded-[1.5rem] bg-indigo-600 dark:bg-indigo-500 flex items-center justify-center text-white shadow-2xl shadow-indigo-500/30 transform -rotate-3 hover:rotate-0 transition-transform duration-500">
|
||||
<Brain size={32} strokeWidth={2.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-black tracking-tight text-white uppercase tracking-[0.1em]">{title || "Memory Match"}</h3>
|
||||
<p className="text-[10px] font-black text-indigo-400 uppercase tracking-widest">Brain Training Exercise</p>
|
||||
<h3 className="text-3xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase leading-none">{title || "Synaptic Recall"}</h3>
|
||||
<p className="text-[10px] font-black text-indigo-600 dark:text-indigo-400 uppercase tracking-[0.3em] mt-2">Mnemonic Calibration Engine</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||
{Array.from({ length: Math.min(pairs.length * 2, 8) }).map((_, i) => (
|
||||
<div key={i} className="h-24 rounded-2xl bg-white/5 border-2 border-dashed border-white/5 flex items-center justify-center">
|
||||
<HelpCircle className="text-white/5" size={32} />
|
||||
<div key={i} className="h-28 rounded-[2rem] bg-slate-50 dark:bg-white/5 border-2 border-dashed border-slate-200 dark:border-white/10 flex items-center justify-center relative group/mcard">
|
||||
<HelpCircle className="text-slate-200 dark:text-gray-800 group-hover/mcard:text-indigo-500/40 transition-colors" size={40} strokeWidth={1} />
|
||||
<div className="absolute inset-2 border border-indigo-500/5 rounded-[1.5rem]"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-center py-4 bg-indigo-500/5 rounded-2xl border border-indigo-500/10">
|
||||
<p className="text-[10px] font-bold text-indigo-300 uppercase tracking-widest">Memory Game with {pairs.length} pairs defined</p>
|
||||
<div className="text-center py-6 bg-indigo-50 dark:bg-indigo-500/5 rounded-[2.5rem] border border-indigo-100 dark:border-indigo-500/10 shadow-inner">
|
||||
<p className="text-[10px] font-black text-indigo-700 dark:text-indigo-300 uppercase tracking-[0.4em] italic">{pairs.length} Bilateral Nodes Configured</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -65,69 +66,71 @@ export default function MemoryBlock({ id, title, pairs = [], editMode, onChange
|
||||
|
||||
return (
|
||||
<div className="space-y-6" id={id}>
|
||||
<div className="p-8 glass border-white/5 bg-white/5 space-y-8 rounded-3xl">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500 pl-1">Game Description / Title</label>
|
||||
<div className="p-10 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 space-y-12 rounded-[3.5rem] shadow-xl shadow-slate-200/50 dark:shadow-none relative overflow-hidden group/memeditor">
|
||||
<div className="absolute top-0 left-0 w-80 h-80 bg-indigo-500/5 rounded-full blur-[100px] -translate-y-1/2 -translate-x-1/2 group-hover/memeditor:bg-indigo-500/10 transition-colors"></div>
|
||||
|
||||
<div className="space-y-4 relative z-10">
|
||||
<label className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-400 dark:text-gray-500 pl-2">System Designation (Title)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title || ""}
|
||||
onChange={(e) => onChange({ title: e.target.value })}
|
||||
placeholder="e.g. Vocabulary Memory Match..."
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-sm font-bold focus:border-indigo-500/50 focus:outline-none transition-all"
|
||||
className="w-full bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-[2.5rem] px-8 py-5 text-lg font-black italic uppercase tracking-tight text-slate-800 dark:text-white focus:ring-8 focus:ring-indigo-500/5 focus:border-indigo-500 transition-all outline-none shadow-inner"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between pl-1">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500">Pairs to Match</label>
|
||||
<span className="text-[10px] font-bold text-indigo-400 bg-indigo-500/10 px-2 py-1 rounded-md uppercase tracking-widest">{pairs.length} Pairs</span>
|
||||
<div className="space-y-8 relative z-10">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 italic">Bilateral Pair Configuration</label>
|
||||
<span className="text-[9px] font-black text-indigo-700 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-500/10 px-4 py-2 rounded-xl border border-indigo-100 dark:border-indigo-500/20 shadow-inner uppercase tracking-widest">{pairs.length} Pairs Synchronized</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-6">
|
||||
{pairs.map((pair, idx) => (
|
||||
<div key={pair.id || idx} className="grid grid-cols-1 sm:grid-cols-9 gap-4 p-4 bg-black/20 rounded-2xl border border-white/5 group animate-in slide-in-from-left-4 duration-300">
|
||||
<div className="sm:col-span-4 space-y-1">
|
||||
<span className="text-[9px] font-black uppercase tracking-tight text-gray-600 pl-1">Card A</span>
|
||||
<div key={pair.id || idx} className="grid grid-cols-1 sm:grid-cols-10 gap-6 p-8 bg-slate-50/50 dark:bg-white/5 rounded-[2.5rem] border border-slate-100 dark:border-white/10 group animate-in slide-in-from-left-4 duration-500 hover:scale-[1.01] hover:bg-white dark:hover:bg-white/10 transition-all shadow-sm">
|
||||
<div className="sm:col-span-4 space-y-3">
|
||||
<span className="text-[9px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-600 pl-1 italic">Synapse Alpha</span>
|
||||
<input
|
||||
value={pair.left}
|
||||
onChange={(e) => updatePair(idx, { left: e.target.value })}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-sm font-bold focus:border-indigo-500/50 focus:outline-none transition-all"
|
||||
placeholder="Term, Image URL, or Word..."
|
||||
className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-4 text-sm font-black text-slate-800 dark:text-white focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500 transition-all outline-none shadow-inner"
|
||||
placeholder="Term / ID A..."
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:col-span-1 flex items-center justify-center pt-4 sm:pt-0">
|
||||
<div className="w-8 h-8 rounded-full bg-indigo-500/10 border border-indigo-500/20 flex items-center justify-center">
|
||||
<div className="w-1 h-1 rounded-full bg-indigo-500" />
|
||||
<div className="sm:col-span-2 flex items-center justify-center pt-8 sm:pt-6">
|
||||
<div className="w-12 h-12 rounded-full bg-white dark:bg-black/40 border border-slate-100 dark:border-white/10 flex items-center justify-center shadow-lg transform group-hover:rotate-180 transition-transform duration-700">
|
||||
<div className="w-2 h-2 rounded-full bg-indigo-500 animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="sm:col-span-3 space-y-1">
|
||||
<span className="text-[9px] font-black uppercase tracking-tight text-gray-600 pl-1">Card B</span>
|
||||
<input
|
||||
value={pair.right}
|
||||
onChange={(e) => updatePair(idx, { right: e.target.value })}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-sm font-bold focus:border-indigo-500/50 focus:outline-none transition-all"
|
||||
placeholder="Matching Item..."
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:col-span-1 flex items-end justify-end pb-1 pr-1">
|
||||
<button
|
||||
onClick={() => removePair(idx)}
|
||||
className="p-2 text-gray-500 hover:text-red-400 hover:bg-red-500/10 rounded-lg transition-all"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
<div className="sm:col-span-4 space-y-3 relative">
|
||||
<span className="text-[9px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-600 pl-1 italic">Synapse Beta</span>
|
||||
<div className="flex items-center gap-4">
|
||||
<input
|
||||
value={pair.right}
|
||||
onChange={(e) => updatePair(idx, { right: e.target.value })}
|
||||
className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-4 text-sm font-black text-slate-800 dark:text-white focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500 transition-all outline-none shadow-inner"
|
||||
placeholder="Term / ID B..."
|
||||
/>
|
||||
<button
|
||||
onClick={() => removePair(idx)}
|
||||
className="p-4 bg-red-50 dark:bg-red-500/5 hover:bg-red-500 hover:text-white rounded-2xl text-red-500 transition-all active:scale-90 opacity-0 group-hover:opacity-100"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
onClick={addPair}
|
||||
className="w-full py-6 border-dashed border-2 border-white/5 text-gray-500 hover:text-indigo-400 hover:border-indigo-500/30 hover:bg-indigo-500/5 transition-all font-black text-[10px] uppercase tracking-widest rounded-3xl flex flex-col items-center gap-2 group"
|
||||
className="w-full py-12 border-dashed border-4 border-slate-100 dark:border-white/10 text-slate-300 dark:text-gray-700 hover:text-indigo-600 dark:hover:text-indigo-400 hover:border-indigo-500/40 hover:bg-white dark:hover:bg-white/5 transition-all font-black text-[11px] uppercase tracking-[0.5em] rounded-[3rem] flex flex-col items-center gap-6 group/addbtn shadow-inner"
|
||||
>
|
||||
<div className="p-2 rounded-xl bg-white/5 group-hover:bg-indigo-500/20 transition-all">
|
||||
<Plus size={20} />
|
||||
<div className="p-5 rounded-[1.5rem] bg-slate-50 dark:bg-white/5 group-hover/addbtn:bg-indigo-600 group-hover/addbtn:text-white group-hover/addbtn:rotate-90 transition-all duration-500 shadow-xl">
|
||||
<Plus size={32} strokeWidth={3} />
|
||||
</div>
|
||||
<span>Add New Memory Pair</span>
|
||||
<span>Initialize New Synaptic Link</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { ChevronUp, ChevronDown, X, CheckCircle2, XCircle, Clock } from "lucide-react";
|
||||
|
||||
interface OrderingBlockProps {
|
||||
id: string;
|
||||
@@ -39,29 +40,30 @@ export default function OrderingBlock({ id, title, items, editMode, onChange }:
|
||||
<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>
|
||||
<div className="space-y-4 p-8 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2rem] mb-6 shadow-inner relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-1 h-full bg-blue-500/40"></div>
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Activity Title (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title || ""}
|
||||
onChange={(e) => onChange({ title: e.target.value })}
|
||||
placeholder="e.g. Sequence of Events..."
|
||||
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"
|
||||
className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-3 text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all outline-none shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white">
|
||||
{title || "Sequence Ordering"}
|
||||
<h3 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase border-l-4 border-blue-600 pl-6 py-1 flex items-center gap-3">
|
||||
<Clock className="w-6 h-6 text-blue-600" /> {title || "Sequence Ordering"}
|
||||
</h3>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editMode ? (
|
||||
<div className="space-y-4">
|
||||
<p className="text-[10px] text-gray-500 uppercase tracking-widest mb-2">Define items in their CORRECT order:</p>
|
||||
<div className="space-y-6">
|
||||
<p className="text-[10px] text-slate-400 dark:text-gray-500 uppercase font-black tracking-[0.2em] pl-1">Target Chronology (Define items in CORRECT order):</p>
|
||||
{items.map((item, idx) => (
|
||||
<div key={idx} className="flex gap-4 items-center animate-in slide-in-from-left-4 duration-300">
|
||||
<span className="text-blue-500 font-black w-6">{idx + 1}.</span>
|
||||
<div key={idx} className="flex gap-4 items-center bg-white dark:bg-white/5 p-4 rounded-[2rem] border border-slate-100 dark:border-white/10 shadow-sm animate-in slide-in-from-left-4 duration-300 group/ord">
|
||||
<span className="text-blue-600 font-black w-8 text-center italic">{idx + 1}.</span>
|
||||
<input
|
||||
value={item}
|
||||
onChange={(e) => {
|
||||
@@ -69,10 +71,10 @@ export default function OrderingBlock({ id, title, items, editMode, onChange }:
|
||||
newItems[idx] = e.target.value;
|
||||
onChange({ items: newItems });
|
||||
}}
|
||||
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-sm focus:border-blue-500/50 focus:outline-none"
|
||||
placeholder={`Step ${idx + 1}`}
|
||||
className="flex-1 bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-2xl px-6 py-3 text-sm font-bold text-slate-700 dark:text-gray-200 focus:ring-4 focus:ring-blue-500/10 transition-all outline-none shadow-inner"
|
||||
placeholder={`Event Marker ${idx + 1}`}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
disabled={idx === 0}
|
||||
onClick={() => {
|
||||
@@ -80,9 +82,9 @@ export default function OrderingBlock({ id, title, items, editMode, onChange }:
|
||||
[newItems[idx], newItems[idx - 1]] = [newItems[idx - 1], newItems[idx]];
|
||||
onChange({ items: newItems });
|
||||
}}
|
||||
className="p-2 text-gray-500 hover:text-white disabled:opacity-20"
|
||||
className="p-3 bg-slate-50 dark:bg-white/5 hover:bg-blue-600 hover:text-white rounded-xl text-slate-400 dark:text-gray-500 transition-all disabled:opacity-5 disabled:hover:bg-transparent"
|
||||
>
|
||||
↑
|
||||
<ChevronUp size={16} strokeWidth={3} />
|
||||
</button>
|
||||
<button
|
||||
disabled={idx === items.length - 1}
|
||||
@@ -91,9 +93,9 @@ export default function OrderingBlock({ id, title, items, editMode, onChange }:
|
||||
[newItems[idx], newItems[idx + 1]] = [newItems[idx + 1], newItems[idx]];
|
||||
onChange({ items: newItems });
|
||||
}}
|
||||
className="p-2 text-gray-500 hover:text-white disabled:opacity-20"
|
||||
className="p-3 bg-slate-50 dark:bg-white/5 hover:bg-blue-600 hover:text-white rounded-xl text-slate-400 dark:text-gray-500 transition-all disabled:opacity-5 disabled:hover:bg-transparent"
|
||||
>
|
||||
↓
|
||||
<ChevronDown size={16} strokeWidth={3} />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@@ -101,25 +103,27 @@ export default function OrderingBlock({ id, title, items, editMode, onChange }:
|
||||
const newItems = items.filter((_, i) => i !== idx);
|
||||
onChange({ items: newItems });
|
||||
}}
|
||||
className="p-2 text-gray-500 hover:text-red-400"
|
||||
className="p-3 bg-red-50 dark:bg-red-500/5 hover:bg-red-500 hover:text-white rounded-xl text-red-500 transition-all opacity-0 group-hover/ord:opacity-100"
|
||||
>
|
||||
×
|
||||
<X size={18} strokeWidth={3} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
onClick={() => onChange({ items: [...items, ""] })}
|
||||
className="w-full py-4 border-dashed border-2 border-white/10 text-gray-400 hover:text-white hover:border-blue-500/30 transition-all font-bold text-xs uppercase tracking-widest rounded-xl"
|
||||
className="w-full py-6 bg-slate-50 dark:bg-black/20 border-2 border-dashed border-slate-200 dark:border-white/10 text-slate-400 dark:text-gray-600 hover:text-blue-600 dark:hover:text-blue-400 hover:border-blue-500/40 hover:bg-white dark:hover:bg-white/5 transition-all font-black text-[10px] uppercase tracking-[0.3em] rounded-[2rem] shadow-inner"
|
||||
>
|
||||
+ Add Step
|
||||
+ Append Temporal Marker
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-8 p-8 glass border-white/5 rounded-3xl">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
|
||||
<div className="space-y-4">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-4 block">Available Items</label>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<div className="space-y-12 p-10 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[3rem] shadow-sm relative overflow-hidden group/ordplay">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-indigo-500/5 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2 group-hover/ordplay:bg-indigo-500/10 transition-colors"></div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-16 relative z-10">
|
||||
<div className="space-y-8">
|
||||
<label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 mb-8 block pl-1 italic">Synaptic Shards (Available Items)</label>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{shuffledItems.map((item, i) => {
|
||||
const isPicked = userOrder.includes(item.originalIdx);
|
||||
return (
|
||||
@@ -127,8 +131,8 @@ export default function OrderingBlock({ id, title, items, editMode, onChange }:
|
||||
key={i}
|
||||
disabled={isPicked || submitted}
|
||||
onClick={() => handlePick(item.originalIdx)}
|
||||
className={`px-6 py-3 rounded-full border text-sm font-bold transition-all ${isPicked ? "opacity-20 grayscale border-white/5 bg-white/5" :
|
||||
"border-white/10 bg-white/5 text-gray-200 hover:border-blue-500/50 hover:bg-blue-500/5"
|
||||
className={`px-8 py-4 rounded-full border-2 text-[11px] font-black uppercase tracking-widest transition-all duration-500 shadow-sm ${isPicked ? "opacity-10 grayscale border-slate-100 bg-slate-50 -translate-y-1" :
|
||||
"border-slate-100 dark:border-white/10 bg-slate-50 dark:bg-black/20 text-slate-700 dark:text-gray-300 hover:border-blue-600 hover:bg-blue-600 hover:text-white hover:shadow-2xl hover:shadow-blue-500/40 hover:-translate-y-1"
|
||||
}`}
|
||||
>
|
||||
{item.value}
|
||||
@@ -138,10 +142,14 @@ export default function OrderingBlock({ id, title, items, editMode, onChange }:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-4 block">Your Sequence</label>
|
||||
<div className="space-y-3">
|
||||
{userOrder.length === 0 && <p className="text-xs text-gray-600 italic py-4">Click items to build the sequence...</p>}
|
||||
<div className="space-y-8">
|
||||
<label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 mb-8 block pl-1 italic">Reconstructed Timeline</label>
|
||||
<div className="space-y-4">
|
||||
{userOrder.length === 0 && (
|
||||
<div className="bg-slate-50 dark:bg-black/20 border-2 border-dashed border-slate-200 dark:border-white/10 p-12 rounded-[2.5rem] text-center">
|
||||
<p className="text-[9px] text-slate-400 dark:text-gray-600 uppercase font-black tracking-widest italic pt-2">Sequencing standby: Capture shards to proceed...</p>
|
||||
</div>
|
||||
)}
|
||||
{userOrder.map((idx, i) => {
|
||||
const isItemCorrect = submitted && idx === i;
|
||||
const isItemWrong = submitted && idx !== i;
|
||||
@@ -150,16 +158,16 @@ export default function OrderingBlock({ id, title, items, editMode, onChange }:
|
||||
<div
|
||||
key={i}
|
||||
onClick={() => !submitted && handlePick(idx)}
|
||||
className={`flex items-center gap-4 p-4 rounded-xl border text-sm font-bold transition-all cursor-pointer ${isItemCorrect ? "border-green-500 bg-green-500/20 text-green-400" :
|
||||
isItemWrong ? "border-red-500 bg-red-500/20 text-red-100" :
|
||||
"border-blue-500/30 bg-blue-500/5 text-blue-400 hover:bg-blue-500/10"
|
||||
className={`flex items-center gap-6 p-6 rounded-[1.5rem] border-2 text-[11px] font-black uppercase tracking-[0.1em] transition-all duration-500 cursor-pointer shadow-sm ${isItemCorrect ? "border-green-500 bg-green-50 dark:bg-green-500/10 text-green-700 dark:text-green-400 shadow-xl shadow-green-500/20" :
|
||||
isItemWrong ? "border-red-500 bg-red-50 dark:bg-red-500/10 text-red-700 dark:text-red-100 shadow-xl shadow-red-500/20 translate-x-1" :
|
||||
"border-blue-500/20 bg-blue-50 dark:bg-blue-500/5 text-blue-600 dark:text-blue-400 hover:bg-blue-600 hover:text-white hover:border-blue-600 hover:shadow-2xl hover:shadow-blue-500/30"
|
||||
}`}
|
||||
>
|
||||
<span className="opacity-50 text-xs">{i + 1}.</span>
|
||||
<span className="opacity-40 text-xs italic">MARK {i + 1}</span>
|
||||
<span className="flex-1">{items[idx]}</span>
|
||||
{!submitted && <span className="text-xs opacity-50">×</span>}
|
||||
{isItemCorrect && <span>✅</span>}
|
||||
{isItemWrong && <span>❌</span>}
|
||||
{!submitted && <span className="text-[10px] opacity-30 hover:opacity-100 transition-opacity">SCRAPE</span>}
|
||||
{isItemCorrect && <CheckCircle2 className="w-6 h-6 text-green-500" />}
|
||||
{isItemWrong && <XCircle className="w-6 h-6 text-red-500 animate-pulse" />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -167,21 +175,21 @@ export default function OrderingBlock({ id, title, items, editMode, onChange }:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-8 border-t border-white/5">
|
||||
<div className="pt-10 border-t border-slate-50 dark:border-white/5 relative z-10">
|
||||
{!submitted && userOrder.length === items.length && (
|
||||
<button
|
||||
onClick={() => setSubmitted(true)}
|
||||
className="btn-premium w-full py-4 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20"
|
||||
className="w-full py-6 bg-blue-600 text-white font-black text-[10px] uppercase tracking-[0.3em] shadow-2xl shadow-blue-500/40 hover:scale-[1.02] active:scale-[0.98] transition-all rounded-[2rem] flex items-center justify-center gap-4 animate-in fade-in zoom-in duration-500"
|
||||
>
|
||||
Validate Sequence
|
||||
Validate Temporal Cohesion
|
||||
</button>
|
||||
)}
|
||||
{submitted && (
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="w-full py-4 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-xl border-white/5"
|
||||
className="w-full py-6 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-blue-600 dark:text-blue-400 font-black text-[10px] uppercase tracking-[0.3em] hover:bg-slate-50 transition-all rounded-[2rem] active:scale-[0.98] shadow-sm"
|
||||
>
|
||||
Try Again
|
||||
Deconstruct Sequence
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Users, Send, CheckCircle2, ClipboardList, Info } from "lucide-react";
|
||||
|
||||
interface PeerReviewBlockProps {
|
||||
id: string;
|
||||
@@ -16,72 +17,84 @@ export default function PeerReviewBlock({ id, title, prompt, reviewCriteria, edi
|
||||
<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>
|
||||
<div className="space-y-4 p-8 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2rem] mb-6 shadow-inner relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-1 h-full bg-purple-500/40"></div>
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">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"
|
||||
className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-3 text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-purple-500/10 focus:border-purple-500 transition-all outline-none shadow-sm"
|
||||
/>
|
||||
</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 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase border-l-4 border-purple-600 pl-6 py-1 flex items-center gap-3">
|
||||
<Users className="w-6 h-6 text-purple-600" /> {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>
|
||||
<div className="space-y-8">
|
||||
<div className="p-8 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[2.5rem] space-y-4 shadow-sm relative overflow-hidden group/instr">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-500/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2"></div>
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] pl-1">Assignment Directive (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...')"
|
||||
className="w-full bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-2xl p-6 min-h-[120px] text-lg font-black uppercase tracking-tight text-slate-800 dark:text-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 transition-all shadow-inner placeholder:opacity-20"
|
||||
placeholder="DESCRIBE THE CORE SUBMISSION PARAMETERS..."
|
||||
/>
|
||||
</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>
|
||||
<div className="p-8 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[2.5rem] space-y-4 shadow-sm relative overflow-hidden group/rubric">
|
||||
<div className="absolute bottom-0 left-0 w-32 h-32 bg-purple-500/5 rounded-full blur-3xl translate-y-1/2 -translate-x-1/2"></div>
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] pl-1">Evaluative Matrix (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..."
|
||||
className="w-full bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-2xl p-6 min-h-[120px] text-sm font-bold text-slate-700 dark:text-gray-300 focus:outline-none focus:ring-4 focus:ring-purple-500/10 transition-all shadow-inner"
|
||||
placeholder="Guide the peer reviewers on evaluative heuristics..."
|
||||
/>
|
||||
<p className="text-[10px] text-gray-500 uppercase tracking-wider">Instructions for the student who will review this work.</p>
|
||||
<p className="text-[9px] text-slate-400 dark:text-gray-600 uppercase font-black italic pl-1 italic">Structural guidance for cross-student validation.</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 className="p-10 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[3rem] space-y-12 shadow-sm relative overflow-hidden group/peerplay">
|
||||
<div className="absolute top-0 right-0 w-96 h-96 bg-purple-500/5 rounded-full blur-[120px] -translate-y-1/2 translate-x-1/2 group-hover/peerplay:bg-purple-500/10 transition-colors"></div>
|
||||
|
||||
<div className="space-y-6 relative z-10">
|
||||
<div className="flex items-center gap-2">
|
||||
<Info className="w-4 h-4 text-slate-300 dark:text-gray-600" />
|
||||
<label className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-300 dark:text-gray-600 italic">Instructional Directive</label>
|
||||
</div>
|
||||
<p className="text-2xl font-black text-slate-800 dark:text-gray-100 leading-tight uppercase italic tracking-tight">{prompt || "Awaiting submission parameters..."}</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 className="p-10 bg-slate-50 dark:bg-white/5 border-2 border-dashed border-slate-200 dark:border-white/10 rounded-[2.5rem] relative z-10 hover:border-blue-500 transition-colors duration-500">
|
||||
<div className="flex items-start gap-8 mb-8">
|
||||
<div className="p-5 rounded-2xl bg-blue-100 dark:bg-blue-500/10 text-blue-700 dark:text-blue-400 shadow-xl ring-4 ring-blue-500/10 animate-bounce">
|
||||
<Send className="w-8 h-8" />
|
||||
</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 className="space-y-1">
|
||||
<h5 className="text-[11px] font-black uppercase tracking-[0.2em] text-slate-900 dark:text-white">Submission Terminal Interface</h5>
|
||||
<p className="text-[9px] text-slate-400 dark:text-gray-500 uppercase font-black tracking-widest italic pt-1">Active node for student document upload and textual stream.</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 className="h-40 bg-white dark:bg-black/40 rounded-[2rem] border border-slate-100 dark:border-white/10 flex flex-col items-center justify-center text-slate-400 dark:text-gray-600 text-[10px] font-black uppercase tracking-[0.4em] italic shadow-inner">
|
||||
<div className="w-12 h-1 bg-slate-100 dark:bg-white/5 rounded-full mb-6"></div>
|
||||
[ Simulation: Subjunctive Flux ]
|
||||
</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 className="space-y-6 border-t border-slate-50 dark:border-white/5 pt-10 relative z-10">
|
||||
<div className="flex items-center gap-2">
|
||||
<ClipboardList className="w-4 h-4 text-slate-300 dark:text-gray-600" />
|
||||
<label className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-300 dark:text-gray-600 italic">Heuristic Evaluative Metrics (Rubric)</label>
|
||||
</div>
|
||||
<p className="text-sm font-bold text-slate-600 dark:text-gray-400 whitespace-pre-wrap leading-relaxed">{reviewCriteria}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Check, CheckCircle2, Plus, Target } from "lucide-react";
|
||||
|
||||
interface QuizQuestion {
|
||||
id: string;
|
||||
@@ -75,19 +76,20 @@ export default function QuizBlock({ id, title, quizData, editMode, onChange }: Q
|
||||
{/* Block Header */}
|
||||
<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>
|
||||
<div className="space-y-4 p-8 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2rem] mb-6 shadow-inner relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-1 h-full bg-blue-500/40"></div>
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Activity Title (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title || ""}
|
||||
onChange={(e) => onChange({ title: e.target.value })}
|
||||
placeholder="e.g. Final Evaluation, Knowledge Check..."
|
||||
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"
|
||||
placeholder="e.g. Mastery Challenge, Knowledge Synthesis..."
|
||||
className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-3 text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all outline-none shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white">
|
||||
{title || "Knowledge Check"}
|
||||
<h3 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase border-l-4 border-blue-600 pl-6 py-1">
|
||||
{title || "Knowledge Synthesis"}
|
||||
</h3>
|
||||
)}
|
||||
</div>
|
||||
@@ -95,26 +97,26 @@ export default function QuizBlock({ id, title, quizData, editMode, onChange }: Q
|
||||
{editMode ? (
|
||||
<div className="space-y-6">
|
||||
{questions.map((q, idx) => (
|
||||
<div key={q.id} className="p-8 glass border-white/5 space-y-6 rounded-3xl relative group/question animate-in slide-in-from-left-4 duration-500">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex bg-white/5 rounded-lg p-1 border border-white/5">
|
||||
<div key={q.id} className="p-10 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 space-y-8 rounded-[2.5rem] relative group/question animate-in slide-in-from-bottom-4 duration-500 shadow-sm hover:shadow-xl hover:border-blue-500/20 transition-all">
|
||||
<div className="flex items-center justify-between gap-6">
|
||||
<div className="flex bg-slate-100 dark:bg-white/5 rounded-2xl p-1.5 border border-slate-200 dark:border-white/5 shadow-inner">
|
||||
<button
|
||||
onClick={() => updateQuestion(idx, { type: 'multiple-choice', correct: [q.correct?.[0] || 0] })}
|
||||
className={`px-3 py-1.5 text-[9px] uppercase font-black tracking-widest rounded-md transition-all ${q.type === 'multiple-choice' ? "bg-blue-500 text-white shadow-lg" : "text-gray-500 hover:text-white"}`}
|
||||
className={`px-4 py-2 text-[9px] uppercase font-black tracking-widest rounded-xl transition-all ${q.type === 'multiple-choice' ? "bg-white dark:bg-blue-600 text-slate-900 dark:text-white shadow-md" : "text-slate-400 hover:text-slate-600"}`}
|
||||
>
|
||||
MCQ
|
||||
Choice
|
||||
</button>
|
||||
<button
|
||||
onClick={() => updateQuestion(idx, { type: 'multiple-select', correct: [q.correct?.[0] || 0] })}
|
||||
className={`px-3 py-1.5 text-[9px] uppercase font-black tracking-widest rounded-md transition-all ${q.type === 'multiple-select' ? "bg-blue-500 text-white shadow-lg" : "text-gray-500 hover:text-white"}`}
|
||||
className={`px-4 py-2 text-[9px] uppercase font-black tracking-widest rounded-xl transition-all ${q.type === 'multiple-select' ? "bg-white dark:bg-blue-600 text-slate-900 dark:text-white shadow-md" : "text-slate-400 hover:text-slate-600"}`}
|
||||
>
|
||||
MSQ
|
||||
Multi
|
||||
</button>
|
||||
<button
|
||||
onClick={() => updateQuestion(idx, { type: 'true-false', options: ["True", "False"], correct: [0] })}
|
||||
className={`px-3 py-1.5 text-[9px] uppercase font-black tracking-widest rounded-md transition-all ${q.type === 'true-false' ? "bg-blue-500 text-white shadow-lg" : "text-gray-500 hover:text-white"}`}
|
||||
className={`px-4 py-2 text-[9px] uppercase font-black tracking-widest rounded-xl transition-all ${q.type === 'true-false' ? "bg-white dark:bg-blue-600 text-slate-900 dark:text-white shadow-md" : "text-slate-400 hover:text-slate-600"}`}
|
||||
>
|
||||
T / F
|
||||
Binary
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@@ -122,73 +124,86 @@ export default function QuizBlock({ id, title, quizData, editMode, onChange }: Q
|
||||
const newQuestions = questions.filter((_, i) => i !== idx);
|
||||
onChange({ quiz_data: { questions: newQuestions } });
|
||||
}}
|
||||
className="p-2 text-gray-500 hover:text-red-400 transition-colors"
|
||||
className="w-10 h-10 rounded-full bg-red-50 dark:bg-red-500/10 text-red-400 hover:bg-red-500 hover:text-white transition-all flex items-center justify-center border border-red-100 dark:border-transparent active:scale-90"
|
||||
>
|
||||
<span className="text-xl">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input
|
||||
value={q.question}
|
||||
onChange={(e) => updateQuestion(idx, { question: e.target.value })}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl p-4 text-lg font-bold focus:outline-none focus:border-blue-500/50 transition-all placeholder:text-gray-600"
|
||||
placeholder="What is the question?"
|
||||
/>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Inquiry Description</label>
|
||||
<textarea
|
||||
value={q.question}
|
||||
onChange={(e) => updateQuestion(idx, { question: e.target.value })}
|
||||
className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl p-6 text-lg font-black tracking-tight focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all placeholder:text-slate-300 resize-none shadow-inner h-24"
|
||||
placeholder="What is the core problem being evaluated?"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Configuration Matrix</label>
|
||||
{q.type === 'true-false' ? (
|
||||
<div className="flex gap-4">
|
||||
{["True", "False"].map((opt, oIdx) => (
|
||||
<button
|
||||
key={oIdx}
|
||||
onClick={() => updateQuestion(idx, { correct: [oIdx] })}
|
||||
className={`flex-1 py-4 rounded-xl border-2 transition-all font-black text-xs uppercase tracking-widest ${q.correct?.includes(oIdx) ? "border-blue-500 bg-blue-500/10 text-white" : "border-white/5 bg-white/5 text-gray-500"}`}
|
||||
className={`flex-1 py-5 rounded-2xl border-2 transition-all font-black text-[10px] uppercase tracking-[0.2em] shadow-sm ${q.correct?.includes(oIdx) ? "border-blue-600 bg-blue-50 dark:bg-blue-500/10 text-blue-700 dark:text-blue-400 scale-[1.02] shadow-blue-500/10" : "border-slate-100 dark:border-white/5 bg-slate-50 dark:bg-white/5 text-slate-400 dark:text-gray-500 hover:border-slate-200"}`}
|
||||
>
|
||||
{opt}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
q.options?.map((opt, oIdx) => (
|
||||
<div key={oIdx} className="flex gap-3 items-center group/opt">
|
||||
<input
|
||||
type={q.type === 'multiple-select' ? "checkbox" : "radio"}
|
||||
checked={q.correct?.includes(oIdx)}
|
||||
onChange={() => toggleCorrectOption(idx, oIdx, q.type === 'multiple-select')}
|
||||
className="w-5 h-5 accent-blue-500 cursor-pointer"
|
||||
/>
|
||||
<input
|
||||
value={opt}
|
||||
onChange={(e) => {
|
||||
const newOpts = [...q.options];
|
||||
newOpts[oIdx] = e.target.value;
|
||||
updateQuestion(idx, { options: newOpts });
|
||||
}}
|
||||
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-sm font-medium focus:outline-none focus:border-blue-500/30 transition-all"
|
||||
placeholder={`Option ${oIdx + 1}`}
|
||||
/>
|
||||
{q.options.length > 2 && (
|
||||
<button
|
||||
onClick={() => {
|
||||
const newOpts = q.options.filter((_, i) => i !== oIdx);
|
||||
const newCorrect = q.correct?.filter(i => i !== oIdx).map(i => i > oIdx ? i - 1 : i);
|
||||
updateQuestion(idx, { options: newOpts, correct: newCorrect?.length ? newCorrect : [0] });
|
||||
<div className="space-y-3">
|
||||
{q.options?.map((opt, oIdx) => (
|
||||
<div key={oIdx} className="flex gap-4 items-center group/opt">
|
||||
<div className="relative flex items-center justify-center">
|
||||
<input
|
||||
type={q.type === 'multiple-select' ? "checkbox" : "radio"}
|
||||
checked={q.correct?.includes(oIdx)}
|
||||
onChange={() => toggleCorrectOption(idx, oIdx, q.type === 'multiple-select')}
|
||||
className="w-6 h-6 rounded-lg border-2 border-slate-200 dark:border-white/10 appearance-none checked:bg-blue-600 checked:border-blue-600 transition-all cursor-pointer shadow-sm"
|
||||
/>
|
||||
{q.correct?.includes(oIdx) && (
|
||||
<div className="absolute pointer-events-none text-white text-[10px]">
|
||||
<Check size={14} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
value={opt}
|
||||
onChange={(e) => {
|
||||
const newOpts = [...q.options];
|
||||
newOpts[oIdx] = e.target.value;
|
||||
updateQuestion(idx, { options: newOpts });
|
||||
}}
|
||||
className="opacity-0 group-hover/opt:opacity-100 p-2 text-gray-500 hover:text-red-400 transition-all"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
className="flex-1 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-4 text-sm font-bold focus:ring-4 focus:ring-blue-500/5 focus:border-blue-500 transition-all outline-none shadow-inner"
|
||||
placeholder={`Option Vector ${String.fromCharCode(65 + oIdx)}`}
|
||||
/>
|
||||
{q.options.length > 2 && (
|
||||
<button
|
||||
onClick={() => {
|
||||
const newOpts = q.options.filter((_, i) => i !== oIdx);
|
||||
const newCorrect = q.correct?.filter(i => i !== oIdx).map(i => i > oIdx ? i - 1 : i);
|
||||
updateQuestion(idx, { options: newOpts, correct: newCorrect?.length ? newCorrect : [0] });
|
||||
}}
|
||||
className="w-10 h-10 rounded-xl bg-slate-50 dark:bg-white/5 text-slate-300 hover:text-red-500 transition-all border border-slate-200 dark:border-transparent flex items-center justify-center active:scale-90"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{q.type !== 'true-false' && (
|
||||
<button
|
||||
onClick={() => updateQuestion(idx, { options: [...q.options, `Option ${q.options.length + 1}`] })}
|
||||
className="text-[10px] font-black uppercase tracking-widest text-blue-500/50 hover:text-blue-500 transition-colors pl-8 mt-2"
|
||||
className="text-[10px] font-black uppercase tracking-[0.2em] text-blue-600 hover:text-blue-700 transition-all flex items-center gap-2 pl-10 mt-4 active:scale-95"
|
||||
>
|
||||
+ Add Option
|
||||
<Plus size={14} /> Add Theoretical Vector
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -196,16 +211,17 @@ export default function QuizBlock({ id, title, quizData, editMode, onChange }: Q
|
||||
))}
|
||||
<button
|
||||
onClick={addQuestion}
|
||||
className="w-full py-4 glass border-dashed border-2 border-white/10 text-gray-500 hover:text-white hover:border-blue-500/30 hover:bg-blue-500/5 transition-all font-bold text-xs uppercase tracking-widest rounded-2xl"
|
||||
className="w-full py-6 bg-white dark:bg-white/5 border-2 border-dashed border-slate-200 dark:border-white/10 text-slate-400 dark:text-gray-500 hover:text-blue-600 hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-500/5 transition-all font-black text-[10px] uppercase tracking-[0.3em] rounded-[2rem] active:scale-[0.98] shadow-sm"
|
||||
>
|
||||
+ Add Question
|
||||
+ Expand Assessment Vector
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-8">
|
||||
{questions.map((q) => (
|
||||
<div key={q.id} className="space-y-4 p-6 glass border-white/5 rounded-2xl">
|
||||
<h4 className="font-bold text-xl text-gray-100 leading-tight">{q.question}</h4>
|
||||
<div key={q.id} className="space-y-6 p-10 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/5 rounded-[2.5rem] shadow-sm hover:shadow-xl transition-all duration-700 group/qitem relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2 group-hover/qitem:bg-indigo-500/10 transition-colors"></div>
|
||||
<h4 className="font-black text-2xl text-slate-900 dark:text-white leading-tight uppercase tracking-tight relative z-10">{q.question}</h4>
|
||||
<div className="grid gap-3">
|
||||
{q.options && q.options.length > 0 ? (
|
||||
q.options.map((opt, oIdx) => {
|
||||
@@ -215,27 +231,33 @@ export default function QuizBlock({ id, title, quizData, editMode, onChange }: Q
|
||||
const isWrongSelection = !isCorrect && isSelected;
|
||||
const missedCorrect = isCorrect && !isSelected;
|
||||
|
||||
let style = "glass border-white/10 hover:bg-white/5";
|
||||
if (submitted) {
|
||||
if (isActuallyCorrect) style = "bg-green-500/20 border-green-500 text-green-400";
|
||||
else if (isWrongSelection) style = "bg-red-500/20 border-red-500 text-red-100";
|
||||
else if (missedCorrect) style = "border-orange-500/50 text-orange-400 animate-pulse";
|
||||
else style = "opacity-50 grayscale border-white/5";
|
||||
} else if (isSelected) {
|
||||
style = "bg-blue-500/20 border-blue-500 text-white shadow-[0_0_20px_rgba(59,130,246,0.2)]";
|
||||
}
|
||||
const style = submitted
|
||||
? (isActuallyCorrect ? "bg-green-50 dark:bg-green-500/10 border-green-200 dark:border-green-500/30 text-green-700 dark:text-green-400 shadow-green-500/5 shadow-inner"
|
||||
: isWrongSelection ? "bg-red-50 dark:bg-red-500/10 border-red-200 dark:border-red-500/30 text-red-700 dark:text-red-400 shadow-red-500/5 shadow-inner"
|
||||
: missedCorrect ? "border-orange-500/50 bg-orange-50 dark:bg-orange-500/5 text-orange-700 dark:text-orange-400 border-dashed animate-pulse"
|
||||
: "opacity-40 grayscale border-slate-100 dark:border-white/5 bg-slate-50 dark:bg-white/5")
|
||||
: isSelected ? "bg-blue-50 dark:bg-blue-500/10 border-blue-600 text-blue-700 dark:text-blue-400 shadow-xl shadow-blue-500/10 ring-4 ring-blue-500/5"
|
||||
: "bg-white dark:bg-white/5 border-slate-100 dark:border-white/5 text-slate-500 dark:text-gray-400 hover:border-blue-500/50 hover:bg-slate-50 dark:hover:bg-blue-500/5 shadow-sm";
|
||||
|
||||
return (
|
||||
<button
|
||||
key={oIdx}
|
||||
onClick={() => handleAnswer(q.id, oIdx, q.type === 'multiple-select')}
|
||||
className={`p-5 rounded-xl border transition-all text-left text-sm font-bold ${style}`}
|
||||
className={`p-6 rounded-2xl border transition-all text-left text-sm font-black uppercase tracking-tight relative overflow-hidden group/optans ${style}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center justify-between relative z-10">
|
||||
<span>{opt}</span>
|
||||
{submitted && isActuallyCorrect && <span>✅</span>}
|
||||
{submitted && isWrongSelection && <span>❌</span>}
|
||||
{submitted && missedCorrect && <span className="text-[10px] uppercase font-black tracking-tighter">Correct Answer</span>}
|
||||
{submitted ? (
|
||||
<div className="flex items-center gap-2">
|
||||
{isActuallyCorrect && <span className="text-green-600 dark:text-green-400"><CheckCircle2 size={18} /></span>}
|
||||
{isWrongSelection && <span className="text-red-600 dark:text-red-400">❌</span>}
|
||||
{missedCorrect && <span className="text-[8px] uppercase font-black tracking-widest bg-orange-100 dark:bg-orange-500/20 text-orange-600 dark:text-orange-400 px-3 py-1 rounded-full">Recall target</span>}
|
||||
</div>
|
||||
) : (
|
||||
<div className={`w-6 h-6 rounded-lg border-2 transition-all flex items-center justify-center ${isSelected ? 'bg-blue-600 border-blue-600 shadow-lg shadow-blue-500/40 text-white' : 'border-slate-200 dark:border-white/10'}`}>
|
||||
{isSelected && <Check size={14} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
@@ -251,17 +273,18 @@ export default function QuizBlock({ id, title, quizData, editMode, onChange }: Q
|
||||
{!submitted && questions.length > 0 && (
|
||||
<button
|
||||
onClick={() => setSubmitted(true)}
|
||||
className="btn-premium w-full py-5 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20"
|
||||
className="w-full py-6 bg-indigo-600 text-white font-black text-[10px] uppercase tracking-[0.3em] shadow-2xl shadow-indigo-500/40 hover:scale-[1.02] active:scale-[0.98] transition-all rounded-[2rem] flex items-center justify-center gap-4"
|
||||
>
|
||||
Validate Answers
|
||||
<Target size={18} />
|
||||
Validate Synaptic Map
|
||||
</button>
|
||||
)}
|
||||
{submitted && (
|
||||
<button
|
||||
onClick={() => { setSubmitted(false); setUserAnswers({}); }}
|
||||
className="w-full py-5 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-2xl"
|
||||
className="w-full py-6 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-indigo-600 dark:text-indigo-400 font-black text-[10px] uppercase tracking-[0.3em] hover:bg-slate-50 transition-all rounded-[2rem] active:scale-[0.98] shadow-sm"
|
||||
>
|
||||
Try Again
|
||||
Reset Neural Chain
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -26,67 +26,70 @@ export default function ShortAnswerBlock({ id, title, prompt, correctAnswers, ed
|
||||
<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>
|
||||
<div className="space-y-4 p-8 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2rem] mb-6 shadow-inner relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-1 h-full bg-blue-500/40"></div>
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Activity Title (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title || ""}
|
||||
onChange={(e) => onChange({ title: e.target.value })}
|
||||
placeholder="e.g. Critical Thinking, Quick Response..."
|
||||
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"
|
||||
className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-3 text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all outline-none shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white">
|
||||
<h3 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase border-l-4 border-blue-600 pl-6 py-1">
|
||||
{title || "Short Answer"}
|
||||
</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">Question Prompt</label>
|
||||
<div className="space-y-8">
|
||||
<div className="p-10 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2.5rem] space-y-4 shadow-sm">
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Inquiry Vector (Question Prompt)</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="Type the question for the student..."
|
||||
className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl p-6 min-h-[120px] text-lg font-black tracking-tight text-slate-800 dark:text-white focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all placeholder:text-slate-300 resize-none shadow-inner"
|
||||
placeholder="Type the synaptic trigger for the student..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-6 glass border-white/5 space-y-4">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Correct Answers (One per line)</label>
|
||||
<div className="p-10 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2.5rem] space-y-4 shadow-sm relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-green-500/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2"></div>
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Target Response Matrix (One per line)</label>
|
||||
<textarea
|
||||
value={correctAnswers ? correctAnswers.join("\n") : ""}
|
||||
onChange={(e) => onChange({ correctAnswers: e.target.value.split("\n").filter(a => a.trim() !== "") })}
|
||||
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="Answer 1 Answer 2 (Alternative)"
|
||||
className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl p-6 min-h-[120px] text-sm font-bold text-green-700 dark:text-green-400 focus:ring-4 focus:ring-green-500/10 focus:border-green-500 transition-all outline-none shadow-inner"
|
||||
placeholder="Primary Solution Secondary Vector (Alternative)"
|
||||
/>
|
||||
<p className="text-[10px] text-gray-500 uppercase tracking-wider">Validation is case-insensitive. Provide multiple alternatives if necessary.</p>
|
||||
<p className="text-[9px] text-slate-400 dark:text-gray-500 uppercase font-black italic pl-1 leading-relaxed">Validation employs case-insensitive string matching across all vectors.</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 glass border-white/5 rounded-3xl space-y-8">
|
||||
<p className="text-xl font-bold text-gray-100">{prompt || "Please enter your answer below:"}</p>
|
||||
<div className="p-10 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[3rem] space-y-10 shadow-sm hover:shadow-xl transition-all duration-700 relative overflow-hidden group/saitem">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-blue-500/5 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2 group-hover/saitem:bg-blue-500/10 transition-colors"></div>
|
||||
<p className="text-2xl font-black text-slate-900 dark:text-white uppercase tracking-tight leading-tight relative z-10">{prompt || "Please formulate your response vector:"}</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-6 relative z-10">
|
||||
<input
|
||||
type="text"
|
||||
value={userAnswer}
|
||||
onChange={(e) => setUserAnswer(e.target.value)}
|
||||
disabled={submitted}
|
||||
className={`w-full bg-white/5 border-2 rounded-2xl px-6 py-4 text-lg transition-all focus:outline-none ${submitted
|
||||
? (isCorrect ? "border-green-500 bg-green-500/10 text-green-400" : "border-red-500 bg-red-500/10 text-red-100")
|
||||
: "border-white/10 focus:border-blue-500 text-white"
|
||||
className={`w-full bg-slate-50 dark:bg-black/40 border-2 rounded-[2rem] px-8 py-6 text-xl font-black tracking-tight transition-all focus:outline-none shadow-inner ${submitted
|
||||
? (isCorrect ? "border-green-500 text-green-700 dark:text-green-400 ring-4 ring-green-500/5" : "border-red-500 text-red-700 dark:text-red-100 ring-4 ring-red-500/5")
|
||||
: "border-slate-100 dark:border-white/10 focus:border-blue-600 text-slate-800 dark:text-white focus:ring-8 focus:ring-blue-500/5"
|
||||
}`}
|
||||
placeholder="Type your answer..."
|
||||
placeholder="Ingest solution..."
|
||||
/>
|
||||
|
||||
{submitted && !isCorrect && (
|
||||
<div className="p-4 bg-orange-500/10 border border-orange-500/20 rounded-xl">
|
||||
<p className="text-[10px] text-orange-400 uppercase font-black tracking-widest">Suggested Answer(s):</p>
|
||||
<p className="text-sm text-gray-400 mt-1">{correctAnswers && correctAnswers[0]}</p>
|
||||
<div className="p-6 bg-orange-50 dark:bg-orange-500/10 border border-orange-200 dark:border-orange-500/20 rounded-2xl animate-in slide-in-from-top-2 duration-500">
|
||||
<p className="text-[10px] text-orange-600 dark:text-orange-400 uppercase font-black tracking-[0.2em]">Validated Synaptic Marker:</p>
|
||||
<p className="text-lg font-black text-slate-700 dark:text-gray-300 mt-2 italic">"{correctAnswers && correctAnswers[0]}"</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -95,18 +98,18 @@ export default function ShortAnswerBlock({ id, title, prompt, correctAnswers, ed
|
||||
<button
|
||||
onClick={() => setSubmitted(true)}
|
||||
disabled={!userAnswer.trim()}
|
||||
className="btn-premium w-full py-4 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20 disabled:opacity-50 disabled:grayscale"
|
||||
className="w-full py-6 bg-blue-600 text-white font-black text-[10px] uppercase tracking-[0.3em] shadow-2xl shadow-blue-500/40 hover:scale-[1.02] active:scale-[0.98] transition-all rounded-[2rem] flex items-center justify-center gap-4 disabled:opacity-20 disabled:hover:scale-100"
|
||||
>
|
||||
Submit Answer
|
||||
Execute Validation
|
||||
</button>
|
||||
)}
|
||||
|
||||
{submitted && (
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="w-full py-4 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-xl border-white/5"
|
||||
className="w-full py-6 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-blue-600 dark:text-blue-400 font-black text-[10px] uppercase tracking-[0.3em] hover:bg-slate-50 transition-all rounded-[2rem] active:scale-[0.98] shadow-sm"
|
||||
>
|
||||
Try Again
|
||||
Reset Response Vector
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -75,26 +75,27 @@ export default function VideoMarkerBlock({
|
||||
|
||||
if (!editMode) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-xs font-black uppercase tracking-widest text-gray-400">
|
||||
{title || "Video con Marcadores"}
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 pl-1">
|
||||
{title || "Interactive Temporal Nodes"}
|
||||
</h3>
|
||||
<div className="glass-card p-6 border-indigo-500/20 bg-indigo-500/5">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-indigo-500/10 text-indigo-400">
|
||||
<Clock size={20} />
|
||||
<div className="bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 p-8 rounded-[2.5rem] shadow-sm relative overflow-hidden group/vm">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2 group-hover/vm:bg-indigo-500/10 transition-colors"></div>
|
||||
<div className="flex items-center gap-4 mb-8 relative z-10">
|
||||
<div className="p-3 rounded-2xl bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 shadow-inner">
|
||||
<Clock size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-white">Video Interactivo</p>
|
||||
<p className="text-xs text-gray-500">{markers.length} marcadores configurados</p>
|
||||
<p className="text-lg font-black text-slate-900 dark:text-white uppercase tracking-tight italic">Interactive Stream</p>
|
||||
<p className="text-[10px] text-slate-400 dark:text-gray-500 uppercase font-black tracking-widest">{markers.length} SYNC MARKERS DETECTED</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-3 relative z-10">
|
||||
{markers.map((marker, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2 text-xs text-gray-400">
|
||||
<span className="font-mono text-indigo-400">{formatTime(marker.timestamp)}</span>
|
||||
<span>→</span>
|
||||
<span className="truncate">{marker.question}</span>
|
||||
<div key={idx} className="flex items-center gap-4 text-[10px] text-slate-500 dark:text-gray-400 font-black uppercase tracking-wider bg-slate-50/50 dark:bg-white/5 p-3 rounded-xl border border-slate-100 dark:border-white/5">
|
||||
<span className="font-mono text-indigo-600 dark:text-indigo-400 bg-white dark:bg-black/40 px-2 py-1 rounded-lg border border-slate-100 dark:border-white/10 shadow-sm">{formatTime(marker.timestamp)}</span>
|
||||
<span className="opacity-20">/</span>
|
||||
<span className="truncate italic">{marker.question}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -106,137 +107,147 @@ export default function VideoMarkerBlock({
|
||||
return (
|
||||
<div className="space-y-6 animate-in fade-in duration-300">
|
||||
{/* Title Editor */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Título del Bloque</label>
|
||||
<div className="space-y-4 p-8 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2rem] mb-6 shadow-inner relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-1 h-full bg-indigo-500/40"></div>
|
||||
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Activity Title (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => onChange({ title: e.target.value })}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
placeholder="Ej: Video Tutorial - Introducción"
|
||||
className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-3 text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500 transition-all outline-none shadow-sm"
|
||||
placeholder="e.g. Masterclass Stream, Acoustic Analysis..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Video Preview with Timeline */}
|
||||
<div className="glass-card p-4 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-sm font-bold text-white flex items-center gap-2">
|
||||
<Play size={16} className="text-indigo-400" />
|
||||
Vista Previa del Video
|
||||
<div className="bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 p-6 rounded-[3rem] space-y-6 shadow-xl relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-indigo-500/5 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2"></div>
|
||||
<div className="flex items-center justify-between relative z-10 px-2">
|
||||
<h4 className="text-[10px] font-black text-slate-900 dark:text-white flex items-center gap-3 uppercase tracking-widest italic">
|
||||
<Play size={16} className="text-indigo-600 dark:text-indigo-400" />
|
||||
Temporal Scrutiny
|
||||
</h4>
|
||||
<span className="text-xs font-mono text-gray-500">{formatTime(currentTime)}</span>
|
||||
<span className="text-xs font-black font-mono text-indigo-600 dark:text-indigo-400 bg-indigo-50 dark:bg-black/40 px-3 py-1 rounded-full border border-indigo-100 dark:border-white/10">{formatTime(currentTime)}</span>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg overflow-hidden">
|
||||
<div className="rounded-[2rem] overflow-hidden border border-slate-100 dark:border-white/10 shadow-2xl relative z-10">
|
||||
<MediaPlayer
|
||||
src={videoUrl}
|
||||
type="video"
|
||||
isGraded={isGraded}
|
||||
showInteractive={false} // Interactive markers are separate here
|
||||
showInteractive={false}
|
||||
onTimeUpdate={setCurrentTime}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={addMarker}
|
||||
className="w-full py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg font-bold flex items-center justify-center gap-2 transition-all"
|
||||
className="w-full py-5 bg-indigo-600 hover:bg-indigo-500 text-white rounded-[2rem] font-black text-[10px] uppercase tracking-[0.3em] flex items-center justify-center gap-4 transition-all shadow-2xl shadow-indigo-500/40 active:scale-95 group relative z-10"
|
||||
>
|
||||
<Plus size={16} />
|
||||
Agregar Marcador en {formatTime(currentTime)}
|
||||
<Plus size={18} className="group-hover:rotate-90 transition-transform" />
|
||||
Inject Marker at {formatTime(currentTime)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Markers List */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-sm font-bold text-white flex items-center gap-2">
|
||||
<Clock size={16} className="text-amber-400" />
|
||||
Marcadores ({markers.length})
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<h4 className="text-[10px] font-black text-slate-400 dark:text-gray-500 flex items-center gap-3 uppercase tracking-[0.2em]">
|
||||
<Clock size={16} className="text-amber-500" />
|
||||
Interactive Sequence ({markers.length})
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
{markers.length === 0 && (
|
||||
<div className="glass-card p-8 text-center border-dashed">
|
||||
<AlertCircle className="w-12 h-12 text-gray-600 mx-auto mb-3" />
|
||||
<p className="text-sm text-gray-500">No hay marcadores configurados.</p>
|
||||
<p className="text-xs text-gray-600 mt-1">Reproduce el video y haz clic en "Agregar Marcador"</p>
|
||||
<div className="bg-slate-50 dark:bg-black/20 border-2 border-dashed border-slate-200 dark:border-white/10 p-16 rounded-[3rem] text-center space-y-4">
|
||||
<AlertCircle className="w-12 h-12 text-slate-300 dark:text-gray-700 mx-auto opacity-40" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest">No interactive nodes detected</p>
|
||||
<p className="text-[9px] text-slate-300 dark:text-gray-600 uppercase font-black italic">Initiate playback and execute "Inject Marker"</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{markers.map((marker, idx) => (
|
||||
<div key={idx} className="glass-card p-4 space-y-3 border-l-4 border-indigo-500">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs font-mono font-bold text-indigo-400 bg-indigo-500/10 px-2 py-1 rounded">
|
||||
{formatTime(marker.timestamp)}
|
||||
</span>
|
||||
<div key={idx} className="bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[2.5rem] shadow-sm overflow-hidden group/mkr transition-all duration-500 hover:shadow-xl hover:border-indigo-500/20">
|
||||
<div className="p-6 border-l-[6px] border-indigo-600 flex flex-col gap-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-[10px] font-black text-indigo-700 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-500/10 px-4 py-2 rounded-xl border border-indigo-100 dark:border-indigo-500/20 shadow-inner italic">
|
||||
SYNC {formatTime(marker.timestamp)}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setEditingIndex(editingIndex === idx ? null : idx)}
|
||||
className="text-[9px] font-black uppercase tracking-[0.2em] text-slate-400 hover:text-indigo-600 transition-colors"
|
||||
>
|
||||
{editingIndex === idx ? "COLLAPSE" : "SCRUTINIZE"}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setEditingIndex(editingIndex === idx ? null : idx)}
|
||||
className="text-xs text-gray-500 hover:text-white transition-colors"
|
||||
onClick={() => deleteMarker(idx)}
|
||||
className="p-3 bg-red-50 dark:bg-red-500/5 hover:bg-red-500 hover:text-white rounded-xl text-red-500 transition-all active:scale-90"
|
||||
>
|
||||
{editingIndex === idx ? "Colapsar" : "Editar"}
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => deleteMarker(idx)}
|
||||
className="p-2 hover:bg-red-500/10 rounded-lg text-red-500 transition-all"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{editingIndex === idx ? (
|
||||
<div className="space-y-3 pt-2 border-t border-white/5">
|
||||
{/* Timestamp Editor */}
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-bold text-gray-600 uppercase tracking-widest">Timestamp (MM:SS)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formatTime(marker.timestamp)}
|
||||
onChange={(e) => updateMarker(idx, { timestamp: parseTime(e.target.value) })}
|
||||
className="w-full bg-black/20 border border-white/10 rounded px-3 py-1.5 text-sm font-mono text-white focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Question Editor */}
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-bold text-gray-600 uppercase tracking-widest">Pregunta</label>
|
||||
<input
|
||||
type="text"
|
||||
value={marker.question}
|
||||
onChange={(e) => updateMarker(idx, { question: e.target.value })}
|
||||
className="w-full bg-black/20 border border-white/10 rounded px-3 py-2 text-sm text-white focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||
placeholder="¿Qué concepto se explicó?"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Options Editor */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold text-gray-600 uppercase tracking-widest">Opciones de Respuesta</label>
|
||||
{marker.options.map((option, optIdx) => (
|
||||
<div key={optIdx} className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
name={`correct-${idx}`}
|
||||
checked={marker.correctIndex === optIdx}
|
||||
onChange={() => updateMarker(idx, { correctIndex: optIdx })}
|
||||
className="w-4 h-4 text-green-500"
|
||||
/>
|
||||
{editingIndex === idx ? (
|
||||
<div className="space-y-6 pt-6 border-t border-slate-50 dark:border-white/5 animate-in slide-in-from-top-2 duration-500">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Timestamp Editor */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-[9px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Temporal Alignment (MM:SS)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={option}
|
||||
onChange={(e) => updateOption(idx, optIdx, e.target.value)}
|
||||
className="flex-1 bg-black/20 border border-white/10 rounded px-3 py-1.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-green-500"
|
||||
placeholder={`Opción ${optIdx + 1}`}
|
||||
value={formatTime(marker.timestamp)}
|
||||
onChange={(e) => updateMarker(idx, { timestamp: parseTime(e.target.value) })}
|
||||
className="w-full bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-2xl px-5 py-3 text-sm font-black font-mono text-slate-800 dark:text-white focus:ring-4 focus:ring-indigo-500/10 transition-all outline-none"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<p className="text-[10px] text-gray-600 italic">Selecciona el radio button de la respuesta correcta</p>
|
||||
|
||||
{/* Question Editor */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-[9px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Inquiry Scoped Prompt</label>
|
||||
<input
|
||||
type="text"
|
||||
value={marker.question}
|
||||
onChange={(e) => updateMarker(idx, { question: e.target.value })}
|
||||
className="w-full bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-2xl px-5 py-3 text-sm font-black text-slate-800 dark:text-white focus:ring-4 focus:ring-indigo-500/10 transition-all outline-none"
|
||||
placeholder="¿Critical concept?"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Options Editor */}
|
||||
<div className="space-y-4">
|
||||
<label className="text-[9px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Probability Vectors (Options)</label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{marker.options.map((option, optIdx) => (
|
||||
<div key={optIdx} className="flex items-center gap-3 bg-slate-50 dark:bg-black/20 p-3 rounded-[1.5rem] border border-slate-100 dark:border-white/10 group/opt">
|
||||
<input
|
||||
type="radio"
|
||||
name={`correct-${idx}`}
|
||||
checked={marker.correctIndex === optIdx}
|
||||
onChange={() => updateMarker(idx, { correctIndex: optIdx })}
|
||||
className="w-5 h-5 text-green-600 dark:text-green-500 bg-white dark:bg-black/40 border-slate-300 dark:border-white/10 focus:ring-green-500"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={option}
|
||||
onChange={(e) => updateOption(idx, optIdx, e.target.value)}
|
||||
className="flex-1 bg-transparent border-none p-0 text-sm font-bold text-slate-700 dark:text-white focus:ring-0 outline-none"
|
||||
placeholder={`Vector ${optIdx + 1}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-[9px] text-slate-400 dark:text-gray-600 uppercase font-black italic pl-1 italic">Identify the absolute truth vector via the terminal radio toggle.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-400 truncate">{marker.question}</p>
|
||||
)}
|
||||
) : (
|
||||
<p className="text-xl font-black text-slate-800 dark:text-gray-200 tracking-tight uppercase italic">{marker.question}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user