chatbot / src /App.js
bughunter230's picture
Update src/App.js
8af7296 verified
/* 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;