import React, { useState, useEffect, useRef } from 'react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged, GoogleAuthProvider, signInWithPopup } from 'firebase/auth'; import { getFirestore, doc, setDoc, onSnapshot, deleteDoc, collection, getDocs } from 'firebase/firestore'; function App() { // State for game logic const [challenge, setChallenge] = useState(''); // Stores the current full story string const [userResponse, setUserResponse] = useState(''); const [aiFeedback, setAiFeedback] = useState(''); const [isLoading, setIsLoading] = useState(false); // State for Firebase const [db, setDb] = useState(null); const [auth, setAuth] = useState(null); const [userId, setUserId] = useState(null); const [appId, setAppId] = useState(null); const [isAuthReady, setIsAuthReady] = useState(false); // To ensure auth is complete before Firestore ops const [isLoggedIn, setIsLoggedIn] = useState(false); // To track if a user is authenticated (Google or Anonymous) const [isGoogleSignedIn, setIsGoogleSignedIn] = useState(false); // New state to track Google sign-in const [showAuthOptions, setShowAuthOptions] = useState(false); // To control visibility of sign-in buttons // State for collaborative story const [currentCollaborativeStory, setCurrentCollaborativeStory] = useState(''); const [isStoryLoading, setIsStoryLoading] = useState(true); const [activeUsersCount, setActiveUsersCount] = useState(0); // New state for active users // State for Game Codes const [gameCodeInput, setGameCodeInput] = useState(''); // User's input for game code const [currentGameCode, setCurrentGameCode] = useState(null); // The active game code const [gameCodeError, setGameCodeError] = useState(''); // Error message for game code const [isGameSessionActive, setIsGameSessionActive] = useState(false); // True when a game code is set and loaded const unsubscribeStoryRef = useRef(null); // Ref to store the Firestore story unsubscribe function const unsubscribeUsersRef = useRef(null); // Ref to store the Firestore users unsubscribe function // Dynamic MAX_PLAYERS based on login status const MAX_PLAYERS = isGoogleSignedIn ? 18 : 4; // Helper function to generate a random 4-digit code const generateRandomCode = () => { return Math.floor(1000 + Math.random() * 9000).toString(); }; // Firebase Initialization and Authentication useEffect(() => { try { const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {}; const currentAppId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; setAppId(currentAppId); const app = initializeApp(firebaseConfig); const firestoreDb = getFirestore(app); const firebaseAuth = getAuth(app); setDb(firestoreDb); setAuth(firebaseAuth); onAuthStateChanged(firebaseAuth, async (user) => { if (user) { setUserId(user.uid); setIsLoggedIn(true); // Determine if signed in via Google (user.providerData will contain google.com) const isGoogle = user.providerData.some(provider => provider.providerId === GoogleAuthProvider.PROVIDER_ID); setIsGoogleSignedIn(isGoogle); setIsAuthReady(true); setShowAuthOptions(false); // Hide auth options once logged in } else { setIsLoggedIn(false); setIsGoogleSignedIn(false); // Reset Google sign-in status setUserId(null); // Clear userId if logged out setIsAuthReady(true); // Still ready, just not authenticated yet // Only show auth options if no token is provided initially and not already logged in const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null; if (!initialAuthToken) { setShowAuthOptions(true); } } }); // Initial sign-in attempt if a custom token is provided by the environment const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null; if (initialAuthToken && firebaseAuth && !firebaseAuth.currentUser) { signInWithCustomToken(firebaseAuth, initialAuthToken).catch(e => console.error("Custom token sign-in failed:", e)); } else if (!initialAuthToken && !firebaseAuth.currentUser) { // If no custom token, and not already signed in, wait for user's action (Google/Anonymous) setShowAuthOptions(true); // Ensure auth options are shown if no auto-login } } catch (error) { console.error("Firebase initialization error:", error); setUserId(crypto.randomUUID()); // Fallback for very early errors setIsAuthReady(true); setShowAuthOptions(true); // Show options if init fails } }, []); // Firebase user presence functions const addUserPresence = async (code, id) => { if (!db || !appId || !id || !code) { console.warn("Attempted to add user presence before all necessary data was available."); return; } try { const userDocRef = doc(db, 'artifacts', appId, 'public', 'data', 'game_sessions', code, 'users', id); await setDoc(userDocRef, { online: true, lastSeen: new Date().toISOString() }, { merge: true }); console.log(`User ${id} joined session ${code}`); } catch (error) { console.error("Error adding user presence:", error); } }; const removeUserPresence = async (code, id) => { if (!db || !appId || !id || !code) { console.warn("Attempted to remove user presence before all necessary data was available."); return; } try { const userDocRef = doc(db, 'artifacts', appId, 'public', 'data', 'game_sessions', code, 'users', id); await deleteDoc(userDocRef); console.log(`User ${id} left session ${code}`); } catch (error) { console.error("Error removing user presence:", error); } }; // Firestore Real-time Listener for Collaborative Story and User Presence useEffect(() => { // Cleanup previous listeners if (unsubscribeStoryRef.current) { unsubscribeStoryRef.current(); unsubscribeStoryRef.current = null; } if (unsubscribeUsersRef.current) { unsubscribeUsersRef.current(); unsubscribeUsersRef.current = null; } if (!db || !isAuthReady || !appId || !currentGameCode || !userId) { // Wait for all dependencies to be ready, including userId setCurrentCollaborativeStory(''); setIsGameSessionActive(false); setIsStoryLoading(false); setActiveUsersCount(0); return; } // Add user presence when a session becomes active // Only add presence if userId is not null (i.e., user is authenticated) if (userId) { addUserPresence(currentGameCode, userId); } // 1. Story Listener const storyDocRef = doc(db, 'artifacts', appId, 'public', 'data', 'game_sessions', currentGameCode); setIsStoryLoading(true); const unsubscribeStory = onSnapshot(storyDocRef, (docSnap) => { if (docSnap.exists()) { const storyData = docSnap.data(); setCurrentCollaborativeStory(storyData.content || ''); setChallenge(storyData.content || ''); setIsGameSessionActive(true); } else { setCurrentCollaborativeStory('This game session is new. Be the first to start the story!'); setChallenge(''); setIsGameSessionActive(true); } setIsStoryLoading(false); setGameCodeError(''); }, (error) => { console.error("Error listening to story updates:", error); setCurrentCollaborativeStory("Failed to load collaborative story for this code."); setIsGameSessionActive(false); setIsStoryLoading(false); setGameCodeError("Could not load game for this code. It might not exist or there's a connection issue."); }); unsubscribeStoryRef.current = unsubscribeStory; // 2. Users Presence Listener const usersCollectionRef = collection(db, 'artifacts', appId, 'public', 'data', 'game_sessions', currentGameCode, 'users'); const unsubscribeUsers = onSnapshot(usersCollectionRef, (snapshot) => { setActiveUsersCount(snapshot.size); }, (error) => { console.error("Error listening to active users:", error); setActiveUsersCount(0); // Reset or handle error }); unsubscribeUsersRef.current = unsubscribeUsers; // Cleanup function for this useEffect return () => { // Remove user presence when component unmounts or dependencies change (e.g., game code changes) if (userId && currentGameCode) { removeUserPresence(currentGameCode, userId); } // Unsubscribe from both listeners if (unsubscribeStoryRef.current) { unsubscribeStoryRef.current(); } if (unsubscribeUsersRef.current) { unsubscribeUsersRef.current(); } }; }, [db, isAuthReady, appId, currentGameCode, userId]); // userId added to dependencies // Function to call the Gemini API const callGeminiApi = async (prompt, currentStory = '', userPart = '') => { setIsLoading(true); let chatHistory = []; if (currentStory && userPart) { chatHistory.push({ role: "user", parts: [{ text: `Current story so far: "${currentStory}"\nUser's continuation: "${userPart}"\n${prompt}` }] }); } else { chatHistory.push({ role: "user", parts: [{ text: prompt }] }); } const payload = { contents: chatHistory }; const apiKey = ""; const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`; try { const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); 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 text = result.candidates[0].content.parts[0].text; setIsLoading(false); return text; } else { console.error("Unexpected API response structure:", result); setIsLoading(false); return "Failed to generate content. Please try again."; } } catch (error) { console.error("Error calling Gemini API:", error); setIsLoading(false); return "An error occurred. Please check your network connection or try again later."; } }; // Function to update the story in Firestore const updateCollaborativeStory = async (storyContent, codeToUpdate) => { const targetCode = codeToUpdate || currentGameCode; if (!db || !appId || !targetCode) { console.error("Firestore DB, App ID, or Game Code not available for update."); return; } try { const storyDocRef = doc(db, 'artifacts', appId, 'public', 'data', 'game_sessions', targetCode); await setDoc(storyDocRef, { content: storyContent }, { merge: true }); } catch (error) { console.error("Error updating collaborative story:", error); } }; // Google Sign-In Function const handleGoogleSignIn = async () => { if (!auth) { setGameCodeError("Firebase Auth not initialized."); return; } setIsLoading(true); setGameCodeError(''); try { const provider = new GoogleAuthProvider(); await signInWithPopup(auth, provider); // onAuthStateChanged listener will handle setting userId and isLoggedIn } catch (error) { console.error("Google Sign-In Error:", error); setGameCodeError(error.message || "Failed to sign in with Google."); } finally { setIsLoading(false); } }; // Anonymous Sign-In Function const handleAnonymousSignIn = async () => { if (!auth) { setGameCodeError("Firebase Auth not initialized."); return; } setIsLoading(true); setGameCodeError(''); try { await signInAnonymously(auth); // onAuthStateChanged listener will handle setting userId and isLoggedIn } catch (error) { console.error("Anonymous Sign-In Error:", error); setGameCodeError(error.message || "Failed to sign in anonymously."); } finally { setIsLoading(false); } }; const handleJoinGame = async () => { if (!gameCodeInput.trim() || gameCodeInput.length !== 4) { setGameCodeError("Please enter a valid 4-digit game code."); return; } if (!db || !appId || !userId) { // Ensure userId is available before checking capacity/joining setGameCodeError("Authentication not ready. Please wait a moment or sign in."); return; } setIsLoading(true); setGameCodeError(''); try { // Check current number of users for this game code const usersCollectionRef = collection(db, 'artifacts', appId, 'public', 'data', 'game_sessions', gameCodeInput, 'users'); const usersSnapshot = await getDocs(usersCollectionRef); // Use the dynamically calculated MAX_PLAYERS if (usersSnapshot.size >= MAX_PLAYERS) { setGameCodeError(`Session ${gameCodeInput} is full. (Max ${MAX_PLAYERS} players)`); setIsLoading(false); return; } // Proceed to join if not full setIsStoryLoading(true); setCurrentGameCode(gameCodeInput); // This will trigger the useEffect listener to add presence } catch (error) { console.error("Error checking session capacity:", error); setGameCodeError("Failed to check session capacity. Please try again."); setIsLoading(false); } }; const handleGenerateAndStartNewGame = async () => { if (isLoading) return; if (!db || !appId || !userId) { // Ensure userId is available before starting setGameCodeError("Authentication not ready. Please wait a moment or sign in."); return; } setIsLoading(true); setAiFeedback(''); setGameCodeError(''); const newCode = generateRandomCode(); setCurrentGameCode(newCode); const prompt = "Generate a very short, creative story opening that a user can continue. Keep it around 3-4 sentences. It should be open-ended and intriguing. The story should be a unique challenge for the user to continue. Start directly with the story, no introductory phrases."; const newChallenge = await callGeminiApi(prompt); setChallenge(newChallenge); setUserResponse(''); await updateCollaborativeStory(newChallenge, newCode); setIsLoading(false); }; const submitUserResponse = async () => { if (isLoading || !userResponse.trim()) { if (!userResponse.trim()) { setAiFeedback("Please write something before submitting!"); } return; } setIsLoading(true); const evaluationPrompt = "Evaluate the user's continuation for creativity, coherence, and how well it builds upon the existing story. Then, write the next 3-4 sentences of the story, making it more challenging or adding a twist that the user will have to respond to next. Make sure your continuation is cohesive with the user's part. Format your response by first providing a brief evaluation (e.g., 'Excellent continuation!', 'Interesting twist.', or 'Good effort, but try to stay more on track.'), followed by your story continuation on a new line."; const fullStorySoFar = `${currentCollaborativeStory}\n${userResponse}`; const aiResponse = await callGeminiApi(evaluationPrompt, currentCollaborativeStory, userResponse); const lines = aiResponse.split('\n'); let evaluation = ''; let storyContinuation = ''; if (lines.length > 0) { evaluation = lines[0]; storyContinuation = lines.slice(1).join('\n'); } else { evaluation = "The AI responded, but the format was unexpected."; storyContinuation = aiResponse; } setAiFeedback(evaluation); const updatedFullStory = currentCollaborativeStory + "\n" + userResponse + "\n" + storyContinuation; setChallenge(updatedFullStory); setUserResponse(''); await updateCollaborativeStory(updatedFullStory, currentGameCode); setIsLoading(false); }; const handleLeaveGame = async () => { if (currentGameCode && userId) { await removeUserPresence(currentGameCode, userId); } setCurrentGameCode(null); setIsGameSessionActive(false); setGameCodeInput(''); setUserResponse(''); setAiFeedback(''); setChallenge(''); setGameCodeError(''); setIsStoryLoading(true); // Reset loading state for next session setActiveUsersCount(0); // Reset user count }; return (

