diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..7db10e9 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,5 @@ +CEREBRAS_API_KEY=your_cerebras_api_key_here +MONGO_URI=mongodb+srv://username:password@cluster.mongodb.net/database_name?retryWrites=true&w=majority + +# Google OAuth Configuration (optional - leave empty to disable OAuth) +GOOGLE_CLIENT_ID=your_google_client_id_here diff --git a/backend/app.py b/backend/app.py index 352a44f..5e55641 100644 --- a/backend/app.py +++ b/backend/app.py @@ -5,6 +5,7 @@ from datetime import datetime from dotenv import load_dotenv from llm_service import LLM +from auth import SimpleAuth # Load environment variables load_dotenv() @@ -12,8 +13,9 @@ app = Flask(__name__) CORS(app) -# Initialize LLM service +# Initialize services llm_service = LLM() +auth_service = SimpleAuth() # In-memory storage for demo purposes (use a database in production) # Data structure: users -> sessions -> quizzes @@ -21,6 +23,151 @@ sessions = {} # session_id -> session_data quizzes = {} # quiz_id -> quizzes +# Add test data +def initialize_test_data(): + """Initialize test data for development and testing""" + global users, sessions, quizzes + + # Test user + users['1'] = { + 'id': '1', + 'username': 'alice', + 'created_at': '2024-01-15T10:30:00', + 'sessions': ['session1', 'session2'] + } + + # Test sessions + sessions['session1'] = { + 'id': 'session1', + 'user_id': '1', + 'created_at': '2024-01-15T10:30:00', + 'title': 'React component', + 'messages': [ + { + 'id': 'msg1', + 'user_message': 'How to create a button component?', + 'chat_response': 'Here is how to create a button component...', + 'timestamp': '2024-01-15T10:30:00' + } + ], + 'quizzes': [], + 'conversation_history': [ + {"role": "user", "content": "How to create a button component?"}, + {"role": "assistant", "content": "Here is how to create a button component..."} + ] + } + + sessions['session2'] = { + 'id': 'session2', + 'user_id': '1', + 'created_at': '2024-01-14T15:45:00', + 'title': 'JavaScript patterns', + 'messages': [ + { + 'id': 'msg2', + 'user_message': 'What are async/await best practices?', + 'chat_response': 'Async/await best practices include...', + 'timestamp': '2024-01-14T15:45:00' + } + ], + 'quizzes': [], + 'conversation_history': [ + {"role": "user", "content": "What are async/await best practices?"}, + {"role": "assistant", "content": "Async/await best practices include..."} + ] + } + +# Initialize test data +initialize_test_data() + +# Authentication Routes +@app.route('/api/auth/google', methods=['POST']) +def google_oauth(): + """Handle Google OAuth authentication""" + try: + data = request.get_json() + id_token = data.get('id_token') + + if not id_token: + return jsonify({'error': 'ID token is required'}), 400 + + # Verify Google token + user_info = auth_service.verify_google_token(id_token) + if not user_info: + return jsonify({'error': 'Invalid Google token or OAuth not configured'}), 401 + + # Check if user exists, create if not + user_id = user_info['id'] + if user_id not in users: + users[user_id] = { + 'id': user_id, + 'username': user_info['name'], + 'email': user_info['email'], + 'picture': user_info.get('picture', ''), + 'is_guest': False, + 'created_at': datetime.now().isoformat(), + 'sessions': [] + } + + return jsonify({ + 'user': { + 'id': user_id, + 'username': user_info['name'], + 'email': user_info['email'], + 'picture': user_info.get('picture', ''), + 'is_guest': False + } + }) + + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/api/auth/guest', methods=['POST']) +def guest_login(): + """Handle guest login with just a username""" + try: + data = request.get_json() + username = data.get('username', '').strip() + + if not username: + return jsonify({'error': 'Username is required'}), 400 + + # Create guest user + user_info = auth_service.create_guest_user(username) + user_id = user_info['id'] + + # Store guest user + users[user_id] = { + 'id': user_id, + 'username': user_info['name'], + 'email': '', + 'picture': '', + 'is_guest': True, + 'created_at': datetime.now().isoformat(), + 'sessions': [] + } + + return jsonify({ + 'user': { + 'id': user_id, + 'username': user_info['name'], + 'email': '', + 'picture': '', + 'is_guest': True + } + }) + + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/api/auth/config', methods=['GET']) +def auth_config(): + """Get authentication configuration""" + return jsonify({ + 'google_oauth_enabled': bool(auth_service.google_client_id), + 'guest_enabled': True + }) + @app.route('/api/users', methods=['POST']) def create_user(): """Create a new user""" @@ -320,52 +467,34 @@ def continue_quiz_conversation(quiz_id): except Exception as e: return jsonify({'error': str(e)}), 500 -# @app.route('/api/session/', methods=['GET']) -# def get_session(session_id): -# """Get session history""" -# try: -# if session_id not in sessions: -# return jsonify({'error': 'Session not found'}), 404 - -# session = sessions[session_id] -# session_quizzes = [] - -# for quiz_id in session['quizzes']: -# if quiz_id in quizzes: -# quiz = quizzes[quiz_id] -# session_quizzes.append({ -# 'quiz_id': quiz_id, -# 'completed': quiz['completed'] -# }) - -# return jsonify({ -# 'session_id': session_id, -# 'user_id': session['user_id'], -# 'created_at': session['created_at'], -# 'messages': session['messages'], -# 'quizzes': session_quizzes -# }) - -# except Exception as e: -# return jsonify({'error': str(e)}), 500 - -def get_all_conversations(): - """Get all conversations with id, lastMessage, and timestamp""" +def get_all_conversations(user_id): + """Get all conversations for a specific user with id, lastMessage, and timestamp""" try: conversations = [] - for session_id, session_data in sessions.items(): + # Get user's sessions directly + if user_id not in users: + return [] + + user_sessions = users[user_id].get('sessions', []) + + for session_id in user_sessions: + if session_id not in sessions: + continue + + session_data = sessions[session_id] + # Get the last message from conversation history last_message = "" timestamp = session_data.get('created_at', '') # Check if there are messages in the session - if session_data.get('messages'): + if session_data.get('messages') and len(session_data['messages']) > 0: # Get the last message from the messages array last_message_obj = session_data['messages'][-1] last_message = last_message_obj.get('user_message', '') timestamp = last_message_obj.get('timestamp', timestamp) - elif session_data.get('conversation_history'): + elif session_data.get('conversation_history') and len(session_data['conversation_history']) > 0: # Fallback to conversation history if no messages array # Find the last user message for msg in reversed(session_data['conversation_history']): @@ -375,8 +504,10 @@ def get_all_conversations(): conversations.append({ 'id': session_id, + 'title': session_data.get('title', ''), 'lastMessage': last_message, - 'timestamp': timestamp + 'timestamp': timestamp, + 'isActive': False }) # Sort by timestamp (most recent first) @@ -385,47 +516,21 @@ def get_all_conversations(): return conversations except Exception as e: - print(f"Error getting all conversations: {e}") + print(f"Error getting conversations for user {user_id}: {e}") return [] -@app.route('/api/conversations', methods=['GET']) -def get_all_conversations_endpoint(): - """Get all conversations endpoint""" +@app.route('/api/users//conversations', methods=['GET']) +def get_all_conversations_endpoint(user_id): + """Get all conversations for a specific user""" try: - conversations = get_all_conversations() + if user_id not in users: + return jsonify({'error': 'User not found'}), 404 + + conversations = get_all_conversations(user_id) return jsonify(conversations) except Exception as e: return jsonify({'error': str(e)}), 500 -# @app.route('/api/users//sessions', methods=['GET']) -# def get_user_sessions(user_id): -# """Get all sessions for a user""" -# try: -# if user_id not in users: -# return jsonify({'error': 'User not found'}), 404 - -# user = users[user_id] -# user_sessions = [] - -# for session_id in user['sessions']: -# if session_id in sessions: -# session = sessions[session_id] -# user_sessions.append({ -# 'session_id': session_id, -# 'created_at': session['created_at'], -# 'message_count': len(session['messages']), -# 'quiz_count': len(session['quizzes']) -# }) - -# return jsonify({ -# 'user_id': user_id, -# 'username': user['username'], -# 'sessions': user_sessions -# }) - -# except Exception as e: -# return jsonify({'error': str(e)}), 500 - if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=5000) diff --git a/backend/auth.py b/backend/auth.py new file mode 100644 index 0000000..b53d992 --- /dev/null +++ b/backend/auth.py @@ -0,0 +1,45 @@ +import os +import uuid +from datetime import datetime +from google.auth.transport import requests as google_requests +from google.oauth2 import id_token + +class SimpleAuth: + def __init__(self): + self.google_client_id = os.getenv('GOOGLE_CLIENT_ID') + + def verify_google_token(self, token): + """Verify Google OAuth token and return user info""" + try: + if not self.google_client_id: + return None + + # Verify the token with Google + idinfo = id_token.verify_oauth2_token( + token, google_requests.Request(), self.google_client_id + ) + + # Check if token is from correct issuer + if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']: + raise ValueError('Wrong issuer.') + + return { + 'id': idinfo['sub'], + 'email': idinfo['email'], + 'name': idinfo.get('name', ''), + 'picture': idinfo.get('picture', ''), + 'verified_email': idinfo.get('email_verified', False) + } + except Exception as e: + print(f"Token verification failed: {e}") + return None + + def create_guest_user(self, username): + """Create a guest user with just a username""" + return { + 'id': str(uuid.uuid4()), + 'name': username, + 'email': '', + 'picture': '', + 'is_guest': True + } diff --git a/backend/llm_service.py b/backend/llm_service.py index fb732a7..811b3e9 100644 --- a/backend/llm_service.py +++ b/backend/llm_service.py @@ -66,24 +66,24 @@ def get_chat_response(self, message: str, conversation_history: List[Dict] = Non except requests.exceptions.RequestException as e: return f"Error communicating with Cerebras API: {str(e)}" - def generate_quiz_questions(self, response_text: str, user_highlight: str = None) -> str: + def generate_quiz_questions(self, response_text: str, user_highlight: str = None, conversation_history: List[Dict] = None,) -> str: """ Generate a single long-answer question based on response text Args: response_text (str): Text to generate questions from - user_highlight + user_highlight (str): Text highlighted by the user to get quiz question on + coversation_history: Conversation history for context Returns: List[Dict]: List containing one long-answer question """ system_prompt = """"You are a question writer. - Write EXACTLY ONE long-answer question. - PRIORITIZE HIGHLIGHTS that also appear in . - Stay 100% within ; do not use outside knowledge or add new terms. - Output must be ONLY the question text—no rationale, no preface, no JSON, no bullets. - Max 55 words. No examples. No answers.""" + Write EXACTLY ONE brief long-answer question. + If there are , prioritize appearing in + Output must be related to —no rationale, no preface, no JSON, no bullets. + No examples. No answers.""" user_prompt = f""" {response_text} @@ -94,149 +94,111 @@ def generate_quiz_questions(self, response_text: str, user_highlight: str = None Requirements: - - Center the question on highlight terms that appear in . - - Ask about relationships, mechanisms, trade-offs, or synthesis already present in . - - Use ONLY terms that occur in . + - Focus on relationships, trade-offs, mechanisms, synthesis, or other questions that test mastering of knowledge from the conversation history """ - conversation = [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt}, - ] + conversation = conversation_history or list() + + conversation.append({"role": "system", "content": system_prompt}) + conversation.append({"role": "user", "content": user_prompt}) return self.get_chat_response(system_prompt, conversation_history = conversation) def evaluate_answer(self, conversation_history : List[Dict], question: str, user_answer: str) -> str: - """ - Returns evaluation of a long-answer response using LLM - """ - prompt = f""" - You are an educational assessment AI. Please evaluate the following student's answer to a comprehension question. - - ORIGINAL QUESTION: - {question} - - SOURCE TEXT: - {source_text} - - STUDENT'S ANSWER: - {user_answer} - - Please provide: - 1. A score from 0-100 based on accuracy, completeness, and understanding - 2. Specific feedback on what they got right and what could be improved - 3. An explanation of the key concepts they should have covered - - Format your response as: - SCORE: [0-100] - FEEDBACK: [detailed feedback] - EXPLANATION: [key concepts explanation] - """ + # """ + # Returns evaluation of a long-answer response using LLM + # """ + # prompt = f""" + # You are an educational assessment AI. Please evaluate the following student's answer to a comprehension question. + + # ORIGINAL QUESTION: + # {question} + + # SOURCE TEXT: + # {source_text} + + # STUDENT'S ANSWER: + # {user_answer} + + # Please provide: + # 1. A score from 0-100 based on accuracy, completeness, and understanding + # 2. Specific feedback on what they got right and what could be improved + # 3. An explanation of the key concepts they should have covered + + # Format your response as: + # SCORE: [0-100] + # FEEDBACK: [detailed feedback] + # EXPLANATION: [key concepts explanation] + # """ - judgment_response = self.get_chat_response(prompt) + # judgment_response = self.get_chat_response(prompt) - # Parse the response - lines = judgment_response.split('\n') - score = 0 - feedback = "" - explanation = "" + # # Parse the response + # lines = judgment_response.split('\n') + # score = 0 + # feedback = "" + # explanation = "" - for line in lines: - line = line.strip() - if line.startswith('SCORE:'): - try: - score = int(line.replace('SCORE:', '').strip()) - except: - score = 50 # Default score if parsing fails - elif line.startswith('FEEDBACK:'): - feedback = line.replace('FEEDBACK:', '').strip() - elif line.startswith('EXPLANATION:'): - explanation = line.replace('EXPLANATION:', '').strip() + # for line in lines: + # line = line.strip() + # if line.startswith('SCORE:'): + # try: + # score = int(line.replace('SCORE:', '').strip()) + # except: + # score = 50 # Default score if parsing fails + # elif line.startswith('FEEDBACK:'): + # feedback = line.replace('FEEDBACK:', '').strip() + # elif line.startswith('EXPLANATION:'): + # explanation = line.replace('EXPLANATION:', '').strip() - return { - "score": score, - "feedback": feedback, - "explanation": explanation, - "raw_judgment": judgment_response - } + # return { + # "score": score, + # "feedback": feedback, + # "explanation": explanation, + # "raw_judgment": judgment_response + # } + + return 'eval in progress' def get_title(self, response : str): """ Generates chat title from first LLM response in conversation history """ + return response[:10] def main(): - """Test all methods in the class""" - print("=" * 60) - print("TESTING LLM SERVICE") - print("=" * 60) - - # Initialize the LLM service llm = LLM() - - # # Test 3: Basic chat response (if API key is configured) - # print("\n3. Testing Basic Chat Response:") - # print("-" * 30) - # test_message = "What is artificial intelligence?" - # print(f"Test message: '{test_message}'") - - # gen = llm.get_chat_response(test_message) - # for response in gen: - # print(response,end='',flush=True) - - # # Test 4: Chat response with specific model - # print("\n4. Testing Chat Response with Specific Model:") - # print("-" * 30) - # specific_model = "llama3.1-8b" - # print(f"Using model: {specific_model}") - - # response_with_model = llm.get_chat_response(test_message, model=specific_model) - # history = "" - # for response in response_with_model: - # history += response - # print(response,end='',flush=True) - - # # Test 4.5: Chat response with conversation history - # print("\n4.5 Testing Chat Response with Conversation History:") - # print("-" * 30) - - # response_with_history = llm.get_chat_response('Why are these developments important?', conversation_history = [{'role':'user','content':test_message}, {'role':'assistant','content':history}]) - # for response in response_with_history: - # print(response,end='',flush=True) - - # Test 5: Generate simple quiz questions - print("\n5. Testing Simple Quiz Generation:") - print("-" * 30) - sample_text = "Artificial Intelligence (AI) is a branch of computer science that aims to create machines capable of intelligent behavior. Machine learning is a subset of AI that focuses on algorithms that can learn from data. Deep learning uses neural networks with multiple layers to process complex patterns." - print(f"Sample text: '{sample_text[:100]}...'") - - simple_quiz = llm.generate_quiz_questions(sample_text) - print(f"Generated simple quiz questions:") - for response in simple_quiz: + history = [] + + message = "What are some graph algorithms?" + # message = "What is the probability of rolling two dice and getting a sum of 7?" + response = "" + gen = llm.get_chat_response(message) + for s in gen: + response += s + print(s,end='',flush=True) + + history.append({'role':'user','content':message}) + history.append({'role':'assistant','content':response}) + + message = "I understand Dijkstra's, but I do not understand Bellman Ford" + # message = 'How about 8?' + response = '' + gen = llm.get_chat_response(message, conversation_history = history) + for s in gen: + response += s + print(s,end='',flush=True) + + history.append({'role':'user','content':message}) + history.append({'role':'assistant','content':response}) + + quiz_gen = llm.generate_quiz_questions(response, None, history) + for response in quiz_gen: print(response, end='',flush=True) - - # # Test 7: Error handling - test with empty message - # print("\n7. Testing Error Handling:") - # print("-" * 30) - # empty_response = llm.get_chat_response("") - # print(f"Empty message response: {empty_response}") - - # # Test 8: Edge cases for quiz generation - # print("\n8. Testing Edge Cases:") - # print("-" * 30) - - # # Test with very short text - # short_text = "AI is smart." - # short_quiz = llm.generate_quiz_questions(short_text, num_questions=5) - # print(f"Short text quiz (requested 5, got {len(short_quiz)}): {len(short_quiz)} questions") - - # # Test with empty text - # empty_quiz = llm.generate_quiz_questions("", num_questions=3) - # print(f"Empty text quiz: {len(empty_quiz)} questions") - - # print("\n" + "=" * 60) - # print("TESTING COMPLETE") - # print("=" * 60) + + answers = ["It just makes the algorithm run faster.", + "Relaxing edges V-1 times ensures that all paths of length up to V−1 are considered, so the algorithm can update distances enough times to reach every vertex.", + "Relaxing edges V-1 times guarantees that the shortest paths—which can use at most V−1 edges—are fully propagated throughout the graph. This step ensures each vertex's distance reflects the true minimum cost from the source, enabling Bellman-Ford to correctly compute all shortest paths and later detect negative cycles."] if __name__ == "__main__": diff --git a/backend/requirements.txt b/backend/requirements.txt index 0880145..41acd1e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -4,3 +4,4 @@ requests==2.31.0 python-dotenv==1.0.0 cerebras.cloud.sdk==1.50.1 pymongo==4.6.0 +google-auth==2.23.4 diff --git a/frontend/app/components/AuthPage.tsx b/frontend/app/components/AuthPage.tsx new file mode 100644 index 0000000..e6e1a4e --- /dev/null +++ b/frontend/app/components/AuthPage.tsx @@ -0,0 +1,228 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { GoogleLogin } from '@react-oauth/google'; + +interface User { + id: string; + username: string; + email: string; + picture?: string; + is_guest: boolean; +} + +interface AuthPageProps { + onLogin: (user: User) => void; +} + +export default function AuthPage({ onLogin }: AuthPageProps) { + const [showGuestForm, setShowGuestForm] = useState(false); + const [guestName, setGuestName] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [authConfig, setAuthConfig] = useState({ google_oauth_enabled: false, guest_enabled: true }); + + useEffect(() => { + // Check auth configuration + fetch('http://localhost:5000/api/auth/config') + .then(res => res.json()) + .then(config => setAuthConfig(config)) + .catch(() => setAuthConfig({ google_oauth_enabled: false, guest_enabled: true })); + }, []); + + const handleGoogleSuccess = async (credentialResponse: any) => { + setIsLoading(true); + setError(null); + + try { + const response = await fetch('http://localhost:5000/api/auth/google', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + id_token: credentialResponse.credential, + }), + }); + + const data = await response.json(); + + if (response.ok) { + localStorage.setItem('user_data', JSON.stringify(data.user)); + onLogin(data.user); + } else { + setError(data.error || 'Google authentication failed'); + } + } catch (error) { + console.error('OAuth error:', error); + setError('Network error. Make sure the backend is running.'); + } finally { + setIsLoading(false); + } + }; + + const handleGoogleError = () => { + setError('Google authentication failed'); + }; + + const handleGuestLogin = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!guestName.trim()) { + setError('Please enter your name'); + return; + } + + setIsLoading(true); + setError(null); + + try { + const response = await fetch('http://localhost:5000/api/auth/guest', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + username: guestName.trim() + }), + }); + + const data = await response.json(); + + if (response.ok) { + localStorage.setItem('user_data', JSON.stringify(data.user)); + onLogin(data.user); + } else { + setError(data.error || 'Guest login failed'); + } + } catch (error) { + console.error('Guest login error:', error); + setError('Network error. Make sure the backend is running.'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+

