Spaces:
Running
Running
GitHub Action
commited on
Commit
·
39b5ef2
0
Parent(s):
Clean deployment - 0 binaries
Browse files- .github/workflows/main.yml +49 -0
- .gitignore +37 -0
- CardImges/Card1.png +0 -0
- CardImges/Card2.png +0 -0
- CardImges/Card3.png +0 -0
- CardImges/Card4.png +0 -0
- Dockerfile +33 -0
- MICROSERVICES_ARCHITECTURE.md +62 -0
- Procfile +1 -0
- README.md +83 -0
- bundle_images.py +28 -0
- docker-compose.yml +50 -0
- requirements.txt +16 -0
- run_dev.py +72 -0
- services/ai-service/.env.example +4 -0
- services/ai-service/Dockerfile +6 -0
- services/ai-service/app.py +389 -0
- services/ai-service/requirements.txt +6 -0
- services/api-gateway/.env.example +5 -0
- services/api-gateway/Dockerfile +6 -0
- services/api-gateway/app.py +171 -0
- services/api-gateway/requirements.txt +4 -0
- services/auth-service/.env.example +2 -0
- services/auth-service/Dockerfile +6 -0
- services/auth-service/app.py +53 -0
- services/auth-service/requirements.txt +5 -0
- services/file-parser-service/.env.example +1 -0
- services/file-parser-service/Dockerfile +6 -0
- services/file-parser-service/app.py +102 -0
- services/file-parser-service/requirements.txt +5 -0
- services/frontend-service/.env.example +1 -0
- services/frontend-service/Dockerfile +6 -0
- services/frontend-service/app.py +27 -0
- services/frontend-service/requirements.txt +3 -0
- services/frontend-service/static/css/style.css +1522 -0
- services/frontend-service/static/images/.gitkeep +0 -0
- services/frontend-service/static/js/app.js +1121 -0
- services/frontend-service/static/js/image_bundle.js +0 -0
- services/frontend-service/templates/index.html +407 -0
- start.sh +12 -0
.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
|