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;