Spaces:
Running
Running
File size: 19,514 Bytes
3950d35 8b6d000 fa6c6da 8b6d000 fa6c6da a12568d 8b6d000 8af7296 8b6d000 8af7296 8b6d000 8af7296 8b6d000 8af7296 8b6d000 fa6c6da 8b6d000 8af7296 8b6d000 fa6c6da 8b6d000 8af7296 8b6d000 fa6c6da 8b6d000 8af7296 8b6d000 8af7296 8b6d000 8af7296 8b6d000 a12568d |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 |
/* 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;
|