Spaces:
Sleeping
Sleeping
chih.yikuan
Add paragraph details and summary CSV files; enhance question generation with localized prompts and display names
dbc4467 | 'use client'; | |
| import { useState, useEffect } from 'react'; | |
| import { GeneratedQuestion, QuestionType } from '@/types/quiz'; | |
| import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Label } from '@/components/ui/label'; | |
| import { Textarea } from '@/components/ui/textarea'; | |
| import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; | |
| export interface QuestionEditModalProps { | |
| question: GeneratedQuestion | null; | |
| questionTypes: QuestionType[]; | |
| isOpen: boolean; | |
| onClose: () => void; | |
| onSave: (updatedQuestion: GeneratedQuestion) => void; | |
| } | |
| export default function QuestionEditModal({ | |
| question, | |
| questionTypes, | |
| isOpen, | |
| onClose, | |
| onSave, | |
| }: QuestionEditModalProps) { | |
| const [editedQuestion, setEditedQuestion] = useState<GeneratedQuestion | null>(null); | |
| const [isSaving, setIsSaving] = useState(false); | |
| // Initialize form when question changes | |
| useEffect(() => { | |
| if (question) { | |
| setEditedQuestion({ ...question }); | |
| } | |
| }, [question]); | |
| const handleSave = async () => { | |
| if (!editedQuestion) return; | |
| // Basic validation | |
| if (!editedQuestion.stem.trim()) { | |
| alert('Please enter a question'); | |
| return; | |
| } | |
| if (!editedQuestion.content.Options.A.trim() || | |
| !editedQuestion.content.Options.B.trim() || | |
| !editedQuestion.content.Options.C.trim() || | |
| !editedQuestion.content.Options.D.trim()) { | |
| alert('Please fill in all answer options'); | |
| return; | |
| } | |
| setIsSaving(true); | |
| try { | |
| // Call the update API | |
| const response = await fetch('/api/update-question', { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(editedQuestion), | |
| }); | |
| if (response.ok) { | |
| const updatedQuestion = await response.json(); | |
| onSave(updatedQuestion); | |
| onClose(); | |
| } else { | |
| const errorData = await response.json(); | |
| alert(`Failed to update question: ${errorData.error || 'Unknown error'}`); | |
| } | |
| } catch (error) { | |
| console.error('Error updating question:', error); | |
| alert('Failed to update question. Please try again.'); | |
| } finally { | |
| setIsSaving(false); | |
| } | |
| }; | |
| const handleCancel = () => { | |
| setEditedQuestion(question ? { ...question } : null); | |
| onClose(); | |
| }; | |
| const updateQuestionField = (field: string, value: string | number | boolean) => { | |
| if (!editedQuestion) return; | |
| setEditedQuestion(prev => { | |
| if (!prev) return null; | |
| if (field === 'stem') { | |
| return { ...prev, stem: String(value) }; | |
| } else if (field.startsWith('content.Options.')) { | |
| const optionKey = field.replace('content.Options.', ''); | |
| return { | |
| ...prev, | |
| content: { | |
| ...prev.content, | |
| Options: { | |
| ...prev.content.Options, | |
| [optionKey]: String(value) | |
| } | |
| } | |
| }; | |
| } else if (field.startsWith('content.')) { | |
| const contentField = field.replace('content.', ''); | |
| return { | |
| ...prev, | |
| content: { | |
| ...prev.content, | |
| [contentField]: String(value) | |
| } | |
| }; | |
| } | |
| return prev; | |
| }); | |
| }; | |
| if (!editedQuestion) return null; | |
| return ( | |
| <Dialog open={isOpen} onOpenChange={onClose}> | |
| <DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto"> | |
| <DialogHeader> | |
| <DialogTitle>Edit Question</DialogTitle> | |
| </DialogHeader> | |
| <div className="space-y-6 py-4"> | |
| {/* Question Type - Read Only */} | |
| <div className="space-y-2"> | |
| <Label htmlFor="question-type">Question Type</Label> | |
| <div className="flex items-center space-x-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md border w-full"> | |
| <span className="text-lg flex-shrink-0"> | |
| {questionTypes.find(t => t.id === editedQuestion.type)?.icon || '📝'} | |
| </span> | |
| <span className="text-sm font-medium break-words"> | |
| {editedQuestion.displayName || questionTypes.find(t => t.id === editedQuestion.type)?.name || 'Unknown Type'} | |
| </span> | |
| </div> | |
| </div> | |
| {/* Points - Read Only */} | |
| <div className="space-y-2"> | |
| <Label htmlFor="question-points">Points</Label> | |
| <div className="flex items-center space-x-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md border w-20"> | |
| <span className="text-sm font-medium">{editedQuestion.points} pts</span> | |
| </div> | |
| </div> | |
| {/* Question Stem */} | |
| <div className="space-y-2"> | |
| <Label htmlFor="question-stem">Question</Label> | |
| <Textarea | |
| id="question-stem" | |
| value={editedQuestion.stem} | |
| onChange={(e) => updateQuestionField('stem', e.target.value)} | |
| placeholder="Enter your question here..." | |
| className="min-h-[120px] w-full resize-y" | |
| rows={4} | |
| /> | |
| </div> | |
| {/* Options */} | |
| <div className="space-y-4"> | |
| <Label>Options</Label> | |
| <div className="grid grid-cols-1 gap-4"> | |
| {Object.entries(editedQuestion.content.Options).map(([key, value]) => ( | |
| <div key={key} className="flex items-start space-x-3"> | |
| <Label className="w-8 text-sm font-medium mt-2">{key})</Label> | |
| <Textarea | |
| value={value} | |
| onChange={(e) => updateQuestionField(`content.Options.${key}`, e.target.value)} | |
| placeholder={`Option ${key}`} | |
| className="flex-1 min-h-[40px] resize-y" | |
| rows={2} | |
| /> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Correct Answer */} | |
| <div className="space-y-2"> | |
| <Label htmlFor="correct-answer">Correct Answer</Label> | |
| <Select | |
| value={editedQuestion.content.Answer} | |
| onValueChange={(value) => updateQuestionField('content.Answer', value)} | |
| > | |
| <SelectTrigger className="w-full"> | |
| <SelectValue placeholder="Select correct answer" /> | |
| </SelectTrigger> | |
| <SelectContent className="max-w-full"> | |
| {Object.keys(editedQuestion.content.Options).map((key) => ( | |
| <SelectItem key={key} value={key} className="whitespace-normal"> | |
| <span className="font-medium">{key})</span> {editedQuestion.content.Options[key as keyof typeof editedQuestion.content.Options]} | |
| </SelectItem> | |
| ))} | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| </div> | |
| <DialogFooter> | |
| <Button | |
| variant="outline" | |
| onClick={handleCancel} | |
| disabled={isSaving} | |
| > | |
| Cancel | |
| </Button> | |
| <Button | |
| onClick={handleSave} | |
| disabled={isSaving} | |
| > | |
| {isSaving ? 'Saving...' : 'Save Changes'} | |
| </Button> | |
| </DialogFooter> | |
| </DialogContent> | |
| </Dialog> | |
| ); | |
| } | |