Spaces:
Running
Running
| /* global __app_id, __firebase_config, __initial_auth_token */ | |
| import React, { useState, useEffect, useRef } from 'react'; | |
| import { initializeApp } from 'firebase/app'; | |
| import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from 'firebase/auth'; | |
| import { getFirestore, collection, addDoc, query, orderBy, onSnapshot, serverTimestamp } from 'firebase/firestore'; | |
| // Global variables provided by the Canvas environment for Firebase setup. | |
| // These variables are populated automatically at runtime by the platform. | |
| function App() { | |
| // State to store chat messages. Each message object will include text, sender, and timestamp. | |
| const [messages, setMessages] = useState([]); | |
| // State to hold the current message being typed by the user. | |
| const [newMessage, setNewMessage] = useState(''); | |
| // State to indicate if the bot is currently processing a response. | |
| const [isLoading, setIsLoading] = useState(false); | |
| // State to store the current user's ID for chat history. | |
| const [userId, setUserId] = useState(null); | |
| // State to hold the Firebase Firestore database instance. | |
| const [db, setDb] = useState(null); | |
| // State to hold the Firebase Auth instance. | |
| const [auth, setAuth] = useState(null); | |
| // State to track if Firebase authentication has been initialized and ready. | |
| const [isAuthReady, setIsAuthReady] = useState(false); | |
| // State to store any error messages for display. | |
| const [error, setError] = useState(null); | |
| // Ref to automatically scroll to the latest message in the chat. | |
| const messagesEndRef = useRef(null); | |
| // useEffect hook for Firebase initialization and authentication. | |
| // This runs once when the component mounts. | |
| useEffect(() => { | |
| try { | |
| // Get the application ID from the global variable, or use a default. | |
| const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; | |
| // Parse the Firebase configuration from the global variable. | |
| const firebaseConfig = JSON.parse(typeof __firebase_config !== 'undefined' ? __firebase_config : '{}'); | |
| // Log Firebase config for debugging, but don't expose sensitive info directly. | |
| // console.log("Firebase Config:", firebaseConfig); | |
| // Check if firebase config is valid. | |
| if (Object.keys(firebaseConfig).length === 0) { | |
| console.error("Firebase config is empty. Cannot initialize Firebase."); | |
| setError("Firebase setup incomplete. Please ensure your Firebase project is configured correctly in the Canvas environment."); | |
| return; | |
| } | |
| // Initialize Firebase app with the provided configuration. | |
| const app = initializeApp(firebaseConfig); | |
| // Get Firestore and Auth instances. | |
| const firestoreDb = getFirestore(app); | |
| const firebaseAuth = getAuth(app); | |
| // Set the Firebase instances in state. | |
| setDb(firestoreDb); | |
| setAuth(firebaseAuth); | |
| // Set up an authentication state change listener. | |
| // This ensures that the user is signed in before attempting Firestore operations. | |
| const unsubscribeAuth = onAuthStateChanged(firebaseAuth, async (user) => { | |
| if (user) { | |
| // If a user is already signed in, set their UID. | |
| setUserId(user.uid); | |
| setIsAuthReady(true); | |
| } else { | |
| try { | |
| // If no user is signed in, try to sign in with a custom token | |
| // provided by the environment, or anonymously if no token exists. | |
| if (typeof __initial_auth_token !== 'undefined') { | |
| await signInWithCustomToken(firebaseAuth, __initial_auth_token); | |
| } else { | |
| await signInAnonymously(firebaseAuth); | |
| } | |
| } catch (authError) { | |
| // Log and display authentication errors. | |
| console.error("Firebase Auth Error:", authError); | |
| setError(`Authentication failed: ${authError.message}. Please reload.`); | |
| } | |
| setIsAuthReady(true); // Mark auth as ready even if anonymous or error occurred | |
| } | |
| }); | |
| // Cleanup function: unsubscribe from the auth listener when the component unmounts. | |
| return () => unsubscribeAuth(); | |
| } catch (initError) { | |
| // Catch and display errors during Firebase initialization. | |
| console.error("Error initializing Firebase:", initError); | |
| setError(`Failed to initialize application: ${initError.message}. This might be due to incorrect Firebase configuration.`); | |
| } | |
| }, []); // Empty dependency array means this effect runs only once on mount. | |
| // useEffect hook for real-time message fetching from Firestore. | |
| // This runs whenever `db` or `isAuthReady` state changes, ensuring Firebase is ready. | |
| useEffect(() => { | |
| // Proceed only if Firestore database and authentication are ready. | |
| if (db && isAuthReady) { | |
| // Construct the Firestore collection path for public chat data. | |
| const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; // Re-declare appId for scope | |
| const chatCollectionRef = collection(db, `artifacts/${appId}/public/data/chats`); | |
| // Create a query to order messages by timestamp. | |
| // Note: For large datasets or complex queries, Firestore may require composite indexes. | |
| // For this simple chat, 'timestamp' ordering should work without explicit index creation. | |
| const q = query(chatCollectionRef, orderBy('timestamp')); | |
| // Set up a real-time listener (onSnapshot) to get updates whenever messages change. | |
| const unsubscribeSnapshot = onSnapshot(q, (snapshot) => { | |
| // Map the document snapshots to an array of message objects. | |
| const fetchedMessages = snapshot.docs.map(doc => ({ | |
| id: doc.id, // Document ID as key | |
| ...doc.data() // All other fields from the document | |
| })); | |
| // Update the messages state with the fetched messages. | |
| setMessages(fetchedMessages); | |
| }, (snapshotError) => { | |
| // Handle errors during real-time fetching. | |
| console.error("Error fetching messages:", snapshotError); | |
| setError(`Failed to load messages from Firestore: ${snapshotError.message}. Check your database rules.`); | |
| }); | |
| // Cleanup function: unsubscribe from the snapshot listener when the component unmounts | |
| // or when `db` or `isAuthReady` changes (causing re-run of this effect). | |
| return () => unsubscribeSnapshot(); | |
| } | |
| }, [db, isAuthReady]); // Dependencies: runs when 'db' or 'isAuthReady' changes. | |
| // useEffect hook to scroll to the bottom of the chat window whenever messages change. | |
| useEffect(() => { | |
| messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); | |
| }, [messages]); // Dependency: runs whenever 'messages' state updates. | |
| // Function to handle sending a new message and triggering bot response. | |
| const handleSendMessage = async () => { | |
| // Prevent sending empty messages or if Firebase is not ready. | |
| if (!newMessage.trim() || !db || !userId) return; | |
| setIsLoading(true); // Set loading state to true. | |
| setError(null); // Clear any previous errors. | |
| // --- Step 1: Add the user's message to Firestore --- | |
| try { | |
| const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; // Re-declare appId for scope | |
| await addDoc(collection(db, `artifacts/${appId}/public/data/chats`), { | |
| userId: userId, // User ID of the sender. | |
| text: newMessage, // The message text. | |
| timestamp: serverTimestamp(), // Firestore's server-side timestamp for accurate ordering. | |
| sender: 'user', // Mark the sender as 'user'. | |
| }); | |
| setNewMessage(''); // Clear the input field after sending. | |
| } catch (e) { | |
| // Log and display any errors during adding the user message. | |
| console.error("Error adding user message:", e); | |
| setError(`Failed to send message to database: ${e.message}`); | |
| setIsLoading(false); // Stop loading. | |
| return; // Stop execution if user message failed to send. | |
| } | |
| // --- Step 2: Generate bot response using Gemini API --- | |
| try { | |
| // Prepare chat history for the Gemini API call. | |
| const chatHistory = [{ role: "user", parts: [{ text: newMessage }] }]; | |
| const payload = { contents: chatHistory }; | |
| const apiKey = ""; // API key is automatically provided by Canvas runtime if empty. | |
| const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`; | |
| // Make a POST request to the Gemini API. | |
| const response = await fetch(apiUrl, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload) | |
| }); | |
| // Check for HTTP errors from the API. | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(`API Error: ${response.status} ${response.statusText} - ${errorData.error?.message || 'Unknown error'}`); | |
| } | |
| // Parse the JSON response from the API. | |
| const result = await response.json(); | |
| let botResponseText = "Sorry, I couldn't generate a response. Please try again."; | |
| // Extract the bot's response text from the API result. | |
| if (result.candidates && result.candidates.length > 0 && | |
| result.candidates[0].content && result.candidates[0].content.parts && | |
| result.candidates[0].content.parts.length > 0) { | |
| botResponseText = result.candidates[0].content.parts[0].text; | |
| } else { | |
| console.warn("Unexpected API response structure:", result); | |
| } | |
| // --- Step 3: Add the bot's response message to Firestore --- | |
| const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; // Re-declare appId for scope | |
| await addDoc(collection(db, `artifacts/${appId}/public/data/chats`), { | |
| userId: 'bot', // Mark the sender as 'bot'. | |
| text: botResponseText, // The bot's generated response. | |
| timestamp: serverTimestamp(), // Server-side timestamp. | |
| sender: 'bot', // Mark the sender as 'bot'. | |
| }); | |
| } catch (apiError) { | |
| // Catch and display any errors during API call or adding bot message. | |
| console.error("Error generating bot response:", apiError); | |
| setError(`Bot response failed: ${apiError.message}. Check your internet or API key.`); | |
| } finally { | |
| setIsLoading(false); // Always stop loading after the process completes or fails. | |
| } | |
| }; | |
| return ( | |
| // Main container for the chat application, styled with Tailwind CSS for responsiveness. | |
| // Changed background to a blue gradient reminiscent of the sea/sky. | |
| <div className="min-h-screen bg-gradient-to-br from-blue-200 via-blue-400 to-blue-600 flex flex-col items-center justify-center p-4 font-inter"> | |
| {/* Tailwind CSS and Inter font import for consistent styling */} | |
| <link href="https://cdn.tailwindcss.com" rel="stylesheet" /> | |
| <style> | |
| {` | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap'); | |
| body { font-family: 'Inter', sans-serif; } | |
| /* Ensure all elements have rounded corners for a modern look */ | |
| * { border-radius: 0.5rem; } | |
| `} | |
| </style> | |
| {/* Main chat box container - changed shadow and added a subtle border */} | |
| <div className="w-full max-w-2xl bg-white rounded-xl shadow-2xl border-2 border-yellow-500 flex flex-col h-[550px] max-h-[85vh] min-h-[400px]"> | |
| {/* Chat header - changed gradient to a warm orange/red/yellow theme like a sunset/straw hat */} | |
| <header className="bg-gradient-to-r from-orange-600 via-red-500 to-yellow-500 text-white p-4 rounded-t-xl shadow-md text-center"> | |
| <h1 className="text-3xl font-extrabold text-shadow-lg drop-shadow-xl">HuggingChat Grand Line</h1> | |
| {/* SVG for a simple straw hat icon, inline for easy integration */} | |
| <svg className="mx-auto mt-2 h-8 w-8" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> | |
| <path fillRule="evenodd" d="M10 2a8 8 0 00-8 8c0 3.866 2.015 7.234 5 9.071V19a1 1 0 001 1h4a1 1 0 001-1v-1.929c2.985-1.837 5-5.205 5-9.071a8 8 0 00-8-8zm4 7a4 4 0 10-8 0h8z" clipRule="evenodd"></path> | |
| </svg> | |
| {userId && ( | |
| <p className="text-sm mt-1 opacity-80">Your Pirate Crew ID: <span className="font-mono">{userId}</span></p> | |
| )} | |
| </header> | |
| {/* Message display area */} | |
| <div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-100"> {/* Changed background for contrast */} | |
| {messages.length === 0 && !isLoading && !error ? ( | |
| // Display a welcome message if no messages and not loading/error. | |
| <div className="text-center text-gray-600 mt-10 text-lg font-semibold"> | |
| Ahoy, matey! Start your adventure chat! | |
| </div> | |
| ) : ( | |
| // Map through messages and display them. | |
| messages.map((msg) => ( | |
| <div | |
| key={msg.id} | |
| // Align messages based on sender (user on right, bot on left). | |
| className={`flex ${msg.sender === 'user' ? 'justify-end' : 'justify-start'}`} | |
| > | |
| <div | |
| className={`max-w-[75%] px-4 py-2 rounded-xl shadow-md ${ | |
| msg.sender === 'user' | |
| ? 'bg-yellow-400 text-amber-900 rounded-br-none' // User messages: Straw Hat yellow, darker text | |
| : 'bg-blue-600 text-white rounded-bl-none' // Bot messages: Ocean blue | |
| }`} | |
| > | |
| <p className="text-sm break-words">{msg.text}</p> | |
| <span className={`block text-xs text-right mt-1 ${msg.sender === 'user' ? 'text-amber-800 opacity-90' : 'text-blue-200 opacity-90'}`}> | |
| {/* Display message timestamp, converting Firebase timestamp to Date if available. */} | |
| {msg.timestamp?.toDate ? msg.timestamp.toDate().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : 'Setting Sail...'} | |
| </span> | |
| </div> | |
| </div> | |
| )) | |
| )} | |
| {/* Loading indicator for bot typing */} | |
| {isLoading && ( | |
| <div className="flex justify-start"> | |
| <div className="max-w-[75%] px-4 py-2 rounded-xl rounded-bl-none bg-blue-600 text-white shadow-sm"> | |
| <div className="flex items-center space-x-2"> | |
| {/* Typing animation */} | |
| <span className="relative flex h-3 w-3"> | |
| <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-yellow-300 opacity-75"></span> | |
| <span className="relative inline-flex rounded-full h-3 w-3 bg-yellow-400"></span> | |
| </span> | |
| <p>Bot is charting a course...</p> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Error message display */} | |
| {error && ( | |
| <div className="text-red-700 bg-red-200 p-3 rounded-md border border-red-300 text-sm font-bold"> | |
| <p className="font-semibold">Bounty on your head!</p> | |
| <p>{error}</p> | |
| <p className="mt-1 text-xs">If this persists, please ensure your Firebase project details are correctly configured within the Hugging Face Spaces environment, and check your internet connection.</p> | |
| </div> | |
| )} | |
| {/* Empty div to enable scrolling to the bottom */} | |
| <div ref={messagesEndRef} /> | |
| </div> | |
| {/* Message input area */} | |
| <div className="p-4 border-t border-yellow-500 flex items-center bg-gray-50 rounded-b-xl"> | |
| <input | |
| type="text" | |
| className="flex-1 p-3 border border-blue-400 rounded-full focus:outline-none focus:ring-2 focus:ring-yellow-500 placeholder-blue-600" | |
| placeholder="Send a message to the Grand Line..." | |
| value={newMessage} | |
| onChange={(e) => setNewMessage(e.target.value)} | |
| // Allow sending message with Enter key. | |
| onKeyPress={(e) => { | |
| if (e.key === 'Enter' && !isLoading) { | |
| handleSendMessage(); | |
| } | |
| }} | |
| // Disable input while loading or if Firebase is not ready. | |
| disabled={isLoading || !isAuthReady || !db} | |
| /> | |
| <button | |
| onClick={handleSendMessage} | |
| // Styled with a vibrant yellow/orange gradient for a 'fruit' like feel | |
| className="ml-3 px-5 py-3 bg-gradient-to-r from-yellow-500 to-orange-500 text-amber-900 font-bold rounded-full shadow-lg hover:from-yellow-600 hover:to-orange-600 focus:outline-none focus:ring-2 focus:ring-yellow-500 focus:ring-offset-2 transition-all duration-200 ease-in-out disabled:opacity-50 disabled:cursor-not-allowed" | |
| // Disable button while loading or if Firebase is not ready. | |
| disabled={isLoading || !isAuthReady || !db} | |
| > | |
| Sail! | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export default App; | |