diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..eafcb85 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,51 @@ +{ + "name": "Chat MCQ App", + "image": "mcr.microsoft.com/devcontainers/python:3-bullseye", + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + } + }, + "forwardPorts": [8000, 5173], + "portsAttributes": { + "8000": { + "label": "Backend API", + "onAutoForward": "notify", + "visibility": "public" + }, + "5173": { + "label": "Frontend", + "onAutoForward": "notify", + "visibility": "public" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" + ], + "settings": { + "terminal.integrated.defaultProfile.linux": "bash", + "terminal.integrated.profiles.linux": { + "bash": { + "path": "bash", + "icon": "terminal-bash" + } + } + } + } + }, + "initializeCommand": "echo 'Initializing...'", + "onCreateCommand": { + "Fix Script Permissions": "chmod +x ./.devcontainer/fix-permissions.sh && ./.devcontainer/fix-permissions.sh" + }, + "postCreateCommand": "bash ./.devcontainer/postCreateCommand.sh", + "postStartCommand": "bash ./.devcontainer/startup.sh", + "postAttachCommand": { + "Start Backend & Frontend": "bash -c 'echo \"To start the frontend in a separate terminal, run: bash ./.devcontainer/start-frontend.sh\"'" + }, + "remoteUser": "vscode" +} \ No newline at end of file diff --git a/.devcontainer/fix-permissions.sh b/.devcontainer/fix-permissions.sh new file mode 100644 index 0000000..3b4421f --- /dev/null +++ b/.devcontainer/fix-permissions.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +echo "🔑 Fixing permissions for all shell scripts..." + +# Find all .sh files in the .devcontainer directory and make them executable +find "$(dirname "$0")" -name "*.sh" -exec chmod +x {} \; + +# Make the main startup scripts executable +chmod +x "$(dirname "$0")/../startup.ps1" 2>/dev/null || true + +echo "✅ Permissions fixed successfully!" +echo "You can now run the startup scripts." \ No newline at end of file diff --git a/.devcontainer1/devcontainer.json b/.devcontainer1/devcontainer.json new file mode 100644 index 0000000..0e9dcfa --- /dev/null +++ b/.devcontainer1/devcontainer.json @@ -0,0 +1,47 @@ +{ + "name": "Chat MCQ App", + "image": "mcr.microsoft.com/devcontainers/python:3-bullseye", + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + } + }, + "forwardPorts": [8000, 5173], + "portsAttributes": { + "8000": { + "label": "Backend API", + "onAutoForward": "notify", + "visibility": "public" + }, + "5173": { + "label": "Frontend", + "onAutoForward": "notify", + "visibility": "public" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" + ], + "settings": { + "terminal.integrated.defaultProfile.linux": "bash", + "terminal.integrated.profiles.linux": { + "bash": { + "path": "bash", + "icon": "terminal-bash" + } + } + } + } + }, + "postCreateCommand": "bash ./.devcontainer/postCreateCommand.sh", + "postStartCommand": "bash ./.devcontainer/startup.sh", + "postAttachCommand": { + "Start Backend & Frontend": "bash -c 'echo \"To start the frontend in a separate terminal, run: bash ./.devcontainer/start-frontend.sh\"'" + }, + "remoteUser": "vscode" +} \ No newline at end of file diff --git a/.devcontainer1/postCreateCommand.sh b/.devcontainer1/postCreateCommand.sh new file mode 100644 index 0000000..b739be0 --- /dev/null +++ b/.devcontainer1/postCreateCommand.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -e + +echo "Setting up Chat MCQ Application..." + +# Install backend dependencies +cd /workspaces/$(basename $(pwd))/backend || cd backend +python -m venv venv +source venv/bin/activate +pip install --upgrade pip +pip install -r requirements.txt + +# Install frontend dependencies +cd /workspaces/$(basename $(pwd))/frontend || cd ../frontend +npm install + +echo "✅ Dependencies installed successfully!" \ No newline at end of file diff --git a/.devcontainer1/start-frontend.sh b/.devcontainer1/start-frontend.sh new file mode 100644 index 0000000..de93bfc --- /dev/null +++ b/.devcontainer1/start-frontend.sh @@ -0,0 +1,53 @@ +#!/bin/bash +set -e + +echo "===============================================" +echo "🚀 FRONTEND STARTUP SCRIPT - $(date)" +echo "===============================================" + +# Get the workspace root directory +WORKSPACE_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) +echo "Workspace root: $WORKSPACE_ROOT" + +# Create logs directory if it doesn't exist +mkdir -p $WORKSPACE_ROOT/backend/logs + +# Navigate to frontend directory +cd $WORKSPACE_ROOT/frontend + +echo "📌 ENVIRONMENT CHECK:" +echo "- Working Directory: $(pwd)" +echo "- Node Version: $(node -v)" +echo "- NPM Version: $(npm -v)" + +echo "📌 CHECKING FRONTEND FILES:" +if [ -f "package.json" ]; then + echo "✅ package.json found" + cat package.json | grep -E '"name"|"version"|"dev"' +else + echo "❌ package.json not found!" + exit 1 +fi + +if [ -f "vite.config.ts" ]; then + echo "✅ vite.config.ts found" +else + echo "❌ vite.config.ts not found!" +fi + +# Kill any existing Vite processes +echo "📌 CHECKING FOR EXISTING PROCESSES:" +pkill -f "vite" || echo "No vite process found to kill" + +# Ensure dependencies are installed +if [ ! -d "node_modules" ] || [ ! -d "node_modules/vite" ]; then + echo "📥 Installing npm dependencies..." + npm ci || npm install +fi + +# Start frontend in foreground to better see errors +echo "🚀 STARTING FRONTEND SERVER" +echo "- Command: npm run dev -- --host 0.0.0.0 --port 5173" + +# Direct output to both console and log file +exec npm run dev -- --host 0.0.0.0 --port 5173 | tee $WORKSPACE_ROOT/backend/logs/frontend.log \ No newline at end of file diff --git a/.devcontainer1/startup.sh b/.devcontainer1/startup.sh new file mode 100644 index 0000000..e93d8ca --- /dev/null +++ b/.devcontainer1/startup.sh @@ -0,0 +1,98 @@ +#!/bin/bash +set -e + +echo "===============================================" +echo "🔍 BACKEND STARTUP SCRIPT - $(date)" +echo "===============================================" + +# Get the workspace root directory +WORKSPACE_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) +echo "Workspace root: $WORKSPACE_ROOT" + +# Check environment +echo "📌 ENVIRONMENT CHECK:" +echo "- Working Directory: $(pwd)" +echo "- User: $(whoami)" +echo "- Python Version: $(python --version 2>&1)" +echo "- Bash Version: $BASH_VERSION" + +# Create logs directory for backend +mkdir -p $WORKSPACE_ROOT/backend/logs +echo "📁 Created logs directory: $WORKSPACE_ROOT/backend/logs" + +echo "🚀 Starting Backend Server..." + +# Navigate to backend directory +echo "📌 NAVIGATING TO BACKEND DIRECTORY" +cd $WORKSPACE_ROOT/backend +echo "- Working Directory now: $(pwd)" + +echo "📌 CHECKING BACKEND FILES:" +echo "- Main.py exists: $([ -f main.py ] && echo 'Yes' || echo 'No')" +echo "- Requirements.txt exists: $([ -f requirements.txt ] && echo 'Yes' || echo 'No')" + +# Check for virtual environment and activate it +echo "📌 CHECKING VIRTUAL ENVIRONMENT:" +if [ -d "venv" ]; then + echo "- Using venv virtual environment" + VENV_DIR="venv" +elif [ -d "env" ]; then + echo "- Using env virtual environment" + VENV_DIR="env" +else + echo "⚠️ No virtual environment found (venv or env), creating one..." + python -m venv env + VENV_DIR="env" +fi + +echo "🔄 Starting backend server at http://localhost:8000" +echo "- Activating virtual environment..." + +# Activate venv +echo "- Attempting to source $VENV_DIR/bin/activate" +source $VENV_DIR/bin/activate + +# Install dependencies if needed +if [ ! -f "$VENV_DIR/lib/python*/site-packages/fastapi" ]; then + echo "📥 Installing backend dependencies..." + pip install -r requirements.txt +fi + +# Log Python path after venv activation +echo "- Using Python from: $(which python 2>/dev/null || echo 'Unknown')" +echo "- Python version: $(python --version 2>&1)" + +# Kill any existing uvicorn processes +echo "📌 CHECKING FOR EXISTING PROCESSES:" +pkill -f "uvicorn" || echo "No uvicorn process found to kill" + +echo "- Starting uvicorn server with command: python -m uvicorn main:app --reload --host 0.0.0.0" +python -m uvicorn main:app --reload --host 0.0.0.0 > ./logs/backend.log 2>&1 & +BACKEND_PID=$! +echo $BACKEND_PID > /tmp/backend.pid +echo "✅ Backend server started with PID $BACKEND_PID" + +# Check if backend started successfully +sleep 2 +if ps -p $BACKEND_PID > /dev/null; then + echo "✅ Backend process verified running" + echo "- First 10 lines of backend log:" + head -n 10 ./logs/backend.log +else + echo "❌ ERROR: Backend process failed to start!" + echo "- Log content:" + cat ./logs/backend.log + exit 1 +fi + +echo "===============================================" +echo "✅ Backend startup completed!" +echo "🔌 Backend API: http://localhost:8000" +echo "📋 Backend logs are available at: ./logs/backend.log" +echo "" +echo "💻 To start the frontend, open a new terminal and run:" +echo " bash ./.devcontainer/start-frontend.sh" +echo "===============================================" + +# Keep script running to maintain backend process +wait \ No newline at end of file diff --git a/.devcontainer1/stop.sh b/.devcontainer1/stop.sh new file mode 100644 index 0000000..4c3fe0e --- /dev/null +++ b/.devcontainer1/stop.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +echo "Stopping Chat MCQ Application services..." + +# Stop the backend server +if [ -f /tmp/backend.pid ]; then + BACKEND_PID=$(cat /tmp/backend.pid) + if ps -p $BACKEND_PID > /dev/null; then + echo "Stopping backend server (PID: $BACKEND_PID)" + kill $BACKEND_PID + else + echo "Backend server is not running" + fi + rm /tmp/backend.pid +fi + +# Stop the frontend server +if [ -f /tmp/frontend.pid ]; then + FRONTEND_PID=$(cat /tmp/frontend.pid) + if ps -p $FRONTEND_PID > /dev/null; then + echo "Stopping frontend server (PID: $FRONTEND_PID)" + kill $FRONTEND_PID + else + echo "Frontend server is not running" + fi + rm /tmp/frontend.pid +fi + +echo "✅ All services stopped" \ No newline at end of file diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..437e775 --- /dev/null +++ b/.env.sample @@ -0,0 +1,6 @@ +# Backend Environment Variables +OPENAI_API_KEY=your-openai-api-key-here +FRONTEND_URL=https://mcq-frontend.onrender.com + +# Frontend Environment Variables +VITE_API_URL=https://mcq-backend.onrender.com diff --git a/.github/workflows/render-deploy.yml b/.github/workflows/render-deploy.yml new file mode 100644 index 0000000..d8418cb --- /dev/null +++ b/.github/workflows/render-deploy.yml @@ -0,0 +1,31 @@ +name: Deploy to Render + +on: + push: + branches: + - main + - master # Including master in case you use that as your default branch + +jobs: + deploy: + name: Deploy to Render + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Deploy Backend to Render + uses: JorgeLNJunior/render-deploy@v1.4.3 + with: + service_id: ${{ secrets.RENDER_BACKEND_SERVICE_ID }} + api_key: ${{ secrets.RENDER_API_KEY }} + wait_deploy: true + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Deploy Frontend to Render + uses: JorgeLNJunior/render-deploy@v1.4.3 + with: + service_id: ${{ secrets.RENDER_FRONTEND_SERVICE_ID }} + api_key: ${{ secrets.RENDER_API_KEY }} + wait_deploy: true + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/backend/app/api/chat.py b/backend/app/api/chat.py index 0a7ef74..605e401 100644 --- a/backend/app/api/chat.py +++ b/backend/app/api/chat.py @@ -14,7 +14,9 @@ async def process_chat(request: ChatRequest): """Process a chat message and return a response or MCQs""" try: - logger.debug(f"Received chat request: {request}") + # Enhanced logging for debugging + logger.info(f"POST /chat endpoint called with message: {request.message[:30]}...") + logger.debug(f"Full chat request details: {request}") user_query = request.message # Use LLM to detect MCQ intent diff --git a/backend/app/prompts/formulate_mcqs/prompt.txt b/backend/app/prompts/formulate_mcqs/prompt.txt index 7157008..a7012fa 100644 --- a/backend/app/prompts/formulate_mcqs/prompt.txt +++ b/backend/app/prompts/formulate_mcqs/prompt.txt @@ -5,6 +5,75 @@ Each question should have: 2. Four options labeled A, B, C, and D 3. One correct answer 4. A brief explanation for the correct answer +5. A difficulty level tag: "Difficult", "Medium", or "Easy" + +If the topic is related to UPSC (Union Public Service Commission) examinations, especially for subjects like History, Polity, Geography, Economics, Environment, Science & Technology, or Current Affairs: + +Include a variety of question formats that commonly appear in UPSC prelims. For each topic, create a mix of the following question types: + +1. Assertion-Reasoning type questions, formatted as: + Assertion (A): [Statement 1] + Reason (R): [Statement 2] + - Option A: Both A and R are true, and R is the correct explanation of A + - Option B: Both A and R are true, but R is not the correct explanation of A + - Option C: A is true but R is false + - Option D: Both A and R are false + +2. Statement-based questions with multiple statements, such as: + Consider the following statements: + I. [Statement 1] + II. [Statement 2] + III. [Statement 3] + Which of the statements given above is/are correct? + - Option A: I and II only + - Option B: II and III only + - Option C: I and III only + - Option D: All of the above + +3. Match the following / matching pairs format: + Match List I with List II and select the correct answer using the codes given below: + List I List II + A. Item 1 1. Item X + B. Item 2 2. Item Y + C. Item 3 3. Item Z + D. Item 4 4. Item W + Codes: + - Option A: A-1, B-2, C-3, D-4 + - Option B: A-2, B-3, C-4, D-1 + - Option C: A-3, B-1, C-2, D-4 + - Option D: A-4, B-3, C-2, D-1 + +4. Chronological ordering questions: + Arrange the following events in chronological order: + 1. [Event A] + 2. [Event B] + 3. [Event C] + 4. [Event D] + Select the correct answer using the codes below: + - Option A: 1, 2, 3, 4 + - Option B: 4, 3, 2, 1 + - Option C: 2, 1, 4, 3 + - Option D: 3, 1, 2, 4 + +5. "Consider the following" questions: + With reference to [topic], consider the following statements: + 1. [Statement 1] + 2. [Statement 2] + 3. [Statement 3] + Which of the above statements is/are correct? + - Option A: 1 only + - Option B: 1 and 2 only + - Option C: 2 and 3 only + - Option D: All of the above + +6. For Geography topics, include map-based or location identification questions + +7. For Economics or Science topics, include data interpretation questions with small datasets + +Ensure that you generate an equal mix of difficulty levels: +- 1/3 of questions should be difficult (challenging conceptual questions) +- 1/3 of questions should be medium difficulty (require good understanding) +- 1/3 of questions should be easy (test basic knowledge) Format your response as a JSON object with the following structure: {{ @@ -18,7 +87,9 @@ Format your response as a JSON object with the following structure: "D": "Fourth option" }}, "correct_answer": "A/B/C/D", - "explanation": "Explanation for why this is correct" + "explanation": "Explanation for why this is correct", + "difficulty": "Difficult/Medium/Easy", + "question_type": "Standard/Assertion-Reasoning/Statement-Based/Matching/Chronological/Consider-Following/Map-Based/Data-Interpretation" }}, // Additional questions... ] diff --git a/backend/app/prompts/formulate_mcqs/system.txt b/backend/app/prompts/formulate_mcqs/system.txt index e56e0ed..2a69acf 100644 --- a/backend/app/prompts/formulate_mcqs/system.txt +++ b/backend/app/prompts/formulate_mcqs/system.txt @@ -3,4 +3,37 @@ Your questions should be clear, accurate, and educational. Generate questions with exactly 4 options (A, B, C, D) for each question. Each question should have only one correct answer. Provide an explanation for each answer that helps the user understand why it's correct. + +For UPSC (Union Public Service Commission) related topics, include a variety of question formats commonly found in UPSC prelims: + +1. Assertion-Reasoning type questions, where: + - Option A: Both assertion and reason are true, and the reason is the correct explanation of the assertion + - Option B: Both assertion and reason are true, but the reason is not the correct explanation of the assertion + - Option C: The assertion is true but the reason is false + - Option D: Both assertion and reason are false + +2. Statement-based questions with multiple statements (I, II, III, IV) where options include different combinations of correct/incorrect statements + +3. Match the following / Matching pairs format questions + +4. Chronological ordering questions (arranging events, policies, discoveries in correct sequence) + +5. "Consider the following" questions that test multiple facts about a topic + +6. Map-based or location identification type questions for Geography + +7. Data interpretation questions with small tables/charts + +8. "Which of the above statements is/are correct?" style questions with options like: + - Option A: I and II only + - Option B: II and III only + - Option C: I and III only + - Option D: All of the above + +For each topic, create a mix of difficulty levels: +- 1/3 of questions should be difficult (challenging even for well-prepared students) +- 1/3 of questions should be of medium difficulty (require good understanding but not extremely challenging) +- 1/3 of questions should be relatively easy (basic understanding of the topic and simple structured mcqs) + +Give the questions well formatted. Format your response as a JSON object. \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 764fd7b..329ca88 100644 --- a/backend/main.py +++ b/backend/main.py @@ -80,6 +80,41 @@ async def global_exception_middleware(request: Request, call_next): app.include_router(chat.router, prefix="/api") app.include_router(mcq.router, prefix="/api") +# Add a fallback endpoint at the root level for debugging purposes +# This is commented out to avoid route conflicts +# @app.post("/api/chat") +# async def chat_fallback(request: Request): +# logger.info("Fallback /api/chat endpoint called directly") +# # Forward to the actual chat handler by extracting the request body +# try: +# body = await request.json() +# # Import the actual request model +# from app.models.chat import ChatRequest +# # Create the request object and forward to the handler +# chat_request = ChatRequest(**body) +# # Call the actual handler +# return await chat.process_chat(chat_request) +# except Exception as e: +# logger.error(f"Error in chat_fallback: {str(e)}") +# logger.error(traceback.format_exc()) +# return JSONResponse( +# status_code=500, +# content={"detail": f"Error processing request in fallback: {str(e)}"} +# ) + +# Direct debugging endpoint to check routing +@app.get("/api/debug/routes") +def debug_routes(): + """Endpoint to debug available routes""" + routes = [] + for route in app.routes: + routes.append({ + "path": route.path, + "name": route.name, + "methods": list(route.methods) if hasattr(route, "methods") else None + }) + return {"routes": routes} + # Add OPTIONS route handler for CORS preflight requests @app.options("/{rest_of_path:path}") async def options_route(rest_of_path: str): @@ -90,11 +125,10 @@ def read_root(): logger.info("Root endpoint accessed") return {"message": "Welcome to the Chat MCQ App!"} -# Health check endpoint for debugging @app.get("/api/health") -def health_check(): - logger.info("Health check endpoint accessed") - return {"status": "ok", "message": "Backend is running"} +async def health_check(): + """Health check endpoint for Render""" + return {"status": "healthy", "service": "mcq-backend"} # Run the server when the script is executed directly (for local development) if __name__ == "__main__": diff --git a/frontend/src/App.css b/frontend/src/App.css index ccb762d..04c81fd 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,10 +1,4 @@ -:root { - --primary-color: #1976d2; - --secondary-color: #4caf50; - --error-color: #f44336; - --background-color: #f9f9f9; - --border-color: #e0e0e0; -} +/* We're using the CSS variables from theme.css instead of defining them here */ * { box-sizing: border-box; @@ -13,13 +7,15 @@ } body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - background-color: var(--background-color); + background-color: var(--bg-primary); + color: var(--text-primary); height: 100vh; width: 100vw; overflow: hidden; + transition: background-color 0.3s ease, color 0.3s ease; } #root { @@ -33,4 +29,107 @@ body { height: 100%; width: 100%; overflow: auto; + padding: 1rem; +} + +.app-header { + background: var(--header-gradient); + padding: 1rem 2rem; + color: white; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); +} + +.app-header h1 { + margin: 0; + font-weight: 600; + font-size: 1.5rem; +} + +/* Modern card styles */ +.card { + background-color: var(--bg-secondary); + border-radius: 12px; + padding: 1.5rem; + box-shadow: var(--card-shadow); + margin-bottom: 1rem; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.card:hover { + transform: translateY(-2px); + box-shadow: 0 12px 20px -5px rgba(0, 0, 0, 0.1); +} + +/* Button styles */ +button { + border-radius: 8px; + padding: 0.6rem 1.2rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + border: none; +} + +button:focus { + outline: 2px solid var(--accent-primary); + outline-offset: 2px; +} + +/* Make the suggestion container look modern and attractive */ +.suggestion-container { + margin-top: 1.5rem; + padding: 1rem 0; +} + +/* Navigation controls styles */ +.nav-controls { + display: flex; + align-items: center; + gap: 1.25rem; +} + +.nav-link { + text-decoration: none; + color: rgba(255, 255, 255, 0.9); + font-weight: 500; + transition: all 0.2s; + padding: 0.5rem 0; + position: relative; +} + +.nav-link::after { + content: ''; + position: absolute; + width: 0; + height: 2px; + bottom: 0; + left: 0; + background-color: white; + transition: width 0.2s ease; +} + +.nav-link:hover { + color: white; +} + +.nav-link:hover::after { + width: 100%; +} + +.nav-link.sign-up { + background-color: rgba(255, 255, 255, 0.2); + color: white; + padding: 0.5rem 1.25rem; + border-radius: 8px; + backdrop-filter: blur(10px); + transition: all 0.2s; + border: 1px solid rgba(255, 255, 255, 0.3); +} + +.nav-link.sign-up:hover { + background-color: rgba(255, 255, 255, 0.3); + transform: translateY(-2px); } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 44b70b0..2e5ce22 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,7 +6,10 @@ import Layout from './components/Layout'; import ErrorBoundary from './components/ErrorBoundary'; import { ChatSession, Message, MCQQuiz, MCQQuestion, MCQOption } from './types'; import { sendMessage, requestMCQs } from './services/api'; +import { ThemeProvider } from './context/ThemeContext'; +import { ThemeToggle } from './components/Common/ThemeToggle'; import './App.css'; +import './styles/theme.css'; // Define a new type that extends Message to include MCQ data interface MCQMessage extends Message { @@ -83,7 +86,9 @@ const App: React.FC = () => { question: q.question || 'Question', options: options, correctAnswerId: q.correct_answer || '', - explanation: q.explanation || '' + explanation: q.explanation || '', + topic: q.topic || backendData.topic || '', + relatedTopics: q.related_topics || q.relatedTopics || [] }; return question; @@ -224,38 +229,52 @@ const App: React.FC = () => { }; return ( - - } - main={ -
- { - if (message.mcqQuiz) { - return ( - - handleQuizComplete(message.mcqQuiz!.id)} - /> - - ); - } - return null; - }} + + +

