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
+
+
+ }
+ 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) => (
+
+ ))}
+
+ {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 (
- New Chat
+
+
+ New Chat
+
{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 (
);
};
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 (
+
+ {darkMode ? '☀️' : '🌙'}
+
+ );
+};
\ 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 (
-
+ {header ? (
+ // Use custom header if provided
+ header
+ ) : (
+ // Default header
+
+ )}
-
{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