AI Story Challenge (Collaborative)

{/* Authentication Options */} {!isLoggedIn && showAuthOptions && isAuthReady ? (

Please sign in to join or start a collaborative story session.

{gameCodeError && (

{gameCodeError}

)}
) : ( // Game Code Entry / Active Game !isGameSessionActive && isLoggedIn ? (

Enter a 4-digit game code to join an existing story, or generate a new one to start a fresh game session!

Max players per session: {MAX_PLAYERS}

setGameCodeInput(e.target.value.replace(/[^0-9]/g, ''))} // Only allow digits placeholder="Enter 4-digit code" className="w-48 p-3 rounded-lg bg-gray-700 bg-opacity-70 border border-gray-600 text-white placeholder-gray-400 text-center text-xl tracking-widest focus:outline-none focus:ring-2 focus:ring-purple-500" disabled={isLoading} />
{gameCodeError && (

{gameCodeError}

)}

- OR -

) : ( // Active Game Session UI isGameSessionActive && isLoggedIn && ( // Ensure game is active and user is logged in <>

Game Code: {currentGameCode}

Players in Session: {activeUsersCount} / {MAX_PLAYERS}

The Story So Far:

{isStoryLoading ? (
Loading story...
) : ( currentCollaborativeStory )}
{aiFeedback && (

AI's Feedback:

{aiFeedback}

)}

Your Turn:

) ) )}
{userId && (
Your User ID: {userId}
)}
); } export default App;