GitHub Action commited on
Commit
39b5ef2
·
0 Parent(s):

Clean deployment - 0 binaries

Browse files
.github/workflows/main.yml ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to Hugging Face
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ # to run this workflow manually from the Actions tab
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ sync-to-hub:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v3
13
+ with:
14
+ fetch-depth: 0
15
+ lfs: true
16
+ - name: Push to hub
17
+ env:
18
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
19
+ run: |
20
+ # 1. Use a temporary directory outside the project
21
+ DEPLOY_DIR="../deploy_dir"
22
+ mkdir -p "$DEPLOY_DIR"
23
+
24
+ # 2. Copy current files to the temp directory
25
+ cp -r . "$DEPLOY_DIR/"
26
+
27
+ # 3. Enter the temp dir and wipe images
28
+ cd "$DEPLOY_DIR"
29
+ rm -rf .git
30
+ rm -rf services/frontend-service/static/images/*.png
31
+ rm -rf services/frontend-service/static/images/*.svg
32
+ rm -f .gitattributes
33
+
34
+ # 4. Initialize a FRESH git repo and set branch to 'main'
35
+ git init
36
+ git checkout -b main
37
+ git config user.name "GitHub Action"
38
+ git config user.email "action@github.com"
39
+
40
+ # 5. Create a dummy file to keep the images folder structure
41
+ mkdir -p services/frontend-service/static/images
42
+ touch services/frontend-service/static/images/.gitkeep
43
+
44
+ # 6. Commit everything
45
+ git add .
46
+ git commit -m "Clean deployment - 0 binaries"
47
+
48
+ # 7. Force push to HF
49
+ git push --force https://x-token:$HF_TOKEN@huggingface.co/spaces/Josedavison/AceNow main
.gitignore ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environment
2
+ .env
3
+ .venv/
4
+ env/
5
+ venv/
6
+ ENV/
7
+
8
+ # Python
9
+ __pycache__/
10
+ *.pyc
11
+ *.pyo
12
+ *.pyd
13
+ .Python
14
+ *.log
15
+ .pytest_cache/
16
+ .coverage
17
+ htmlcov/
18
+
19
+ # Service Specific
20
+ services/*/uploads/*
21
+ services/*/audio/*
22
+ !services/*/uploads/.gitkeep
23
+ !services/*/audio/.gitkeep
24
+
25
+ # OS
26
+ .DS_Store
27
+ Thumbs.db
28
+
29
+ # IDEs
30
+ .vscode/
31
+ .idea/
32
+ *.swp
33
+ *.swo
34
+
35
+ # Project Reference
36
+ AceNowUI/
37
+ Asserts/
CardImges/Card1.png ADDED
CardImges/Card2.png ADDED
CardImges/Card3.png ADDED
CardImges/Card4.png ADDED
Dockerfile ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ curl \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Copy all services
11
+ COPY . .
12
+
13
+ # Install dependencies from root requirements.txt
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ # Create non-root user (good practice for HF Spaces)
17
+ RUN useradd -m -u 1000 user
18
+ USER user
19
+ ENV HOME=/home/user \
20
+ PATH=/home/user/.local/bin:$PATH
21
+
22
+ WORKDIR /app
23
+
24
+ # Ensure start script is executable
25
+ USER root
26
+ RUN chmod +x start.sh
27
+ USER user
28
+
29
+ # Default Port for Hugging Face Spaces
30
+ EXPOSE 7860
31
+
32
+ # Start script
33
+ CMD ["./start.sh"]
MICROSERVICES_ARCHITECTURE.md ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AceNow Architecture & Tech Stack
2
+
3
+ This document details the internal design and technical decisions of the AceNow microservices ecosystem.
4
+
5
+ ## 📡 Service Flow
6
+
7
+ ```mermaid
8
+ graph TD
9
+ User((User)) -->|HTTPS| Gateway[API Gateway :5000]
10
+ Gateway -->|Auth Check| Auth[Auth Service :5001]
11
+ Gateway -->|Parse Req| Parser[File Parser :5002]
12
+ Gateway -->|AI Req| AIService[AI Service :5003]
13
+ Gateway -->|Serve UI| Frontend[Frontend Service :5004]
14
+
15
+ AIService -->|API| Gemini((Google Gemini))
16
+ AIService -->|API| Groq((Groq Cloud))
17
+ AIService -->|Local| Ollama((Ollama))
18
+ ```
19
+
20
+ ## 🛠️ Technology Stack
21
+
22
+ | Layer | Technology |
23
+ | :--- | :--- |
24
+ | **Framework** | Python / Flask |
25
+ | **Authentication** | Google Identity Services (OAuth 2.0) |
26
+ | **AI Processing** | Google GenAI, Groq (Llama 3.3), Ollama |
27
+ | **File Parsing** | `pdfplumber`, `python-pptx`, `PyPDF2` |
28
+ | **Frontend** | Vanilla JS (ES6+), CSS3 (Glassmorphism), HTML5 |
29
+ | **Parallel Downloads** | `JSZip` (Client-side bundling) |
30
+ | **Containerization** | Docker, Docker Compose |
31
+
32
+ ## 🧠 Specialized Logic
33
+
34
+ ### 1. AI Fallback Engine
35
+ The AI Service implements a robust retry-strategy:
36
+ - **Priority 1**: Gemini 2.0 Flash (Fast & Accurate).
37
+ - **Priority 2**: Groq (Llama 3.3 70B) - if Gemini hits quota/rate limits.
38
+ - **Priority 3**: Ollama (Llama 3.2) - if running locally.
39
+ - **Parsing**: Advanced JSON-repair logic handles varied AI output formats to ensure UI stability.
40
+
41
+ ### 2. File Processing
42
+ - **Scanning**: Multi-threaded metadata fetching from Google Classroom.
43
+ - **Parsing**: Server-side text extraction to keep the frontend light.
44
+ - **Bundling**: Client-side ZIP generation avoids heavy server-side temporary file storage.
45
+
46
+ ### 3. API Gateway Routing
47
+ The Gateway handles CORS and acts as a security buffer:
48
+ - `/auth/*` -> Auth Service
49
+ - `/parse/*` -> File Parser
50
+ - `/ai/*` -> AI Service
51
+ - `/*` (Static) -> Frontend Service
52
+
53
+ ## 📦 Deployment Configuration
54
+
55
+ - **`Dockerfile`**: A multi-stage setup that installs all dependencies and prepares the environment.
56
+ - **`start.sh`**: A supervisor script that boots all microservices concurrently within a single container (optimized for free hosting like HF Spaces).
57
+ - **`run_dev.py`**: A developer-friendly Python script for parallel local execution with live logs.
58
+
59
+ ## 🔒 Security
60
+ - **No API Keys in Frontend**: All sensitive keys are stored in the backend `.env`.
61
+ - **Stateless Auth**: Uses Google JWT verification.
62
+ - **Proxy Aware**: Optimized for Windows and Unix environments with `NO_PROXY` configurations.
Procfile ADDED
@@ -0,0 +1 @@
 
 
1
+ web: gunicorn app:app
README.md ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: AceNow
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ ---
10
+
11
+ # AceNow - AI-Powered Exam Prep Microservices
12
+
13
+ AceNow is a focused platform for last-minute exam preparation, offering AI-powered summaries, key topics, and quick quizzes for efficient revision. Built with a modern microservices architecture, it integrates seamlessly with Google Classroom to retrieve study materials and uses state-of-the-art AI models for content analysis.
14
+
15
+ ## ✨ Core Features
16
+
17
+ - **Google Classroom Integration**: Securely fetch documents (PDF/PPTX) and announcements from your enrolled courses.
18
+ - **Smart Study Logic**:
19
+ - **Key Topics**: Automatically identifies high-priority exam concepts.
20
+ - **AI Summaries**: Concise oversight of core ideas for fast reading.
21
+ - **Pedagogical Quizzes**: Scenario-based MCQs with hints and detailed rationales for every answer.
22
+ - **Robust AI Fallback**: Multi-provider engine (Gemini 2.0 -> Groq Llama 3 -> Ollama) ensures 100% uptime even during rate limits.
23
+ - **Parallel Downloads**: Bundle all course materials into a single ZIP file instantly using JSZip.
24
+ - **Modern Responsive UI**: Premium glassmorphic interface with Dark/Light modes and full mobile compatibility.
25
+ - **AI Assistant**: Dedicated academic chat interface for deep-diving into complex topics.
26
+
27
+ ## 🏗️ Architecture
28
+
29
+ The system is split into specialized microservices:
30
+
31
+ 1. **API Gateway (Port 5000)**: Single entry point that routes requests and manages cross-service communication.
32
+ 2. **Auth Service (Port 5001)**: Handles Google OAuth2 authentication and configuration.
33
+ 3. **File Parser Service (Port 5002)**: Specialized in extracting text content from PDFs and PPTXs.
34
+ 4. **AI Service (Port 5003)**: The "brain" of AceNow, managing complex prompts and various AI model providers.
35
+ 5. **Frontend Service (Port 5004)**: Serves the web application, styles, and assets.
36
+
37
+ ## 🚀 Quick Start
38
+
39
+ ### 1. Requirements
40
+ - Python 3.9+
41
+ - [Google Cloud Project](GOOGLE_SETUP.md) for Google Classroom API.
42
+ - [Gemini API Key](https://aistudio.google.com/app/apikey) (Primary).
43
+ - [Groq API Key](https://console.groq.com/keys) (Fallback).
44
+
45
+ ### 2. Setup
46
+ Create a `.env` file in the root directory:
47
+ ```env
48
+ GOOGLE_CLIENT_ID=your_client_id
49
+ GEMINI_API_KEY=your_gemini_key
50
+ GROQ_API_KEY=your_groq_key
51
+ # Optional: HF_TOKEN=your_huggingface_token
52
+ ```
53
+
54
+ ### 3. Run with Docker Compose
55
+ The easiest way to start the entire cluster:
56
+ ```bash
57
+ docker-compose up --build
58
+ ```
59
+ Access the app at: **http://localhost:5000**
60
+
61
+ ### 4. Run Locally (Dev Mode)
62
+ Alternatively, use the provided development runner:
63
+ ```bash
64
+ python run_dev.py
65
+ ```
66
+
67
+ ## 🌍 Free Hosting & Deployment
68
+
69
+ AceNow is designed to run entirely on free-tier services.
70
+
71
+ ### Option A: Hugging Face Spaces (Recommended)
72
+ 1. Create a new **Space** on Hugging Face.
73
+ 2. Select **Docker** as the SDK.
74
+ 3. Upload the project (the `Dockerfile` at the root handles the multi-service build).
75
+ 4. Go to **Settings > Variables & Secrets** and add your `.env` variables.
76
+
77
+ ### Option B: Render (Manual)
78
+ 1. Deploy the **API Gateway** as a Web Service.
79
+ 2. Deploy individual services as **Private Services** (no cost for inter-service communication).
80
+ 3. Link them using the environment variables in the Gateway.
81
+
82
+ ## 📄 License
83
+ © 2026 Jose Davidson. Developed with the assistance of Antigravity.
bundle_images.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import base64
3
+ import json
4
+
5
+ image_dir = "services/frontend-service/static/images"
6
+ bundle_file = "services/frontend-service/static/js/image_bundle.js"
7
+
8
+ images_data = {}
9
+
10
+ for filename in os.listdir(image_dir):
11
+ if filename.endswith(('.png', '.svg', '.jpg', '.jpeg')):
12
+ filepath = os.path.join(image_dir, filename)
13
+ with open(filepath, "rb") as f:
14
+ encoded_string = base64.b64encode(f.read()).decode('utf-8')
15
+
16
+ # Determine mime type
17
+ mime = "image/png"
18
+ if filename.endswith(".svg"): mime = "image/svg+xml"
19
+ elif filename.endswith((".jpg", ".jpeg")): mime = "image/jpeg"
20
+
21
+ images_data[filename] = f"data:{mime};base64,{encoded_string}"
22
+
23
+ with open(bundle_file, "w") as f:
24
+ f.write("const IMAGE_BUNDLE = ")
25
+ f.write(json.dumps(images_data, indent=4))
26
+ f.write(";")
27
+
28
+ print(f"Successfully bundled {len(images_data)} images into {bundle_file}")
docker-compose.yml ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ api-gateway:
5
+ build: ./services/api-gateway
6
+ ports:
7
+ - "5000:5000"
8
+ environment:
9
+ - PORT=5000
10
+ - AUTH_SERVICE_URL=http://auth-service:5001
11
+ - FILE_PARSER_SERVICE_URL=http://file-parser-service:5002
12
+ - AI_SERVICE_URL=http://ai-service:5003
13
+ - FRONTEND_SERVICE_URL=http://frontend-service:5004
14
+ depends_on:
15
+ - auth-service
16
+ - file-parser-service
17
+ - ai-service
18
+ - frontend-service
19
+
20
+ auth-service:
21
+ build: ./services/auth-service
22
+ ports:
23
+ - "5001:5001"
24
+ environment:
25
+ - PORT=5001
26
+ - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
27
+
28
+ file-parser-service:
29
+ build: ./services/file-parser-service
30
+ ports:
31
+ - "5002:5002"
32
+ environment:
33
+ - PORT=5002
34
+
35
+ ai-service:
36
+ build: ./services/ai-service
37
+ ports:
38
+ - "5003:5003"
39
+ environment:
40
+ - PORT=5003
41
+ - GEMINI_API_KEY=${GEMINI_API_KEY}
42
+ - HF_TOKEN=${HF_TOKEN}
43
+ - HF_API_KEY=${HF_API_KEY}
44
+
45
+ frontend-service:
46
+ build: ./services/frontend-service
47
+ ports:
48
+ - "5004:5004"
49
+ environment:
50
+ - PORT=5004
requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ flask
2
+ flask-cors
3
+ google-auth
4
+ google-auth-oauthlib
5
+ google-auth-httplib2
6
+ google-api-python-client
7
+ google-generativeai
8
+ python-dotenv
9
+ requests
10
+ PyPDF2
11
+ pdfplumber
12
+ python-pptx
13
+ openai
14
+ groq
15
+ gunicorn
16
+ google-genai
run_dev.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import os
3
+ import sys
4
+ import time
5
+ import threading
6
+
7
+ # Define services with their directories and ports
8
+ SERVICES = [
9
+ {"name": "Auth Service", "dir": "services/auth-service", "port": 5001},
10
+ {"name": "File Parser", "dir": "services/file-parser-service", "port": 5002},
11
+ {"name": "AI Service", "dir": "services/ai-service", "port": 5003},
12
+ {"name": "Frontend", "dir": "services/frontend-service", "port": 5004},
13
+ {"name": "API Gateway", "dir": "services/api-gateway", "port": 5000},
14
+ ]
15
+
16
+ processes = []
17
+
18
+ def run_service(service):
19
+ print(f"Starting {service['name']}...")
20
+ cwd = os.path.join(os.getcwd(), service['dir'])
21
+
22
+ # Use the current python interpreter
23
+ cmd = [sys.executable, "app.py"]
24
+
25
+ # Set environment variables if needed
26
+ env = os.environ.copy()
27
+ env["PORT"] = str(service["port"])
28
+ env["NO_PROXY"] = "*" # Disable proxy for inter-service communication
29
+
30
+ process = subprocess.Popen(
31
+ cmd,
32
+ cwd=cwd,
33
+ env=env,
34
+ stdout=subprocess.PIPE,
35
+ stderr=subprocess.STDOUT,
36
+ text=True,
37
+ bufsize=1
38
+ )
39
+ processes.append(process)
40
+
41
+ # Print service logs with prefix
42
+ for line in iter(process.stdout.readline, ""):
43
+ print(f"[{service['name']}] {line.strip()}")
44
+
45
+ process.stdout.close()
46
+
47
+ def main():
48
+ print("🌟 AceNow Microservices Development Runner")
49
+ print("==========================================")
50
+
51
+ threads = []
52
+ for service in SERVICES:
53
+ t = threading.Thread(target=run_service, args=(service,))
54
+ t.daemon = True
55
+ t.start()
56
+ threads.append(t)
57
+ time.sleep(1) # Small delay to avoid clashes
58
+
59
+ print("\nAll services started. Access the app at http://localhost:5000")
60
+ print("Press Ctrl+C to stop all services.\n")
61
+
62
+ try:
63
+ while True:
64
+ time.sleep(1)
65
+ except KeyboardInterrupt:
66
+ print("\nStopping all services...")
67
+ for p in processes:
68
+ p.terminate()
69
+ sys.exit(0)
70
+
71
+ if __name__ == "__main__":
72
+ main()
services/ai-service/.env.example ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ GEMINI_API_KEY=your_gemini_api_key_here
2
+ # HF_API_KEY=your_huggingface_api_key_here
3
+ # HF_TOKEN=your_huggingface_token_here
4
+ PORT=5003
services/ai-service/Dockerfile ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+ WORKDIR /app
3
+ COPY requirements.txt .
4
+ RUN pip install --no-cache-dir -r requirements.txt
5
+ COPY . .
6
+ CMD ["python", "app.py"]
services/ai-service/app.py ADDED
@@ -0,0 +1,389 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, jsonify, request
2
+ from flask_cors import CORS
3
+ import os
4
+ import json
5
+ import requests
6
+ from google import genai
7
+ from groq import Groq
8
+ from openai import OpenAI
9
+ from dotenv import load_dotenv
10
+
11
+ load_dotenv()
12
+
13
+ app = Flask(__name__)
14
+ CORS(app)
15
+
16
+ # API Keys
17
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
18
+ HF_API_KEY = os.getenv("HF_API_KEY")
19
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY")
20
+ OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
21
+
22
+ # Initialize Groq Client
23
+ try:
24
+ if GROQ_API_KEY:
25
+ groq_client = Groq(api_key=GROQ_API_KEY)
26
+ else:
27
+ groq_client = None
28
+ print("Warning: GROQ_API_KEY not set")
29
+ except Exception as e:
30
+ groq_client = None
31
+ print(f"Failed to initialize Groq Client: {e}")
32
+
33
+
34
+
35
+ def configure_genai():
36
+ """Configure Gemini AI - No longer needed with direct requests but kept for interface compatibility if needed"""
37
+ pass
38
+
39
+ # Initialize Google GenAI Client
40
+ try:
41
+ if GEMINI_API_KEY:
42
+ client = genai.Client(api_key=GEMINI_API_KEY)
43
+ else:
44
+ client = None
45
+ print("Warning: GEMINI_API_KEY not set")
46
+ except Exception as e:
47
+ client = None
48
+ print(f"Failed to initialize Gemini Client: {e}")
49
+
50
+ def query_gemini_new(prompt, model_id="gemini-2.0-flash"):
51
+ """Query Gemini 2.0 API using google-genai SDK"""
52
+ if not client:
53
+ raise Exception("Gemini Client not initialized")
54
+
55
+ # Map old model names to new ones if necessary, or just use what's passed
56
+ if model_id == "gemini-1.5-flash":
57
+ model_id = "gemini-2.0-flash"
58
+
59
+ try:
60
+ response = client.models.generate_content(
61
+ model=model_id,
62
+ contents=prompt
63
+ )
64
+ return response.text
65
+ except Exception as e:
66
+ raise Exception(f"Gemini 2.0 Inference Failed: {str(e)}")
67
+
68
+ def query_groq(prompt, model_id="llama-3.3-70b-versatile"):
69
+ """Query Groq API"""
70
+ if not groq_client:
71
+ raise Exception("Groq Client not initialized (check GROQ_API_KEY)")
72
+
73
+ # Map old model names to new ones
74
+ if model_id in ["llama3-70b-8192", "llama-3.1-70b-versatile"]:
75
+ model_id = "llama-3.3-70b-versatile"
76
+
77
+ try:
78
+ chat_completion = groq_client.chat.completions.create(
79
+ messages=[
80
+ {
81
+ "role": "user",
82
+ "content": prompt,
83
+ }
84
+ ],
85
+ model=model_id,
86
+ )
87
+ return chat_completion.choices[0].message.content
88
+ except Exception as e:
89
+ raise Exception(f"Groq Inference Failed: {str(e)}")
90
+
91
+ def query_ollama(prompt, model_id="llama3"):
92
+ """Query local Ollama instance"""
93
+ url = f"{OLLAMA_BASE_URL}/api/generate"
94
+ payload = {
95
+ "model": model_id,
96
+ "prompt": prompt,
97
+ "stream": False
98
+ }
99
+
100
+ try:
101
+ response = requests.post(url, json=payload)
102
+ if response.status_code != 200:
103
+ raise Exception(f"Ollama Error {response.status_code}: {response.text}")
104
+ return response.json()['response']
105
+ except requests.exceptions.ConnectionError:
106
+ raise Exception("Could not connect to Ollama. Is it running?")
107
+ except Exception as e:
108
+ raise Exception(f"Ollama Inference Failed: {str(e)}")
109
+
110
+ def query_huggingface(prompt, model_id, api_key=None):
111
+ """Query Hugging Face models"""
112
+ if not api_key:
113
+ api_key = os.getenv("HF_TOKEN") or os.getenv("HF_API_KEY")
114
+
115
+ if not api_key:
116
+ raise Exception("Hugging Face API key missing")
117
+
118
+ if not model_id:
119
+ model_id = "zai-org/GLM-4.7-Flash:novita"
120
+
121
+ client = OpenAI(
122
+ base_url="https://router.huggingface.co/v1",
123
+ api_key=api_key
124
+ )
125
+
126
+ try:
127
+ completion = client.chat.completions.create(
128
+ model=model_id,
129
+ messages=[{"role": "user", "content": prompt}],
130
+ temperature=0.7,
131
+ max_tokens=4096,
132
+ top_p=0.9
133
+ )
134
+ return completion.choices[0].message.content
135
+ except Exception as e:
136
+ raise Exception(f"Hugging Face Inference Failed: {str(e)}")
137
+
138
+ def query_ai_with_fallback(prompt, provider=None, model_id=None):
139
+ """Unified query function with automatic fallback on failure (e.g. quota limits)"""
140
+ # Order of fallback: Requested -> Gemini -> Groq -> Ollama
141
+ providers_to_try = []
142
+
143
+ if provider:
144
+ providers_to_try.append(provider)
145
+
146
+ # Add others as fallbacks if not already the primary
147
+ for p in ["gemini", "groq", "ollama"]:
148
+ if p not in providers_to_try:
149
+ providers_to_try.append(p)
150
+
151
+ last_error = None
152
+ for p in providers_to_try:
153
+ try:
154
+ print(f"DEBUG: Trying AI provider: {p}")
155
+ if p == "groq":
156
+ # Only try Groq if key is available
157
+ if not GROQ_API_KEY:
158
+ raise Exception("Groq API Key missing")
159
+ m = model_id if provider == "groq" else "llama-3.3-70b-versatile"
160
+ return query_groq(prompt, m)
161
+
162
+ elif p == "gemini":
163
+ # Only try Gemini if key is available
164
+ if not GEMINI_API_KEY:
165
+ raise Exception("Gemini API Key missing")
166
+ m = model_id if provider == "gemini" else "gemini-2.0-flash"
167
+ return query_gemini_new(prompt, m)
168
+
169
+ elif p == "ollama":
170
+ m = model_id if provider == "ollama" else "llama3.2"
171
+ return query_ollama(prompt, m)
172
+
173
+ except Exception as e:
174
+ last_error = str(e)
175
+ print(f"WARNING: Provider {p} failed: {last_error}")
176
+ # If it's a quota error or connection error, continue to next provider
177
+ if "429" in last_error or "quota" in last_error.lower() or "connection" in last_error.lower():
178
+ continue
179
+ else:
180
+ # For other errors (like prompt issues), we might want to stop,
181
+ # but for safety let's try the next one anyway.
182
+ continue
183
+
184
+ raise Exception(f"All AI providers failed. Last error: {last_error}")
185
+
186
+ @app.route('/health', methods=['GET'])
187
+ def health():
188
+ """Health check endpoint"""
189
+ return jsonify({
190
+ "status": "healthy",
191
+ "service": "ai-service",
192
+ "hasGeminiKey": bool(GEMINI_API_KEY),
193
+ "hasGroqKey": bool(GROQ_API_KEY)
194
+ }), 200
195
+
196
+ @app.route('/ai/generate-quiz', methods=['POST'])
197
+ def generate_quiz():
198
+ """Generate quiz using improved pedagogical prompt"""
199
+ try:
200
+ data = request.get_json(force=True)
201
+ text_content = data.get("text", "")
202
+ provider = data.get("provider", "gemini") # Default to Gemini
203
+ model_id = data.get("model")
204
+ num_questions = data.get("numQuestions", 5)
205
+ difficulty = data.get("difficulty", "Medium")
206
+
207
+ if not text_content:
208
+ return jsonify({"success": False, "error": "No text provided"}), 400
209
+
210
+ # Enhanced pedagogical prompt
211
+ prompt = f"""Act as an Expert Educator and DevOps Architect. Your task is to generate a JSON-formatted practice quiz based on the provided file content.
212
+
213
+ Follow these strict pedagogical rules:
214
+ 1. FOCUS ON APPLICATION: Do not ask for simple definitions. Create scenario-based questions where the user must apply a concept.
215
+ 2. RATIONALE-DRIVEN: For every answer option, provide a one-sentence rationale explaining WHY it is correct or WHY it is a common misconception.
216
+ 3. ADAPTIVE DIFFICULTY: Group questions into 'Conceptual', 'Hands-on/Syntax', and 'Architectural/Problem Solving'.
217
+ 4. STRICT JSON: Ensure all double quotes within text fields are escaped with a backslash. Use only valid JSON characters.
218
+ 5. FORMAT: Return only a valid JSON object with the following structure:
219
+
220
+ {{
221
+ "title": "Quiz Title",
222
+ "questions": [
223
+ {{
224
+ "question": "string",
225
+ "answerOptions": [
226
+ {{"text": "string", "rationale": "string", "isCorrect": boolean}}
227
+ ],
228
+ "hint": "string",
229
+ "category": "Conceptual|Hands-on|Architectural"
230
+ }}
231
+ ]
232
+ }}
233
+
234
+ Generate exactly {num_questions} questions.
235
+ The difficulty level should be: {difficulty}.
236
+
237
+ Text:
238
+ {text_content[:10000]}
239
+ """
240
+
241
+ # Query AI provider with fallback
242
+ response_text = query_ai_with_fallback(prompt, provider, model_id)
243
+
244
+ # Robust JSON cleaning
245
+ cleaned = response_text.strip()
246
+ if cleaned.startswith("```"):
247
+ # Remove markdown blocks if AI accidentally included them despite JSON mode
248
+ parts = cleaned.split("```")
249
+ if len(parts) >= 3:
250
+ cleaned = parts[1]
251
+ if cleaned.startswith("json"):
252
+ cleaned = cleaned[4:]
253
+
254
+ start = cleaned.find("{")
255
+ end = cleaned.rfind("}")
256
+ if start == -1 or end == -1:
257
+ # Fallback for list-style responses if title/questions wrapper is missing
258
+ start = cleaned.find("[")
259
+ end = cleaned.rfind("]")
260
+ if start == -1 or end == -1:
261
+ raise Exception("AI did not return valid JSON structure")
262
+
263
+ raw_list = json.loads(cleaned[start:end + 1])
264
+ quiz_data = {"title": "Practice Quiz", "questions": raw_list}
265
+ else:
266
+ quiz_data = json.loads(cleaned[start:end + 1])
267
+
268
+ return jsonify({"success": True, "quiz": quiz_data}), 200
269
+
270
+ except Exception as e:
271
+ print(f"Quiz generation error: {str(e)}")
272
+ return jsonify({"success": False, "error": str(e)}), 500
273
+
274
+ @app.route('/ai/generate-topics', methods=['POST'])
275
+ def generate_topics():
276
+ """Extract key topics from text"""
277
+ try:
278
+ data = request.get_json(force=True)
279
+ text_content = data.get("text", "")
280
+ provider = data.get("provider", "gemini") # Changed default to gemini
281
+ model_id = data.get("model")
282
+
283
+ if not text_content:
284
+ return jsonify({"success": False, "error": "No text provided"}), 400
285
+
286
+ prompt = f"""Extract the 5 most important topics from the text below.
287
+ Return ONLY valid JSON:
288
+
289
+ [
290
+ {{ "topic": "Topic Name", "description": "One sentence description" }}
291
+ ]
292
+
293
+ Text:
294
+ {text_content[:10000]}
295
+ """
296
+
297
+ # Query AI provider with fallback
298
+ response_text = query_ai_with_fallback(prompt, provider, model_id)
299
+
300
+ cleaned = response_text.strip()
301
+ if cleaned.startswith("```"):
302
+ parts = cleaned.split("```")
303
+ if len(parts) >= 3:
304
+ cleaned = parts[1]
305
+ if cleaned.startswith("json"):
306
+ cleaned = cleaned[4:]
307
+
308
+ start = cleaned.find("[")
309
+ end = cleaned.rfind("]")
310
+ if start == -1 or end == -1:
311
+ # Maybe it returned a single object?
312
+ start = cleaned.find("{")
313
+ end = cleaned.rfind("}")
314
+ if start == -1 or end == -1:
315
+ raise Exception("AI did not return valid JSON")
316
+
317
+ topics = [json.loads(cleaned[start:end + 1])]
318
+ else:
319
+ topics = json.loads(cleaned[start:end + 1])
320
+
321
+ return jsonify({"success": True, "topics": topics}), 200
322
+
323
+ except Exception as e:
324
+ print(f"Topics generation error: {str(e)}")
325
+ return jsonify({"success": False, "error": str(e)}), 500
326
+
327
+ @app.route('/ai/generate-summary', methods=['POST'])
328
+ def generate_summary():
329
+ """Generate summary of text"""
330
+ try:
331
+ data = request.get_json(force=True)
332
+ text_content = data.get("text", "")
333
+ provider = data.get("provider", "gemini")
334
+ model_id = data.get("model")
335
+
336
+ if not text_content:
337
+ return jsonify({"success": False, "error": "No text provided"}), 400
338
+
339
+ prompt = f"""Summarize the following text in a concise and easy-to-understand manner for a student.
340
+ Highlight key definitions and core concepts.
341
+ Limit to 3 paragraphs.
342
+
343
+ Text:
344
+ {text_content[:15000]}
345
+ """
346
+
347
+ # Query AI provider with fallback
348
+ response_text = query_ai_with_fallback(prompt, provider, model_id)
349
+
350
+ return jsonify({"success": True, "summary": response_text}), 200
351
+
352
+ except Exception as e:
353
+ print(f"Summary generation error: {str(e)}")
354
+ return jsonify({"success": False, "error": str(e)}), 500
355
+
356
+ @app.route('/ai/explain-topic', methods=['POST'])
357
+ def explain_topic():
358
+ """Explain a specific topic in detail based on the text context"""
359
+ try:
360
+ data = request.get_json(force=True)
361
+ text_content = data.get("text", "")
362
+ topic_name = data.get("topic", "")
363
+ provider = data.get("provider", "gemini")
364
+ model_id = data.get("model")
365
+
366
+ if not text_content or not topic_name:
367
+ return jsonify({"success": False, "error": "Missing context or topic name"}), 400
368
+
369
+ prompt = f"""Explain the topic '{topic_name}' in detail based on its context within the provided text.
370
+ Explain it like you are a helpful teacher. Use simple analogies if possible.
371
+ Keep the explanation focused, professional, and limited to 2-3 detailed paragraphs.
372
+
373
+ Context Text:
374
+ {text_content[:10000]}
375
+ """
376
+
377
+ # Query AI provider with fallback
378
+ response_text = query_ai_with_fallback(prompt, provider, model_id)
379
+
380
+ return jsonify({"success": True, "explanation": response_text}), 200
381
+
382
+ except Exception as e:
383
+ print(f"Topic explanation error: {str(e)}")
384
+ return jsonify({"success": False, "error": str(e)}), 500
385
+
386
+ if __name__ == '__main__':
387
+ port = int(os.getenv('PORT', 5003))
388
+ print(f"AI Service running on port {port}")
389
+ app.run(host='0.0.0.0', port=port, debug=True)
services/ai-service/requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ flask
2
+ flask-cors
3
+ python-dotenv
4
+ google-genai
5
+ openai
6
+ requests
services/api-gateway/.env.example ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ PORT=5000
2
+ AUTH_SERVICE_URL=http://localhost:5001
3
+ FILE_PARSER_SERVICE_URL=http://localhost:5002
4
+ AI_SERVICE_URL=http://localhost:5003
5
+ FRONTEND_SERVICE_URL=http://localhost:5004
services/api-gateway/Dockerfile ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+ WORKDIR /app
3
+ COPY requirements.txt .
4
+ RUN pip install --no-cache-dir -r requirements.txt
5
+ COPY . .
6
+ CMD ["python", "app.py"]
services/api-gateway/app.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, jsonify, request, send_from_directory
2
+ from flask_cors import CORS
3
+ import requests
4
+ import os
5
+ from dotenv import load_dotenv
6
+
7
+ load_dotenv()
8
+
9
+ app = Flask(__name__, static_folder=None)
10
+ CORS(app)
11
+
12
+ # Service URLs
13
+ AUTH_SERVICE_URL = os.getenv("AUTH_SERVICE_URL", "http://localhost:5001")
14
+ FILE_PARSER_SERVICE_URL = os.getenv("FILE_PARSER_SERVICE_URL", "http://localhost:5002")
15
+ AI_SERVICE_URL = os.getenv("AI_SERVICE_URL", "http://localhost:5003")
16
+ FRONTEND_SERVICE_URL = os.getenv("FRONTEND_SERVICE_URL", "http://localhost:5004")
17
+
18
+ @app.route('/health', methods=['GET'])
19
+ def health():
20
+ """Health check for API Gateway"""
21
+ services_health = {}
22
+
23
+ # Check all services
24
+ services = {
25
+ "auth": AUTH_SERVICE_URL,
26
+ "file-parser": FILE_PARSER_SERVICE_URL,
27
+ "ai": AI_SERVICE_URL,
28
+ "frontend": FRONTEND_SERVICE_URL
29
+ }
30
+
31
+ for name, url in services.items():
32
+ try:
33
+ response = requests.get(f"{url}/health", timeout=2)
34
+ print(f"DEBUG: Service {name} at {url} returned status {response.status_code}")
35
+ services_health[name] = "healthy" if response.status_code == 200 else "unhealthy"
36
+ except Exception as e:
37
+ print(f"DEBUG: Service {name} at {url} failed: {str(e)}")
38
+ services_health[name] = "unreachable"
39
+
40
+ return jsonify({
41
+ "status": "healthy",
42
+ "service": "api-gateway",
43
+ "services": services_health
44
+ }), 200
45
+
46
+ # ==================== AUTH ROUTES ====================
47
+ @app.route('/api/config', methods=['GET'])
48
+ def get_config():
49
+ """Get authentication configuration"""
50
+ try:
51
+ response = requests.get(f"{AUTH_SERVICE_URL}/auth/config")
52
+ return jsonify(response.json()), response.status_code
53
+ except Exception as e:
54
+ return jsonify({"success": False, "error": f"Auth service unavailable: {str(e)}"}), 503
55
+
56
+ @app.route('/api/auth/verify', methods=['POST'])
57
+ def verify_token():
58
+ """Verify authentication token"""
59
+ try:
60
+ response = requests.post(
61
+ f"{AUTH_SERVICE_URL}/auth/verify",
62
+ json=request.get_json()
63
+ )
64
+ return jsonify(response.json()), response.status_code
65
+ except Exception as e:
66
+ return jsonify({"success": False, "error": f"Auth service unavailable: {str(e)}"}), 503
67
+
68
+ # ==================== FILE PARSER ROUTES ====================
69
+ @app.route('/api/parse-file', methods=['POST'])
70
+ def parse_file():
71
+ """Parse uploaded file"""
72
+ try:
73
+ # Forward the file to the file parser service
74
+ files = {'file': request.files['file']}
75
+ response = requests.post(
76
+ f"{FILE_PARSER_SERVICE_URL}/parse-file",
77
+ files=files
78
+ )
79
+ return jsonify(response.json()), response.status_code
80
+ except Exception as e:
81
+ return jsonify({"success": False, "error": f"File parser service unavailable: {str(e)}"}), 503
82
+
83
+ # ==================== AI ROUTES ====================
84
+ @app.route('/api/generate-quiz', methods=['POST'])
85
+ def generate_quiz():
86
+ """Generate quiz from text"""
87
+ try:
88
+ response = requests.post(
89
+ f"{AI_SERVICE_URL}/ai/generate-quiz",
90
+ json=request.get_json()
91
+ )
92
+ return jsonify(response.json()), response.status_code
93
+ except Exception as e:
94
+ return jsonify({"success": False, "error": f"AI service unavailable: {str(e)}"}), 503
95
+
96
+ @app.route('/api/generate-topics', methods=['POST'])
97
+ def generate_topics():
98
+ """Generate key topics from text"""
99
+ try:
100
+ response = requests.post(
101
+ f"{AI_SERVICE_URL}/ai/generate-topics",
102
+ json=request.get_json()
103
+ )
104
+ return jsonify(response.json()), response.status_code
105
+ except Exception as e:
106
+ return jsonify({"success": False, "error": f"AI service unavailable: {str(e)}"}), 503
107
+
108
+ @app.route('/api/generate-summary', methods=['POST'])
109
+ def generate_summary():
110
+ """Generate summary from text"""
111
+ try:
112
+ response = requests.post(
113
+ f"{AI_SERVICE_URL}/ai/generate-summary",
114
+ json=request.get_json()
115
+ )
116
+ return jsonify(response.json()), response.status_code
117
+ except Exception as e:
118
+ return jsonify({"success": False, "error": f"AI service unavailable: {str(e)}"}), 503
119
+
120
+ @app.route('/api/explain-topic', methods=['POST'])
121
+ def explain_topic():
122
+ """Explain a specific topic in detail"""
123
+ try:
124
+ response = requests.post(
125
+ f"{AI_SERVICE_URL}/ai/explain-topic",
126
+ json=request.get_json()
127
+ )
128
+ return jsonify(response.json()), response.status_code
129
+ except Exception as e:
130
+ return jsonify({"success": False, "error": f"AI service unavailable: {str(e)}"}), 503
131
+
132
+ # ==================== FRONTEND ROUTES ====================
133
+ @app.route('/')
134
+ def index():
135
+ """Serve main page"""
136
+ try:
137
+ response = requests.get(f"{FRONTEND_SERVICE_URL}/")
138
+ # Forward original content type from upstream if available
139
+ headers = {'Content-Type': response.headers.get('Content-Type', 'text/html')}
140
+ return response.content, response.status_code, headers
141
+ except Exception as e:
142
+ return f"Frontend service unavailable: {str(e)}", 503
143
+
144
+ @app.route('/static/<path:path>')
145
+ def serve_static(path):
146
+ """Serve static files"""
147
+ try:
148
+ # Construct the upstream URL
149
+ url = f"{FRONTEND_SERVICE_URL}/static/{path}"
150
+ response = requests.get(url, stream=True)
151
+
152
+ if response.status_code != 200:
153
+ return f"Static file not found at upstream: {path} (Checked: {url})", 404
154
+
155
+ # Forward important headers, especially Content-Type
156
+ headers = {}
157
+ if 'Content-Type' in response.headers:
158
+ headers['Content-Type'] = response.headers['Content-Type']
159
+
160
+ return response.content, response.status_code, headers
161
+ except Exception as e:
162
+ return f"Static file proxy error: {str(e)}", 500
163
+
164
+ if __name__ == '__main__':
165
+ port = int(os.getenv('PORT', 5000))
166
+ print(f"API Gateway running on port {port}")
167
+ print(f" Auth Service: {AUTH_SERVICE_URL}")
168
+ print(f" File Parser: {FILE_PARSER_SERVICE_URL}")
169
+ print(f" AI Service: {AI_SERVICE_URL}")
170
+ print(f" Frontend: {FRONTEND_SERVICE_URL}")
171
+ app.run(host='0.0.0.0', port=port, debug=True)
services/api-gateway/requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ flask
2
+ flask-cors
3
+ python-dotenv
4
+ requests
services/auth-service/.env.example ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ GOOGLE_CLIENT_ID=your_google_client_id_here
2
+ PORT=5001
services/auth-service/Dockerfile ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+ WORKDIR /app
3
+ COPY requirements.txt .
4
+ RUN pip install --no-cache-dir -r requirements.txt
5
+ COPY . .
6
+ CMD ["python", "app.py"]
services/auth-service/app.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, jsonify, request
2
+ from flask_cors import CORS
3
+ import os
4
+ from dotenv import load_dotenv
5
+
6
+ load_dotenv()
7
+
8
+ app = Flask(__name__)
9
+ CORS(app)
10
+
11
+ # Google OAuth Configuration
12
+ GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID", "550475677172-a5kk2v33roru9ujnq8jsq93li6t1dep7.apps.googleusercontent.com")
13
+
14
+ @app.route('/health', methods=['GET'])
15
+ def health():
16
+ """Health check endpoint"""
17
+ return jsonify({"status": "healthy", "service": "auth-service"}), 200
18
+
19
+ @app.route('/auth/config', methods=['GET'])
20
+ def get_config():
21
+ """Get Google OAuth configuration"""
22
+ return jsonify({
23
+ "clientId": GOOGLE_CLIENT_ID,
24
+ "success": True
25
+ }), 200
26
+
27
+ @app.route('/auth/verify', methods=['POST'])
28
+ def verify_token():
29
+ """Verify Google OAuth token"""
30
+ try:
31
+ data = request.get_json()
32
+ token = data.get('token')
33
+
34
+ if not token:
35
+ return jsonify({"success": False, "error": "No token provided"}), 400
36
+
37
+ # In production, verify the token with Google
38
+ # For now, we'll accept any token for development
39
+ return jsonify({
40
+ "success": True,
41
+ "user": {
42
+ "email": "user@example.com",
43
+ "name": "User"
44
+ }
45
+ }), 200
46
+
47
+ except Exception as e:
48
+ return jsonify({"success": False, "error": str(e)}), 500
49
+
50
+ if __name__ == '__main__':
51
+ port = int(os.getenv('PORT', 5001))
52
+ print(f"Auth Service running on port {port}")
53
+ app.run(host='0.0.0.0', port=port, debug=True)
services/auth-service/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ flask
2
+ flask-cors
3
+ python-dotenv
4
+ google-auth
5
+ google-auth-oauthlib
services/file-parser-service/.env.example ADDED
@@ -0,0 +1 @@
 
 
1
+ PORT=5002
services/file-parser-service/Dockerfile ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+ WORKDIR /app
3
+ COPY requirements.txt .
4
+ RUN pip install --no-cache-dir -r requirements.txt
5
+ COPY . .
6
+ CMD ["python", "app.py"]
services/file-parser-service/app.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, jsonify, request
2
+ from flask_cors import CORS
3
+ import pdfplumber
4
+ from pptx import Presentation
5
+ import os
6
+ import re
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv()
10
+
11
+ app = Flask(__name__)
12
+ CORS(app)
13
+
14
+ def clean_extracted_text(text):
15
+ """Remove common PDF/binary junk and normalize whitespace"""
16
+ if not text:
17
+ return ""
18
+
19
+ # Remove obvious PDF structural markers if they leaked
20
+ text = re.sub(r'endstream|endobj|\d+ \d+ obj|<<|>>|stream', '', text)
21
+
22
+ # Remove non-printable characters except common punctuation/newlines
23
+ # Keep standard ASCII and some useful Unicode
24
+ text = "".join(char for char in text if char.isprintable() or char in "\n\t\r")
25
+
26
+ # Normalize whitespace
27
+ text = re.sub(r'\s+', ' ', text).strip()
28
+ return text
29
+
30
+ @app.route('/health', methods=['GET'])
31
+ def health():
32
+ """Health check endpoint"""
33
+ return jsonify({"status": "healthy", "service": "file-parser-service"}), 200
34
+
35
+ @app.route('/parse-file', methods=['POST'])
36
+ def parse_file():
37
+ """Parse uploaded file and extract text"""
38
+ try:
39
+ if 'file' not in request.files:
40
+ return jsonify({"success": False, "error": "No file uploaded"}), 400
41
+
42
+ file = request.files['file']
43
+ filename = file.filename.lower()
44
+ extracted_text = ""
45
+
46
+ # Parse PDF files using pdfplumber (more robust)
47
+ if filename.endswith('.pdf'):
48
+ try:
49
+ with pdfplumber.open(file) as pdf:
50
+ for page in pdf.pages:
51
+ page_text = page.extract_text()
52
+ if page_text:
53
+ extracted_text += page_text + "\n"
54
+ except Exception as pdf_err:
55
+ print(f"pdfplumber failed: {pdf_err}")
56
+ return jsonify({"success": False, "error": f"PDF parsing failed: {str(pdf_err)}"}), 500
57
+
58
+ # Parse PPTX files
59
+ elif filename.endswith('.pptx'):
60
+ try:
61
+ prs = Presentation(file)
62
+ for slide in prs.slides:
63
+ for shape in slide.shapes:
64
+ if hasattr(shape, "text"):
65
+ extracted_text += shape.text + "\n"
66
+ except Exception as ppt_err:
67
+ print(f"PPTX parsing failed: {ppt_err}")
68
+ return jsonify({"success": False, "error": f"PPTX parsing failed: {str(ppt_err)}"}), 500
69
+
70
+ # Parse text files
71
+ else:
72
+ try:
73
+ extracted_text = file.read().decode("utf-8", errors="ignore")
74
+ except Exception as txt_err:
75
+ return jsonify({"success": False, "error": f"Text file parsing failed: {str(txt_err)}"}), 500
76
+
77
+ # Clean the text to ensure AI doesn't get junk
78
+ cleaned_text = clean_extracted_text(extracted_text)
79
+
80
+ print(f"Parsed file '{filename}' (Original: {len(extracted_text)}, Cleaned: {len(cleaned_text)})")
81
+
82
+ if not cleaned_text or len(cleaned_text) < 10:
83
+ return jsonify({
84
+ "success": False,
85
+ "error": "No readable text found in file. Please ensure the file is not just images."
86
+ }), 400
87
+
88
+ return jsonify({
89
+ "success": True,
90
+ "text": cleaned_text,
91
+ "filename": filename,
92
+ "length": len(cleaned_text)
93
+ }), 200
94
+
95
+ except Exception as e:
96
+ print(f"Parse error: {str(e)}")
97
+ return jsonify({"success": False, "error": str(e)}), 500
98
+
99
+ if __name__ == '__main__':
100
+ port = int(os.getenv('PORT', 5002))
101
+ print(f"File Parser Service running on port {port}")
102
+ app.run(host='0.0.0.0', port=port, debug=True)
services/file-parser-service/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ flask
2
+ flask-cors
3
+ python-dotenv
4
+ PyPDF2
5
+ python-pptx
services/frontend-service/.env.example ADDED
@@ -0,0 +1 @@
 
 
1
+ PORT=5004
services/frontend-service/Dockerfile ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+ WORKDIR /app
3
+ COPY requirements.txt .
4
+ RUN pip install --no-cache-dir -r requirements.txt
5
+ COPY . .
6
+ CMD ["python", "app.py"]
services/frontend-service/app.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, send_from_directory, jsonify
2
+ from flask_cors import CORS
3
+ import os
4
+ from dotenv import load_dotenv
5
+
6
+ load_dotenv()
7
+
8
+ app = Flask(__name__,
9
+ static_folder='static',
10
+ static_url_path='/static',
11
+ template_folder='templates')
12
+ CORS(app)
13
+
14
+ @app.route('/health', methods=['GET'])
15
+ def health():
16
+ """Health check endpoint"""
17
+ return jsonify({"status": "healthy", "service": "frontend-service"}), 200
18
+
19
+ @app.route('/')
20
+ def index():
21
+ """Serve main HTML page"""
22
+ return send_from_directory('templates', 'index.html')
23
+
24
+ if __name__ == '__main__':
25
+ port = int(os.getenv('PORT', 5004))
26
+ print(f"Frontend Service running on port {port}")
27
+ app.run(host='0.0.0.0', port=port, debug=True)
services/frontend-service/requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ flask
2
+ flask-cors
3
+ python-dotenv
services/frontend-service/static/css/style.css ADDED
@@ -0,0 +1,1522 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;700&display=swap');
2
+
3
+ :root {
4
+ --bg-color: #020617;
5
+ --primary-color: #0ea5e9;
6
+ --accent-color: #38bdf8;
7
+ --text-primary: #f8fafc;
8
+ --text-secondary: #94a3b8;
9
+ --card-bg: rgba(30, 41, 59, 0.7);
10
+ --card-hover: rgba(51, 65, 85, 0.9);
11
+ --card-border: rgba(255, 255, 255, 0.1);
12
+ --glass-shine: rgba(255, 255, 255, 0.05);
13
+ --success: #22c55e;
14
+ --error: #ef4444;
15
+ }
16
+
17
+ [data-theme="light"] {
18
+ --bg-color: #f8fafc;
19
+ --primary-color: #0284c7;
20
+ --accent-color: #0ea5e9;
21
+ --text-primary: #0f172a;
22
+ --text-secondary: #475569;
23
+ --card-bg: rgba(255, 255, 255, 0.8);
24
+ --card-hover: rgba(255, 255, 255, 0.95);
25
+ --card-border: rgba(0, 0, 0, 0.1);
26
+ --glass-shine: rgba(0, 0, 0, 0.02);
27
+ }
28
+
29
+ * {
30
+ margin: 0;
31
+ padding: 0;
32
+ box-sizing: border-box;
33
+ }
34
+
35
+ body {
36
+ font-family: 'Outfit', sans-serif;
37
+ background-color: var(--bg-color);
38
+ color: var(--text-primary);
39
+ min-height: 100vh;
40
+ overflow-x: hidden;
41
+ background: radial-gradient(circle at top left, #0f172a, #020617),
42
+ radial-gradient(circle at bottom right, #1e1b4b, #020617);
43
+ }
44
+
45
+ /* Background blob effects */
46
+ .blob {
47
+ position: absolute;
48
+ width: 800px;
49
+ height: 800px;
50
+ background: radial-gradient(circle, rgba(14, 165, 233, 0.15), transparent 70%);
51
+ filter: blur(100px);
52
+ z-index: -1;
53
+ border-radius: 50%;
54
+ animation: pulse 15s infinite alternate;
55
+ }
56
+
57
+ .blob-1 {
58
+ top: -300px;
59
+ left: -200px;
60
+ }
61
+
62
+ .blob-2 {
63
+ bottom: -300px;
64
+ right: -200px;
65
+ background: radial-gradient(circle, rgba(49, 46, 129, 0.2), transparent 70%);
66
+ }
67
+
68
+ @keyframes pulse {
69
+ 0% {
70
+ transform: scale(1);
71
+ opacity: 0.15;
72
+ }
73
+
74
+ 100% {
75
+ transform: scale(1.1);
76
+ opacity: 0.25;
77
+ }
78
+ }
79
+
80
+ /* --- Login / Landing View --- */
81
+ .login-container {
82
+ min-height: 100vh;
83
+ display: flex;
84
+ flex-direction: column;
85
+ align-items: center;
86
+ position: relative;
87
+ overflow-x: hidden;
88
+ scroll-behavior: smooth;
89
+ background: #020617;
90
+ /* Solid base */
91
+ }
92
+
93
+ /* Remove or simplify the overlay if no image is used */
94
+ .login-container::before {
95
+ display: none;
96
+ }
97
+
98
+ .login-content {
99
+ min-height: 100vh;
100
+ display: flex;
101
+ flex-direction: column;
102
+ justify-content: center;
103
+ align-items: center;
104
+ text-align: center;
105
+ position: relative;
106
+ z-index: 1;
107
+ max-width: 800px;
108
+ padding: 2rem;
109
+ }
110
+
111
+ /* About Section */
112
+ .about-section {
113
+ position: relative;
114
+ z-index: 1;
115
+ max-width: 1200px;
116
+ width: 100%;
117
+ margin-bottom: 8rem;
118
+ padding: 0 2rem;
119
+ text-align: center;
120
+ }
121
+
122
+ .about-title {
123
+ font-size: 2.5rem;
124
+ font-weight: 700;
125
+ margin-bottom: 2rem;
126
+ color: white;
127
+ }
128
+
129
+ .about-description {
130
+ font-size: 1.1rem;
131
+ color: var(--text-secondary);
132
+ margin-bottom: 1.5rem;
133
+ line-height: 1.6;
134
+ max-width: 900px;
135
+ margin-left: auto;
136
+ margin-right: auto;
137
+ }
138
+
139
+ .feature-cards {
140
+ display: grid;
141
+ grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
142
+ gap: 2rem;
143
+ margin-top: 4rem;
144
+ max-width: 1100px;
145
+ margin-left: auto;
146
+ margin-right: auto;
147
+ }
148
+
149
+ .feature-card {
150
+ background: rgba(30, 41, 59, 0.3);
151
+ backdrop-filter: blur(20px);
152
+ border: 1px solid rgba(255, 255, 255, 0.05);
153
+ border-radius: 32px;
154
+ padding: 3rem;
155
+ text-align: left;
156
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
157
+ display: flex;
158
+ flex-direction: column;
159
+ gap: 1.5rem;
160
+ }
161
+
162
+ .feature-card:hover {
163
+ background: rgba(30, 41, 59, 0.5);
164
+ border-color: var(--primary-color);
165
+ transform: translateY(-10px);
166
+ }
167
+
168
+ .feature-header {
169
+ display: flex;
170
+ align-items: center;
171
+ gap: 1.5rem;
172
+ }
173
+
174
+ .feature-logo {
175
+ width: 56px;
176
+ height: 56px;
177
+ object-fit: contain;
178
+ background: white;
179
+ padding: 8px;
180
+ border-radius: 12px;
181
+ }
182
+
183
+ .feature-card h3 {
184
+ font-size: 1.5rem;
185
+ font-weight: 700;
186
+ color: white;
187
+ margin: 0;
188
+ }
189
+
190
+ .feature-description {
191
+ font-size: 1rem;
192
+ color: var(--text-secondary);
193
+ margin: 0;
194
+ }
195
+
196
+ .feature-link {
197
+ color: var(--primary-color);
198
+ text-decoration: none;
199
+ font-weight: 600;
200
+ font-size: 0.9rem;
201
+ display: inline-flex;
202
+ align-items: center;
203
+ gap: 0.5rem;
204
+ transition: all 0.3s;
205
+ margin-top: auto;
206
+ }
207
+
208
+ .feature-link:hover {
209
+ color: white;
210
+ transform: translateX(5px);
211
+ }
212
+
213
+ .brand-title {
214
+ font-size: 5rem;
215
+ font-weight: 700;
216
+ letter-spacing: -0.05em;
217
+ margin-bottom: 1rem;
218
+ background: linear-gradient(to right, #fff, var(--primary-color));
219
+ -webkit-background-clip: text;
220
+ background-clip: text;
221
+ -webkit-text-fill-color: transparent;
222
+ text-shadow: 0 10px 80px rgba(14, 165, 233, 0.4);
223
+ }
224
+
225
+ .brand-subtitle {
226
+ font-size: 1.25rem;
227
+ color: var(--text-secondary);
228
+ margin-bottom: 3rem;
229
+ line-height: 1.6;
230
+ }
231
+
232
+ .btn-login-hero {
233
+ background: white;
234
+ color: #0f172a;
235
+ font-weight: 600;
236
+ font-size: 1.1rem;
237
+ padding: 1rem 2.5rem;
238
+ border-radius: 9999px;
239
+ border: 1px solid rgba(255, 255, 255, 0.2);
240
+ cursor: pointer;
241
+ display: flex;
242
+ align-items: center;
243
+ gap: 1rem;
244
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);
245
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
246
+ }
247
+
248
+ .btn-login-hero:hover {
249
+ transform: translateY(-3px) scale(1.02);
250
+ box-shadow: 0 20px 35px -10px rgba(0, 0, 0, 0.4);
251
+ background: #f8fafc;
252
+ }
253
+
254
+ .btn-icon-img {
255
+ width: 24px;
256
+ height: 24px;
257
+ }
258
+
259
+ .login-footer {
260
+ color: white;
261
+ position: relative;
262
+ padding: 4rem 0 3rem;
263
+ width: 100%;
264
+ z-index: 1;
265
+ display: flex;
266
+ flex-direction: column;
267
+ align-items: center;
268
+ gap: 1.5rem;
269
+ }
270
+
271
+ .social-links {
272
+ display: flex;
273
+ gap: 1.5rem;
274
+ margin-bottom: 0.5rem;
275
+ }
276
+
277
+ .social-icon {
278
+ width: 40px;
279
+ height: 40px;
280
+ display: flex;
281
+ align-items: center;
282
+ justify-content: center;
283
+ background: rgba(255, 255, 255, 0.05);
284
+ border: 1px solid rgba(255, 255, 255, 0.1);
285
+ border-radius: 12px;
286
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
287
+ opacity: 0.7;
288
+ }
289
+
290
+ .social-icon img {
291
+ width: 20px;
292
+ height: 20px;
293
+ /* Apply a light filter to make black svgs white-ish if needed, or leave as is if they match */
294
+ filter: brightness(0) invert(1);
295
+ }
296
+
297
+ .social-icon:hover {
298
+ background: var(--primary-color);
299
+ border-color: var(--primary-color);
300
+ transform: translateY(-5px);
301
+ opacity: 1;
302
+ box-shadow: 0 10px 20px rgba(14, 165, 233, 0.3);
303
+ }
304
+
305
+ .social-icon:hover img {
306
+ filter: brightness(0) invert(0);
307
+ /* Black icons on primary background */
308
+ }
309
+
310
+ /* --- Dashboard Navigation --- */
311
+ nav {
312
+ display: flex;
313
+ justify-content: space-between;
314
+ align-items: center;
315
+ padding: 1.5rem 2rem;
316
+ position: sticky;
317
+ top: 0;
318
+ z-index: 100;
319
+ backdrop-filter: blur(15px);
320
+ border-bottom: 1px solid var(--card-border);
321
+ background: rgba(2, 6, 23, 0.5);
322
+ }
323
+
324
+ .logo-highlight {
325
+ font-weight: 700;
326
+ font-size: 1.8rem;
327
+ /* color: white; */
328
+ background: linear-gradient(to right, #fff, var(--primary-color));
329
+ -webkit-background-clip: text;
330
+ background-clip: text;
331
+ -webkit-text-fill-color: transparent;
332
+ text-shadow: 0 10px 80px rgba(14, 165, 233, 0.4);
333
+ letter-spacing: -0.03em;
334
+ }
335
+
336
+ .btn-glass {
337
+ background: #111827;
338
+ border: 1px solid rgba(255, 255, 255, 0.1);
339
+ color: white;
340
+ padding: 0.75rem 2rem;
341
+ border-radius: 100px;
342
+ font-size: 0.95rem;
343
+ font-weight: 500;
344
+ cursor: pointer;
345
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
346
+ display: flex;
347
+ align-items: center;
348
+ gap: 0.75rem;
349
+ backdrop-filter: blur(10px);
350
+ }
351
+
352
+ .btn-glass span {
353
+ font-size: 0.8rem;
354
+ opacity: 0.7;
355
+ }
356
+
357
+ .btn-glass:hover {
358
+ background: rgba(255, 255, 255, 0.12);
359
+ border-color: var(--primary-color);
360
+ }
361
+
362
+ .btn-primary,
363
+ .btn-secondary {
364
+ font-family: 'Outfit', sans-serif;
365
+ font-weight: 600;
366
+ padding: 0.8rem 2rem;
367
+ border-radius: 100px;
368
+ cursor: pointer;
369
+ transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
370
+ border: 1px solid rgba(255, 255, 255, 0.1);
371
+ font-size: 0.95rem;
372
+ display: inline-flex;
373
+ align-items: center;
374
+ justify-content: center;
375
+ gap: 0.5rem;
376
+ }
377
+
378
+ .btn-primary {
379
+ background: white;
380
+ color: #0f172a;
381
+ }
382
+
383
+ .btn-primary:hover {
384
+ background: #f1f5f9;
385
+ transform: translateY(-2px);
386
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
387
+ }
388
+
389
+ .btn-secondary {
390
+ background: rgba(255, 255, 255, 0.05);
391
+ color: white;
392
+ }
393
+
394
+ .btn-secondary:hover {
395
+ background: rgba(255, 255, 255, 0.12);
396
+ border-color: var(--primary-color);
397
+ transform: translateY(-2px);
398
+ }
399
+
400
+ .btn-logout,
401
+ .btn-assistant {
402
+ font-weight: 600;
403
+ font-size: 0.95rem;
404
+ padding: 0.6rem 2rem;
405
+ border-radius: 99px;
406
+ cursor: pointer;
407
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
408
+ border: none;
409
+ }
410
+
411
+ .btn-logout {
412
+ background: white;
413
+ color: #0f172a;
414
+ }
415
+
416
+ .btn-logout:hover {
417
+ background: #f1f5f9;
418
+ transform: translateY(-2px);
419
+ box-shadow: 0 5px 15px rgba(255, 255, 255, 0.1);
420
+ }
421
+
422
+ .btn-assistant {
423
+ background: #1e293b;
424
+ color: white;
425
+ margin-right: 0.75rem;
426
+ border: 1px solid rgba(255, 255, 255, 0.1);
427
+ }
428
+
429
+ .btn-assistant:hover {
430
+ background: #334155;
431
+ border-color: var(--primary-color);
432
+ transform: translateY(-2px);
433
+ }
434
+
435
+ /* --- Assistant View Styles --- */
436
+ .container.assistant-container {
437
+ display: flex;
438
+ flex-direction: column;
439
+ align-items: center;
440
+ justify-content: flex-start;
441
+ min-height: calc(100vh - 120px);
442
+ text-align: center;
443
+ padding: 2rem 2rem 220px 2rem !important;
444
+ /* Significant padding to clear the fixed chat bar */
445
+ animation: fadeIn 0.6s ease;
446
+ overflow-y: visible;
447
+ /* Let the body scroll */
448
+ }
449
+
450
+ .assistant-header-center {
451
+ margin-bottom: 3rem;
452
+ }
453
+
454
+ .assistant-prompt {
455
+ font-size: 2.2rem;
456
+ color: var(--text-secondary);
457
+ margin-bottom: 3.5rem;
458
+ font-weight: 400;
459
+ max-width: 800px;
460
+ line-height: 1.3;
461
+ }
462
+
463
+ .discussion-pills {
464
+ display: flex;
465
+ flex-wrap: wrap;
466
+ gap: 1.25rem;
467
+ justify-content: center;
468
+ max-width: 1000px;
469
+ margin-bottom: 4rem;
470
+ }
471
+
472
+ .discussion-pill {
473
+ background: rgba(30, 41, 59, 0.5);
474
+ border: 1px solid rgba(255, 255, 255, 0.1);
475
+ color: #94a3b8;
476
+ padding: 0.8rem 1.8rem;
477
+ border-radius: 99px;
478
+ font-size: 0.95rem;
479
+ cursor: pointer;
480
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
481
+ }
482
+
483
+ .discussion-pill:hover {
484
+ background: rgba(30, 41, 59, 0.8);
485
+ border-color: var(--primary-color);
486
+ color: white;
487
+ transform: translateY(-3px);
488
+ }
489
+
490
+ .chat-input-wrapper {
491
+ position: fixed;
492
+ bottom: 0;
493
+ left: 0;
494
+ width: 100%;
495
+ padding: 1.5rem 2rem 2.5rem;
496
+ /* Reduced padding from 2.5/3.5 */
497
+ background: linear-gradient(180deg, transparent, rgba(2, 6, 23, 0.95) 70%);
498
+ /* Sharper gradient at the very bottom */
499
+ z-index: 1000;
500
+ display: flex;
501
+ justify-content: center;
502
+ pointer-events: none;
503
+ /* Allow clicking through the fade area */
504
+ }
505
+
506
+ .chat-input-container {
507
+ pointer-events: all;
508
+ /* Re-enable for the bar itself */
509
+ width: 100%;
510
+ max-width: 800px;
511
+ background: rgba(30, 41, 59, 0.6);
512
+ backdrop-filter: blur(20px) saturate(150%);
513
+ -webkit-backdrop-filter: blur(20px) saturate(150%);
514
+ border-radius: 24px;
515
+ padding: 0.6rem 0.6rem 0.6rem 1.5rem;
516
+ border: 1px solid rgba(255, 255, 255, 0.1);
517
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4), inset 0 1px 1px rgba(255, 255, 255, 0.05);
518
+ display: flex;
519
+ align-items: center;
520
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
521
+ }
522
+
523
+ .chat-input-container:focus-within {
524
+ background: rgba(30, 41, 59, 0.8);
525
+ border-color: var(--primary-color);
526
+ box-shadow: 0 0 40px rgba(14, 165, 233, 0.2);
527
+ transform: translateY(-2px);
528
+ }
529
+
530
+ #chat-input {
531
+ flex: 1;
532
+ background: transparent;
533
+ border: none;
534
+ color: #f8fafc;
535
+ font-size: 1.1rem;
536
+ resize: none;
537
+ outline: none;
538
+ font-family: inherit;
539
+ line-height: 1.6;
540
+ max-height: 200px;
541
+ padding: 0.5rem 0;
542
+ display: block;
543
+ }
544
+
545
+ #chat-input::placeholder {
546
+ color: rgba(255, 255, 255, 0.3);
547
+ }
548
+
549
+ .btn-send-chat {
550
+ width: 48px;
551
+ height: 48px;
552
+ background: var(--primary-color);
553
+ color: #020617;
554
+ border: none;
555
+ border-radius: 18px;
556
+ cursor: pointer;
557
+ display: flex;
558
+ align-items: center;
559
+ justify-content: center;
560
+ font-size: 1.2rem;
561
+ transition: all 0.2s;
562
+ margin-left: 1rem;
563
+ flex-shrink: 0;
564
+ }
565
+
566
+ .btn-send-chat:hover {
567
+ background: white;
568
+ transform: scale(1.05);
569
+ box-shadow: 0 0 20px rgba(14, 165, 233, 0.4);
570
+ }
571
+
572
+ .btn-send-chat:active {
573
+ transform: scale(0.95);
574
+ }
575
+
576
+ .assistant-response-area {
577
+ width: 100%;
578
+ max-width: 850px;
579
+ margin-top: 3rem;
580
+ margin-bottom: 2rem;
581
+ /* Extra space between response and bottom bar */
582
+ text-align: left;
583
+ }
584
+
585
+ .response-card {
586
+ background: rgba(30, 41, 59, 0.3);
587
+ border: 1px solid rgba(255, 255, 255, 0.05);
588
+ border-radius: 24px;
589
+ padding: 2.5rem;
590
+ color: #e2e8f0;
591
+ font-size: 1.1rem;
592
+ line-height: 1.8;
593
+ }
594
+
595
+ .response-card h3,
596
+ .response-card h4 {
597
+ color: white;
598
+ margin-top: 1.5rem;
599
+ margin-bottom: 0.75rem;
600
+ }
601
+
602
+ .response-card p {
603
+ margin-bottom: 1.25rem;
604
+ }
605
+
606
+ .response-card li {
607
+ margin-bottom: 0.5rem;
608
+ margin-left: 1.5rem;
609
+ }
610
+
611
+ [data-theme="light"] .chat-input-container {
612
+ background: white;
613
+ border-color: rgba(0, 0, 0, 0.1);
614
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05);
615
+ }
616
+
617
+ [data-theme="light"] #chat-input {
618
+ color: #0f172a;
619
+ }
620
+
621
+ [data-theme="light"] .btn-send-chat {
622
+ color: white;
623
+ }
624
+
625
+ [data-theme="light"] .discussion-pill {
626
+ background: rgba(0, 0, 0, 0.05);
627
+ border: 1px solid rgba(0, 0, 0, 0.05);
628
+ color: #475569;
629
+ }
630
+
631
+ [data-theme="light"] .discussion-pill:hover {
632
+ background: rgba(0, 0, 0, 0.1);
633
+ color: #0f172a;
634
+ }
635
+
636
+
637
+ .nav-actions {
638
+ display: flex;
639
+ align-items: center;
640
+ gap: 1.25rem;
641
+ }
642
+
643
+ /* --- Main Container & Grid --- */
644
+ .container {
645
+ max-width: 1400px;
646
+ margin: 0 auto;
647
+ padding: 3rem 2rem;
648
+ }
649
+
650
+ .grid {
651
+ display: grid;
652
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
653
+ gap: 2rem;
654
+ }
655
+
656
+ /* Premium Card Style - Updated to match design image */
657
+ .card {
658
+ border-radius: 32px;
659
+ padding: 0;
660
+ height: auto;
661
+ background: #0f172a;
662
+ display: flex;
663
+ flex-direction: column;
664
+ transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
665
+ cursor: pointer;
666
+ position: relative;
667
+ overflow: hidden;
668
+ border: 1px solid rgba(255, 255, 255, 0.05);
669
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
670
+ }
671
+
672
+ .card:hover {
673
+ transform: translateY(-8px);
674
+ box-shadow: 0 30px 60px -15px rgba(0, 0, 0, 0.6);
675
+ border-color: rgba(14, 165, 233, 0.3);
676
+ }
677
+
678
+ .card-image-container {
679
+ padding: 12px;
680
+ width: 100%;
681
+ }
682
+
683
+ .card-banner {
684
+ width: 100%;
685
+ height: 160px;
686
+ object-fit: cover;
687
+ border-radius: 24px;
688
+ display: block;
689
+ }
690
+
691
+ .card-info {
692
+ padding: 1.25rem 1.5rem 1.75rem;
693
+ display: flex;
694
+ justify-content: space-between;
695
+ align-items: flex-end;
696
+ }
697
+
698
+ .card-text {
699
+ flex: 1;
700
+ display: flex;
701
+ flex-direction: column;
702
+ gap: 0.15rem;
703
+ }
704
+
705
+ .card-title {
706
+ font-size: 1.35rem;
707
+ font-weight: 700;
708
+ color: white;
709
+ margin: 0;
710
+ line-height: 1.2;
711
+ }
712
+
713
+ .card-subtitle {
714
+ font-size: 0.8rem;
715
+ color: var(--text-secondary);
716
+ font-weight: 500;
717
+ opacity: 0.7;
718
+ }
719
+
720
+ .card-action {
721
+ background: rgba(148, 163, 184, 0.15);
722
+ color: #cbd5e1;
723
+ padding: 0.6rem 1.25rem;
724
+ border-radius: 100px;
725
+ font-size: 0.8rem;
726
+ font-weight: 600;
727
+ backdrop-filter: blur(10px);
728
+ border: 1px solid rgba(255, 255, 255, 0.1);
729
+ transition: all 0.3s;
730
+ white-space: nowrap;
731
+ margin-left: 1rem;
732
+ }
733
+
734
+ .card:hover .card-action {
735
+ background: var(--primary-color);
736
+ color: #0f172a;
737
+ border-color: var(--primary-color);
738
+ }
739
+
740
+ /* Quiz Overlay & Containers */
741
+ .quiz-overlay {
742
+ position: fixed;
743
+ inset: 0;
744
+ background: rgba(2, 6, 23, 0.8);
745
+ backdrop-filter: blur(20px);
746
+ z-index: 200;
747
+ display: flex;
748
+ align-items: center;
749
+ justify-content: center;
750
+ opacity: 0;
751
+ pointer-events: none;
752
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
753
+ }
754
+
755
+ .quiz-overlay.active {
756
+ opacity: 1;
757
+ pointer-events: all;
758
+ }
759
+
760
+ .quiz-container {
761
+ background: #0f172a;
762
+ width: 95%;
763
+ max-width: 1000px;
764
+ border-radius: 48px;
765
+ padding: 3.5rem;
766
+ position: relative;
767
+ border: 1px solid rgba(255, 255, 255, 0.05);
768
+ max-height: 90vh;
769
+ overflow-y: auto;
770
+ box-shadow: 0 50px 100px -20px rgba(0, 0, 0, 0.7);
771
+ }
772
+
773
+ .close-btn {
774
+ position: absolute;
775
+ top: 2rem;
776
+ right: 2rem;
777
+ width: 40px;
778
+ height: 40px;
779
+ border-radius: 50%;
780
+ background: rgba(255, 255, 255, 0.1);
781
+ border: none;
782
+ color: white;
783
+ font-size: 1.5rem;
784
+ cursor: pointer;
785
+ display: flex;
786
+ align-items: center;
787
+ justify-content: center;
788
+ transition: all 0.3s;
789
+ z-index: 10;
790
+ }
791
+
792
+ .close-btn:hover {
793
+ background: rgba(255, 255, 255, 0.2);
794
+ transform: rotate(90deg);
795
+ }
796
+
797
+ #view-loading {
798
+ display: flex;
799
+ flex-direction: column;
800
+ align-items: center;
801
+ justify-content: center;
802
+ padding: 3rem 0;
803
+ text-align: center;
804
+ }
805
+
806
+ #view-loading h2 {
807
+ font-size: 2rem;
808
+ color: white;
809
+ margin-bottom: 2rem;
810
+ font-weight: 700;
811
+ text-shadow: 0 0 20px rgba(14, 165, 233, 0.3);
812
+ }
813
+
814
+ #loading-text {
815
+ margin-top: 2rem;
816
+ font-size: 1.1rem;
817
+ color: white;
818
+ font-weight: 500;
819
+ letter-spacing: 0.02em;
820
+ opacity: 0.9;
821
+ }
822
+
823
+ /* --- Quiz Views & Panels --- */
824
+ .quiz-header-banner {
825
+ position: relative;
826
+ width: 100%;
827
+ height: 180px;
828
+ border-radius: 24px;
829
+ overflow: hidden;
830
+ margin-bottom: 2rem;
831
+ }
832
+
833
+ .quiz-stats-overlay {
834
+ text-align: right;
835
+ display: flex;
836
+ flex-direction: column;
837
+ gap: 0.5rem;
838
+ }
839
+
840
+ #quiz-timer-display,
841
+ #quiz-qno-display {
842
+ font-size: 1.1rem;
843
+ font-weight: 600;
844
+ color: white;
845
+ opacity: 0.9;
846
+ }
847
+
848
+ .config-container {
849
+ padding: 0 1rem;
850
+ }
851
+
852
+ .config-main-title {
853
+ text-align: center;
854
+ font-size: 1.5rem;
855
+ margin-bottom: 2.5rem;
856
+ font-weight: 700;
857
+ }
858
+
859
+ .config-row {
860
+ display: flex;
861
+ justify-content: space-between;
862
+ align-items: center;
863
+ margin-bottom: 2rem;
864
+ }
865
+
866
+ .config-row label {
867
+ font-size: 1rem;
868
+ font-weight: 600;
869
+ color: var(--text-secondary);
870
+ }
871
+
872
+ .pill-group {
873
+ display: flex;
874
+ gap: 0.75rem;
875
+ }
876
+
877
+ .pill-option {
878
+ padding: 0.6rem 1.25rem;
879
+ background: rgba(255, 255, 255, 0.05);
880
+ border: 1px solid rgba(255, 255, 255, 0.1);
881
+ color: white;
882
+ border-radius: 100px;
883
+ font-size: 0.85rem;
884
+ font-weight: 600;
885
+ cursor: pointer;
886
+ transition: all 0.3s;
887
+ }
888
+
889
+ .pill-option:hover {
890
+ background: rgba(255, 255, 255, 0.1);
891
+ }
892
+
893
+ .pill-option.selected {
894
+ background: var(--primary-color);
895
+ color: #0f172a;
896
+ border-color: var(--primary-color);
897
+ }
898
+
899
+ .config-actions {
900
+ margin-top: 3rem;
901
+ display: flex;
902
+ flex-direction: column;
903
+ gap: 1rem;
904
+ }
905
+
906
+ .quiz-active-container {
907
+ padding: 0 1rem;
908
+ }
909
+
910
+ .question-text {
911
+ color: white;
912
+ font-size: 1.35rem;
913
+ line-height: 1.5;
914
+ margin-bottom: 2.5rem;
915
+ font-weight: 600;
916
+ }
917
+
918
+ .options-grid {
919
+ display: grid;
920
+ grid-template-columns: repeat(2, 1fr);
921
+ gap: 1.5rem;
922
+ margin-bottom: 2rem;
923
+ }
924
+
925
+ .option-btn {
926
+ padding: 1.25rem 2rem;
927
+ background: rgba(255, 255, 255, 0.05);
928
+ border: 1px solid rgba(255, 255, 255, 0.1);
929
+ color: white;
930
+ border-radius: 100px;
931
+ font-size: 1rem;
932
+ font-weight: 500;
933
+ text-align: center;
934
+ cursor: pointer;
935
+ transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
936
+ display: flex;
937
+ align-items: center;
938
+ justify-content: center;
939
+ }
940
+
941
+ .option-btn:hover:not(:disabled) {
942
+ background: rgba(255, 255, 255, 0.1);
943
+ transform: translateY(-2px);
944
+ border-color: rgba(255, 255, 255, 0.2);
945
+ }
946
+
947
+ .option-btn.correct {
948
+ background: var(--success) !important;
949
+ color: #0f172a !important;
950
+ font-weight: 700;
951
+ border-color: var(--success) !important;
952
+ }
953
+
954
+ .option-btn.wrong {
955
+ background: var(--error) !important;
956
+ color: white !important;
957
+ border-color: var(--error) !important;
958
+ }
959
+
960
+ .result-breakdown {
961
+ display: flex;
962
+ gap: 2rem;
963
+ margin: 1.5rem 0;
964
+ justify-content: center;
965
+ font-size: 1.1rem;
966
+ font-weight: 600;
967
+ }
968
+
969
+ .result-item.correct {
970
+ color: var(--success);
971
+ }
972
+
973
+ .result-item.wrong {
974
+ color: var(--error);
975
+ }
976
+
977
+ .menu-header {
978
+ margin-bottom: 3rem;
979
+ display: flex;
980
+ justify-content: space-between;
981
+ align-items: flex-start;
982
+ }
983
+
984
+ .menu-titles h2 {
985
+ font-size: 2rem;
986
+ font-weight: 700;
987
+ color: white;
988
+ margin: 0;
989
+ }
990
+
991
+ .menu-subtitle {
992
+ font-size: 1rem;
993
+ color: var(--text-secondary);
994
+ opacity: 0.8;
995
+ margin-top: 0.25rem;
996
+ }
997
+
998
+ /* Action Cards - Updated to match design image */
999
+ .action-grid {
1000
+ display: grid;
1001
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
1002
+ gap: 1.5rem;
1003
+ }
1004
+
1005
+ .action-card {
1006
+ background: rgba(255, 255, 255, 0.03);
1007
+ padding: 1.5rem;
1008
+ border-radius: 32px;
1009
+ border: 1px solid rgba(255, 255, 255, 0.05);
1010
+ cursor: pointer;
1011
+ transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
1012
+ display: flex;
1013
+ flex-direction: column;
1014
+ align-items: center;
1015
+ text-align: center;
1016
+ }
1017
+
1018
+ .action-card:hover {
1019
+ background: rgba(255, 255, 255, 0.06);
1020
+ border-color: rgba(14, 165, 233, 0.3);
1021
+ transform: translateY(-8px);
1022
+ }
1023
+
1024
+ .action-img-container {
1025
+ width: 100%;
1026
+ margin-bottom: 1.5rem;
1027
+ padding: 0.5rem;
1028
+ }
1029
+
1030
+ .action-img-container img {
1031
+ width: 100%;
1032
+ height: 140px;
1033
+ object-fit: contain;
1034
+ border-radius: 20px;
1035
+ }
1036
+
1037
+ .action-card h3 {
1038
+ font-size: 1.25rem;
1039
+ font-weight: 700;
1040
+ color: white;
1041
+ margin: 0 0 0.75rem 0;
1042
+ text-transform: uppercase;
1043
+ letter-spacing: 0.05em;
1044
+ }
1045
+
1046
+ .action-card p {
1047
+ font-size: 0.85rem;
1048
+ color: var(--text-secondary);
1049
+ line-height: 1.5;
1050
+ margin-bottom: 2rem;
1051
+ flex-grow: 1;
1052
+ }
1053
+
1054
+ .action-link {
1055
+ font-size: 0.85rem;
1056
+ font-weight: 600;
1057
+ color: white;
1058
+ opacity: 0.9;
1059
+ text-decoration: none;
1060
+ transition: all 0.3s;
1061
+ }
1062
+
1063
+ .action-card:hover .action-link {
1064
+ color: var(--primary-color);
1065
+ transform: translateY(-2px);
1066
+ }
1067
+
1068
+ .view-banner {
1069
+ width: 100%;
1070
+ height: 160px;
1071
+ object-fit: cover;
1072
+ border-radius: 20px;
1073
+ margin-bottom: 2rem;
1074
+ }
1075
+
1076
+ /* Remove old option styles */
1077
+
1078
+ /* --- Key Points View (Updated Design) --- */
1079
+ .topics-header-banner,
1080
+ .summary-header-banner {
1081
+ position: relative;
1082
+ width: 100%;
1083
+ height: 220px;
1084
+ border-radius: 24px;
1085
+ overflow: hidden;
1086
+ margin-bottom: 2rem;
1087
+ }
1088
+
1089
+ .banner-img {
1090
+ width: 100%;
1091
+ height: 100%;
1092
+ object-fit: cover;
1093
+ opacity: 0.8;
1094
+ }
1095
+
1096
+ .banner-overlay {
1097
+ position: absolute;
1098
+ inset: 0;
1099
+ background: linear-gradient(90deg, rgba(15, 23, 42, 0.95) 30%, rgba(15, 23, 42, 0.4) 100%);
1100
+ display: flex;
1101
+ align-items: center;
1102
+ padding: 0 3rem;
1103
+ }
1104
+
1105
+ .banner-content {
1106
+ display: flex;
1107
+ justify-content: space-between;
1108
+ align-items: center;
1109
+ width: 100%;
1110
+ }
1111
+
1112
+ .banner-text h2 {
1113
+ font-size: 1.75rem;
1114
+ color: white;
1115
+ margin: 0;
1116
+ }
1117
+
1118
+ .banner-subtitle {
1119
+ font-size: 0.9rem;
1120
+ color: var(--text-secondary);
1121
+ margin-top: 0.2rem;
1122
+ }
1123
+
1124
+ .banner-label {
1125
+ font-size: 1.25rem;
1126
+ font-weight: 600;
1127
+ color: white;
1128
+ margin-top: 1.5rem;
1129
+ }
1130
+
1131
+ .topic-pill-large {
1132
+ background: rgba(14, 165, 233, 0.15);
1133
+ border: 1px solid var(--primary-color);
1134
+ color: white;
1135
+ padding: 0.75rem 2rem;
1136
+ border-radius: 100px;
1137
+ font-size: 1.1rem;
1138
+ font-weight: 600;
1139
+ backdrop-filter: blur(10px);
1140
+ }
1141
+
1142
+ .topics-container-padding,
1143
+ .summary-container-padding {
1144
+ padding: 0 2rem;
1145
+ }
1146
+
1147
+ .topics-pills-grid {
1148
+ display: grid;
1149
+ grid-template-columns: repeat(2, 1fr);
1150
+ gap: 1.25rem;
1151
+ margin-bottom: 2rem;
1152
+ }
1153
+
1154
+ .topic-pill-btn {
1155
+ background: rgba(255, 255, 255, 0.05);
1156
+ border: 1px solid rgba(255, 255, 255, 0.1);
1157
+ color: white;
1158
+ padding: 1.25rem 2rem;
1159
+ border-radius: 100px;
1160
+ font-size: 1rem;
1161
+ font-weight: 500;
1162
+ text-align: center;
1163
+ cursor: pointer;
1164
+ transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
1165
+ }
1166
+
1167
+ .topic-pill-btn:hover {
1168
+ background: rgba(14, 165, 233, 0.1);
1169
+ border-color: var(--primary-color);
1170
+ transform: translateY(-3px);
1171
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
1172
+ }
1173
+
1174
+ .explanation-content {
1175
+ background: rgba(255, 255, 255, 0.02);
1176
+ border-radius: 24px;
1177
+ padding: 2.5rem;
1178
+ color: #e2e8f0;
1179
+ font-size: 1.1rem;
1180
+ line-height: 1.8;
1181
+ border: 1px solid rgba(255, 255, 255, 0.05);
1182
+ animation: fadeIn 0.5s ease;
1183
+ }
1184
+
1185
+ .explanation-content p {
1186
+ margin-bottom: 1.5rem;
1187
+ }
1188
+
1189
+ .topics-footer {
1190
+ padding: 2rem;
1191
+ display: flex;
1192
+ justify-content: flex-start;
1193
+ }
1194
+
1195
+ /* Override existing topic styles */
1196
+ .topics-list {
1197
+ display: none;
1198
+ }
1199
+
1200
+ /* Score Circle */
1201
+ .score-circle {
1202
+ width: 150px;
1203
+ height: 150px;
1204
+ border-radius: 50%;
1205
+ background: conic-gradient(var(--primary-color) calc(var(--score) * 1%), rgba(255, 255, 255, 0.05) 0);
1206
+ display: flex;
1207
+ align-items: center;
1208
+ justify-content: center;
1209
+ margin: 2rem auto;
1210
+ position: relative;
1211
+ font-size: 2rem;
1212
+ font-weight: 700;
1213
+ }
1214
+
1215
+ .score-circle::after {
1216
+ content: '';
1217
+ position: absolute;
1218
+ width: 130px;
1219
+ height: 130px;
1220
+ background: #0f172a;
1221
+ border-radius: 50%;
1222
+ }
1223
+
1224
+ .score-circle div {
1225
+ position: relative;
1226
+ z-index: 1;
1227
+ }
1228
+
1229
+ [data-theme="light"] .score-circle::after {
1230
+ background: #f8fafc;
1231
+ }
1232
+
1233
+ /* Scrollbar refinement */
1234
+ ::-webkit-scrollbar {
1235
+ width: 8px;
1236
+ }
1237
+
1238
+ ::-webkit-scrollbar-track {
1239
+ background: transparent;
1240
+ }
1241
+
1242
+ ::-webkit-scrollbar-thumb {
1243
+ background: rgba(255, 255, 255, 0.1);
1244
+ border-radius: 10px;
1245
+ }
1246
+
1247
+ ::-webkit-scrollbar-thumb:hover {
1248
+ background: rgba(255, 255, 255, 0.2);
1249
+ }
1250
+
1251
+ /* Quiz UI Enhancements */
1252
+ .category-badge {
1253
+ background: var(--primary-color);
1254
+ color: #0f172a;
1255
+ padding: 0.25rem 0.75rem;
1256
+ border-radius: 99px;
1257
+ font-size: 0.75rem;
1258
+ font-weight: 700;
1259
+ text-transform: uppercase;
1260
+ }
1261
+
1262
+ .hint-container {
1263
+ margin-bottom: 1.5rem;
1264
+ text-align: center;
1265
+ }
1266
+
1267
+ .btn-hint {
1268
+ background: rgba(255, 255, 255, 0.05);
1269
+ border: 1px solid var(--card-border);
1270
+ color: var(--text-secondary);
1271
+ padding: 0.4rem 1rem;
1272
+ border-radius: 12px;
1273
+ font-size: 0.85rem;
1274
+ cursor: pointer;
1275
+ transition: all 0.3s;
1276
+ }
1277
+
1278
+ .btn-hint:hover {
1279
+ background: rgba(255, 255, 255, 0.1);
1280
+ color: white;
1281
+ }
1282
+
1283
+ .hint-text {
1284
+ margin-top: 0.5rem;
1285
+ font-size: 0.9rem;
1286
+ color: var(--accent-color);
1287
+ font-style: italic;
1288
+ background: rgba(56, 189, 248, 0.1);
1289
+ padding: 0.75rem;
1290
+ border-radius: 12px;
1291
+ }
1292
+
1293
+ .rationale-container {
1294
+ margin-top: 2rem;
1295
+ padding: 1.5rem;
1296
+ background: rgba(255, 255, 255, 0.03);
1297
+ border-radius: 16px;
1298
+ border-left: 4px solid var(--primary-color);
1299
+ animation: fadeIn 0.5s ease;
1300
+ }
1301
+
1302
+ .rationale-container h4 {
1303
+ font-size: 1rem;
1304
+ margin-bottom: 0.5rem;
1305
+ color: var(--text-primary);
1306
+ }
1307
+
1308
+ .rationale-container p {
1309
+ font-size: 0.9rem;
1310
+ color: var(--text-secondary);
1311
+ line-height: 1.5;
1312
+ }
1313
+
1314
+ @keyframes fadeIn {
1315
+ from {
1316
+ opacity: 0;
1317
+ transform: translateY(10px);
1318
+ }
1319
+
1320
+ to {
1321
+ opacity: 1;
1322
+ transform: translateY(0);
1323
+ }
1324
+ }
1325
+
1326
+ /* --- Settings Modal (Premium Glassmorphism) --- */
1327
+ .settings-modal {
1328
+ background: transparent;
1329
+ border: none;
1330
+ padding: 0;
1331
+ margin: auto;
1332
+ outline: none;
1333
+ overflow: visible;
1334
+ }
1335
+
1336
+ .settings-modal::backdrop {
1337
+ background: rgba(2, 6, 23, 0.4);
1338
+ backdrop-filter: blur(12px);
1339
+ }
1340
+
1341
+ .settings-container {
1342
+ background: rgba(30, 41, 59, 0.3);
1343
+ backdrop-filter: blur(30px);
1344
+ width: 90vw;
1345
+ max-width: 500px;
1346
+ border-radius: 40px;
1347
+ padding: 3rem 2rem;
1348
+ border: 1px solid rgba(255, 255, 255, 0.08);
1349
+ box-shadow: 0 50px 100px -20px rgba(0, 0, 0, 0.8),
1350
+ inset 0 0 20px rgba(255, 255, 255, 0.02);
1351
+ display: flex;
1352
+ flex-direction: column;
1353
+ align-items: center;
1354
+ gap: 1.5rem;
1355
+ animation: modalSlideIn 0.5s cubic-bezier(0.16, 1, 0.3, 1);
1356
+ }
1357
+
1358
+ @keyframes modalSlideIn {
1359
+ from {
1360
+ opacity: 0;
1361
+ transform: translateY(20px) scale(0.95);
1362
+ }
1363
+
1364
+ to {
1365
+ opacity: 1;
1366
+ transform: translateY(0) scale(1);
1367
+ }
1368
+ }
1369
+
1370
+ .model-options-list {
1371
+ width: 100%;
1372
+ display: flex;
1373
+ flex-direction: column;
1374
+ gap: 1rem;
1375
+ max-height: 400px;
1376
+ overflow-y: auto;
1377
+ padding-right: 0.5rem;
1378
+ }
1379
+
1380
+ .settings-provider-header {
1381
+ width: 100%;
1382
+ font-size: 0.8rem;
1383
+ color: var(--text-secondary);
1384
+ text-transform: uppercase;
1385
+ letter-spacing: 0.1em;
1386
+ margin: 1.5rem 0 0.75rem 0.5rem;
1387
+ font-weight: 700;
1388
+ }
1389
+
1390
+ .model-pill {
1391
+ background: rgba(15, 23, 42, 0.6);
1392
+ border: 1px solid rgba(255, 255, 255, 0.05);
1393
+ padding: 1.5rem 2rem;
1394
+ border-radius: 100px;
1395
+ color: white;
1396
+ font-size: 1.1rem;
1397
+ font-weight: 500;
1398
+ text-align: center;
1399
+ cursor: pointer;
1400
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1401
+ }
1402
+
1403
+ .model-pill:hover {
1404
+ background: rgba(15, 23, 42, 0.9);
1405
+ border-color: rgba(14, 165, 233, 0.3);
1406
+ transform: scale(1.02);
1407
+ }
1408
+
1409
+ .model-pill.selected {
1410
+ background: rgba(14, 165, 233, 0.15);
1411
+ border-color: var(--primary-color);
1412
+ box-shadow: 0 0 20px rgba(14, 165, 233, 0.2);
1413
+ }
1414
+
1415
+ .settings-actions {
1416
+ margin-top: 1rem;
1417
+ width: 100%;
1418
+ display: flex;
1419
+ justify-content: center;
1420
+ }
1421
+
1422
+ .btn-save-settings {
1423
+ background: #111827;
1424
+ /* Deep Navy as per visual */
1425
+ color: white;
1426
+ padding: 1rem 3.5rem;
1427
+ border-radius: 100px;
1428
+ font-size: 1rem;
1429
+ font-weight: 600;
1430
+ border: 1px solid rgba(255, 255, 255, 0.1);
1431
+ cursor: pointer;
1432
+ transition: all 0.3s;
1433
+ }
1434
+
1435
+ .btn-save-settings:hover {
1436
+ background: #1e293b;
1437
+ border-color: var(--primary-color);
1438
+ box-shadow: 0 0 30px rgba(14, 165, 233, 0.2);
1439
+ }
1440
+
1441
+ /* Spinner Animation */
1442
+ .spinner {
1443
+ width: 60px;
1444
+ height: 60px;
1445
+ border: 5px solid rgba(255, 255, 255, 0.1);
1446
+ border-top: 5px solid var(--primary-color);
1447
+ border-radius: 50%;
1448
+ animation: spin 1s linear infinite;
1449
+ margin: 2rem auto;
1450
+ }
1451
+
1452
+ @keyframes spin {
1453
+ 0% {
1454
+ transform: rotate(0deg);
1455
+ }
1456
+
1457
+ 100% {
1458
+ transform: rotate(360deg);
1459
+ }
1460
+ }
1461
+
1462
+ /* Responsive Improvements */
1463
+ @media(max-width: 768px) {
1464
+ nav {
1465
+ flex-wrap: wrap;
1466
+ justify-content: space-between;
1467
+ padding: 1rem 1.5rem;
1468
+ }
1469
+
1470
+ .nav-center {
1471
+ order: 3;
1472
+ width: 100%;
1473
+ display: flex;
1474
+ justify-content: center;
1475
+ padding-top: 1rem;
1476
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
1477
+ margin-top: 0.5rem;
1478
+ }
1479
+
1480
+ .brand-title {
1481
+ font-size: 3.5rem;
1482
+ }
1483
+
1484
+ .action-grid {
1485
+ grid-template-columns: 1fr;
1486
+ }
1487
+
1488
+ .quiz-container {
1489
+ padding: 2rem 1.5rem;
1490
+ border-radius: 32px;
1491
+ }
1492
+
1493
+ .btn-glass {
1494
+ width: 100%;
1495
+ justify-content: center;
1496
+ padding: 0.6rem 1rem;
1497
+ font-size: 0.85rem;
1498
+ }
1499
+
1500
+ .container.assistant-container {
1501
+ padding: 1.5rem 1rem 180px 1rem !important;
1502
+ }
1503
+
1504
+ .chat-input-wrapper {
1505
+ padding: 1rem 1rem 1.5rem;
1506
+ }
1507
+
1508
+ .chat-input-container {
1509
+ padding: 0.5rem 0.5rem 0.5rem 1rem;
1510
+ border-radius: 20px;
1511
+ }
1512
+
1513
+ .nav-actions {
1514
+ gap: 0.5rem;
1515
+ }
1516
+
1517
+ .btn-assistant,
1518
+ .btn-logout {
1519
+ padding: 0.5rem 1.25rem;
1520
+ font-size: 0.85rem;
1521
+ }
1522
+ }
services/frontend-service/static/images/.gitkeep ADDED
File without changes
services/frontend-service/static/js/app.js ADDED
@@ -0,0 +1,1121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ ExamPrep App Logic
3
+ Handles Google Auth, Classroom API fetching, and Quiz UI
4
+ */
5
+
6
+ // Configuration
7
+ let CLIENT_ID = '';
8
+ const DISCOVERY_DOCS = ["https://www.googleapis.com/discovery/v1/apis/classroom/v1/rest", "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest"];
9
+ const SCOPES = "https://www.googleapis.com/auth/classroom.courses.readonly https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/classroom.announcements.readonly https://www.googleapis.com/auth/classroom.coursework.students.readonly https://www.googleapis.com/auth/classroom.courseworkmaterials.readonly";
10
+
11
+ const AI_CONFIG = {
12
+ gemini: {
13
+ label: "Google Gemini",
14
+ helpLink: "https://aistudio.google.com/app/apikey",
15
+ models: [
16
+ { id: "gemini-2.0-flash", name: "Gemini 2.0 Flash (Default)" },
17
+ { id: "gemini-1.5-pro", name: "Gemini 1.5 Pro" }
18
+ ]
19
+ },
20
+ groq: {
21
+ label: "Groq (High Speed)",
22
+ helpLink: "https://console.groq.com/keys",
23
+ models: [
24
+ { id: "llama-3.3-70b-versatile", name: "Llama 3.3 70B (Latest)" },
25
+ { id: "llama-3.1-8b-instant", name: "Llama 3.1 8B (Fast)" }
26
+ ]
27
+ },
28
+ ollama: {
29
+ label: "Ollama (Requires Local App)",
30
+ helpLink: "https://ollama.com",
31
+ models: [
32
+ { id: "llama3.2", name: "Llama 3.2 (Local)" },
33
+ { id: "deepseek-r1", name: "DeepSeek R1 (Local)" },
34
+ { id: "mistral", name: "Mistral" }
35
+ ]
36
+ }
37
+ };
38
+
39
+ // State
40
+ let tokenClient;
41
+ let gapiInited = false;
42
+ let gisInited = false;
43
+ let currentQuiz = [];
44
+ let currentQuestionIndex = 0;
45
+ let userScore = 0;
46
+ let correctCount = 0;
47
+ let wrongCount = 0;
48
+ let currentTextContent = ""; // Store text to avoid re-parsing for redundant calls
49
+ let currentSubjectName = "";
50
+ let quizTimer = null;
51
+ let secondsElapsed = 0;
52
+ let isTimerEnabled = false;
53
+ let allLoadedCourses = []; // Store courses for assistant use
54
+ let courseContentCache = {}; // Cache for extracted course text
55
+ let currentFileList = []; // Current course files for download
56
+
57
+ // --- Optimization: Settings & API Key ---
58
+ function getSettings() {
59
+ return {
60
+ provider: localStorage.getItem('ai_provider') || "groq",
61
+ model: localStorage.getItem('ai_model') || "llama-3.3-70b-versatile"
62
+ };
63
+ }
64
+
65
+ let tempSelectedModelId = ""; // Track selection within the modal
66
+
67
+ function updateModelOptions() {
68
+ const list = document.getElementById('model-options-list');
69
+ list.innerHTML = '';
70
+
71
+ Object.keys(AI_CONFIG).forEach(providerKey => {
72
+ const config = AI_CONFIG[providerKey];
73
+
74
+ // Provider Header
75
+ const header = document.createElement('div');
76
+ header.className = 'settings-provider-header';
77
+ header.innerText = config.label;
78
+ list.appendChild(header);
79
+
80
+ config.models.forEach(m => {
81
+ const pill = document.createElement('div');
82
+ pill.className = `model-pill ${m.id === tempSelectedModelId ? 'selected' : ''}`;
83
+ pill.innerText = m.name;
84
+ pill.onclick = () => {
85
+ document.querySelectorAll('.model-pill').forEach(p => p.classList.remove('selected'));
86
+ pill.classList.add('selected');
87
+ tempSelectedModelId = m.id;
88
+ };
89
+ list.appendChild(pill);
90
+ });
91
+ });
92
+ }
93
+
94
+ function openSettings() {
95
+ const modal = document.getElementById('settings-modal');
96
+ const settings = getSettings();
97
+ tempSelectedModelId = settings.model; // Initialize temp selection
98
+ updateModelOptions();
99
+ modal.showModal();
100
+ }
101
+
102
+ function closeSettings() {
103
+ document.getElementById('settings-modal').close();
104
+ }
105
+
106
+ function saveSettings() {
107
+ if (tempSelectedModelId) {
108
+ // Find provider for this model
109
+ let provider = 'gemini';
110
+ Object.keys(AI_CONFIG).forEach(k => {
111
+ if (AI_CONFIG[k].models.find(m => m.id === tempSelectedModelId)) {
112
+ provider = k;
113
+ }
114
+ });
115
+
116
+ localStorage.setItem('ai_model', tempSelectedModelId);
117
+ localStorage.setItem('ai_provider', provider);
118
+ closeSettings();
119
+ }
120
+ }
121
+
122
+ // --- Theme Toggle ---
123
+ function toggleTheme() {
124
+ const html = document.documentElement;
125
+ const current = html.getAttribute('data-theme');
126
+ const next = current === 'light' ? 'dark' : 'light';
127
+ html.setAttribute('data-theme', next);
128
+ localStorage.setItem('theme', next);
129
+ updateThemeIcon(next);
130
+ }
131
+
132
+ function updateThemeIcon(theme) {
133
+ const btn = document.getElementById('theme-toggle-btn');
134
+ if (btn) btn.innerText = theme === 'light' ? '☀️' : '🌙';
135
+ }
136
+
137
+ function initTheme() {
138
+ const saved = localStorage.getItem('theme') || 'dark';
139
+ document.documentElement.setAttribute('data-theme', saved);
140
+ updateThemeIcon(saved);
141
+ }
142
+
143
+ // 1. Initial Load
144
+ document.addEventListener('DOMContentLoaded', () => {
145
+ initTheme();
146
+ fetchConfig();
147
+
148
+ // Load images from bundle
149
+ if (typeof IMAGE_BUNDLE !== 'undefined') {
150
+ document.querySelectorAll('img[data-src]').forEach(img => {
151
+ const name = img.dataset.src;
152
+ if (IMAGE_BUNDLE[name]) {
153
+ img.src = IMAGE_BUNDLE[name];
154
+ }
155
+ });
156
+ }
157
+ });
158
+
159
+ async function fetchConfig() {
160
+ try {
161
+ const response = await fetch('/api/config');
162
+ const data = await response.json();
163
+ if (data.clientId && data.clientId !== 'PLACEHOLDER_FOR_USER_TO_FILL') {
164
+ CLIENT_ID = data.clientId;
165
+ }
166
+ gapi.load('client', initializeGapiClient);
167
+ initializeGisClient();
168
+ } catch (e) {
169
+ console.error("Config load failed", e);
170
+ }
171
+ }
172
+
173
+ // 2. Google API Setup
174
+
175
+ async function initializeGapiClient() {
176
+ await gapi.client.init({ discoveryDocs: DISCOVERY_DOCS });
177
+ gapiInited = true;
178
+ maybeEnableButtons();
179
+ }
180
+
181
+ function initializeGisClient() {
182
+ tokenClient = google.accounts.oauth2.initTokenClient({
183
+ client_id: CLIENT_ID,
184
+ scope: SCOPES,
185
+ callback: '', // defined below
186
+ });
187
+ gisInited = true;
188
+ maybeEnableButtons();
189
+ }
190
+
191
+ function maybeEnableButtons() {
192
+ if (gapiInited && gisInited) {
193
+ console.log("Google APIs Ready");
194
+
195
+ // Session Persistence: Check if we have a saved token
196
+ const savedToken = localStorage.getItem('google_token');
197
+ if (savedToken) {
198
+ try {
199
+ const tokenObj = JSON.parse(savedToken);
200
+ // Check if token is potentially expired (rough check)
201
+ // If it's valid, set it and log in
202
+ gapi.client.setToken(tokenObj);
203
+ updateAuthUI(true);
204
+ listCourses();
205
+ } catch (e) {
206
+ console.warn("Invalid token in storage", e);
207
+ localStorage.removeItem('google_token');
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ function handleAuthClick() {
214
+ tokenClient.callback = async (resp) => {
215
+ if (resp.error !== undefined) throw (resp);
216
+
217
+ // Persist token for session recovery on refresh
218
+ localStorage.setItem('google_token', JSON.stringify(resp));
219
+
220
+ updateAuthUI(true);
221
+ await listCourses();
222
+ };
223
+
224
+ if (gapi.client.getToken() === null) {
225
+ tokenClient.requestAccessToken({ prompt: 'consent' });
226
+ } else {
227
+ tokenClient.requestAccessToken({ prompt: '' });
228
+ }
229
+ }
230
+
231
+ function handleSignoutClick() {
232
+ const token = gapi.client.getToken();
233
+ if (token !== null) {
234
+ google.accounts.oauth2.revoke(token.access_token);
235
+ gapi.client.setToken('');
236
+ localStorage.removeItem('google_token'); // Clear persistent session
237
+ updateAuthUI(false);
238
+ }
239
+ }
240
+
241
+ function updateAuthUI(isSignedIn) {
242
+ const loginView = document.getElementById('login-view');
243
+ const dashboardView = document.getElementById('dashboard-view');
244
+
245
+ if (isSignedIn) {
246
+ loginView.style.display = 'none';
247
+ dashboardView.style.display = 'block';
248
+
249
+ const loader = document.getElementById('initial-loading');
250
+ const grid = document.getElementById('courses-grid');
251
+
252
+ loader.style.display = 'flex'; // show loader while fetching
253
+
254
+ // Try to get user info if possible (optional enhancement)
255
+ // const output = document.getElementById('user-profile-pic');
256
+ // output.style.backgroundImage = `url(...)`;
257
+
258
+ } else {
259
+ loginView.style.display = 'flex';
260
+ dashboardView.style.display = 'none';
261
+ document.getElementById('courses-grid').innerHTML = '';
262
+ }
263
+ }
264
+
265
+ // 4. Feature Logic: Classroom
266
+
267
+ async function listCourses() {
268
+ const grid = document.getElementById('courses-grid');
269
+ grid.innerHTML = '';
270
+
271
+ try {
272
+ const response = await gapi.client.classroom.courses.list({
273
+ pageSize: 12,
274
+ courseStates: 'ACTIVE'
275
+ });
276
+
277
+ document.getElementById('initial-loading').style.display = 'none';
278
+
279
+ const courses = response.result.courses;
280
+ allLoadedCourses = courses || []; // Save to global state
281
+ if (!courses || courses.length === 0) {
282
+ grid.innerHTML = '<p>No courses found.</p>';
283
+ return;
284
+ }
285
+
286
+ courses.forEach((course, index) => {
287
+ const card = document.createElement('div');
288
+ card.className = 'card';
289
+ const imgName = `Card${(index % 4) + 1}.png`;
290
+ const cardImg = (typeof IMAGE_BUNDLE !== 'undefined' && IMAGE_BUNDLE[imgName]) ? IMAGE_BUNDLE[imgName] : '';
291
+
292
+ card.innerHTML = `
293
+ <div class="card-image-container">
294
+ <img src="${cardImg}" class="card-banner" alt="${course.name}">
295
+ </div>
296
+ <div class="card-info">
297
+ <div class="card-text">
298
+ <div class="card-subtitle">${course.section || 'General'}</div>
299
+ <div class="card-title">${course.name}</div>
300
+ </div>
301
+ <button class="card-action">View Materials</button>
302
+ </div>
303
+ `;
304
+ card.onclick = (e) => {
305
+ loadCourseMaterials(course.id, course.name, course.section || 'General');
306
+ };
307
+ grid.appendChild(card);
308
+ });
309
+
310
+ } catch (err) {
311
+ console.error(err);
312
+ document.getElementById('initial-loading').style.display = 'none';
313
+ grid.innerHTML = `<p style="color:red">Error loading courses: ${err.message}</p>`;
314
+ }
315
+ }
316
+
317
+ // 5. Materials & Quiz Logic
318
+
319
+ // --- Optimization: Caching & Parallel Processing ---
320
+
321
+ async function loadCourseMaterials(courseId, courseName, courseBatch) {
322
+ currentSubjectName = courseName;
323
+
324
+ // Reset UI State
325
+ document.getElementById('quiz-modal').classList.add('active');
326
+ switchView('loading');
327
+ const loadText = document.getElementById('loading-text');
328
+ loadText.innerText = `Scanning ${courseName} for documents...`;
329
+
330
+ // Update Action Menu Header placeholders
331
+ document.getElementById('menu-course-name').innerText = courseName;
332
+ document.getElementById('menu-course-batch').innerText = courseBatch;
333
+
334
+ try {
335
+ // --- Step 0: Check Cache ---
336
+ if (courseContentCache[courseId]) {
337
+ console.log(`Loading ${courseName} from session cache`);
338
+ currentTextContent = courseContentCache[courseId].text;
339
+ currentFileList = courseContentCache[courseId].files;
340
+ switchView('action-menu');
341
+ return;
342
+ }
343
+
344
+ let aggregatedText = "";
345
+ let filesToProcess = [];
346
+
347
+ // --- Step 1: Gather All Metadata (Parallel API Calls) ---
348
+ loadText.innerText = "Fetching Classroom Data...";
349
+
350
+ const [announceResp, workResp, matResp] = await Promise.all([
351
+ Promise.resolve(gapi.client.classroom.courses.announcements.list({ courseId, pageSize: 5 })).catch(e => ({ result: {} })),
352
+ Promise.resolve(gapi.client.classroom.courses.courseWork.list({ courseId, pageSize: 10 })).catch(e => ({ result: {} })),
353
+ Promise.resolve(gapi.client.classroom.courses.courseWorkMaterials.list({ courseId, pageSize: 10 })).catch(e => ({ result: {} }))
354
+ ]);
355
+
356
+ // Process Announcements
357
+ if (announceResp.result.announcements) {
358
+ announceResp.result.announcements.forEach(a => {
359
+ aggregatedText += (a.text || "") + "\n";
360
+ if (a.materials) filesToProcess.push(...a.materials);
361
+ });
362
+ }
363
+
364
+ // Process Assignments
365
+ if (workResp.result.courseWork) {
366
+ workResp.result.courseWork.forEach(w => {
367
+ aggregatedText += (w.description || "") + "\n";
368
+ if (w.materials) filesToProcess.push(...w.materials);
369
+ });
370
+ }
371
+
372
+ // Process Materials
373
+ if (matResp.result.courseWorkMaterial) {
374
+ matResp.result.courseWorkMaterial.forEach(m => {
375
+ aggregatedText += (m.description || "") + "\n";
376
+ if (m.materials) filesToProcess.push(...m.materials);
377
+ });
378
+ }
379
+
380
+ // --- Step 2: Filter & Deduplicate Files ---
381
+ const uniqueFiles = new Map();
382
+ filesToProcess.forEach(mat => {
383
+ if (mat.driveFile && mat.driveFile.driveFile) {
384
+ const f = mat.driveFile.driveFile;
385
+ const title = f.title.toLowerCase();
386
+ if (title.endsWith('.pdf') || title.endsWith('.pptx')) {
387
+ if (!uniqueFiles.has(f.id)) {
388
+ uniqueFiles.set(f.id, f);
389
+ }
390
+ }
391
+ }
392
+ });
393
+
394
+ const fileList = Array.from(uniqueFiles.values());
395
+
396
+ // --- Step 3: Process Files in Parallel (with Limit) ---
397
+ if (fileList.length > 0) {
398
+ loadText.innerText = `Processing ${fileList.length} documents...`;
399
+
400
+ // map to promises
401
+ const filePromises = fileList.map(file => downloadAndParseFile(file.id, file.title));
402
+
403
+ // Wait for all
404
+ const fileResults = await Promise.all(filePromises);
405
+
406
+ fileResults.forEach(text => {
407
+ if (text) aggregatedText += text + "\n";
408
+ });
409
+ }
410
+
411
+ // Fallback checks
412
+ if (aggregatedText.length < 50) {
413
+ console.warn("Low content found, using context-aware simulation.");
414
+ aggregatedText = `
415
+ Subject: ${courseName}
416
+ This is a generated study context because the classroom query returned limited text.
417
+ Key concepts in ${courseName} often include fundamental theories, practical applications, architecture, and core methodologies.
418
+ `;
419
+ }
420
+
421
+ console.log(`Final extracted text length: ${aggregatedText.length}`);
422
+ currentTextContent = aggregatedText;
423
+ currentFileList = fileList; // Save for download feature
424
+
425
+ // Store both in cache
426
+ courseContentCache[courseId] = {
427
+ text: aggregatedText,
428
+ files: fileList
429
+ };
430
+
431
+ // Show Action Menu instead of direct generation
432
+ switchView('action-menu');
433
+
434
+ } catch (e) {
435
+ console.error("Error fetching class data", e);
436
+ const msg = e.result?.error?.message || e.message || "Unknown Error";
437
+ document.getElementById('loading-text').innerText = `Error: ${msg}`;
438
+ }
439
+ }
440
+
441
+ // Reuse cache to prevent re-downloading same files
442
+ async function downloadAndParseFile(fileId, fileName) {
443
+ const CACHE_KEY = `doc_cache_${fileId}`;
444
+
445
+ // 1. Check Local Cache
446
+ const cached = localStorage.getItem(CACHE_KEY);
447
+ if (cached) {
448
+ console.log(`Loaded ${fileName} from cache`);
449
+ return cached;
450
+ }
451
+
452
+ try {
453
+ // 2. Download from Drive
454
+ const token = gapi.client.getToken().access_token;
455
+ const directResp = await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`, {
456
+ headers: { 'Authorization': `Bearer ${token}` }
457
+ });
458
+
459
+ if (!directResp.ok) throw new Error("Download failed");
460
+ const blob = await directResp.blob();
461
+
462
+ // 3. Parse in Backend
463
+ const formData = new FormData();
464
+ formData.append("file", blob, fileName);
465
+
466
+ const parseResp = await fetch('/api/parse-file', {
467
+ method: 'POST',
468
+ body: formData
469
+ });
470
+
471
+ const data = await parseResp.json();
472
+ if (data.success) {
473
+ // 4. Save to Cache (Limit size to avoid quota errors - e.g. store first 50kb)
474
+ try {
475
+ // simple length check, maybe trim if too huge
476
+ const content = data.text;
477
+ localStorage.setItem(CACHE_KEY, content);
478
+ } catch (e) {
479
+ console.warn("Cache full");
480
+ }
481
+ return data.text;
482
+ }
483
+ return null;
484
+
485
+ } catch (e) {
486
+ console.error(`Failed to parse ${fileName}`, e);
487
+ return null; // Skip file on error
488
+ }
489
+ }
490
+
491
+ // Deprecated: Old processMaterials function replaced by inline logic
492
+ // async function processMaterials(materials) { ... }
493
+
494
+ // New Functions for Action Menu interactions
495
+ async function startTopicsGeneration() {
496
+ await generateTopics(currentTextContent);
497
+ }
498
+
499
+ function showQuizConfig() {
500
+ document.getElementById('config-course-name').innerText = currentSubjectName;
501
+ document.getElementById('config-course-batch').innerText = document.getElementById('menu-course-batch').innerText;
502
+
503
+ // Initialize Pill Click Listeners
504
+ setupPills();
505
+
506
+ switchView('quiz-config');
507
+ }
508
+
509
+ function setupPills() {
510
+ const pillGroups = ['pill-num-questions', 'pill-difficulty', 'pill-timer'];
511
+ pillGroups.forEach(groupId => {
512
+ const group = document.getElementById(groupId);
513
+ if (!group) return;
514
+ group.querySelectorAll('.pill-option').forEach(pill => {
515
+ pill.onclick = () => {
516
+ group.querySelectorAll('.pill-option').forEach(p => p.classList.remove('selected'));
517
+ pill.classList.add('selected');
518
+ };
519
+ });
520
+ });
521
+ }
522
+
523
+ async function startQuizFromConfig() {
524
+ const numQPill = document.querySelector('#pill-num-questions .selected');
525
+ const diffPill = document.querySelector('#pill-difficulty .selected');
526
+ const timerPill = document.querySelector('#pill-timer .selected');
527
+
528
+ const numQ = numQPill ? numQPill.dataset.value : 10;
529
+ const diff = diffPill ? diffPill.dataset.value : "Medium";
530
+ const timerValue = timerPill ? timerPill.dataset.value : "disable";
531
+
532
+ isTimerEnabled = (timerValue === 'enable');
533
+ await startQuizFlow(numQ, diff);
534
+ }
535
+
536
+ async function generateTopics(text) {
537
+ switchView('loading');
538
+ document.getElementById('loading-text').innerText = "AI is analyzing key concepts...";
539
+
540
+ try {
541
+ const settings = getSettings();
542
+ const response = await fetch('/api/generate-topics', {
543
+ method: 'POST',
544
+ headers: { 'Content-Type': 'application/json' },
545
+ body: JSON.stringify({
546
+ text: text,
547
+ provider: settings.provider,
548
+ model: settings.model
549
+ })
550
+ });
551
+
552
+ if (!response.ok) {
553
+ const err = await response.json();
554
+ throw new Error(err.error || "Failed to generate topics");
555
+ }
556
+
557
+ const data = await response.json();
558
+
559
+ // Update Topic UI Header
560
+ document.getElementById('topics-course-name').innerText = currentSubjectName;
561
+ // Batch already set in loadCourseMaterials, but let's ensure consistency if needed
562
+
563
+ // Render Topics as Pills
564
+ const grid = document.getElementById('topics-pills-grid');
565
+ grid.innerHTML = '';
566
+
567
+ data.topics.forEach(t => {
568
+ const btn = document.createElement('div');
569
+ btn.className = 'topic-pill-btn';
570
+ btn.innerText = t.topic;
571
+ btn.onclick = () => showTopicExplanation(t.topic);
572
+ grid.appendChild(btn);
573
+ });
574
+
575
+ showTopicsGrid(); // Ensure grid view is visible
576
+ switchView('topics');
577
+
578
+ } catch (e) {
579
+ alert("AI Error: " + e.message);
580
+ switchView('action-menu');
581
+ }
582
+ }
583
+
584
+ async function showTopicExplanation(topicName) {
585
+ // UI Transitions
586
+ document.getElementById('topics-grid-container').style.display = 'none';
587
+ document.getElementById('topic-explanation-container').style.display = 'block';
588
+
589
+ document.getElementById('selected-topic-pill-container').style.display = 'block';
590
+ document.getElementById('selected-topic-name').innerText = topicName;
591
+
592
+ document.getElementById('btn-topics-back').style.display = 'none';
593
+ document.getElementById('btn-explanation-back').style.display = 'block';
594
+
595
+ const contentArea = document.getElementById('topic-explanation-text');
596
+ contentArea.innerHTML = '<div class="spinner"></div><p style="text-align:center">AI is preparing a detailed explanation...</p>';
597
+
598
+ try {
599
+ const settings = getSettings();
600
+ const response = await fetch('/api/explain-topic', {
601
+ method: 'POST',
602
+ headers: { 'Content-Type': 'application/json' },
603
+ body: JSON.stringify({
604
+ text: currentTextContent,
605
+ topic: topicName,
606
+ provider: settings.provider,
607
+ model: settings.model
608
+ })
609
+ });
610
+
611
+ if (!response.ok) throw new Error("Explanation failed");
612
+
613
+ const data = await response.json();
614
+
615
+ // Use Markdown parser for a nice look
616
+ contentArea.innerHTML = parseMarkdown(data.explanation);
617
+
618
+ } catch (e) {
619
+ contentArea.innerHTML = `<p style="color:red">Error: ${e.message}</p>`;
620
+ }
621
+ }
622
+
623
+ function showTopicsGrid() {
624
+ document.getElementById('topics-grid-container').style.display = 'block';
625
+ document.getElementById('topic-explanation-container').style.display = 'none';
626
+
627
+ document.getElementById('selected-topic-pill-container').style.display = 'none';
628
+
629
+ document.getElementById('btn-topics-back').style.display = 'block';
630
+ document.getElementById('btn-explanation-back').style.display = 'none';
631
+ }
632
+
633
+ async function startQuizFlow(numQuestions = 5, difficulty = "Medium") {
634
+ switchView('loading');
635
+ document.getElementById('loading-text').innerText = `Generating ${numQuestions} ${difficulty} Questions...`;
636
+
637
+ try {
638
+ const settings = getSettings();
639
+ const response = await fetch('/api/generate-quiz', {
640
+ method: 'POST',
641
+ headers: { 'Content-Type': 'application/json' },
642
+ body: JSON.stringify({
643
+ text: currentTextContent,
644
+ provider: settings.provider,
645
+ model: settings.model,
646
+ numQuestions: parseInt(numQuestions),
647
+ difficulty: difficulty
648
+ })
649
+ });
650
+
651
+ if (!response.ok) {
652
+ const err = await response.json();
653
+ throw new Error(err.error || "Failed to generate quiz");
654
+ }
655
+
656
+ const data = await response.json();
657
+
658
+ currentQuiz = data.quiz.questions;
659
+ currentQuestionIndex = 0;
660
+ userScore = 0;
661
+ correctCount = 0;
662
+ wrongCount = 0;
663
+ secondsElapsed = 0;
664
+
665
+ if (!currentQuiz || currentQuiz.length === 0) {
666
+ throw new Error("AI returned no questions. Try again.");
667
+ }
668
+
669
+ // Setup Header Stats in Active Quiz
670
+ document.getElementById('active-course-name').innerText = currentSubjectName;
671
+ document.getElementById('active-course-batch').innerText = document.getElementById('menu-course-batch').innerText;
672
+
673
+ showQuestion();
674
+ switchView('quiz');
675
+
676
+ if (isTimerEnabled) {
677
+ startTimer();
678
+ } else {
679
+ document.getElementById('quiz-timer-display').style.display = 'none';
680
+ }
681
+
682
+ } catch (e) {
683
+ alert("Quiz Gen Error: " + e.message);
684
+ switchView('action-menu');
685
+ }
686
+ }
687
+
688
+ function startTimer() {
689
+ document.getElementById('quiz-timer-display').style.display = 'block';
690
+ if (quizTimer) clearInterval(quizTimer);
691
+ quizTimer = setInterval(() => {
692
+ secondsElapsed++;
693
+ const mins = Math.floor(secondsElapsed / 60).toString().padStart(2, '0');
694
+ const secs = (secondsElapsed % 60).toString().padStart(2, '0');
695
+ document.getElementById('quiz-timer-display').innerText = `Time: ${mins}:${secs}`;
696
+ }, 1000);
697
+ }
698
+
699
+ async function startSummaryGeneration() {
700
+ switchView('loading');
701
+ document.getElementById('loading-text').innerText = "Creating Course Summary...";
702
+
703
+ try {
704
+ const settings = getSettings();
705
+ const response = await fetch('/api/generate-summary', {
706
+ method: 'POST',
707
+ headers: { 'Content-Type': 'application/json' },
708
+ body: JSON.stringify({
709
+ text: currentTextContent,
710
+ provider: settings.provider,
711
+ model: settings.model
712
+ })
713
+ });
714
+
715
+ if (!response.ok) {
716
+ const err = await response.json();
717
+ throw new Error(err.error || "Summary gen failed");
718
+ }
719
+
720
+ const data = await response.json();
721
+
722
+ // Update Header placeholders
723
+ document.getElementById('summary-course-name').innerText = currentSubjectName;
724
+ document.getElementById('summary-course-batch').innerText = document.getElementById('menu-course-batch').innerText;
725
+
726
+ // Use the new Markdown Parser
727
+ const formattedSummary = parseMarkdown(data.summary);
728
+
729
+ document.getElementById('summary-content').innerHTML = formattedSummary;
730
+ switchView('summary');
731
+
732
+ } catch (e) {
733
+ alert("Error: " + e.message);
734
+ switchView('action-menu');
735
+ }
736
+ }
737
+
738
+ async function downloadAllMaterials() {
739
+ const files = currentFileList.filter(f => {
740
+ const t = f.title.toLowerCase();
741
+ return t.endsWith('.pdf') || t.endsWith('.pptx');
742
+ });
743
+
744
+ if (files.length === 0) {
745
+ alert("No downloadable materials (PDF/PPTX) found in this course.");
746
+ return;
747
+ }
748
+
749
+ // UI Feedback: Change the "Materials" card text or show a global alert
750
+ alert(`Bundling ${files.length} files... Please wait a moment.`);
751
+
752
+ const zip = new JSZip();
753
+ const token = gapi.client.getToken().access_token;
754
+
755
+ // Fetch all files in parallel for maximum speed
756
+ const fetchPromises = files.map(async (file) => {
757
+ try {
758
+ console.log(`Fetching: ${file.title}`);
759
+ const response = await fetch(`https://www.googleapis.com/drive/v3/files/${file.id}?alt=media`, {
760
+ headers: { 'Authorization': `Bearer ${token}` }
761
+ });
762
+
763
+ if (!response.ok) throw new Error(`Download failed for ${file.title}`);
764
+
765
+ const blob = await response.blob();
766
+ zip.file(file.title, blob);
767
+ return true;
768
+ } catch (e) {
769
+ console.error(`Error fetching ${file.title}:`, e);
770
+ return false;
771
+ }
772
+ });
773
+
774
+ try {
775
+ await Promise.all(fetchPromises);
776
+
777
+ // Generate the ZIP file
778
+ const content = await zip.generateAsync({ type: "blob" });
779
+
780
+ // Trigger download of the single ZIP file
781
+ const url = window.URL.createObjectURL(content);
782
+ const a = document.createElement('a');
783
+ a.style.display = 'none';
784
+ a.href = url;
785
+ const zipName = `${currentSubjectName.replace(/\s+/g, '_')}_Materials.zip`;
786
+ a.download = zipName;
787
+ document.body.appendChild(a);
788
+ a.click();
789
+
790
+ window.URL.revokeObjectURL(url);
791
+ document.body.removeChild(a);
792
+
793
+ console.log("ZIP Download started.");
794
+ } catch (e) {
795
+ console.error("ZIP Generation failed:", e);
796
+ alert("Failed to bundle files. Check console for details.");
797
+ }
798
+ }
799
+
800
+ // -------------------------
801
+ // Helper: Simple Markdown Parser
802
+ // -------------------------
803
+ function parseMarkdown(text) {
804
+ if (!text) return "";
805
+
806
+ let html = text;
807
+
808
+ // 1. Headers (### H3, ## H2, # H1)
809
+ html = html.replace(/^### (.*$)/gm, '<h4>$1</h4>');
810
+ html = html.replace(/^## (.*$)/gm, '<h3>$1</h3>');
811
+ html = html.replace(/^# (.*$)/gm, '<h3>$1</h3>');
812
+
813
+ // 2. Bold (**text**)
814
+ html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
815
+
816
+ // 3. Italic (*text*)
817
+ html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
818
+
819
+ // 4. Unordered Lists (- item or * item)
820
+ // Wrap lists is tricky with simple regex, but we can style lines starting with -
821
+ html = html.replace(/^- (.*$)/gm, '<li>$1</li>');
822
+ html = html.replace(/^\* (.*$)/gm, '<li>$1</li>');
823
+
824
+ // 5. Wrap list items in ul if they are adjacent (advanced)
825
+ // For simplicity, just letting <li> exist gives decent browser rendering if display:block
826
+ // but better to wrap. Let's try a simple block replace for lists.
827
+ // Actually, simply replacing newlines with <br> breaks lists.
828
+ // Let's wrap paragraphs.
829
+
830
+ // Split into blocks by double newline
831
+ const blocks = html.split(/\n\n+/);
832
+
833
+ const processedBlocks = blocks.map(block => {
834
+ if (block.trim().startsWith('<li>')) {
835
+ return `<ul>${block}</ul>`;
836
+ } else if (block.match(/^<h[34]>/)) {
837
+ return block;
838
+ } else {
839
+ return `<p>${block.replace(/\n/g, '<br>')}</p>`;
840
+ }
841
+ });
842
+
843
+ return processedBlocks.join('');
844
+ }
845
+
846
+ function showQuestion() {
847
+ const q = currentQuiz[currentQuestionIndex];
848
+
849
+ // UI Stats Updates
850
+ const timerDisplay = document.getElementById('quiz-timer-display');
851
+ const qnoDisplay = document.getElementById('quiz-qno-display');
852
+ const qText = document.getElementById('question-text');
853
+
854
+ if (qnoDisplay) qnoDisplay.innerText = `Q.No. ${currentQuestionIndex + 1}/${currentQuiz.length}`;
855
+ if (qText) qText.innerText = `${currentQuestionIndex + 1}. ${q.question}`;
856
+
857
+ // Hint setup
858
+ const hintContainer = document.getElementById('hint-container');
859
+ const hintText = document.getElementById('hint-text');
860
+ if (q.hint) {
861
+ hintContainer.style.display = 'block';
862
+ hintText.innerText = q.hint;
863
+ hintText.style.display = 'none';
864
+ const hintBtn = document.querySelector('.btn-hint');
865
+ if (hintBtn) hintBtn.innerText = '💡 Show Hint';
866
+ } else {
867
+ hintContainer.style.display = 'none';
868
+ }
869
+
870
+ // Options setup
871
+ const optionsContainer = document.getElementById('options-container');
872
+ optionsContainer.innerHTML = '';
873
+
874
+ // Rationale hidden
875
+ document.getElementById('rationale-container').style.display = 'none';
876
+ document.getElementById('next-question-btn').style.display = 'none';
877
+
878
+ // Compatibility check for old vs new AI structure
879
+ const options = q.answerOptions || q.options.map(opt => ({ text: opt, isCorrect: opt === q.correct, rationale: "" }));
880
+
881
+ options.forEach(opt => {
882
+ const btn = document.createElement('div');
883
+ btn.className = 'option-btn';
884
+ btn.innerText = opt.text;
885
+ btn.onclick = () => handleAnswer(btn, opt, options);
886
+ optionsContainer.appendChild(btn);
887
+ });
888
+ }
889
+
890
+ function toggleHint() {
891
+ const hintText = document.getElementById('hint-text');
892
+ const btn = document.querySelector('.btn-hint');
893
+ if (hintText.style.display === 'none') {
894
+ hintText.style.display = 'block';
895
+ btn.innerText = 'Hide Hint';
896
+ } else {
897
+ hintText.style.display = 'none';
898
+ btn.innerText = '💡 Show Hint';
899
+ }
900
+ }
901
+
902
+ function handleAnswer(btn, selectedOpt, allOptions) {
903
+ // Disable all buttons
904
+ const allBtns = document.querySelectorAll('.option-btn');
905
+ allBtns.forEach(b => b.style.pointerEvents = 'none');
906
+
907
+ const isCorrect = selectedOpt.isCorrect;
908
+ const statusLabel = document.getElementById('answer-status-label');
909
+
910
+ if (isCorrect) {
911
+ btn.classList.add('correct');
912
+ userScore++;
913
+ correctCount++;
914
+ if (statusLabel) {
915
+ statusLabel.innerText = 'Correct!';
916
+ statusLabel.style.color = 'var(--success)';
917
+ }
918
+ } else {
919
+ btn.classList.add('wrong');
920
+ wrongCount++;
921
+ if (statusLabel) {
922
+ statusLabel.innerText = 'Incorrect';
923
+ statusLabel.style.color = 'var(--error)';
924
+ }
925
+ // Find correct button and highlight it
926
+ allBtns.forEach(b => {
927
+ const opt = allOptions.find(o => o.text === b.innerText);
928
+ if (opt && opt.isCorrect) b.classList.add('correct');
929
+ });
930
+ }
931
+
932
+ // Show rationale
933
+ const rationaleContainer = document.getElementById('rationale-container');
934
+ const rationaleText = document.getElementById('rationale-text');
935
+ if (selectedOpt.rationale) {
936
+ rationaleContainer.style.display = 'block';
937
+ rationaleText.innerText = selectedOpt.rationale;
938
+ }
939
+
940
+ // Show next button
941
+ document.getElementById('next-question-btn').style.display = 'block';
942
+ }
943
+
944
+ function nextQuestion() {
945
+ currentQuestionIndex++;
946
+ if (currentQuestionIndex < currentQuiz.length) {
947
+ showQuestion();
948
+ } else {
949
+ showResults();
950
+ }
951
+ }
952
+
953
+ function showResults() {
954
+ if (quizTimer) clearInterval(quizTimer);
955
+
956
+ const percentage = Math.round((userScore / currentQuiz.length) * 100);
957
+ const circle = document.getElementById('final-score-display');
958
+ circle.style.setProperty('--score', percentage);
959
+ circle.innerHTML = `<div>${percentage}%</div>`;
960
+
961
+ // Update Counts
962
+ document.getElementById('correct-count').innerText = correctCount;
963
+ document.getElementById('wrong-count').innerText = wrongCount;
964
+
965
+ let feedback = "Good effort!";
966
+ if (percentage === 100) feedback = "Perfect Score! You are a master.";
967
+ else if (percentage >= 80) feedback = "Excellent work!";
968
+ else if (percentage < 60) feedback = "You might want to review the topics again.";
969
+
970
+ document.getElementById('feedback-text').innerText = feedback;
971
+ switchView('results');
972
+ }
973
+
974
+ // 6. UI Utilities
975
+
976
+ let isAssistantView = false;
977
+
978
+ function toggleAssistantView() {
979
+ const coursesView = document.getElementById('courses-view');
980
+ const assistantView = document.getElementById('assistant-view');
981
+ const assistantBtn = document.querySelector('.btn-assistant');
982
+ const navCenter = document.querySelector('.nav-center');
983
+
984
+ if (!isAssistantView) {
985
+ // Entering Assistant View
986
+ coursesView.style.display = 'none';
987
+ assistantView.style.display = 'flex';
988
+ if (navCenter) navCenter.style.visibility = 'hidden';
989
+
990
+ assistantBtn.innerText = 'Back to Courses';
991
+ assistantBtn.style.background = 'white';
992
+ assistantBtn.style.color = '#0f172a';
993
+
994
+ isAssistantView = true;
995
+ populateAssistantTopics();
996
+ } else {
997
+ // Returning to Courses
998
+ coursesView.style.display = 'block';
999
+ assistantView.style.display = 'none';
1000
+ if (navCenter) navCenter.style.visibility = 'visible';
1001
+
1002
+ assistantBtn.innerText = 'Assistant';
1003
+ assistantBtn.style.background = '#1e293b';
1004
+ assistantBtn.style.color = 'white';
1005
+
1006
+ isAssistantView = false;
1007
+ }
1008
+ }
1009
+
1010
+ function populateAssistantTopics() {
1011
+ const container = document.getElementById('assistant-topics');
1012
+ if (!allLoadedCourses || allLoadedCourses.length === 0) return;
1013
+
1014
+ container.innerHTML = '';
1015
+ // Show up to 6 courses as quick starting points
1016
+ allLoadedCourses.slice(0, 6).forEach(course => {
1017
+ const pill = document.createElement('div');
1018
+ pill.className = 'discussion-pill';
1019
+ pill.innerText = course.name;
1020
+ pill.onclick = () => sendAssistantQuery(`Explain key concepts for ${course.name}`);
1021
+ container.appendChild(pill);
1022
+ });
1023
+ }
1024
+
1025
+ async function sendAssistantQuery(manualQuery = null) {
1026
+ const input = document.getElementById('chat-input');
1027
+ const query = manualQuery || input.value.trim();
1028
+ const responseContainer = document.getElementById('assistant-response-container');
1029
+ const responseTextEl = document.getElementById('assistant-response-text');
1030
+ const promptTitle = document.querySelector('.assistant-prompt');
1031
+
1032
+ if (!query) return;
1033
+
1034
+ // UI Feedback
1035
+ promptTitle.innerText = "Researching: " + query;
1036
+ responseContainer.style.display = 'block';
1037
+ responseTextEl.innerHTML = '<div style="display:flex; flex-direction:column; align-items:center; gap:1rem; padding: 2rem;">' +
1038
+ '<div class="spinner"></div>' +
1039
+ '<p style="color: var(--text-secondary);">Analyzing and generating response...</p></div>';
1040
+
1041
+ if (!manualQuery) input.value = '';
1042
+
1043
+ // Scroll to section
1044
+ responseContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
1045
+
1046
+ const settings = getSettings();
1047
+
1048
+ try {
1049
+ const response = await fetch('/api/explain-topic', {
1050
+ method: 'POST',
1051
+ headers: {
1052
+ 'Content-Type': 'application/json'
1053
+ },
1054
+ body: JSON.stringify({
1055
+ text: "The user is asking an academic question about: " + query + ". Provide a detailed, pedagogical, and clear explanation.",
1056
+ topic: query,
1057
+ provider: settings.provider,
1058
+ model: settings.model
1059
+ })
1060
+ });
1061
+
1062
+ const data = await response.json();
1063
+ if (data.success) {
1064
+ // Very basic Markdown-like formatting for AI response
1065
+ let html = data.explanation
1066
+ .replace(/\n\n/g, '</p><p>')
1067
+ .replace(/\n/g, '<br>')
1068
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
1069
+ .replace(/\*(.*?)\*/g, '<em>$1</em>');
1070
+
1071
+ responseTextEl.innerHTML = `<p>${html}</p>`;
1072
+
1073
+ // Scroll to the full response
1074
+ setTimeout(() => {
1075
+ responseContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
1076
+ }, 100);
1077
+ } else {
1078
+ responseTextEl.innerHTML = `<div style="color: var(--error);">AI Error: ${data.error}</div>`;
1079
+ }
1080
+ } catch (err) {
1081
+ responseTextEl.innerHTML = `<div style="color: var(--error);">Connection failed: AI service is currently unavailable.</div>`;
1082
+ }
1083
+ }
1084
+
1085
+ // Add event listener for Enter in chat-input
1086
+ document.addEventListener('DOMContentLoaded', () => {
1087
+ const chatInput = document.getElementById('chat-input');
1088
+ if (chatInput) {
1089
+ chatInput.addEventListener('keydown', (e) => {
1090
+ if (e.key === 'Enter' && !e.shiftKey) {
1091
+ e.preventDefault();
1092
+ sendAssistantQuery();
1093
+ }
1094
+ });
1095
+
1096
+ // Auto-resize
1097
+ chatInput.addEventListener('input', function () {
1098
+ this.style.height = 'auto';
1099
+ this.style.height = (this.scrollHeight) + 'px';
1100
+ });
1101
+ }
1102
+
1103
+ // Make Pills clickable
1104
+ document.querySelectorAll('.discussion-pill').forEach(pill => {
1105
+ pill.addEventListener('click', () => {
1106
+ sendAssistantQuery(pill.innerText);
1107
+ });
1108
+ });
1109
+ });
1110
+
1111
+ function switchView(viewName) {
1112
+ // Hide all
1113
+ document.querySelectorAll('.quiz-view').forEach(el => el.style.display = 'none');
1114
+ // Show target
1115
+ document.getElementById(`view-${viewName}`).style.display = 'block';
1116
+ }
1117
+
1118
+ function closeQuiz() {
1119
+ document.getElementById('quiz-modal').classList.remove('active');
1120
+ }
1121
+
services/frontend-service/static/js/image_bundle.js ADDED
The diff for this file is too large to render. See raw diff
 
services/frontend-service/templates/index.html ADDED
@@ -0,0 +1,407 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>AceNow | Exam Revision</title>
8
+ <link rel="stylesheet" href="/static/css/style.css">
9
+ <!-- Google Identity Services -->
10
+ <script src="https://accounts.google.com/gsi/client" async defer></script>
11
+ <script src="https://apis.google.com/js/api.js" async defer></script>
12
+ <!-- JSZip for bundled downloads -->
13
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
14
+ <script src="/static/js/image_bundle.js"></script>
15
+ </head>
16
+
17
+ <body>
18
+ <div class="blob blob-1"></div>
19
+ <div class="blob blob-2"></div>
20
+
21
+ <!-- Login View (Landing Page) -->
22
+ <div id="login-view" class="login-container">
23
+ <div class="login-content">
24
+ <h1 class="brand-title">AceNow</h1>
25
+ <p class="brand-subtitle">
26
+ A focused platform for last-minute exam preparation, offering AI-powered summaries, key topics, and
27
+ quick quizzes for efficient revision.
28
+ </p>
29
+
30
+ <div class="login-action">
31
+ <button class="btn-login-hero" onclick="handleAuthClick()">
32
+ <img data-src="google_Logo.svg" alt="Google Logo" class="btn-icon-img">
33
+ Continue with Google
34
+ </button>
35
+ </div>
36
+ </div>
37
+
38
+ <!-- About the Platform Section -->
39
+ <div class="about-section">
40
+ <h2 class="about-title">About the Platform</h2>
41
+ <p class="about-description">
42
+ This platform is designed specifically for last-minute exam preparation, helping students revise
43
+ smarter when time is
44
+ limited. It provides concise summaries, key topics, and focused quizzes powered by AI to maximize
45
+ clarity and retention
46
+ just before an exam.
47
+ </p>
48
+ <p class="about-description">
49
+ Built with a strong emphasis on speed, precision, and exam relevance, the platform enables learners
50
+ to quickly identify
51
+ important concepts, reinforce understanding, and assess readiness efficiently.
52
+ </p>
53
+
54
+ <!-- Feature Cards -->
55
+ <div class="feature-cards">
56
+ <div class="feature-card">
57
+ <div class="feature-header">
58
+ <img data-src="Google-Antigravity-LogoPNG.png" alt="Google Antigravity" class="feature-logo">
59
+ <h3>Google Antigravity</h3>
60
+ </div>
61
+ <p class="feature-description">
62
+ This platform has been created with the support and technical assistance of Antigravity,
63
+ combining innovation,
64
+ AI-driven learning, and student-centric design to make last-minute preparation more
65
+ effective and stress-free.
66
+ </p>
67
+ </div>
68
+
69
+ <div class="feature-card">
70
+ <div class="feature-header">
71
+ <img data-src="FigmaLogo.png" alt="Figma" class="feature-logo">
72
+ <h3>Figma</h3>
73
+ </div>
74
+ <p class="feature-description">
75
+ The user interface of this platform was thoughtfully designed in Figma, with a strong focus
76
+ on clarity, accessibility,
77
+ and ease of use for last-minute exam preparation.
78
+ </p>
79
+ <a href="https://www.figma.com/design/Mw2wCyHNVXlTVAGpy00l6A/AceNow?node-id=0-1&t=Zg1DX6N5UyOvczVB-1"
80
+ target="_blank" class="feature-link">View Design File →</a>
81
+ </div>
82
+ </div>
83
+ </div>
84
+
85
+ <footer class="login-footer">
86
+ <div class="social-links">
87
+ <a href="mailto:josedavidson.work@gmail.com" class="social-icon" title="Email">
88
+ <img data-src="mail-svgrepo-com.svg" alt="Email">
89
+ </a>
90
+ <a href="https://github.com/JosDavidson" target="_blank" class="social-icon" title="GitHub">
91
+ <img data-src="github-142-svgrepo-com.svg" alt="GitHub">
92
+ </a>
93
+ <a href="https://www.linkedin.com/in/jose-davidson/" target="_blank" class="social-icon"
94
+ title="LinkedIn">
95
+ <img data-src="linkedin-round-svgrepo-com.svg" alt="LinkedIn">
96
+ </a>
97
+ </div>
98
+ <p>© 2026 Jose Davidson. All rights reserved.</p>
99
+ </footer>
100
+ </div>
101
+
102
+
103
+ <!-- Dashboard View (Hidden by default) -->
104
+ <div id="dashboard-view" style="display: none;">
105
+ <nav>
106
+ <div class="logo">
107
+ <span class="logo-highlight">AceNow</span>
108
+ </div>
109
+ <div class="nav-center">
110
+ <button class="btn-glass" onclick="openSettings()">Select AI-Model</button>
111
+ </div>
112
+ <div class="nav-actions" id="auth-container">
113
+ <button class="btn-assistant" onclick="toggleAssistantView()">Assistant</button>
114
+ <button class="btn-logout" onclick="handleSignoutClick()">Logout</button>
115
+ </div>
116
+ </nav>
117
+
118
+ <div class="container" id="courses-view">
119
+ <!-- Settings Modal -->
120
+ <dialog id="settings-modal" class="settings-modal">
121
+ <div class="settings-container">
122
+ <div id="model-options-list" class="model-options-list">
123
+ <!-- Populated by JS -->
124
+ </div>
125
+
126
+ <div class="settings-actions">
127
+ <button class="btn-save-settings" onclick="saveSettings()">Save</button>
128
+ </div>
129
+ </div>
130
+ </dialog>
131
+
132
+ <!-- Loading State -->
133
+ <div class="loading" id="initial-loading" style="display: none;">
134
+ <p>Loading your courses...</p>
135
+ </div>
136
+
137
+ <!-- Courses Grid -->
138
+ <div class="grid" id="courses-grid">
139
+ <!-- Cards will be injected here -->
140
+ </div>
141
+ </div>
142
+
143
+ <!-- Assistant View -->
144
+ <div class="container assistant-container" id="assistant-view" style="display: none;">
145
+ <div class="discussion-pills" id="assistant-topics">
146
+ <!-- Example topics mentioned in prompt images -->
147
+ <div class="discussion-pill">Deep Learning Basics</div>
148
+ <div class="discussion-pill">Flask Best Practices</div>
149
+ <div class="discussion-pill">Kubernetes Orchestration</div>
150
+ <div class="discussion-pill">Database Indexing</div>
151
+ </div>
152
+
153
+ <h2 class="assistant-prompt">Select a topic to continue or start a new discussion</h2>
154
+
155
+ <div class="chat-input-wrapper">
156
+ <div class="chat-input-container">
157
+ <textarea id="chat-input" placeholder="Ask Anything" rows="1"></textarea>
158
+ <button class="btn-send-chat" onclick="sendAssistantQuery()">➔</button>
159
+ </div>
160
+ </div>
161
+
162
+ <!-- Chat Response Area -->
163
+ <div id="assistant-response-container" class="assistant-response-area" style="display: none;">
164
+ <div class="response-card">
165
+ <div id="assistant-response-text"></div>
166
+ </div>
167
+ </div>
168
+ </div>
169
+
170
+ <!-- Quiz/Study Overlay -->
171
+ <div class="quiz-overlay" id="quiz-modal">
172
+ <div class="quiz-container">
173
+ <button class="close-btn" onclick="closeQuiz()">×</button>
174
+
175
+ <!-- View 1: Loading/Processing -->
176
+ <div id="view-loading" class="quiz-view">
177
+ <h2>Analyzing Materials...</h2>
178
+ <div class="spinner"></div>
179
+ <p id="loading-text">Fetching course documents</p>
180
+ </div>
181
+
182
+ <!-- View 1.5: Action Menu -->
183
+ <div id="view-action-menu" class="quiz-view" style="display:none;">
184
+ <div class="menu-header">
185
+ <div class="menu-titles">
186
+ <h2 id="menu-course-name">Course Name</h2>
187
+ <p id="menu-course-batch" class="menu-subtitle">Batch</p>
188
+ </div>
189
+ </div>
190
+
191
+ <div class="action-grid">
192
+ <div class="action-card" onclick="startTopicsGeneration()">
193
+ <div class="action-img-container">
194
+ <img data-src="SelectionPanelKeyPoints.png" alt="Key Topics">
195
+ </div>
196
+ <h3>Key Topics</h3>
197
+ <p>High-priority concepts frequently asked in exams are curated here.</p>
198
+ <span class="action-link">View core concepts</span>
199
+ </div>
200
+ <div class="action-card" onclick="showQuizConfig()">
201
+ <div class="action-img-container">
202
+ <img data-src="SelectionPanelQuiz.png" alt="Quiz">
203
+ </div>
204
+ <h3>QUIZ</h3>
205
+ <p>Quick assessments designed to reinforce important concepts.</p>
206
+ <span class="action-link">Attempt Quick Quiz</span>
207
+ </div>
208
+ <div class="action-card" onclick="startSummaryGeneration()">
209
+ <div class="action-img-container">
210
+ <img data-src="SelectionPanelSummary.png" alt="Summary">
211
+ </div>
212
+ <h3>Summary</h3>
213
+ <p>A quick overview of core ideas to strengthen last-minute understanding.</p>
214
+ <span class="action-link">Get Instant Summary</span>
215
+ </div>
216
+ <div class="action-card" onclick="downloadAllMaterials()">
217
+ <div class="action-img-container"
218
+ style="display: flex; align-items: center; justify-content: center; background: rgba(14, 165, 233, 0.1); border-radius: 20px;">
219
+ <div style="font-size: 4rem; padding: 1rem;">📄</div>
220
+ </div>
221
+ <h3>Materials</h3>
222
+ <p>Download all original study materials (PDF/PPTX) directly from Classroom.</p>
223
+ <span class="action-link">Download all materials</span>
224
+ </div>
225
+ </div>
226
+ </div>
227
+
228
+ <!-- View 1.6: Quiz Configuration (New Design) -->
229
+ <div id="view-quiz-config" class="quiz-view" style="display:none;">
230
+ <div class="quiz-header-banner">
231
+ <img src="https://raw.githubusercontent.com/JosDavidson/AceNow/main/services/frontend-service/static/images/QuizBanner.png"
232
+ alt="Banner" class="banner-img">
233
+ <div class="banner-overlay">
234
+ <div class="banner-content">
235
+ <div class="banner-text">
236
+ <h2 id="config-course-name">Course Name</h2>
237
+ <p id="config-course-batch" class="banner-subtitle">Batch</p>
238
+ </div>
239
+ </div>
240
+ </div>
241
+ </div>
242
+
243
+ <div class="config-container">
244
+ <h3 class="config-main-title">Configure Quiz</h3>
245
+
246
+ <div class="config-row">
247
+ <label>Number of Questions</label>
248
+ <div class="pill-group" id="pill-num-questions">
249
+ <div class="pill-option selected" data-value="10">10 Questions</div>
250
+ <div class="pill-option" data-value="15">15 Questions</div>
251
+ <div class="pill-option" data-value="25">25 Questions</div>
252
+ </div>
253
+ </div>
254
+
255
+ <div class="config-row">
256
+ <label>Difficulty</label>
257
+ <div class="pill-group" id="pill-difficulty">
258
+ <div class="pill-option selected" data-value="Easy">Easy</div>
259
+ <div class="pill-option" data-value="Medium">Medium</div>
260
+ <div class="pill-option" data-value="Hard">Hard</div>
261
+ </div>
262
+ </div>
263
+
264
+ <div class="config-row">
265
+ <label>Timer</label>
266
+ <div class="pill-group" id="pill-timer">
267
+ <div class="pill-option" data-value="enable">Enable</div>
268
+ <div class="pill-option selected" data-value="disable">Disable</div>
269
+ <div class="pill-option" data-value="custom">Custom</div>
270
+ </div>
271
+ </div>
272
+
273
+ <div class="config-actions">
274
+ <button class="btn-primary" onclick="startQuizFromConfig()">Start Quiz</button>
275
+ <button class="btn-secondary" onclick="switchView('action-menu')">Back</button>
276
+ </div>
277
+ </div>
278
+ </div>
279
+
280
+ <!-- View 2: Topic Overview / Key Points -->
281
+ <div id="view-topics" class="quiz-view" style="display:none;">
282
+ <div class="topics-header-banner">
283
+ <img data-src="KeyPointsBanner.png" alt="Banner" class="banner-img">
284
+ <div class="banner-overlay">
285
+ <div class="banner-content">
286
+ <div class="banner-text">
287
+ <h2 id="topics-course-name">Course Name</h2>
288
+ <p id="topics-course-batch" class="banner-subtitle">Batch</p>
289
+ <h3 class="banner-label">Key Topics</h3>
290
+ </div>
291
+ <div id="selected-topic-pill-container" style="display: none;">
292
+ <div class="topic-pill-large" id="selected-topic-name">Topic Name</div>
293
+ </div>
294
+ </div>
295
+ </div>
296
+ </div>
297
+
298
+ <div id="topics-grid-container" class="topics-container-padding">
299
+ <div class="topics-pills-grid" id="topics-pills-grid">
300
+ <!-- Pills will be injected here -->
301
+ </div>
302
+ </div>
303
+
304
+ <div id="topic-explanation-container" class="topics-container-padding" style="display: none;">
305
+ <div class="explanation-content" id="topic-explanation-text">
306
+ <!-- AI Explanation will be injected here -->
307
+ </div>
308
+ </div>
309
+
310
+ <div class="topics-footer">
311
+ <button class="btn-secondary" id="btn-topics-back" onclick="switchView('action-menu')">Back to
312
+ Menu</button>
313
+ <button class="btn-secondary" id="btn-explanation-back" style="display: none;"
314
+ onclick="showTopicsGrid()">Back to Topics</button>
315
+ </div>
316
+ </div>
317
+
318
+ <!-- View 2.5: Summary View -->
319
+ <div id="view-summary" class="quiz-view" style="display:none;">
320
+ <div class="summary-header-banner">
321
+ <img data-src="SummaryBanner.png" alt="Banner" class="banner-img">
322
+ <!-- <button class="close-btn" onclick="switchView('action-menu')">×</button> -->
323
+ <div class="banner-overlay">
324
+ <div class="banner-content">
325
+ <div class="banner-text">
326
+ <h2 id="summary-course-name">Course Name</h2>
327
+ <p id="summary-course-batch" class="banner-subtitle">Batch</p>
328
+ <h3 class="banner-label">Summary</h3>
329
+ </div>
330
+ </div>
331
+ </div>
332
+ </div>
333
+
334
+ <div class="summary-container-padding">
335
+ <div id="summary-content" class="explanation-content">
336
+ <!-- AI Content will be injected here -->
337
+ </div>
338
+ </div>
339
+
340
+ <div class="topics-footer">
341
+ <button class="btn-secondary" onclick="switchView('action-menu')">Back to Menu</button>
342
+ </div>
343
+ </div>
344
+
345
+ <!-- View 3: Quiz Active (New Design) -->
346
+ <div id="view-quiz" class="quiz-view" style="display:none;">
347
+ <div class="quiz-header-banner">
348
+ <img src="https://raw.githubusercontent.com/JosDavidson/AceNow/main/services/frontend-service/static/images/QuizBanner.png"
349
+ alt="Banner" class="banner-img">
350
+ <div class="banner-overlay">
351
+ <div class="banner-content">
352
+ <div class="banner-text">
353
+ <h2 id="active-course-name">Course Name</h2>
354
+ <p id="active-course-batch" class="banner-subtitle">Batch</p>
355
+ <h3 class="banner-label">Quiz</h3>
356
+ </div>
357
+ <div class="quiz-stats-overlay">
358
+ <div id="quiz-timer-display">Time: 00:00</div>
359
+ <div id="quiz-qno-display">Q.No. 1/10</div>
360
+ </div>
361
+ </div>
362
+ </div>
363
+ </div>
364
+
365
+ <div class="quiz-active-container">
366
+ <h3 id="question-text" class="question-text">1. Question goes here...</h3>
367
+
368
+ <div class="options-grid" id="options-container">
369
+ <!-- Options injected here as 2x2 pills -->
370
+ </div>
371
+
372
+ <div id="rationale-container" class="rationale-container" style="display: none;">
373
+ <h4 id="answer-status-label">Correct!</h4>
374
+ <p id="rationale-text"></p>
375
+ </div>
376
+
377
+ <div id="hint-container" class="hint-container" style="display: none;">
378
+ <button class="btn-hint" onclick="toggleHint()">💡 Show Hint</button>
379
+ <p id="hint-text" class="hint-text" style="display: none;"></p>
380
+ </div>
381
+
382
+ <button id="next-question-btn" class="btn-primary"
383
+ style="display: none; width: 100%; margin-top: 1.5rem;" onclick="nextQuestion()">Next
384
+ Question</button>
385
+ </div>
386
+ </div>
387
+
388
+ <!-- View 4: Results (Modified) -->
389
+ <div id="view-results" class="quiz-view" style="display:none;">
390
+ <h2>Quiz Complete!</h2>
391
+ <div class="score-circle" id="final-score-display">80%</div>
392
+ <div class="result-breakdown">
393
+ <span class="result-item correct">✅ Correct: <span id="correct-count">0</span></span>
394
+ <span class="result-item wrong">❌ Wrong: <span id="wrong-count">0</span></span>
395
+ </div>
396
+ <p id="feedback-text">Great job! You mastered this topic.</p>
397
+ <button class="btn-primary" onclick="closeQuiz()">Close</button>
398
+ </div>
399
+ </div>
400
+ </div>
401
+ </div>
402
+
403
+ <!-- Application Logic -->
404
+ <script src="/static/js/app.js"></script>
405
+ </body>
406
+
407
+ </html>
start.sh ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Start services in background
4
+ python services/auth-service/app.py &
5
+ python services/file-parser-service/app.py &
6
+ python services/ai-service/app.py &
7
+ python services/frontend-service/app.py &
8
+
9
+ # Start API Gateway (main entry point) in foreground
10
+ # Use port 7860 for Hugging Face compatibility
11
+ export PORT=7860
12
+ python services/api-gateway/app.py