import React, { useState, useEffect, useCallback } from 'react';
import { initializeApp } from 'firebase/app';
import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from 'firebase/auth';
import { getFirestore, doc, setDoc, collection, query, onSnapshot, orderBy, getDocs, addDoc } from 'firebase/firestore';
// Define the main App component
const App = () => {
// State variables for Firebase and user authentication
const [app, setApp] = useState(null);
const [db, setDb] = useState(null);
const [auth, setAuth] = useState(null);
const [userId, setUserId] = useState(null);
const [isAuthReady, setIsAuthReady] = useState(false);
// State variables for CER input
const [prompt, setPrompt] = useState('');
const [claim, setClaim] = useState('');
const [evidence, setEvidence] = useState('');
const [reasoning, setReasoning] = useState('');
const [rubric, setRubric] = useState(''); // New state for rubric
// State variables for feedback and comprehension
const [feedback, setFeedback] = useState(null);
const [comprehensionQuestions, setComprehensionQuestions] = useState([]);
const [comprehensionAnswers, setComprehensionAnswers] = useState({});
const [submissions, setSubmissions] = useState([]);
// State variables for UI feedback
const [isLoadingFeedback, setIsLoadingFeedback] = useState(false);
const [isLoadingQuestions, setIsLoadingQuestions] = useState(false);
const [isSubmittingCER, setIsSubmittingCER] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [successMessage, setSuccessMessage] = useState('');
// Constants for app ID and Firebase config
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-cer-app';
const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {};
const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null;
// Initialize Firebase and set up authentication listener
useEffect(() => {
try {
const firebaseApp = initializeApp(firebaseConfig);
const firestoreDb = getFirestore(firebaseApp);
const firebaseAuth = getAuth(firebaseApp);
setApp(firebaseApp);
setDb(firestoreDb);
setAuth(firebaseAuth);
// Listen for auth state changes
const unsubscribe = onAuthStateChanged(firebaseAuth, async (user) => {
if (user) {
setUserId(user.uid);
} else {
// Sign in anonymously if no token is provided or user is not logged in
try {
if (initialAuthToken) {
await signInWithCustomToken(firebaseAuth, initialAuthToken);
} else {
await signInAnonymously(firebaseAuth);
}
} catch (error) {
console.error("Firebase authentication error:", error);
setErrorMessage("Failed to authenticate. Please try again.");
}
}
setIsAuthReady(true); // Mark auth as ready after initial check
});
return () => unsubscribe(); // Cleanup auth listener on unmount
} catch (error) {
console.error("Failed to initialize Firebase:", error);
setErrorMessage("Failed to initialize the application. Check console for details.");
}
}, [firebaseConfig, initialAuthToken]);
// Fetch submissions once authentication is ready
useEffect(() => {
if (!db || !userId || !isAuthReady) {
return;
}
const submissionsCollectionRef = collection(db, `artifacts/${appId}/users/${userId}/cer_drafts`);
// Note: orderBy is commented out to avoid potential index errors as per instructions.
// Data will be sorted client-side if needed.
const q = query(submissionsCollectionRef); // , orderBy('timestamp', 'desc'));
const unsubscribe = onSnapshot(q, (snapshot) => {
const fetchedSubmissions = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
// Sort client-side by timestamp if needed
fetchedSubmissions.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
setSubmissions(fetchedSubmissions);
}, (error) => {
console.error("Error fetching submissions:", error);
setErrorMessage("Failed to load past submissions.");
});
return () => unsubscribe(); // Cleanup snapshot listener
}, [db, userId, appId, isAuthReady]);
// Function to call the Gemini API for feedback
const getCERFeedback = async (cerPrompt, cerClaim, cerEvidence, cerReasoning, cerRubric) => {
setIsLoadingFeedback(true);
setErrorMessage('');
setSuccessMessage('');
try {
const chatHistory = [];
let promptText = `As a science teacher, provide constructive feedback on the following Claim, Evidence, and Reasoning (CER) response. Focus on clarity, accuracy, and completeness for each section. Suggest specific ways to refine each part to achieve mastery. Provide the feedback in a structured JSON format with keys: "claim_feedback", "evidence_feedback", "reasoning_feedback", and "overall_suggestions".
Science Prompt: "${cerPrompt}"
Student's Claim: "${cerClaim}"
Student's Evidence: "${cerEvidence}"
Student's Reasoning: "${cerReasoning}"`;
// Add rubric to prompt if provided
if (cerRubric) {
promptText += `\n\nConsider the following rubric when providing feedback:\nRubric: "${cerRubric}"`;
}
chatHistory.push({ role: "user", parts: [{ text: promptText }] });
const payload = {
contents: chatHistory,
generationConfig: {
responseMimeType: "application/json",
responseSchema: {
type: "OBJECT",
properties: {
claim_feedback: { type: "STRING" },
evidence_feedback: { type: "STRING" },
reasoning_feedback: { type: "STRING" },
overall_suggestions: { type: "STRING" }
},
propertyOrdering: ["claim_feedback", "evidence_feedback", "reasoning_feedback", "overall_suggestions"]
}
}
};
const apiKey = ""; // Canvas will provide this if empty
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`;
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`API error: ${response.status} - ${errorData.error?.message || 'Unknown error'}`);
}
const result = await response.json();
if (result.candidates && result.candidates.length > 0 &&
result.candidates[0].content && result.candidates[0].content.parts &&
result.candidates[0].content.parts.length > 0) {
const jsonString = result.candidates[0].content.parts[0].text;
const parsedFeedback = JSON.parse(jsonString);
setFeedback(parsedFeedback);
setSuccessMessage("Feedback generated successfully!");
} else {
setErrorMessage("Failed to get feedback from AI. Please try again.");
}
} catch (error) {
console.error("Error fetching CER feedback:", error);
setErrorMessage(`Error generating feedback: ${error.message}`);
} finally {
setIsLoadingFeedback(false);
}
};
// Function to call the Gemini API for comprehension questions
const getComprehensionQuestions = async (cerPrompt, cerClaim, cerEvidence, cerReasoning) => {
setIsLoadingQuestions(true);
setErrorMessage('');
setSuccessMessage('');
try {
const chatHistory = [];
const promptText = `Based on the following science prompt and the student's CER response, generate 3-5 comprehension questions to assess their understanding of the topic and their CER. Provide the questions in a JSON array format, where each element is a string representing a question.
Science Prompt: "${cerPrompt}"
Student's Claim: "${cerClaim}"
Student's Evidence: "${cerEvidence}"
Student's Reasoning: "${cerReasoning}"`;
chatHistory.push({ role: "user", parts: [{ text: promptText }] });
const payload = {
contents: chatHistory,
generationConfig: {
responseMimeType: "application/json",
responseSchema: {
type: "ARRAY",
items: { type: "STRING" }
}
}
};
const apiKey = ""; // Canvas will provide this if empty
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`;
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`API error: ${response.status} - ${errorData.error?.message || 'Unknown error'}`);
}
const result = await response.json();
if (result.candidates && result.candidates.length > 0 &&
result.candidates[0].content && result.candidates[0].content.parts &&
result.candidates[0].content.parts.length > 0) {
const jsonString = result.candidates[0].content.parts[0].text;
const parsedQuestions = JSON.parse(jsonString);
setComprehensionQuestions(parsedQuestions);
// Initialize answers state
const initialAnswers = parsedQuestions.reduce((acc, _, index) => ({ ...acc, [`q${index}`]: '' }), {});
setComprehensionAnswers(initialAnswers);
setSuccessMessage("Comprehension questions generated!");
} else {
setErrorMessage("Failed to get comprehension questions from AI. Please try again.");
}
} catch (error) {
console.error("Error fetching comprehension questions:", error);
setErrorMessage(`Error generating questions: ${error.message}`);
} finally {
setIsLoadingQuestions(false);
}
};
// Handle form submission
const handleSubmitCER = async (e) => {
e.preventDefault();
if (!db || !userId) {
setErrorMessage("Application not ready. Please wait for authentication.");
return;
}
if (!prompt || !claim || !evidence || !reasoning) {
setErrorMessage("Please fill in all CER fields and the prompt.");
return;
}
setIsSubmittingCER(true);
setErrorMessage('');
setSuccessMessage('');
try {
// Get feedback and questions concurrently
await Promise.all([
getCERFeedback(prompt, claim, evidence, reasoning, rubric), // Pass rubric here
getComprehensionQuestions(prompt, claim, evidence, reasoning)
]);
setSuccessMessage("CER submitted! Please review feedback and answer comprehension questions.");
} catch (error) {
console.error("Error during CER submission process:", error);
setErrorMessage(`Failed to process CER: ${error.message}`);
} finally {
setIsSubmittingCER(false);
}
};
// Handle submission of comprehension answers and save everything to Firestore
const handleSaveFullSubmission = async () => {
if (!db || !userId) {
setErrorMessage("Application not ready. Please wait for authentication.");
return;
}
setIsSubmittingCER(true);
setErrorMessage('');
setSuccessMessage('');
try {
const submissionData = {
prompt,
claim,
evidence,
reasoning,
rubric, // Save the rubric
feedback: feedback, // Save the generated feedback
comprehensionQuestions: comprehensionQuestions,
comprehensionAnswers: comprehensionAnswers,
timestamp: Date.now(),
userId: userId // Include userId for potential teacher review
};
const cerCollectionRef = collection(db, `artifacts/${appId}/users/${userId}/cer_drafts`);
await addDoc(cerCollectionRef, submissionData);
setSuccessMessage("Your full CER and comprehension answers have been saved!");
// Clear form and feedback/questions after successful save
setPrompt('');
setClaim('');
setEvidence('');
setReasoning('');
setRubric(''); // Clear rubric
setFeedback(null);
setComprehensionQuestions([]);
setComprehensionAnswers({});
} catch (error) {
console.error("Error saving full CER submission:", error);
setErrorMessage(`Failed to save submission: ${error.message}`);
} finally {
setIsSubmittingCER(false);
}
};
// Handle change for comprehension answers
const handleComprehensionAnswerChange = (index, value) => {
setComprehensionAnswers(prev => ({ ...prev, [`q${index}`]: value }));
};
// Function to load a previous submission into the form
const loadSubmission = (submission) => {
setPrompt(submission.prompt || '');
setClaim(submission.claim || '');
setEvidence(submission.evidence || '');
setReasoning(submission.reasoning || '');
setRubric(submission.rubric || ''); // Load rubric
setFeedback(submission.feedback || null);
setComprehensionQuestions(submission.comprehensionQuestions || []);
setComprehensionAnswers(submission.comprehensionAnswers || {});
window.scrollTo({ top: 0, behavior: 'smooth' }); // Scroll to top
setSuccessMessage("Previous submission loaded.");
setErrorMessage('');
};
// Tailwind CSS classes for consistent styling
const inputClasses = "w-full p-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 mb-4";
const buttonClasses = "bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-md transition duration-300 ease-in-out shadow-lg";
const sectionTitleClasses = "text-xl font-semibold text-gray-800 mb-4 border-b-2 border-blue-300 pb-2";
const cardClasses = "bg-white p-6 rounded-lg shadow-xl mb-6 border border-gray-200";
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 p-6 font-inter text-gray-800">
<div className="max-w-4xl mx-auto">
<h1 className="text-4xl font-extrabold text-center text-blue-800 mb-10 drop-shadow-md">
Science CER Helper
</h1>
{!isAuthReady && (
<div className="text-center p-4 bg-yellow-100 text-yellow-800 rounded-md mb-6">
Loading application... Please wait.
</div>
)}
{errorMessage && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-6" role="alert">
<strong className="font-bold">Error!</strong>
<span className="block sm:inline"> {errorMessage}</span>
</div>
)}
{successMessage && (
<div className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-6" role="alert">
<strong className="font-bold">Success!</strong>
<span className="block sm:inline"> {successMessage}</span>
</div>
)}
{userId && (
<div className="text-center text-sm text-gray-600 mb-6">
Your User ID: <span className="font-mono bg-gray-100 p-1 rounded">{userId}</span>
</div>
)}
{/* CER Input Form */}
<div className={cardClasses}>
<h2 className={sectionTitleClasses}>Your CER Draft</h2>
<form onSubmit={handleSubmitCER}>
<div className="mb-4">
<label htmlFor="prompt" className="block text-gray-700 text-sm font-bold mb-2">
Science Prompt:
</label>
<textarea
id="prompt"
className={inputClasses + " h-24"}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Enter the science prompt here (e.g., 'Explain why seasons occur')."
required
></textarea>
</div>
<div className="mb-4">
<label htmlFor="claim" className="block text-gray-700 text-sm font-bold mb-2">
Claim:
</label>
<textarea
id="claim"
className={inputClasses + " h-24"}
value={claim}
onChange={(e) => setClaim(e.target.value)}
placeholder="State your main argument or conclusion."
required
></textarea>
</div>
<div className="mb-4">
<label htmlFor="evidence" className="block text-gray-700 text-sm font-bold mb-2">
Evidence:
</label>
<textarea
id="evidence"
className={inputClasses + " h-32"}
value={evidence}
onChange={(e) => setEvidence(e.target.value)}
placeholder="Provide specific data, observations, or facts that support your claim."
required
></textarea>
</div>
<div className="mb-6">
<label htmlFor="reasoning" className="block text-gray-700 text-sm font-bold mb-2">
Reasoning:
</label>
<textarea
id="reasoning"
className={inputClasses + " h-40"}
value={reasoning}
onChange={(e) => setReasoning(e.target.value)}
placeholder="Explain how your evidence supports your claim. Connect the evidence to scientific principles."
required
></textarea>
</div>
{/* New Rubric Input Field */}
<div className="mb-6">
<label htmlFor="rubric" className="block text-gray-700 text-sm font-bold mb-2">
Optional Rubric (as appropriate):
</label>
<textarea
id="rubric"
className={inputClasses + " h-32"}
value={rubric}
onChange={(e) => setRubric(e.target.value)}
placeholder="Enter a rubric or specific criteria for feedback here (e.g., 'Claim must be a single, clear sentence.')."
></textarea>
</div>
<button
type="submit"
className={buttonClasses + " w-full"}
disabled={isLoadingFeedback || isLoadingQuestions || isSubmittingCER || !isAuthReady}
>
{isSubmittingCER ? 'Processing...' : 'Get Feedback & Questions'}
</button>
</form>
</div>
{/* Feedback Display */}
{feedback && (
<div className={cardClasses}>
<h2 className={sectionTitleClasses}>AI Feedback</h2>
<div className="space-y-4">
<div>
<h3 className="font-semibold text-lg text-gray-700 mb-1">Claim Feedback:</h3>
<p className="text-gray-600 leading-relaxed">{feedback.claim_feedback}</p>
</div>
<div>
<h3 className="font-semibold text-lg text-gray-700 mb-1">Evidence Feedback:</h3>
<p className="text-gray-600 leading-relaxed">{feedback.evidence_feedback}</p>
</div>
<div>
<h3 className="font-semibold text-lg text-gray-700 mb-1">Reasoning Feedback:</h3>
<p className="text-gray-600 leading-relaxed">{feedback.reasoning_feedback}</p>
</div>
<div>
<h3 className="font-semibold text-lg text-gray-700 mb-1">Overall Suggestions:</h3>
<p className="text-gray-600 leading-relaxed">{feedback.overall_suggestions}</p>
</div>
</div>
</div>
)}
{/* Comprehension Questions */}
{comprehensionQuestions.length > 0 && (
<div className={cardClasses}>
<h2 className={sectionTitleClasses}>Comprehension Questions</h2>
<p className="text-gray-600 mb-4">Answer these questions to demonstrate your understanding:</p>
<div className="space-y-4">
{comprehensionQuestions.map((question, index) => (
<div key={index}>
<label htmlFor={`q${index}`} className="block text-gray-700 text-sm font-bold mb-2">
{index + 1}. {question}
</label>
<textarea
id={`q${index}`}
className={inputClasses + " h-20"}
value={comprehensionAnswers[`q${index}`] || ''}
onChange={(e) => handleComprehensionAnswerChange(index, e.target.value)}
placeholder="Your answer here..."
></textarea>
</div>
))}
</div>
<button
onClick={handleSaveFullSubmission}
className={buttonClasses + " w-full mt-6"}
disabled={isSubmittingCER || !isAuthReady}
>
{isSubmittingCER ? 'Saving...' : 'Save Full Submission'}
</button>
</div>
)}
{/* Past Submissions */}
{submissions.length > 0 && (
<div className={cardClasses}>
<h2 className={sectionTitleClasses}>Your Past Submissions</h2>
<div className="space-y-4">
{submissions.map((sub) => (
<div key={sub.id} className="border border-gray-200 p-4 rounded-md bg-gray-50 hover:bg-gray-100 transition duration-200 ease-in-out">
<p className="font-semibold text-gray-700">Prompt: <span className="font-normal">{sub.prompt.substring(0, 100)}...</span></p>
<p className="text-sm text-gray-500">Submitted: {new Date(sub.timestamp).toLocaleString()}</p>
<button
onClick={() => loadSubmission(sub)}
className="mt-2 bg-indigo-500 hover:bg-indigo-600 text-white text-sm py-2 px-4 rounded-md shadow-sm transition duration-300 ease-in-out"
>
Load This Submission
</button>
</div>
))}
</div>
</div>
)}
</div>
</div>
);
};
export default App;