Cognify

+

Your AI-powered learning companion

+
+ + {error && ( +
+
{error}
+
+ )} + +
+ {!showGuestForm ? ( + <> + {/* Google OAuth Section */} + {authConfig.google_oauth_enabled && ( +
+

Sign in with Google

+ + {isLoading ? ( +
+
+ Signing in... +
+ ) : ( + + )} +
+ )} + + {/* Divider */} + {authConfig.google_oauth_enabled && authConfig.guest_enabled && ( +
+
+
+
+
+ or +
+
+ )} + + {/* Guest Option */} + {authConfig.guest_enabled && ( +
+ +
+ )} + + ) : ( + /* Guest Form */ +
+
+ + setGuestName(e.target.value)} + placeholder="Your name" + className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + disabled={isLoading} + /> +
+ +
+ + +
+
+ )} +
+ +
+

+ {showGuestForm + ? "Guest sessions are temporary and won't be saved" + : "Choose your preferred way to access Cognify" + } +

+
+
+
+ ); +} diff --git a/frontend/app/components/AuthProvider.tsx b/frontend/app/components/AuthProvider.tsx new file mode 100644 index 0000000..c41f044 --- /dev/null +++ b/frontend/app/components/AuthProvider.tsx @@ -0,0 +1,77 @@ +'use client'; + +import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; + +interface User { + id: string; + username: string; + email: string; + picture?: string; + is_guest: boolean; +} + +interface AuthContextType { + user: User | null; + login: (userData: User) => void; + logout: () => void; + isLoading: boolean; +} + +const AuthContext = createContext(undefined); + +export function useAuth() { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +} + +interface AuthProviderProps { + children: ReactNode; +} + +export function AuthProvider({ children }: AuthProviderProps) { + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + // Check for existing user data on mount + const storedUser = localStorage.getItem('user_data'); + + if (storedUser) { + try { + const userData = JSON.parse(storedUser); + setUser(userData); + } catch (error) { + console.error('Error parsing stored user data:', error); + localStorage.removeItem('user_data'); + } + } + + setIsLoading(false); + }, []); + + const login = (userData: User) => { + setUser(userData); + localStorage.setItem('user_data', JSON.stringify(userData)); + }; + + const logout = () => { + setUser(null); + localStorage.removeItem('user_data'); + }; + + const value = { + user, + login, + logout, + isLoading, + }; + + return ( + + {children} + + ); +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index fc05183..d2b3d29 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -1,6 +1,8 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import { GoogleOAuthProvider } from '@react-oauth/google'; +import { AuthProvider } from './components/AuthProvider'; const geistSans = Geist({ variable: "--font-geist-sans", @@ -27,7 +29,11 @@ export default function RootLayout({ - {children} + + + {children} + + ); diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index c770341..1f3dbf8 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,11 +1,54 @@ +'use client'; + +import { useAuth } from './components/AuthProvider'; +import AuthPage from './components/AuthPage'; import ChatBox from './ChatBox'; import MessageList from './MessageList'; export default function Home() { + const { user, isLoading, login, logout } = useAuth(); + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (!user) { + return ; + } + return (
- +
+ {/* User info and logout button */} +
+ {user.picture && ( + {user.username} + )} +
+ {user.username} + {user.is_guest && ( + Guest + )} +
+ +
+ + +
); } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 76ae498..73b29a9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.1.0", "dependencies": { + "@react-oauth/google": "^0.12.2", "next": "15.5.2", "react": "19.1.0", "react-dom": "19.1.0", @@ -951,6 +952,16 @@ "node": ">=12.4.0" } }, + "node_modules/@react-oauth/google": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.2.tgz", + "integrity": "sha512-d1GVm2uD4E44EJft2RbKtp8Z1fp/gK8Lb6KHgs3pHlM0PxCXGLaq8LLYQYENnN4xPWO1gkL4apBtlPKzpLvZwg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index df4c933..083007b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,7 @@ "lint": "eslint" }, "dependencies": { + "@react-oauth/google": "^0.12.2", "next": "15.5.2", "react": "19.1.0", "react-dom": "19.1.0",