StudyBuddy

+
+ Login + Sign Up + +
+
+ } + sidebar={ + - - } - /> + } + main={ +
+ { + if (message.mcqQuiz) { + return ( + + handleQuizComplete(message.mcqQuiz!.id)} + // No need to add onEvaluation prop here as the ChatPanel component + // will automatically add it through renderMCQWithEvaluation function + /> + + ); + } + return null; + }} + /> +
+ } + /> + ); }; diff --git a/frontend/src/assets/styles/App.css b/frontend/src/assets/styles/App.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/assets/styles/Chat.css b/frontend/src/assets/styles/Chat.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/assets/styles/MCQ.css b/frontend/src/assets/styles/MCQ.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/components/Chat/ChatContainer.tsx b/frontend/src/components/Chat/ChatContainer.tsx new file mode 100644 index 0000000..b91452b --- /dev/null +++ b/frontend/src/components/Chat/ChatContainer.tsx @@ -0,0 +1,75 @@ +import React, { useState, useEffect } from 'react'; +import { Message, MCQEvaluation } from '../../types'; +import { SuggestionBubbles } from '../Common/SuggestionBubble'; +import { getSuggestedTopic } from '../../services/api'; + +interface ChatContainerProps { + messages: Message[]; + sendMessage: (text: string) => void; + mcqEvaluation: MCQEvaluation | null; +} + +export const ChatContainer: React.FC = ({ + messages, + sendMessage, + mcqEvaluation +}) => { + const [suggestions, setSuggestions] = useState<{ text: string; action: () => void }[]>([]); + + useEffect(() => { + if (mcqEvaluation) { + generateSuggestions(mcqEvaluation); + } + }, [mcqEvaluation]); + + const generateSuggestions = async (evaluation: MCQEvaluation) => { + if (evaluation.allCorrect) { + // All answers correct + const suggestedTopic = await getSuggestedTopic(); + + setSuggestions([ + { + text: "Test me more", + action: () => sendMessage("I'd like more MCQs on the same topic please") + }, + { + text: `Ask me questions on ${suggestedTopic}`, + action: () => sendMessage(`I'd like to try MCQs on ${suggestedTopic}`) + } + ]); + } else if (evaluation.suggestedTopics && evaluation.suggestedTopics.length > 0) { + // Incorrect answers, show topics that need improvement + const topicSuggestions = evaluation.suggestedTopics.map(suggestion => ({ + text: `Dive deep into ${suggestion.topic} with more MCQs`, + action: () => sendMessage(`I'd like to learn more about ${suggestion.topic}`) + })); + + if (topicSuggestions.length === 1) { + // Add a fallback option if only one topic suggestion + topicSuggestions.push({ + text: "Help me understand the topics better", + action: () => sendMessage("Can you explain the concepts I got wrong?") + }); + } + + setSuggestions(topicSuggestions); + } + }; + + return ( +
+ {messages.map((message, index) => ( +
+
{message.content}
+
+ ))} + + {suggestions.length > 0 && ( + + )} +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/Chat/ChatHistory.css b/frontend/src/components/Chat/ChatHistory.css index e7332f5..4829569 100644 --- a/frontend/src/components/Chat/ChatHistory.css +++ b/frontend/src/components/Chat/ChatHistory.css @@ -2,51 +2,143 @@ display: flex; flex-direction: column; height: 100%; + width: 100%; } .new-chat-button { - margin-bottom: 1rem; + margin-bottom: 1.25rem; } .new-chat-button button { width: 100%; - padding: 0.75rem; - background-color: #4caf50; + padding: 0.9rem 0.75rem; + background: var(--accent-primary); color: white; border: none; - border-radius: 4px; + border-radius: 10px; cursor: pointer; - font-weight: 500; + font-weight: 600; + font-size: 0.95rem; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + transition: all 0.2s; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + white-space: nowrap; + overflow: hidden; } .new-chat-button button:hover { - background-color: #45a049; + background: var(--accent-primary); + transform: translateY(-2px); + box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15); +} + +.new-chat-button button svg { + width: 18px; + height: 18px; + min-width: 18px; /* Ensure icon doesn't shrink */ } .chat-sessions { overflow-y: auto; + display: flex; + flex-direction: column; + gap: 0.5rem; } .chat-session { - padding: 0.75rem; + padding: 0.85rem 1rem; cursor: pointer; - border-radius: 4px; - margin-bottom: 0.5rem; - background-color: #f9f9f9; - transition: background-color 0.2s; + border-radius: 10px; + background-color: var(--bg-primary); + border: 1px solid var(--border-color); + transition: all 0.2s; + display: flex; + align-items: center; + gap: 0.75rem; + position: relative; } .chat-session:hover { - background-color: #e9e9e9; + background-color: var(--bubble-bg); + transform: translateY(-1px); + border-color: var(--accent-primary); } .chat-session.active { - background-color: #e0e0e0; + background-color: var(--bubble-bg); + border-color: var(--accent-primary); font-weight: 500; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); +} + +.chat-session-icon { + color: var(--text-secondary); + font-size: 1.1rem; + min-width: 20px; /* Ensure icon doesn't shrink */ +} + +.chat-session.active .chat-session-icon { + color: var(--accent-primary); } .chat-session-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + color: var(--text-primary); + font-size: 0.95rem; + flex: 1; +} + +.close-icon { + display: flex; + align-items: center; + justify-content: center; + color: var(--text-secondary); + width: 24px; + height: 24px; + border-radius: 50%; + transition: all 0.2s; + opacity: 0.7; +} + +.close-icon:hover { + background-color: var(--bubble-bg); + color: var(--accent-primary); + opacity: 1; +} + +/* Handling collapsed sidebar state */ +@media (min-width: 769px) { + .sidebar:not(:hover):not(.visible) .new-chat-button button span, + .sidebar:not(:hover):not(.visible) .chat-session-title, + .sidebar:not(:hover):not(.visible) .close-icon { + opacity: 0; + width: 0; + display: none; + } + + .sidebar:not(:hover):not(.visible) .chat-session { + justify-content: center; + padding: 0.85rem 0.5rem; + } + + .sidebar:not(:hover):not(.visible) .chat-session-icon { + margin: 0 auto; + font-size: 1.3rem; + } +} + +/* Mobile responsive styles */ +@media (max-width: 768px) { + .new-chat-button button { + padding: 0.75rem 0.5rem; + } + + .chat-session { + padding: 0.75rem 0.85rem; + } } \ No newline at end of file diff --git a/frontend/src/components/Chat/ChatHistory.tsx b/frontend/src/components/Chat/ChatHistory.tsx index 20db43f..7e1cf24 100644 --- a/frontend/src/components/Chat/ChatHistory.tsx +++ b/frontend/src/components/Chat/ChatHistory.tsx @@ -7,18 +7,41 @@ interface ChatHistoryProps { currentSessionId: string; onSelectSession: (sessionId: string) => void; onNewChat: () => void; + onCloseSession?: (sessionId: string) => void; // Added prop for closing sessions } +const NewChatIcon = () => ( + + + +); + +const ChatIcon = () => ( + + + +); + +const CloseIcon = () => ( + + + +); + const ChatHistory: React.FC = ({ sessions, currentSessionId, onSelectSession, - onNewChat + onNewChat, + onCloseSession }) => { return (
- +
{sessions.map((session) => ( @@ -27,7 +50,21 @@ const ChatHistory: React.FC = ({ className={`chat-session ${session.id === currentSessionId ? 'active' : ''}`} onClick={() => onSelectSession(session.id)} > + + +
{session.title}
+ {onCloseSession && ( + { + e.stopPropagation(); + onCloseSession(session.id); + }} + > + + + )}
))}
diff --git a/frontend/src/components/Chat/ChatInput.css b/frontend/src/components/Chat/ChatInput.css index 2cf302a..25c00b8 100644 --- a/frontend/src/components/Chat/ChatInput.css +++ b/frontend/src/components/Chat/ChatInput.css @@ -2,34 +2,79 @@ display: flex; margin-top: auto; padding: 1rem; - border-top: 1px solid #e0e0e0; - background-color: #fff; + background-color: var(--bg-secondary); +} + +.chat-input-container { + display: flex; + width: 100%; + position: relative; + border: 1px solid var(--border-color); + border-radius: 12px; + background-color: var(--bg-primary); + overflow: hidden; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); + transition: all 0.2s ease; +} + +.chat-input-container:focus-within { + border-color: var(--accent-primary); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + transform: translateY(-1px); } .chat-input input { flex: 1; - padding: 0.75rem; - border: 1px solid #ccc; - border-radius: 4px; - margin-right: 0.5rem; + padding: 0.9rem 1rem; + border: none; font-size: 1rem; + background-color: transparent; + color: var(--text-primary); + outline: none; + font-family: inherit; +} + +.chat-input input::placeholder { + color: var(--text-secondary); + opacity: 0.7; } .chat-input button { - padding: 0.75rem 1.5rem; - background-color: #1976d2; + padding: 0.75rem 1.25rem; + background-color: var(--accent-primary); color: white; border: none; - border-radius: 4px; + border-radius: 0; cursor: pointer; font-weight: 500; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; } .chat-input button:hover:not(:disabled) { - background-color: #1565c0; + background-color: var(--accent-primary); + filter: brightness(110%); } .chat-input button:disabled { - background-color: #cccccc; + background-color: var(--text-secondary); cursor: not-allowed; + opacity: 0.6; +} + +.chat-input button svg { + width: 20px; + height: 20px; +} + +@media (max-width: 640px) { + .chat-input button span { + display: none; + } + + .chat-input button { + padding: 0.75rem; + } } \ No newline at end of file diff --git a/frontend/src/components/Chat/ChatInput.tsx b/frontend/src/components/Chat/ChatInput.tsx index b27ec86..8889f05 100644 --- a/frontend/src/components/Chat/ChatInput.tsx +++ b/frontend/src/components/Chat/ChatInput.tsx @@ -6,6 +6,12 @@ interface ChatInputProps { isLoading: boolean; } +const SendIcon = () => ( + + + +); + const ChatInput: React.FC = ({ onSend, isLoading }) => { const [message, setMessage] = useState(''); @@ -19,16 +25,21 @@ const ChatInput: React.FC = ({ onSend, isLoading }) => { return (
- setMessage(e.target.value)} - placeholder="Type your message here..." - disabled={isLoading} - /> - +
+ setMessage(e.target.value)} + placeholder="Type your message here..." + disabled={isLoading} + /> + +
); }; diff --git a/frontend/src/components/Chat/ChatMessage.css b/frontend/src/components/Chat/ChatMessage.css index 36bb566..b515c94 100644 --- a/frontend/src/components/Chat/ChatMessage.css +++ b/frontend/src/components/Chat/ChatMessage.css @@ -1,24 +1,66 @@ .chat-message { display: flex; - margin-bottom: 1rem; - padding: 1rem; - border-radius: 8px; + margin-bottom: 1.25rem; + padding: 1rem 1.25rem; + border-radius: 16px; + animation: fadeIn 0.3s ease; + transition: all 0.2s ease; + max-width: 85%; } .chat-message.user { - background-color: #f0f0f0; + background-color: var(--accent-primary); + color: white; + margin-left: auto; + border-radius: 18px 4px 18px 18px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } .chat-message.assistant { - background-color: #f9f9f9; + background-color: var(--bg-secondary); + color: var(--text-primary); + margin-right: auto; + border-radius: 4px 18px 18px 18px; + box-shadow: var(--card-shadow); + border: 1px solid var(--border-color); } .avatar { margin-right: 1rem; font-size: 1.5rem; + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 50%; + flex-shrink: 0; +} + +.chat-message.assistant .avatar { + background-color: var(--accent-secondary); + color: white; +} + +.chat-message.user .avatar { + background-color: white; + color: var(--accent-primary); } .message-content { flex: 1; - line-height: 1.5; + line-height: 1.6; + overflow-wrap: break-word; + word-break: break-word; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } } \ No newline at end of file diff --git a/frontend/src/components/Chat/ChatMessage.tsx b/frontend/src/components/Chat/ChatMessage.tsx index 3498317..654c355 100644 --- a/frontend/src/components/Chat/ChatMessage.tsx +++ b/frontend/src/components/Chat/ChatMessage.tsx @@ -6,13 +6,26 @@ interface ChatMessageProps { message: Message; } +const UserIcon = () => ( + + + +); + +const AssistantIcon = () => ( + + + + +); + const ChatMessage: React.FC = ({ message }) => { const isUser = message.role === 'user'; return (
- {isUser ? '👤' : '🤖'} + {isUser ? : }
{message.content} diff --git a/frontend/src/components/Chat/ChatPanel.css b/frontend/src/components/Chat/ChatPanel.css index efad68f..1e5effc 100644 --- a/frontend/src/components/Chat/ChatPanel.css +++ b/frontend/src/components/Chat/ChatPanel.css @@ -11,6 +11,7 @@ overflow-y: auto; padding: 1rem; padding-bottom: 80px; /* Extra space to prevent content from being hidden behind input */ + scroll-behavior: smooth; } .input-container { @@ -18,9 +19,12 @@ bottom: 0; left: 0; right: 0; - background-color: #fff; - border-top: 1px solid #e0e0e0; + background-color: var(--bg-secondary); + border-top: 1px solid var(--border-color); z-index: 10; + padding: 0.75rem 1rem; + border-radius: 0 0 12px 12px; + box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05); } .mcq-container { @@ -28,11 +32,57 @@ animation: fadeIn 0.5s ease; } +.message-container { + padding: 0.75rem; + margin: 0.5rem 0; + border-radius: 12px; + max-width: 85%; + position: relative; + animation: fadeIn 0.3s ease; +} + +.user-message { + background: var(--accent-primary); + color: white; + margin-left: auto; + border-radius: 18px 4px 18px 18px; +} + +.ai-message { + background: var(--bg-secondary); + color: var(--text-primary); + margin-right: auto; + border-radius: 4px 18px 18px 18px; + box-shadow: var(--card-shadow); +} + +.message-content { + padding: 0.5rem; +} + +.suggestion-container { + margin-top: 1.5rem; + padding: 1rem 0; + animation: slideUp 0.4s ease; + border-top: 1px solid var(--border-color); +} + @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + .empty-state { display: flex; flex-direction: column; @@ -40,15 +90,24 @@ justify-content: center; height: 100%; text-align: center; - color: #666; + color: var(--text-secondary); + padding: 2rem; } .empty-state h2 { - margin-bottom: 1rem; + margin-bottom: 1.5rem; + font-weight: 600; + background: var(--header-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; } .example { - margin-top: 1rem; + margin-top: 1.5rem; font-style: italic; - color: #888; + color: var(--text-secondary); + padding: 0.75rem 1.5rem; + background-color: var(--bubble-bg); + border-radius: 8px; + border: 1px solid var(--border-color); } \ No newline at end of file diff --git a/frontend/src/components/Chat/ChatPanel.tsx b/frontend/src/components/Chat/ChatPanel.tsx index 31e82b6..41f6987 100644 --- a/frontend/src/components/Chat/ChatPanel.tsx +++ b/frontend/src/components/Chat/ChatPanel.tsx @@ -1,8 +1,10 @@ -import React, { useRef, useEffect, ReactNode } from 'react'; -import { Message } from '../../types'; +import React, { useRef, useEffect, ReactNode, useState } from 'react'; +import { Message, MCQEvaluation } from '../../types'; import ChatMessage from './ChatMessage'; import ChatInput from './ChatInput'; import ThinkingIndicator from './ThinkingIndicator'; +import { SuggestionBubbles } from '../Common/SuggestionBubble'; +import { getSuggestedTopic } from '../../services/api'; import './ChatPanel.css'; interface ChatPanelProps { @@ -19,12 +21,77 @@ const ChatPanel: React.FC = ({ renderMCQ }) => { const messagesEndRef = useRef(null); + const [mcqEvaluation, setMcqEvaluation] = useState(null); + const [suggestions, setSuggestions] = useState<{ text: string; action: () => void }[]>([]); // Scroll to bottom whenever messages change or when loading state changes useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages, isLoading]); + // Generate suggestions when MCQ evaluation is available + useEffect(() => { + if (mcqEvaluation) { + generateSuggestions(mcqEvaluation); + } + }, [mcqEvaluation]); + + const generateSuggestions = async (evaluation: MCQEvaluation) => { + if (evaluation.allCorrect) { + // All answers correct + const suggestedTopic = await getSuggestedTopic(); + + setSuggestions([ + { + text: "Test me more", + action: () => onSendMessage("I'd like more MCQs on the same topic please") + }, + { + text: `Ask me questions on ${suggestedTopic}`, + action: () => onSendMessage(`I'd like to try MCQs on ${suggestedTopic}`) + } + ]); + } else if (evaluation.suggestedTopics && evaluation.suggestedTopics.length > 0) { + // Incorrect answers, show topics that need improvement + const topicSuggestions = evaluation.suggestedTopics.map(suggestion => ({ + text: `Dive deep into ${suggestion.topic} with more MCQs`, + action: () => onSendMessage(`I'd like to learn more about ${suggestion.topic}`) + })); + + if (topicSuggestions.length === 1) { + // Add a fallback option if only one topic suggestion + topicSuggestions.push({ + text: "Help me understand the topics better", + action: () => onSendMessage("Can you explain the concepts I got wrong?") + }); + } + + setSuggestions(topicSuggestions); + } + }; + + // Handler for MCQ evaluation to be passed down to the MCQCard component + const handleMCQEvaluation = (evaluation: MCQEvaluation) => { + setMcqEvaluation(evaluation); + }; + + // Modify renderMCQ to include the onEvaluation prop + const renderMCQWithEvaluation = (message: Message) => { + if (!renderMCQ) return null; + + const originalRender = renderMCQ(message); + + // If original render is a React element and it's an MCQCard component + if (React.isValidElement(originalRender) && + originalRender.type.name === 'MCQCard') { + return React.cloneElement(originalRender, { + onEvaluation: handleMCQEvaluation + }); + } + + return originalRender; + }; + return (
@@ -41,12 +108,19 @@ const ChatPanel: React.FC = ({
{/* Render MCQ component if this message has MCQ data and we have a renderer */} - {renderMCQ && renderMCQ(message)} + {renderMCQWithEvaluation(message)}
))} {/* Show thinking indicator when loading */} {isLoading && } + + {/* Display suggestion bubbles when available */} + {!isLoading && messages.length > 0 && suggestions.length > 0 && ( +
+ +
+ )} )}
diff --git a/frontend/src/components/ChatBox.tsx b/frontend/src/components/ChatBox.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/components/ChatHistory.tsx b/frontend/src/components/ChatHistory.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/components/ChatMessage.tsx b/frontend/src/components/ChatMessage.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/components/ChatResponse.tsx b/frontend/src/components/ChatResponse.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/components/Common/SuggestionBubble.tsx b/frontend/src/components/Common/SuggestionBubble.tsx new file mode 100644 index 0000000..73a5e9b --- /dev/null +++ b/frontend/src/components/Common/SuggestionBubble.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import '../../styles/SuggestionBubble.css'; + +interface SuggestionBubbleProps { + text: string; + onClick: () => void; +} + +export const SuggestionBubble: React.FC = ({ text, onClick }) => { + return ( +
e.key === 'Enter' && onClick()} + > + {text} +
+ ); +}; + +interface SuggestionBubblesProps { + suggestions: { text: string; action: () => void }[]; +} + +export const SuggestionBubbles: React.FC = ({ suggestions }) => { + if (!suggestions || suggestions.length === 0) return null; + + return ( +
+ {suggestions.map((suggestion, index) => ( + + ))} +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/Common/ThemeToggle.css b/frontend/src/components/Common/ThemeToggle.css new file mode 100644 index 0000000..a5b63a5 --- /dev/null +++ b/frontend/src/components/Common/ThemeToggle.css @@ -0,0 +1,23 @@ +.theme-toggle-button { + background: none; + border: none; + cursor: pointer; + font-size: 0.7rem; /* Reduced from 0.8rem to make it smaller */ + padding: 0.2rem; /* Reduced padding */ + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + width: 22px; /* Reduced from 26px */ + height: 22px; /* Reduced from 26px */ + transition: all 0.2s ease; +} + +.theme-toggle-button:hover { + background-color: var(--hover-bg-color, rgba(0, 0, 0, 0.05)); +} + +/* Ensure the icon doesn't look pixelated when sized down */ +.theme-toggle-button > * { + transform: scale(0.75); /* Reduced from 0.85 */ +} \ No newline at end of file diff --git a/frontend/src/components/Common/ThemeToggle.tsx b/frontend/src/components/Common/ThemeToggle.tsx new file mode 100644 index 0000000..90752c0 --- /dev/null +++ b/frontend/src/components/Common/ThemeToggle.tsx @@ -0,0 +1,17 @@ +import React, { useContext } from 'react'; +import { ThemeContext } from '../../context/ThemeContext'; +import './ThemeToggle.css'; // Added CSS import + +export const ThemeToggle: React.FC = () => { + const { darkMode, toggleTheme } = useContext(ThemeContext); + + return ( + + ); +}; \ No newline at end of file diff --git a/frontend/src/components/Layout/Layout.css b/frontend/src/components/Layout/Layout.css index 7752f35..9f859fb 100644 --- a/frontend/src/components/Layout/Layout.css +++ b/frontend/src/components/Layout/Layout.css @@ -8,73 +8,107 @@ .app-header { display: flex; align-items: center; + justify-content: space-between; padding: 1rem 2rem; - background-color: #ffffff; - border-bottom: 1px solid #e0e0e0; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + background: var(--header-gradient); + color: white; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); position: relative; + z-index: 10; } .title-container { - position: absolute; - width: 100%; - text-align: center; - left: 0; - z-index: 1; + display: flex; + align-items: center; } .app-title { font-size: 1.75rem; - color: #333; margin: 0; font-weight: 700; + letter-spacing: -0.5px; } .nav-links { display: flex; gap: 1.5rem; align-items: center; - margin-left: auto; - position: relative; - z-index: 2; } .nav-link { text-decoration: none; - color: #555; + color: rgba(255, 255, 255, 0.9); font-weight: 500; - transition: color 0.2s; + transition: all 0.2s; + padding: 0.5rem 0; + position: relative; +} + +.nav-link::after { + content: ''; + position: absolute; + width: 0; + height: 2px; + bottom: 0; + left: 0; + background-color: white; + transition: width 0.2s ease; } .nav-link:hover { - color: #4f46e5; + color: white; +} + +.nav-link:hover::after { + width: 100%; } .nav-link.sign-up { - background-color: #4f46e5; + background-color: rgba(255, 255, 255, 0.2); color: white; - padding: 0.5rem 1rem; - border-radius: 4px; - transition: background-color 0.2s; + padding: 0.5rem 1.25rem; + border-radius: 8px; + backdrop-filter: blur(10px); + transition: all 0.2s; + border: 1px solid rgba(255, 255, 255, 0.3); } .nav-link.sign-up:hover { - background-color: #3c35b5; - color: white; + background-color: rgba(255, 255, 255, 0.3); + transform: translateY(-2px); } .content-wrapper { display: flex; flex: 1; overflow: hidden; + position: relative; } .sidebar { - width: 250px; - background-color: #f5f5f5; - border-right: 1px solid #e0e0e0; + width: 280px; + min-width: 280px; + background-color: var(--bg-secondary); + border-right: 1px solid var(--border-color); overflow-y: auto; - padding: 1rem; + padding: 1.25rem 1rem; + transition: all 0.3s ease; + z-index: 100; +} + +/* Collapsible behavior */ +@media (min-width: 769px) { + .sidebar { + width: 70px; + min-width: 70px; + overflow: hidden; + } + + .sidebar:hover, .sidebar.visible { + width: 280px; + min-width: 280px; + box-shadow: 5px 0 15px rgba(0, 0, 0, 0.1); + } } .main-content { @@ -82,4 +116,67 @@ overflow-y: auto; display: flex; flex-direction: column; + background-color: var(--bg-primary); + transition: background-color 0.3s ease; +} + +.sidebar-toggle { + position: fixed; + left: 0; + top: 50%; + transform: translateY(-50%); + background-color: var(--accent-primary); + color: white; + width: 25px; + height: 40px; + border-radius: 0 5px 5px 0; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 101; + box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; +} + +/* Mobile styles */ +@media (max-width: 768px) { + .app-header { + padding: 0.75rem 1rem; + } + + .app-title { + font-size: 1.4rem; + } + + .sidebar { + position: fixed; + top: 0; + left: 0; + height: 100%; + width: 280px; + transform: translateX(-100%); + } + + .sidebar.mobile.visible { + transform: translateX(0); + } + + .sidebar-toggle { + left: 0; + } + + .sidebar.mobile.visible + .sidebar-toggle { + left: 280px; + } +} + +@media (max-width: 576px) { + .nav-links { + gap: 0.75rem; + } + + .nav-link.sign-up { + padding: 0.4rem 0.8rem; + } } \ No newline at end of file diff --git a/frontend/src/components/Layout/Layout.tsx b/frontend/src/components/Layout/Layout.tsx index 3aa6cb9..3b9da52 100644 --- a/frontend/src/components/Layout/Layout.tsx +++ b/frontend/src/components/Layout/Layout.tsx @@ -1,25 +1,59 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import './Layout.css'; interface LayoutProps { sidebar: React.ReactNode; main: React.ReactNode; + header?: React.ReactNode; // Make header customizable } -const Layout: React.FC = ({ sidebar, main }) => { +const Layout: React.FC = ({ sidebar, main, header }) => { + const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); + const [isSidebarVisible, setSidebarVisible] = useState(false); + + // Handle window resize and detect mobile + useEffect(() => { + const handleResize = () => { + setIsMobile(window.innerWidth <= 768); + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + return (
-
-
-

StudyBuddy

-
-
- Login - Sign Up -
-
+ {header ? ( + // Use custom header if provided + header + ) : ( + // Default header +
+
+

LearnWithAI

+
+
+ Login + Sign Up +
+
+ )}
-
{sidebar}
+
setSidebarVisible(true)} + onMouseLeave={() => setSidebarVisible(false)} + > + {sidebar} +
+ {isMobile && ( +
setSidebarVisible(!isSidebarVisible)} + > + {isSidebarVisible ? '←' : '→'} +
+ )}
{main}
diff --git a/frontend/src/components/MCQ/MCQCard.css b/frontend/src/components/MCQ/MCQCard.css index 319991a..2e01645 100644 --- a/frontend/src/components/MCQ/MCQCard.css +++ b/frontend/src/components/MCQ/MCQCard.css @@ -1,32 +1,37 @@ .mcq-card { - background-color: white; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + background-color: var(--bg-secondary); + border-radius: 16px; + box-shadow: var(--card-shadow); padding: 1.5rem; - margin: 1rem 0; + margin: 1.25rem 0; transition: all 0.3s ease; + border: 1px solid var(--border-color); } .mcq-card.minimized { padding: 0.75rem; - margin: 0.5rem 0; + margin: 1rem 0; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05); - border: 1px solid #e0e0e0; + border: 1px solid var(--border-color); } .mcq-title { margin-top: 0; margin-bottom: 1.5rem; font-size: 1.5rem; - color: #333; - border-bottom: 1px solid #e0e0e0; + color: var(--text-primary); + border-bottom: 1px solid var(--border-color); padding-bottom: 0.75rem; + font-weight: 600; + background: var(--header-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; } .mcq-title-minimized { margin: 0; font-size: 1rem; - color: #333; + color: var(--text-primary); display: flex; justify-content: space-between; align-items: center; @@ -43,89 +48,104 @@ display: flex; gap: 1rem; flex-wrap: wrap; - margin-top: 1rem; + margin-top: 1.5rem; + justify-content: flex-end; } .submit-button { - background-color: #4caf50; + background-color: var(--accent-secondary); color: white; border: none; - border-radius: 4px; + border-radius: 8px; padding: 0.75rem 1.5rem; font-size: 1rem; font-weight: 500; cursor: pointer; - transition: background-color 0.2s; + transition: all 0.2s; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .submit-button:hover:not(:disabled) { - background-color: #45a049; + background-color: var(--accent-secondary); + transform: translateY(-2px); + box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15); } .submit-button:disabled { background-color: #cccccc; cursor: not-allowed; + box-shadow: none; } .retry-button { - background-color: #2196f3; + background-color: var(--accent-primary); color: white; border: none; - border-radius: 4px; + border-radius: 8px; padding: 0.75rem 1.5rem; font-size: 1rem; font-weight: 500; cursor: pointer; - transition: background-color 0.2s; + transition: all 0.2s; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .retry-button:hover { - background-color: #0b7dda; + background-color: var(--accent-primary); + transform: translateY(-2px); + box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15); } .continue-button { background-color: #ff9800; color: white; border: none; - border-radius: 4px; + border-radius: 8px; padding: 0.75rem 1.5rem; font-size: 1rem; font-weight: 500; cursor: pointer; - transition: background-color 0.2s; + transition: all 0.2s; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .continue-button:hover { background-color: #e68a00; + transform: translateY(-2px); + box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15); } .skip-button { - background-color: #9e9e9e; + background-color: var(--text-secondary); color: white; border: none; - border-radius: 4px; + border-radius: 8px; padding: 0.75rem 1.5rem; font-size: 1rem; font-weight: 500; cursor: pointer; - transition: background-color 0.2s; + transition: all 0.2s; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .skip-button:hover:not(:disabled) { - background-color: #757575; + background-color: var(--text-secondary); + transform: translateY(-2px); + box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15); } .minimize-button, .expand-button { - background-color: #e0e0e0; - color: #333; - border: none; - border-radius: 4px; + background-color: var(--bg-primary); + color: var(--text-primary); + border: 1px solid var(--border-color); + border-radius: 8px; padding: 0.5rem 0.75rem; font-size: 0.9rem; cursor: pointer; - transition: background-color 0.2s; + transition: all 0.2s; } .minimize-button:hover, .expand-button:hover { - background-color: #ccc; + background-color: var(--bubble-bg); + transform: translateY(-2px); } \ No newline at end of file diff --git a/frontend/src/components/MCQ/MCQCard.tsx b/frontend/src/components/MCQ/MCQCard.tsx index aed13da..2f665ee 100644 --- a/frontend/src/components/MCQ/MCQCard.tsx +++ b/frontend/src/components/MCQ/MCQCard.tsx @@ -8,12 +8,14 @@ import './MCQCard.css'; interface MCQCardProps { quiz: MCQQuiz; onComplete?: () => void; // Callback for when quiz is completed + onEvaluation?: (evaluation: MCQEvaluation) => void; // New callback for evaluation data initiallyMinimized?: boolean; } const MCQCard: React.FC = ({ quiz, onComplete, + onEvaluation, initiallyMinimized = false }) => { const [selectedOptions, setSelectedOptions] = useState>({}); @@ -56,6 +58,11 @@ const MCQCard: React.FC = ({ const result = await submitMCQAnswers(submissions); setEvaluation(result); + // Pass the evaluation to the parent component + if (onEvaluation) { + onEvaluation(result); + } + // Signal completion only once if (onComplete && !completionSignaled.current) { completionSignaled.current = true; diff --git a/frontend/src/components/MCQ/MCQOption.css b/frontend/src/components/MCQ/MCQOption.css index d7fce09..1591f35 100644 --- a/frontend/src/components/MCQ/MCQOption.css +++ b/frontend/src/components/MCQ/MCQOption.css @@ -1,13 +1,14 @@ .mcq-option { - margin-bottom: 0.75rem; - padding: 0.75rem; - border: 1px solid #e0e0e0; - border-radius: 4px; + margin-bottom: 0.5rem; /* Reduced spacing from 0.75rem */ + padding: 0.65rem; /* Reduced padding from 0.75rem */ + border: 1px solid var(--border-color); + border-radius: 8px; transition: all 0.2s; + background-color: var(--bg-primary); } .mcq-option:hover:not(.correct):not(.incorrect) { - background-color: #f5f5f5; + background-color: var(--bubble-bg); } .mcq-option label { @@ -19,10 +20,15 @@ .mcq-option input[type="radio"] { margin-right: 0.75rem; + /* Make radio buttons more visible */ + width: 16px; + height: 16px; + cursor: pointer; } .option-text { flex: 1; + color: var(--text-primary); } .correct-indicator, .incorrect-indicator { @@ -31,7 +37,7 @@ } .correct-indicator { - color: #4caf50; + color: var(--accent-secondary); } .incorrect-indicator { @@ -40,7 +46,7 @@ .mcq-option.correct { background-color: rgba(76, 175, 80, 0.1); - border-color: #4caf50; + border-color: var(--accent-secondary); } .mcq-option.incorrect { diff --git a/frontend/src/components/MCQ/MCQQuestion.css b/frontend/src/components/MCQ/MCQQuestion.css index ae0e7b0..3c20e8e 100644 --- a/frontend/src/components/MCQ/MCQQuestion.css +++ b/frontend/src/components/MCQ/MCQQuestion.css @@ -1,7 +1,7 @@ .mcq-question { - margin-bottom: 2rem; - padding-bottom: 1.5rem; - border-bottom: 1px solid #e0e0e0; + margin-bottom: 1.75rem; /* Reduced from 2rem */ + padding-bottom: 1.25rem; /* Reduced from 1.5rem */ + border-bottom: 1px solid var(--border-color); } .mcq-question:last-child { @@ -9,29 +9,101 @@ } .question-text { - margin-bottom: 1rem; + margin-bottom: 1rem; /* Reduced from 1.25rem */ font-size: 1.1rem; - font-weight: 500; + font-weight: 600; + color: var(--text-primary); + line-height: 1.5; } .options-container { - margin-bottom: 1rem; + margin-bottom: 1rem; /* Reduced from 1.25rem */ + display: flex; + flex-direction: column; + gap: 0.5rem; /* Reduced from 0.75rem */ +} + +.option-item { + position: relative; + cursor: pointer; + padding: 0.65rem 1rem; /* Reduced from 0.75rem 1rem */ + border-radius: 8px; + background-color: var(--bg-primary); + border: 1px solid var(--border-color); + transition: all 0.2s ease; +} + +.option-item:hover:not(.disabled) { + transform: translateY(-2px); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + border-color: var(--accent-primary); +} + +.option-item.selected { + border-color: var(--accent-primary); + background-color: var(--bubble-bg); +} + +.option-item.correct { + border-color: var(--accent-secondary); + background-color: rgba(16, 185, 129, 0.1); +} + +.option-item.incorrect { + border-color: #f44336; + background-color: rgba(244, 67, 54, 0.1); +} + +.option-item.disabled { + opacity: 0.7; + cursor: default; +} + +.option-text { + color: var(--text-primary); + font-size: 1rem; + padding-left: 1.75rem; + position: relative; + display: block; /* Added to contain the text properly */ +} + +/* Remove the additional circle by changing the ::before pseudo element */ +.option-text::before { + display: none; /* Remove the extra circle */ +} + +/* Add styling for real input radio buttons */ +.option-item input[type="radio"] { + position: absolute; + left: 1rem; + top: 50%; + transform: translateY(-50%); + margin: 0; } .explanation { - margin-top: 1rem; - padding: 0.75rem; - background-color: #f5f5f5; - border-radius: 4px; + margin-top: 1.25rem; /* Reduced from 1.5rem */ + padding: 1rem; + background-color: var(--bubble-bg); + border-radius: 8px; + border: 1px solid var(--border-color); + animation: fadeIn 0.3s ease; } .explanation h4 { margin-top: 0; - margin-bottom: 0.5rem; - color: #333; + margin-bottom: 0.6rem; /* Reduced from 0.75rem */ + color: var(--accent-primary); + font-weight: 600; } .explanation p { margin: 0; - color: #555; + color: var(--text-primary); + line-height: 1.6; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } } \ No newline at end of file diff --git a/frontend/src/components/MCQQuestion.tsx b/frontend/src/components/MCQQuestion.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/components/MCQSubmit.tsx b/frontend/src/components/MCQSubmit.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/context/ChatContext.tsx b/frontend/src/context/ChatContext.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/context/ThemeContext.tsx b/frontend/src/context/ThemeContext.tsx new file mode 100644 index 0000000..34fad0a --- /dev/null +++ b/frontend/src/context/ThemeContext.tsx @@ -0,0 +1,37 @@ +import React, { createContext, useState, useEffect, ReactNode } from 'react'; + +type ThemeContextType = { + darkMode: boolean; + toggleTheme: () => void; +}; + +export const ThemeContext = createContext({ + darkMode: false, + toggleTheme: () => {}, +}); + +export const ThemeProvider: React.FC<{children: ReactNode}> = ({ children }) => { + const [darkMode, setDarkMode] = useState(() => { + const savedTheme = localStorage.getItem('theme'); + if (savedTheme) { + return savedTheme === 'dark'; + } + return window.matchMedia('(prefers-color-scheme: dark)').matches; + }); + + useEffect(() => { + document.body.classList.toggle('dark-theme', darkMode); + document.body.classList.toggle('light-theme', !darkMode); + localStorage.setItem('theme', darkMode ? 'dark' : 'light'); + }, [darkMode]); + + const toggleTheme = () => { + setDarkMode(prev => !prev); + }; + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/frontend/src/hooks/useChat.tsx b/frontend/src/hooks/useChat.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index e5843ee..07e4d83 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1,7 +1,18 @@ -import { Message, MCQQuiz, MCQEvaluation, MCQSubmission } from '../types'; +import { Message, MCQQuiz, MCQEvaluation, MCQSubmission, TopicSuggestion } from '../types'; // Dynamic backend URL resolution const determineApiUrl = () => { + // For GitHub Codespaces, use the proper URL format with port forwarding + if (window.location.hostname.endsWith('.github.dev') || + window.location.hostname.includes('.app.github.dev') || + window.location.hostname.includes('-') && window.location.hostname.includes('.preview.app.github.dev')) { + + // Use the specific known backend URL for GitHub Codespaces - with port 8000 + const codespaceBackendUrl = 'https://crispy-carnival-pqwxp5v7jf7g7r-8000.app.github.dev'; + console.log('Using specific GitHub Codespaces backend URL:', codespaceBackendUrl); + return `${codespaceBackendUrl}/api`; + } + // For local development, explicitly use the backend server URL if (window.location.hostname === 'localhost') { console.log('Using local backend API server at localhost:8000'); @@ -32,38 +43,107 @@ console.log('Using API URL:', API_URL); export async function sendMessage(message: string): Promise { try { - // Make sure we're using the full path to the chat endpoint - console.log(`Sending message to ${API_URL}/chat:`, message); - const response = await fetch(`${API_URL}/chat`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ message }), - }); + // Fix the URL construction to avoid double /api in the path + // API_URL already includes '/api' at the end, so we should just append the endpoint name + const endpointUrl = `${API_URL}/chat`; - if (!response.ok) { - console.error(`Error response: ${response.status} ${response.statusText}`); - throw new Error('Failed to send message'); - } - - const data = await response.json(); - console.log('Received response:', data); + console.log(`Sending message to ${endpointUrl}:`, message); - // Check if the response is an MCQ quiz (by looking for typical MCQ fields) - if (data.questions && data.topic) { - console.log('MCQ response detected'); + try { + const response = await fetch(endpointUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ message }), + }); + + // Detailed error logging + if (!response.ok) { + const statusText = response.statusText || 'No status text provided'; + const errorDetail = `Status: ${response.status} ${statusText}`; + + console.error(`API Error: ${errorDetail}`); + console.error(`Request URL: ${endpointUrl}`); + console.error(`Request method: POST`); + + // Try to get response text for more details + try { + const errorText = await response.text(); + console.error(`Response body: ${errorText}`); + } catch (textError) { + console.error(`Couldn't read error response: ${textError}`); + } + + // For 405 errors specifically, try using GET as a fallback for debugging + if (response.status === 405) { + console.warn("Received 405 Method Not Allowed - attempting OPTIONS request to check allowed methods"); + + try { + const optionsResponse = await fetch(endpointUrl, { + method: 'OPTIONS', + }); + + console.log(`OPTIONS response status: ${optionsResponse.status}`); + console.log(`OPTIONS allowed methods:`, optionsResponse.headers.get('Allow') || 'Not specified'); + } catch (optionsError) { + console.error(`OPTIONS request failed: ${optionsError}`); + } + } + + throw new Error(`Failed to send message: ${errorDetail}`); + } + + const data = await response.json(); + console.log('Received response:', data); + + // Check if the response is an MCQ quiz (by looking for typical MCQ fields) + if (data.questions && data.topic) { + console.log('MCQ response detected'); + return { + isMCQ: true, + mcqData: data + }; + } + + // Regular chat response return { - isMCQ: true, - mcqData: data + isMCQ: false, + content: data.message }; + + } catch (fetchError) { + console.error('Fetch error:', fetchError); + + // Try one more time with an alternate path format for GitHub Codespaces + if (window.location.hostname.includes('.github.dev') && !endpointUrl.includes('/chat')) { + console.warn('Attempting fallback request with alternate path format...'); + const fallbackUrl = `${API_URL.replace(/\/api\/?$/, '')}/chat`; + + console.log(`Sending fallback request to: ${fallbackUrl}`); + const fallbackResponse = await fetch(fallbackUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ message }), + }); + + if (!fallbackResponse.ok) { + console.error(`Fallback request failed: ${fallbackResponse.status}`); + throw new Error('Both primary and fallback requests failed'); + } + + const fallbackData = await fallbackResponse.json(); + return { + isMCQ: fallbackData.questions && fallbackData.topic, + mcqData: fallbackData.questions && fallbackData.topic ? fallbackData : undefined, + content: fallbackData.message + }; + } + + throw fetchError; } - - // Regular chat response - return { - isMCQ: false, - content: data.message - }; } catch (error) { console.error('Error sending message:', error); throw error; @@ -72,8 +152,11 @@ export async function sendMessage(message: string): Promise { export async function requestMCQs(topic: string, numQuestions: number = 4): Promise { try { - console.log(`Requesting MCQs from ${API_URL}/mcq/generate:`, { topic, numQuestions }); - const response = await fetch(`${API_URL}/mcq/generate`, { + // Fix the URL construction to avoid double /api in the path + const endpointUrl = `${API_URL}/mcq/generate`; + + console.log(`Requesting MCQs from ${endpointUrl}:`, { topic, numQuestions }); + const response = await fetch(endpointUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -113,7 +196,9 @@ export async function submitMCQAnswers(submissions: MCQSubmission[]): Promise !result.isCorrect); + const allCorrect = incorrectResults.length === 0; + + // Track incorrect topics and their frequencies + const incorrectTopics: Record = {}; + + incorrectResults.forEach((result: any, index: number) => { + const mapping = optionMappings[index]; + if (mapping && mapping.topic) { + // Increment topic frequency + incorrectTopics[mapping.topic] = (incorrectTopics[mapping.topic] || 0) + 1; + + // Add related topics with lower priority + if (mapping.relatedTopics && Array.isArray(mapping.relatedTopics)) { + mapping.relatedTopics.forEach((relatedTopic: string) => { + incorrectTopics[relatedTopic] = (incorrectTopics[relatedTopic] || 0) + 0.5; + }); + } + } + }); + + // Convert to array and sort by frequency + const suggestedTopics = Object.entries(incorrectTopics) + .map(([topic, frequency]) => ({ topic, frequency })) + .sort((a, b) => b.frequency - a.frequency) + .slice(0, 2); // Get top 2 suggestions + + return { + ...data, + suggestedTopics, + allCorrect + }; } catch (error) { console.error('Error submitting MCQ answers:', error); throw error; } +} + +// Add function to get topic suggestions for a different topic +export async function getSuggestedTopic(): Promise { + try { + // Fix the URL construction to avoid double /api in the path + const endpointUrl = `${API_URL}/topics/suggest`; + + const response = await fetch(endpointUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + } + }); + + if (!response.ok) { + return 'Python programming'; // Fallback topic + } + + const data = await response.json(); + return data.topic || 'Programming basics'; + } catch (error) { + console.error('Error getting suggested topic:', error); + return 'Data structures'; // Fallback topic + } } \ No newline at end of file diff --git a/frontend/src/styles/SuggestionBubble.css b/frontend/src/styles/SuggestionBubble.css new file mode 100644 index 0000000..d021bd1 --- /dev/null +++ b/frontend/src/styles/SuggestionBubble.css @@ -0,0 +1,32 @@ +.suggestion-bubbles { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin: 1.5rem 0; +} + +.suggestion-bubble { + background-color: var(--bubble-bg); + color: var(--accent-primary); + padding: 0.75rem 1.25rem; + border-radius: 9999px; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + border: 1px solid var(--border-color); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + backdrop-filter: blur(8px); + opacity: 0.9; +} + +.suggestion-bubble:hover { + opacity: 1; + background-color: var(--bubble-hover); + transform: translateY(-2px); +} + +.suggestion-bubble:focus { + outline: 2px solid var(--accent-primary); + outline-offset: 2px; +} \ No newline at end of file diff --git a/frontend/src/styles/theme.css b/frontend/src/styles/theme.css new file mode 100644 index 0000000..f71888a --- /dev/null +++ b/frontend/src/styles/theme.css @@ -0,0 +1,64 @@ +:root { + /* Light theme (default) */ + --bg-primary: #ffffff; + --bg-secondary: #f8fafc; + --text-primary: #1e293b; + --text-secondary: #64748b; + --accent-primary: #3b82f6; + --accent-secondary: #10b981; + --border-color: #e2e8f0; + --card-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --header-gradient: linear-gradient(to right, #3b82f6, #10b981); + --bubble-bg: rgba(59, 130, 246, 0.1); + --bubble-hover: rgba(59, 130, 246, 0.2); +} + +body.dark-theme { + --bg-primary: #0f172a; + --bg-secondary: #1e293b; + --text-primary: #f8fafc; + --text-secondary: #cbd5e1; + --accent-primary: #60a5fa; + --accent-secondary: #34d399; + --border-color: #334155; + --card-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2); + --header-gradient: linear-gradient(to right, #1e40af, #065f46); + --bubble-bg: rgba(96, 165, 250, 0.15); + --bubble-hover: rgba(96, 165, 250, 0.3); +} + +/* Modern UI Styles */ +body { + background-color: var(--bg-primary); + color: var(--text-primary); + transition: background-color 0.3s ease, color 0.3s ease; + font-family: 'Inter', system-ui, sans-serif; +} + +.theme-toggle-button { + background: transparent; + border: none; + font-size: 1.5rem; + cursor: pointer; + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: background-color 0.2s ease; +} + +.theme-toggle-button:hover { + background-color: rgba(255, 255, 255, 0.2); +} + +.app-header { + background: var(--header-gradient); + padding: 1rem 2rem; + color: white; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); +} \ No newline at end of file diff --git a/frontend/src/types/chat.ts b/frontend/src/types/chat.ts new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index e25143a..650980d 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -27,6 +27,8 @@ export interface MCQQuestion { options: MCQOption[]; correctAnswerId?: string; explanation?: string; + topic?: string; + relatedTopics?: string[]; } export interface MCQQuiz { @@ -49,9 +51,16 @@ export interface MCQResult { explanation?: string; } +export interface TopicSuggestion { + topic: string; + frequency: number; +} + export interface MCQEvaluation { score: number; total: number; percentage: number; results: MCQResult[]; + suggestedTopics?: TopicSuggestion[]; + allCorrect?: boolean; } \ No newline at end of file diff --git a/render.yaml b/render.yaml new file mode 100644 index 0000000..d24c694 --- /dev/null +++ b/render.yaml @@ -0,0 +1,29 @@ +services: + # Backend Service + - type: web + name: mcq-backend + env: python + buildCommand: cd backend && pip install -r requirements.txt + startCommand: cd backend && uvicorn main:app --host 0.0.0.0 --port $PORT + envVars: + - key: PYTHON_VERSION + value: 3.12 + - key: FRONTEND_URL + fromService: + name: mcq-frontend + type: web + property: url + healthCheckPath: /api/health + + # Frontend Service + - type: web + name: mcq-frontend + env: node + buildCommand: cd frontend && npm install && npm run build + startCommand: cd frontend && npm run preview -- --host 0.0.0.0 --port $PORT + envVars: + - key: VITE_API_URL + fromService: + name: mcq-backend + type: web + property: url