Spaces:
Sleeping
Sleeping
Added
Browse files- .env.example +2 -0
- .github/workflows/lint.yml +23 -0
- .gitignore +9 -0
- Dockerfile +20 -0
- app/api/__init__.py +1 -0
- app/api/routes.py +56 -0
- app/core/__init__.py +0 -0
- app/core/config.py +10 -0
- app/core/exceptions.py +15 -0
- app/core/logging.py +11 -0
- app/schemas/__init__.py +1 -0
- app/schemas/schemas.py +18 -0
- app/services/__init__.py +0 -0
- app/services/gemini.py +50 -0
- app/templates/index.html +361 -0
- app/utils/__init__.py +0 -0
- main.py +10 -0
- requirements.txt +51 -0
.env.example
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
GOOGLE_API_KEY=<google_api_key>
|
2 |
+
DATABASE_URL=postgresql+asyncpg://<username>:<password>@<host>:<port>/<database_name>
|
.github/workflows/lint.yml
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Ruff lint
|
2 |
+
|
3 |
+
on: [push, pull_request]
|
4 |
+
|
5 |
+
jobs:
|
6 |
+
ruff:
|
7 |
+
runs-on: ubuntu-latest
|
8 |
+
steps:
|
9 |
+
- name: Checkout code
|
10 |
+
uses: actions/checkout@v2
|
11 |
+
|
12 |
+
- name: Set up Python
|
13 |
+
uses: actions/setup-python@v2
|
14 |
+
with:
|
15 |
+
python-version: '3.10'
|
16 |
+
|
17 |
+
- name: Install dependencies
|
18 |
+
run: |
|
19 |
+
python -m pip install --upgrade pip
|
20 |
+
pip install ruff
|
21 |
+
|
22 |
+
- name: Run Ruff
|
23 |
+
run: ruff check .
|
.gitignore
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
__pycache__/
|
2 |
+
.env
|
3 |
+
|
4 |
+
venv/
|
5 |
+
|
6 |
+
*.log
|
7 |
+
|
8 |
+
app.log
|
9 |
+
|
Dockerfile
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use official Python image
|
2 |
+
FROM python:3.10-slim
|
3 |
+
|
4 |
+
# Set work directory
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# Copy requirements
|
8 |
+
COPY requirements.txt .
|
9 |
+
|
10 |
+
# Install dependencies
|
11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
12 |
+
|
13 |
+
# Copy app code
|
14 |
+
COPY . .
|
15 |
+
|
16 |
+
# Expose port (Hugging Face Spaces expects 7860 or 8000, but FastAPI default is 8000)
|
17 |
+
EXPOSE 7860
|
18 |
+
|
19 |
+
# Start FastAPI with uvicorn on port 7860 for Hugging Face Spaces
|
20 |
+
CMD ["uvicorn", "app.api.routes:router", "--host", "0.0.0.0", "--port", "7860"]
|
app/api/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
from .routes import router #noqa
|
app/api/routes.py
ADDED
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter,status, Query, Request
|
2 |
+
from fastapi.responses import HTMLResponse
|
3 |
+
from app.services.gemini import GenerateAnswer
|
4 |
+
from app.schemas import SOLOLevel, MCQResponse
|
5 |
+
from app.core.exceptions import CustomException
|
6 |
+
from fastapi.templating import Jinja2Templates
|
7 |
+
from app.core.logging import logger
|
8 |
+
|
9 |
+
templates = Jinja2Templates(directory="app/templates")
|
10 |
+
|
11 |
+
answer_service = GenerateAnswer()
|
12 |
+
|
13 |
+
router = APIRouter()
|
14 |
+
|
15 |
+
|
16 |
+
@router.get("/generate_mcq", response_class=HTMLResponse)
|
17 |
+
|
18 |
+
async def generate_mcq(
|
19 |
+
request: Request,
|
20 |
+
topic: str = Query(default=None),
|
21 |
+
solo_level: SOLOLevel = Query(default=None),
|
22 |
+
):
|
23 |
+
"""
|
24 |
+
Generate a multiple-choice question (MCQ) based on the provided topic and SOLO level in jinja template.
|
25 |
+
"""
|
26 |
+
if topic and solo_level:
|
27 |
+
mcq = await answer_service.generate_mcq(
|
28 |
+
topic=topic, solo_level=solo_level
|
29 |
+
) # generates mcq
|
30 |
+
if not mcq:
|
31 |
+
raise CustomException(detail="No MCQ generated.")
|
32 |
+
return templates.TemplateResponse(
|
33 |
+
"index.html", {"request": request, "mcq": mcq}
|
34 |
+
)
|
35 |
+
return templates.TemplateResponse("index.html", {"request": request})
|
36 |
+
|
37 |
+
|
38 |
+
@router.get(
|
39 |
+
"/api/generate_mcq", status_code=status.HTTP_200_OK, tags=["MCQ Generation "]
|
40 |
+
)
|
41 |
+
async def generate_mcq_api(
|
42 |
+
topic: str,
|
43 |
+
solo_level: SOLOLevel = Query(...),
|
44 |
+
) -> MCQResponse:
|
45 |
+
"""
|
46 |
+
Generate a multiple-choice question (MCQ) based on the provided topic and SOLO level.
|
47 |
+
"""
|
48 |
+
try:
|
49 |
+
mcq = await answer_service.generate_mcq(topic=topic, solo_level=solo_level)
|
50 |
+
if not mcq:
|
51 |
+
raise CustomException(detail="No MCQ generated.")
|
52 |
+
return mcq
|
53 |
+
|
54 |
+
except Exception:
|
55 |
+
logger.exception("Unexpected error while generating MCQ.")
|
56 |
+
raise CustomException(detail="Internal server error during MCQ generation.")
|
app/core/__init__.py
ADDED
File without changes
|
app/core/config.py
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
2 |
+
|
3 |
+
class Settings(BaseSettings):
|
4 |
+
GOOGLE_API_KEY: str
|
5 |
+
|
6 |
+
model_config=SettingsConfigDict(
|
7 |
+
env_file=".env"
|
8 |
+
)
|
9 |
+
|
10 |
+
settings = Settings()
|
app/core/exceptions.py
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from http import HTTPStatus
|
2 |
+
from fastapi import HTTPException, status
|
3 |
+
|
4 |
+
|
5 |
+
class CustomException(HTTPException):
|
6 |
+
def __init__(
|
7 |
+
self,
|
8 |
+
status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR,
|
9 |
+
detail: str | None = None,
|
10 |
+
):
|
11 |
+
if not detail:
|
12 |
+
detail = HTTPStatus(status_code).description
|
13 |
+
|
14 |
+
super().__init__(status_code=status_code, detail=detail)
|
15 |
+
|
app/core/logging.py
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
|
3 |
+
def logger_setup():
|
4 |
+
logging.basicConfig(
|
5 |
+
level=logging.INFO,
|
6 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
7 |
+
)
|
8 |
+
logger = logging.getLogger(__name__)
|
9 |
+
return logger
|
10 |
+
|
11 |
+
logger = logger_setup()
|
app/schemas/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
from .schemas import * #noqa
|
app/schemas/schemas.py
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from enum import Enum
|
2 |
+
from pydantic import BaseModel, Field
|
3 |
+
|
4 |
+
class SOLOLevel(str,Enum):
|
5 |
+
"""
|
6 |
+
Enum for SOLO levels.
|
7 |
+
"""
|
8 |
+
UNISTRUCTURAL = "unistructural"
|
9 |
+
MULTISTRUCTURAL = "multistructural"
|
10 |
+
|
11 |
+
|
12 |
+
class MCQResponse(BaseModel):
|
13 |
+
"""
|
14 |
+
Response model for MCQ generation.
|
15 |
+
"""
|
16 |
+
question_text: str = Field(..., description="The generated question text.")
|
17 |
+
options: list[str] = Field(..., description="List of answer options.")
|
18 |
+
correct_answer: str = Field(..., description="The correct answer from the options.")
|
app/services/__init__.py
ADDED
File without changes
|
app/services/gemini.py
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
2 |
+
from langchain_core.prompts import PromptTemplate
|
3 |
+
from app.core.config import settings
|
4 |
+
from app.schemas import MCQResponse
|
5 |
+
|
6 |
+
|
7 |
+
class GenerateAnswer:
|
8 |
+
"""
|
9 |
+
Class to generate answers using Google Gemini API.
|
10 |
+
"""
|
11 |
+
|
12 |
+
def __init__(self):
|
13 |
+
self.llm = ChatGoogleGenerativeAI(
|
14 |
+
model="gemini-2.0-flash",
|
15 |
+
temperature=0.6,
|
16 |
+
api_key=settings.GOOGLE_API_KEY,
|
17 |
+
)
|
18 |
+
|
19 |
+
async def generate_mcq(self, topic: str, solo_level: str):
|
20 |
+
"""
|
21 |
+
Generate an answer to the given question using Google Gemini API.
|
22 |
+
"""
|
23 |
+
prompt = PromptTemplate(
|
24 |
+
template="""You are an AI tutor. Based on the SOLO taxonomy level and the content snippet provided, generate a single multiple-choice question (MCQ) that matches the SOLO level.
|
25 |
+
|
26 |
+
Content Snippet:
|
27 |
+
\"\"\"
|
28 |
+
Photosynthesis is the process by which plants use sunlight, water, and carbon dioxide to create glucose and oxygen. Chlorophyll absorbs sunlight.
|
29 |
+
\"\"\"
|
30 |
+
|
31 |
+
|
32 |
+
SOLO Level: {solo_level}
|
33 |
+
You should be based on this Topic: {topic}
|
34 |
+
|
35 |
+
SOLO Level Consideration:
|
36 |
+
- Unistructural: Focus on recalling a single piece of information from the content_snippet.
|
37 |
+
- Multistructural: Focus on recalling several pieces of information from the content_snippet.
|
38 |
+
|
39 |
+
Generate one MCQ with:
|
40 |
+
- "question_text": A single question aligned to the SOLO level
|
41 |
+
- "options": 3–4 plausible answer choices
|
42 |
+
- "correct_answer": The correct answer (must match one of the options)""",
|
43 |
+
input_variables=["topic", "solo_level"],
|
44 |
+
)
|
45 |
+
model=self.llm.with_structured_output(MCQResponse)
|
46 |
+
chain=prompt | model
|
47 |
+
response= await chain.ainvoke({"topic": topic, "solo_level": solo_level})
|
48 |
+
return response
|
49 |
+
|
50 |
+
|
app/templates/index.html
ADDED
@@ -0,0 +1,361 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html>
|
3 |
+
|
4 |
+
<head>
|
5 |
+
<title>MCQ Generator</title>
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
7 |
+
<style>
|
8 |
+
:root {
|
9 |
+
--primary: #4a6fa5;
|
10 |
+
--secondary: #6a98d8;
|
11 |
+
--accent: #ff6b6b;
|
12 |
+
--light: #f8f9fa;
|
13 |
+
--dark: #343a40;
|
14 |
+
--success: #28a745;
|
15 |
+
--danger: #dc3545;
|
16 |
+
}
|
17 |
+
|
18 |
+
* {
|
19 |
+
margin: 0;
|
20 |
+
padding: 0;
|
21 |
+
box-sizing: border-box;
|
22 |
+
transition: all 0.3s ease;
|
23 |
+
}
|
24 |
+
|
25 |
+
body {
|
26 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
27 |
+
line-height: 1.6;
|
28 |
+
color: var(--dark);
|
29 |
+
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
30 |
+
min-height: 100vh;
|
31 |
+
padding: 0;
|
32 |
+
}
|
33 |
+
|
34 |
+
.container {
|
35 |
+
max-width: 800px;
|
36 |
+
margin: 0 auto;
|
37 |
+
padding: 20px;
|
38 |
+
}
|
39 |
+
|
40 |
+
header {
|
41 |
+
background-color: var(--primary);
|
42 |
+
color: white;
|
43 |
+
padding: 20px 0;
|
44 |
+
border-radius: 0 0 10px 10px;
|
45 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
46 |
+
text-align: center;
|
47 |
+
margin-bottom: 30px;
|
48 |
+
}
|
49 |
+
|
50 |
+
header h1 {
|
51 |
+
margin: 0;
|
52 |
+
font-size: 2.5rem;
|
53 |
+
font-weight: 700;
|
54 |
+
}
|
55 |
+
|
56 |
+
header p {
|
57 |
+
margin-top: 10px;
|
58 |
+
font-size: 1.2rem;
|
59 |
+
opacity: 0.9;
|
60 |
+
}
|
61 |
+
|
62 |
+
.card {
|
63 |
+
background: white;
|
64 |
+
border-radius: 12px;
|
65 |
+
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
66 |
+
padding: 30px;
|
67 |
+
margin-bottom: 30px;
|
68 |
+
position: relative;
|
69 |
+
overflow: hidden;
|
70 |
+
}
|
71 |
+
|
72 |
+
.card::before {
|
73 |
+
content: '';
|
74 |
+
position: absolute;
|
75 |
+
top: 0;
|
76 |
+
left: 0;
|
77 |
+
width: 100%;
|
78 |
+
height: 5px;
|
79 |
+
background: linear-gradient(90deg, var(--primary), var(--secondary), var(--accent));
|
80 |
+
}
|
81 |
+
|
82 |
+
.form-group {
|
83 |
+
margin-bottom: 20px;
|
84 |
+
}
|
85 |
+
|
86 |
+
label {
|
87 |
+
display: block;
|
88 |
+
margin-bottom: 8px;
|
89 |
+
font-weight: 600;
|
90 |
+
color: var(--dark);
|
91 |
+
}
|
92 |
+
|
93 |
+
input,
|
94 |
+
select {
|
95 |
+
width: 100%;
|
96 |
+
padding: 12px 15px;
|
97 |
+
border: 2px solid #dee2e6;
|
98 |
+
border-radius: 8px;
|
99 |
+
font-size: 16px;
|
100 |
+
color: var(--dark);
|
101 |
+
}
|
102 |
+
|
103 |
+
input:focus,
|
104 |
+
select:focus {
|
105 |
+
border-color: var(--secondary);
|
106 |
+
box-shadow: 0 0 0 3px rgba(106, 152, 216, 0.25);
|
107 |
+
outline: none;
|
108 |
+
}
|
109 |
+
|
110 |
+
.btn {
|
111 |
+
display: inline-block;
|
112 |
+
font-weight: 600;
|
113 |
+
text-align: center;
|
114 |
+
white-space: nowrap;
|
115 |
+
vertical-align: middle;
|
116 |
+
user-select: none;
|
117 |
+
border: none;
|
118 |
+
padding: 12px 24px;
|
119 |
+
font-size: 16px;
|
120 |
+
line-height: 1.5;
|
121 |
+
border-radius: 8px;
|
122 |
+
cursor: pointer;
|
123 |
+
transition: all 0.3s ease;
|
124 |
+
}
|
125 |
+
|
126 |
+
.btn-primary {
|
127 |
+
color: white;
|
128 |
+
background: linear-gradient(45deg, var(--primary), var(--secondary));
|
129 |
+
box-shadow: 0 4px 6px rgba(74, 111, 165, 0.2);
|
130 |
+
}
|
131 |
+
|
132 |
+
.btn-primary:hover {
|
133 |
+
transform: translateY(-2px);
|
134 |
+
box-shadow: 0 6px 8px rgba(74, 111, 165, 0.3);
|
135 |
+
}
|
136 |
+
|
137 |
+
.btn-block {
|
138 |
+
display: block;
|
139 |
+
width: 100%;
|
140 |
+
}
|
141 |
+
|
142 |
+
.question {
|
143 |
+
background: white;
|
144 |
+
border-radius: 12px;
|
145 |
+
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
146 |
+
padding: 25px;
|
147 |
+
margin-top: 30px;
|
148 |
+
position: relative;
|
149 |
+
border-left: 5px solid var(--primary);
|
150 |
+
}
|
151 |
+
|
152 |
+
.question strong {
|
153 |
+
color: var(--primary);
|
154 |
+
font-size: 1.2rem;
|
155 |
+
margin-bottom: 15px;
|
156 |
+
display: block;
|
157 |
+
}
|
158 |
+
|
159 |
+
.question ul {
|
160 |
+
list-style-type: none;
|
161 |
+
padding: 0;
|
162 |
+
margin: 20px 0;
|
163 |
+
}
|
164 |
+
|
165 |
+
.question li {
|
166 |
+
padding: 10px 15px;
|
167 |
+
border: 1px solid #dee2e6;
|
168 |
+
border-radius: 8px;
|
169 |
+
margin-bottom: 10px;
|
170 |
+
transition: all 0.2s ease;
|
171 |
+
cursor: pointer;
|
172 |
+
}
|
173 |
+
|
174 |
+
.question li:hover {
|
175 |
+
background-color: #f8f9fa;
|
176 |
+
transform: translateX(5px);
|
177 |
+
}
|
178 |
+
|
179 |
+
.correct-answer {
|
180 |
+
margin-top: 20px;
|
181 |
+
padding: 15px;
|
182 |
+
background-color: rgba(40, 167, 69, 0.1);
|
183 |
+
border-left: 3px solid var(--success);
|
184 |
+
border-radius: 8px;
|
185 |
+
display: none;
|
186 |
+
}
|
187 |
+
|
188 |
+
.correct-answer strong {
|
189 |
+
color: var(--success);
|
190 |
+
font-size: 1.1rem;
|
191 |
+
margin-bottom: 5px;
|
192 |
+
display: block;
|
193 |
+
}
|
194 |
+
|
195 |
+
.action-buttons {
|
196 |
+
margin-top: 15px;
|
197 |
+
display: none;
|
198 |
+
}
|
199 |
+
|
200 |
+
.btn-retry {
|
201 |
+
background-color: var(--primary);
|
202 |
+
color: white;
|
203 |
+
margin-right: 10px;
|
204 |
+
}
|
205 |
+
|
206 |
+
.btn-show-answer {
|
207 |
+
background-color: var(--secondary);
|
208 |
+
color: white;
|
209 |
+
}
|
210 |
+
|
211 |
+
footer {
|
212 |
+
text-align: center;
|
213 |
+
padding: 20px;
|
214 |
+
margin-top: 30px;
|
215 |
+
color: var(--dark);
|
216 |
+
font-size: 0.9rem;
|
217 |
+
}
|
218 |
+
|
219 |
+
/* Responsive design */
|
220 |
+
@media (max-width: 768px) {
|
221 |
+
.container {
|
222 |
+
padding: 10px;
|
223 |
+
}
|
224 |
+
|
225 |
+
header h1 {
|
226 |
+
font-size: 2rem;
|
227 |
+
}
|
228 |
+
|
229 |
+
.card {
|
230 |
+
padding: 20px;
|
231 |
+
}
|
232 |
+
|
233 |
+
.btn {
|
234 |
+
padding: 10px 20px;
|
235 |
+
}
|
236 |
+
}
|
237 |
+
</style>
|
238 |
+
</head>
|
239 |
+
|
240 |
+
<body>
|
241 |
+
<div class="container">
|
242 |
+
<header>
|
243 |
+
<h1>MCQ Generator</h1>
|
244 |
+
<p>Create custom multiple-choice questions </p>
|
245 |
+
</header>
|
246 |
+
|
247 |
+
<div class="card">
|
248 |
+
<form method="get" action="/generate_mcq">
|
249 |
+
<div class="form-group">
|
250 |
+
<label for="topic">Topic:</label>
|
251 |
+
<input type="text" id="topic" name="topic" placeholder="Enter a subject or topic" required>
|
252 |
+
</div>
|
253 |
+
|
254 |
+
<div class="form-group">
|
255 |
+
<label for="solo_level">SOLO Level:</label>
|
256 |
+
<select id="solo_level" name="solo_level" required>
|
257 |
+
<option value="" disabled selected>Select a level</option>
|
258 |
+
<option value="unistructural">Unistructural (Basic facts)</option>
|
259 |
+
<option value="multistructural">Multistructural (Multiple concepts)</option>
|
260 |
+
|
261 |
+
</select>
|
262 |
+
</div>
|
263 |
+
|
264 |
+
<button type="submit" class="btn btn-primary btn-block">Generate Question</button>
|
265 |
+
</form>
|
266 |
+
</div>
|
267 |
+
|
268 |
+
{% if mcq %}
|
269 |
+
<div class="question">
|
270 |
+
<strong>Question:</strong> {{ mcq.question_text }}
|
271 |
+
|
272 |
+
<ul id="options-list">
|
273 |
+
{% for option in mcq.options %}
|
274 |
+
<li data-correct="{{ 'true' if option == mcq.correct_answer else 'false' }}"
|
275 |
+
onclick="checkAnswer(this)">{{ option }}</li>
|
276 |
+
{% endfor %}
|
277 |
+
</ul>
|
278 |
+
|
279 |
+
<div class="action-buttons" id="action-buttons">
|
280 |
+
<button class="btn btn-retry" onclick="resetQuiz()">Try Again</button>
|
281 |
+
<button class="btn btn-show-answer" onclick="showAnswer()">Show Answer</button>
|
282 |
+
</div>
|
283 |
+
|
284 |
+
<div class="correct-answer" id="feedback">
|
285 |
+
<strong>✅ Correct Answer:</strong> {{ mcq.correct_answer }}
|
286 |
+
</div>
|
287 |
+
</div>
|
288 |
+
{% endif %}
|
289 |
+
|
290 |
+
<footer>
|
291 |
+
<p>MCQ Generator</p>
|
292 |
+
</footer>
|
293 |
+
</div>
|
294 |
+
|
295 |
+
<script>
|
296 |
+
// Add some simple interactivity
|
297 |
+
document.addEventListener('DOMContentLoaded', function () {
|
298 |
+
// Highlight form fields on focus
|
299 |
+
const formElements = document.querySelectorAll('input, select');
|
300 |
+
formElements.forEach(element => {
|
301 |
+
element.addEventListener('focus', function () {
|
302 |
+
this.style.transform = 'translateY(-2px)';
|
303 |
+
});
|
304 |
+
|
305 |
+
element.addEventListener('blur', function () {
|
306 |
+
this.style.transform = 'translateY(0)';
|
307 |
+
});
|
308 |
+
});
|
309 |
+
});
|
310 |
+
|
311 |
+
// MCQ interaction functions
|
312 |
+
function checkAnswer(selectedLi) {
|
313 |
+
const isCorrect = selectedLi.dataset.correct === 'true';
|
314 |
+
const options = document.querySelectorAll('#options-list li');
|
315 |
+
|
316 |
+
// Disable all options
|
317 |
+
options.forEach(opt => {
|
318 |
+
opt.onclick = null;
|
319 |
+
opt.style.cursor = 'default';
|
320 |
+
});
|
321 |
+
|
322 |
+
if (isCorrect) {
|
323 |
+
selectedLi.style.backgroundColor = 'rgba(40, 167, 69, 0.1)';
|
324 |
+
selectedLi.style.borderColor = 'var(--success)';
|
325 |
+
document.getElementById('feedback').style.display = 'block';
|
326 |
+
} else {
|
327 |
+
selectedLi.style.backgroundColor = 'rgba(220, 53, 69, 0.1)';
|
328 |
+
selectedLi.style.borderColor = 'var(--danger)';
|
329 |
+
document.getElementById('action-buttons').style.display = 'block';
|
330 |
+
}
|
331 |
+
}
|
332 |
+
|
333 |
+
function resetQuiz() {
|
334 |
+
const options = document.querySelectorAll('#options-list li');
|
335 |
+
options.forEach(opt => {
|
336 |
+
opt.style.backgroundColor = '';
|
337 |
+
opt.style.borderColor = '#dee2e6';
|
338 |
+
opt.onclick = function () { checkAnswer(this); };
|
339 |
+
opt.style.cursor = 'pointer';
|
340 |
+
});
|
341 |
+
|
342 |
+
document.getElementById('action-buttons').style.display = 'none';
|
343 |
+
document.getElementById('feedback').style.display = 'none';
|
344 |
+
}
|
345 |
+
|
346 |
+
function showAnswer() {
|
347 |
+
const options = document.querySelectorAll('#options-list li');
|
348 |
+
options.forEach(opt => {
|
349 |
+
if (opt.dataset.correct === 'true') {
|
350 |
+
opt.style.backgroundColor = 'rgba(40, 167, 69, 0.1)';
|
351 |
+
opt.style.borderColor = 'var(--success)';
|
352 |
+
}
|
353 |
+
});
|
354 |
+
|
355 |
+
document.getElementById('feedback').style.display = 'block';
|
356 |
+
document.getElementById('action-buttons').style.display = 'none';
|
357 |
+
}
|
358 |
+
</script>
|
359 |
+
</body>
|
360 |
+
|
361 |
+
</html>
|
app/utils/__init__.py
ADDED
File without changes
|
main.py
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI
|
2 |
+
from app.api import router
|
3 |
+
import uvicorn
|
4 |
+
|
5 |
+
app=FastAPI()
|
6 |
+
app.include_router(router)
|
7 |
+
|
8 |
+
|
9 |
+
if __name__=="__main__":
|
10 |
+
uvicorn.run(app, host="0.0.0.0", port=3000)
|
requirements.txt
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
annotated-types==0.7.0
|
2 |
+
anyio==4.9.0
|
3 |
+
cachetools==5.5.2
|
4 |
+
certifi==2025.4.26
|
5 |
+
charset-normalizer==3.4.2
|
6 |
+
click==8.1.8
|
7 |
+
fastapi==0.115.12
|
8 |
+
filetype==1.2.0
|
9 |
+
google-ai-generativelanguage==0.6.18
|
10 |
+
google-api-core==2.24.2
|
11 |
+
google-auth==2.40.1
|
12 |
+
googleapis-common-protos==1.70.0
|
13 |
+
greenlet==3.2.1
|
14 |
+
grpcio==1.71.0
|
15 |
+
grpcio-status==1.71.0
|
16 |
+
h11==0.16.0
|
17 |
+
httpcore==1.0.9
|
18 |
+
httpx==0.28.1
|
19 |
+
idna==3.10
|
20 |
+
Jinja2==3.1.6
|
21 |
+
jsonpatch==1.33
|
22 |
+
jsonpointer==3.0.0
|
23 |
+
langchain-core==0.3.59
|
24 |
+
langchain-google-genai==2.1.4
|
25 |
+
langsmith==0.3.42
|
26 |
+
Mako==1.3.10
|
27 |
+
MarkupSafe==3.0.2
|
28 |
+
orjson==3.10.18
|
29 |
+
packaging==24.2
|
30 |
+
proto-plus==1.26.1
|
31 |
+
protobuf==5.29.4
|
32 |
+
pyasn1==0.6.1
|
33 |
+
pyasn1_modules==0.4.2
|
34 |
+
pydantic==2.11.4
|
35 |
+
pydantic-settings==2.9.1
|
36 |
+
pydantic_core==2.33.2
|
37 |
+
python-dotenv==1.1.0
|
38 |
+
PyYAML==6.0.2
|
39 |
+
requests==2.32.3
|
40 |
+
requests-toolbelt==1.0.0
|
41 |
+
rsa==4.9.1
|
42 |
+
ruff==0.11.8
|
43 |
+
sniffio==1.3.1
|
44 |
+
SQLAlchemy==2.0.40
|
45 |
+
starlette==0.46.2
|
46 |
+
tenacity==9.1.2
|
47 |
+
typing-inspection==0.4.0
|
48 |
+
typing_extensions==4.13.2
|
49 |
+
urllib3==2.4.0
|
50 |
+
uvicorn==0.34.2
|
51 |
+
zstandard==0.23.0
|