QuizFlash / src /app /page.tsx
Shih-hungg's picture
Update prompt management
034f126
'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' },
// { id: 'cloze', name: 'Cloze (Fill-in-the-blank)', icon: 'πŸ”€', description: 'Fill in missing words or phrases' },
// { id: 'word-comprehension', name: 'Word Comprehension', icon: 'πŸ“š', description: 'Test vocabulary understanding in context' },
// { id: 'grammatical-structure', name: 'Grammar Structure', icon: 'πŸ“', description: 'Identify grammatical patterns and structures' },
// { id: 'paragraph-structure', name: 'Paragraph Structure', icon: 'πŸ“‹', description: 'Analyze paragraph organization and flow' },
// { id: 'textual-inference', name: 'Textual Inference', icon: '🧠', description: 'Make inferences from text passages' },
];
export default function QuestionBuilder() {
// Chat builder state
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);
// Source article state
const [sourceArticle, setSourceArticle] = useState('');
const [isSourceLocked, setIsSourceLocked] = useState(false);
// Question bank state
const [questions, setQuestions] = useState<GeneratedQuestion[]>([]);
const [selectedQuestions, setSelectedQuestions] = useState<Set<string>>(new Set());
// Edit modal state
const [editingQuestion, setEditingQuestion] = useState<GeneratedQuestion | null>(null);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
// Chat functionality
const { messages, sendMessage, status } = useChat();
const [input, setInput] = useState('');
// Collapse state for question builder panel
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(), // Generate new ID
stem: `${question.stem} (Copy)`, // Add "(Copy)" to distinguish
content: {
...question.content,
Question: `${question.content.Question} (Copy)`, // Also update the content.Question
},
createdAt: new Date(), // Update creation date
};
setQuestions(prev => [...prev, duplicatedQuestion]);
// Simple feedback - you could replace this with a proper toast notification
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>
);
}