| 'use client'; |
|
|
| import { useState } from 'react'; |
| import { useChat } from '@ai-sdk/react'; |
| import { QuestionParameterForm } from '@/components/question-creation'; |
| import { QuestionList, QuestionEditModal } from '@/components/question-output'; |
| import { Article } from '@/components/article'; |
| import { QuestionType, QuestionParameters, GeneratedQuestion } from '@/types/quiz'; |
| import { Button } from '@/components/ui/button'; |
| import { Input } from '@/components/ui/input'; |
| import { ChevronRight, ChevronLeft } from 'lucide-react'; |
|
|
| const QUESTION_TYPES: QuestionType[] = [ |
| { id: 'paragraph-summary', name: 'Paragraph Summary', icon: 'π', description: 'Create summaries of text passages' }, |
| { id: 'paragraph-details', name: 'Paragraph Details', icon: 'π', description: 'Identify key details in paragraphs' }, |
| |
| |
| |
| |
| |
| ]; |
|
|
| export default function QuestionBuilder() { |
| |
| const [currentStep, setCurrentStep] = useState<'type-selection' | 'parameters' | 'generation' | 'editing'>('type-selection'); |
| const [selectedQuestionType, setSelectedQuestionType] = useState<QuestionType | null>(null); |
| const [questionParameters, setQuestionParameters] = useState<QuestionParameters>({}); |
| const [isGenerating, setIsGenerating] = useState(false); |
| |
| |
| const [sourceArticle, setSourceArticle] = useState(''); |
| const [isSourceLocked, setIsSourceLocked] = useState(false); |
| |
| |
| const [questions, setQuestions] = useState<GeneratedQuestion[]>([]); |
| const [selectedQuestions, setSelectedQuestions] = useState<Set<string>>(new Set()); |
| |
| |
| const [editingQuestion, setEditingQuestion] = useState<GeneratedQuestion | null>(null); |
| const [isEditModalOpen, setIsEditModalOpen] = useState(false); |
| |
| |
| const { messages, sendMessage, status } = useChat(); |
| const [input, setInput] = useState(''); |
| |
| |
| const [isQuestionBuilderCollapsed, setIsQuestionBuilderCollapsed] = useState(false); |
|
|
| const handleQuestionTypeSelect = (questionType: QuestionType) => { |
| setSelectedQuestionType(questionType); |
| setCurrentStep('parameters'); |
| }; |
|
|
| const generateQuestion = async () => { |
| if (!selectedQuestionType) return; |
| |
| setIsGenerating(true); |
| try { |
| const response = await fetch('/api/generate-question', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| type: selectedQuestionType.id, |
| parameters: questionParameters, |
| sourceArticle, |
| }), |
| }); |
| |
| if (response.ok) { |
| const newQuestion = await response.json(); |
| setQuestions(prev => [...prev, { |
| ...newQuestion, |
| id: Date.now().toString(), |
| createdAt: new Date(), |
| }]); |
| setCurrentStep('type-selection'); |
| setSelectedQuestionType(null); |
| setQuestionParameters({}); |
| } |
| } catch (error) { |
| console.error('Error generating question:', error); |
| } finally { |
| setIsGenerating(false); |
| } |
| }; |
|
|
| const removeQuestion = (id: string) => { |
| setQuestions(prev => prev.filter(q => q.id !== id)); |
| setSelectedQuestions(prev => { |
| const newSet = new Set(prev); |
| newSet.delete(id); |
| return newSet; |
| }); |
| }; |
|
|
| const reorderQuestions = (reorderedQuestions: GeneratedQuestion[]) => { |
| setQuestions(reorderedQuestions); |
| }; |
|
|
| const handleEditQuestion = (question: GeneratedQuestion) => { |
| setEditingQuestion(question); |
| setIsEditModalOpen(true); |
| }; |
|
|
| const handleSaveEditedQuestion = (updatedQuestion: GeneratedQuestion) => { |
| setQuestions(prev => |
| prev.map(q => q.id === updatedQuestion.id ? updatedQuestion : q) |
| ); |
| setEditingQuestion(null); |
| setIsEditModalOpen(false); |
| }; |
|
|
| const handleCloseEditModal = () => { |
| setEditingQuestion(null); |
| setIsEditModalOpen(false); |
| }; |
|
|
| const handleDuplicateQuestion = (question: GeneratedQuestion) => { |
| const duplicatedQuestion: GeneratedQuestion = { |
| ...question, |
| id: Date.now().toString(), |
| stem: `${question.stem} (Copy)`, |
| content: { |
| ...question.content, |
| Question: `${question.content.Question} (Copy)`, |
| }, |
| createdAt: new Date(), |
| }; |
| |
| setQuestions(prev => [...prev, duplicatedQuestion]); |
| |
| |
| console.log('Question duplicated successfully!'); |
| }; |
|
|
| return ( |
| <div className="min-h-screen bg-gray-50 dark:bg-[#212529]"> |
| {/* Header */} |
| <div className="bg-white dark:bg-blue-900 border-b border-gray-200 dark:border-blue-800"> |
| <div className="px-6 py-2"> |
| <h1 className="text-xl font-bold text-gray-900 dark:text-white"> |
| π QuizFlash |
| </h1> |
| <p className="text-sm text-gray-600 dark:text-blue-200"> |
| AI-powered assessment creation tool for educators |
| </p> |
| </div> |
| </div> |
| |
| {/* Three-panel layout */} |
| <div className="flex h-[calc(100vh-80px)]"> |
| {/* Left Panel: Chat-Driven Question Builder */} |
| <div className={`${isQuestionBuilderCollapsed ? 'w-12' : 'w-2/5'} bg-white dark:bg-[#212529] border-r border-gray-200 dark:border-gray-600 flex flex-col transition-all duration-300`}> |
| <div className="p-4 border-b border-gray-200 dark:border-gray-600 flex items-center justify-between"> |
| {!isQuestionBuilderCollapsed && ( |
| <h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100"> |
| π¬ Question Builder |
| </h2> |
| )} |
| <Button |
| onClick={() => setIsQuestionBuilderCollapsed(!isQuestionBuilderCollapsed)} |
| variant="ghost" |
| size="icon" |
| title={isQuestionBuilderCollapsed ? 'Expand Question Builder' : 'Collapse Question Builder'} |
| > |
| {isQuestionBuilderCollapsed ? ( |
| <ChevronRight className="h-4 w-4" /> |
| ) : ( |
| <ChevronLeft className="h-4 w-4" /> |
| )} |
| </Button> |
| </div> |
| |
| {!isQuestionBuilderCollapsed && ( |
| <div className="flex-1 overflow-y-auto p-4"> |
| {currentStep === 'type-selection' && ( |
| <div className="space-y-4"> |
| <div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-700 rounded-lg p-4"> |
| <p className="text-blue-800 dark:text-blue-200"> |
| π€ Hi! What type of question would you like to create? |
| </p> |
| </div> |
| |
| <div className="space-y-2"> |
| {QUESTION_TYPES.map((type) => ( |
| <Button |
| key={type.id} |
| onClick={() => handleQuestionTypeSelect(type)} |
| variant="outline" |
| className="w-full justify-start p-3 h-auto" |
| > |
| <div className="flex items-center space-x-3"> |
| <span className="text-lg">{type.icon}</span> |
| <div className="text-left"> |
| <div className="font-medium">{type.name}</div> |
| <div className="text-sm text-muted-foreground">{type.description}</div> |
| </div> |
| </div> |
| </Button> |
| ))} |
| </div> |
| </div> |
| )} |
| |
| {currentStep === 'parameters' && selectedQuestionType && ( |
| <div className="space-y-4"> |
| <QuestionParameterForm |
| questionType={selectedQuestionType} |
| parameters={questionParameters} |
| onParametersChange={setQuestionParameters} |
| /> |
| |
| <div className="flex space-x-2"> |
| <Button |
| onClick={() => setCurrentStep('type-selection')} |
| variant="outline" |
| > |
| Back |
| </Button> |
| <Button |
| onClick={generateQuestion} |
| disabled={isGenerating} |
| className="flex-1" |
| > |
| {isGenerating ? 'Generating...' : 'Generate Question'} |
| </Button> |
| </div> |
| </div> |
| )} |
| |
| {/* Chat History */} |
| <div className="mt-6 space-y-4"> |
| {messages.map((message, index) => ( |
| <div |
| key={index} |
| className={`p-3 rounded-lg ${ |
| message.role === 'user' |
| ? 'bg-blue-100 dark:bg-blue-900/30 ml-4' |
| : 'bg-gray-100 dark:bg-gray-700 mr-4' |
| }`} |
| > |
| <div className="text-sm font-medium mb-1"> |
| {message.role === 'user' ? 'You' : 'AI Assistant'} |
| </div> |
| <div> |
| {message.parts.map((part, partIndex) => |
| part.type === 'text' ? <span key={partIndex}>{part.text}</span> : null |
| )} |
| </div> |
| </div> |
| ))} |
| </div> |
| </div> |
| )} |
| |
| {!isQuestionBuilderCollapsed && ( |
| <div className="p-4 border-t border-gray-200 dark:border-gray-600"> |
| <form onSubmit={(e) => { |
| e.preventDefault(); |
| if (input.trim()) { |
| sendMessage({ text: input }); |
| setInput(''); |
| } |
| }} className="flex space-x-2"> |
| <Input |
| value={input} |
| onChange={(e) => setInput(e.target.value)} |
| placeholder="Ask for modifications or help..." |
| className="flex-1" |
| /> |
| <Button |
| type="submit" |
| disabled={status !== 'ready'} |
| > |
| Send |
| </Button> |
| </form> |
| </div> |
| )} |
| |
| {/* Collapsed state indicator */} |
| {isQuestionBuilderCollapsed && ( |
| <div className="flex-1 flex items-center justify-center p-2"> |
| <div className="transform -rotate-90 text-sm font-medium text-gray-500 dark:text-gray-400 whitespace-nowrap"> |
| Question Builder |
| </div> |
| </div> |
| )} |
| </div> |
| |
| {/* Right side: Two-panel stack */} |
| <div className="flex-1 flex flex-col"> |
| {/* Upper Right Panel: Source Article Editor */} |
| <Article |
| className="h-1/2" |
| sourceArticle={sourceArticle} |
| onSourceArticleChange={setSourceArticle} |
| isSourceLocked={isSourceLocked} |
| onSourceLockedChange={setIsSourceLocked} |
| /> |
| |
| {/* Lower Right Panel: Question List */} |
| <QuestionList |
| className="h-1/2" |
| questions={questions} |
| questionTypes={QUESTION_TYPES} |
| selectedQuestions={selectedQuestions} |
| onQuestionSelect={(questionId, selected) => { |
| setSelectedQuestions(prev => { |
| const newSet = new Set(prev); |
| if (selected) { |
| newSet.add(questionId); |
| } else { |
| newSet.delete(questionId); |
| } |
| return newSet; |
| }); |
| }} |
| onQuestionRemove={removeQuestion} |
| onQuestionReorder={reorderQuestions} |
| onQuestionEdit={handleEditQuestion} |
| editingQuestionId={editingQuestion?.id || null} |
| onQuestionDuplicate={handleDuplicateQuestion} |
| onQuestionPreview={(question) => { |
| // TODO: Implement preview functionality |
| console.log('Preview question:', question); |
| }} |
| /> |
| </div> |
| </div> |
| |
| {/* Question Edit Modal */} |
| <QuestionEditModal |
| question={editingQuestion} |
| questionTypes={QUESTION_TYPES} |
| isOpen={isEditModalOpen} |
| onClose={handleCloseEditModal} |
| onSave={handleSaveEditedQuestion} |
| /> |
| </div> |
| ); |
| } |