diff --git a/.env.example b/.env.example index ad1ed2d..d3183a4 100644 --- a/.env.example +++ b/.env.example @@ -3,9 +3,14 @@ # NEVER commit .env to version control. # Google Gemini AI API Key -# Get your key at: https://makersuite.google.com/app/apikey +# Get your key at: https://aistudio.google.com/app/apikey GEMINI_API_KEY=your_gemini_api_key_here +# Google Gemini Model (optional) +# Available models: https://ai.google.dev/gemini-api/docs/models +# Defaults to gemini-2.5-flash if not set. +GEMINI_MODEL=gemini-2.5-flash + # Flask Configuration # Set to "true" only during local development — NEVER in production FLASK_DEBUG=false diff --git a/Dockerfile b/Dockerfile index 8802625..a581602 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,5 +9,9 @@ RUN npm run build # Production stage FROM nginx:alpine COPY --from=build /app/dist /usr/share/nginx/html +# Proxy /chat and /explain_mistake to the chatbot container. +COPY nginx.conf /etc/nginx/conf.d/default.conf +# Serve the same-origin chatbot config (browser talks to the nginx proxy). +COPY docker/chatbotConfig.js /usr/share/nginx/html/chatbotConfig.js EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] diff --git a/Dockerfile.chatbot b/Dockerfile.chatbot new file mode 100644 index 0000000..8739764 --- /dev/null +++ b/Dockerfile.chatbot @@ -0,0 +1,13 @@ +# AI Tutor (Flask + Google Gemini) service image +FROM python:3.11-slim +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY chatbot.py . +COPY templates ./templates +COPY static ./static + +EXPOSE 5000 +CMD ["python", "chatbot.py"] \ No newline at end of file diff --git a/chatbot.py b/chatbot.py index da486f9..742dabf 100644 --- a/chatbot.py +++ b/chatbot.py @@ -5,7 +5,12 @@ from flask import Flask, request, jsonify, render_template from flask_cors import CORS -import google.generativeai as genai +from dotenv import load_dotenv +from google import genai + +# Load the .env file (GEMINI_API_KEY, GEMINI_MODEL, etc.) before +# reading any environment variables so `cp .env.example .env` just works. +load_dotenv() # ----------------------------- # Security: API key via env var @@ -18,17 +23,17 @@ "Set it before running the server." ) -genai.configure(api_key=API_KEY) - # ----------------------------- # App setup # ----------------------------- app = Flask(__name__, template_folder="templates", static_folder="static") CORS(app) -# Create the model once at startup (cheaper than per request) -MODEL_NAME = os.environ.get("GEMINI_MODEL", "gemini-1.5-pro") -model = genai.GenerativeModel(MODEL_NAME) +# REST-based Gemini client (no gRPC dependency) +client = genai.Client(api_key=API_KEY) + +# gemini-2.5-flash is a current, widely-available default; override via GEMINI_MODEL. +MODEL_NAME = os.environ.get("GEMINI_MODEL", "gemini-2.5-flash") # ----------------------------- @@ -75,6 +80,13 @@ def looks_like_abuse(user_text: str) -> bool: return any(k in t for k in ABUSE_KEYWORDS) +def generate_reply(prompt: str, fallback: str) -> str: + """Call Gemini and return plain text, or `fallback` if no usable output.""" + response = client.models.generate_content(model=MODEL_NAME, contents=prompt) + text = (response.text or "").strip() if response is not None else "" + return text if text else fallback + + # ----------------------------- # Rate limiting (per IP) # In-memory sliding window. @@ -155,17 +167,10 @@ def chat(): # Structured prompt (reduces injection impact) prompt = f"{system_guardrails}\n\nUser message: {user_input}" - response = model.generate_content(prompt) - - reply_text = "" - if response and getattr(response, "candidates", None): - cand0 = response.candidates[0] if response.candidates else None - parts = getattr(getattr(cand0, "content", None), "parts", None) - if parts and len(parts) > 0: - reply_text = getattr(parts[0], "text", None) or "" - - if not reply_text: - reply_text = "I’m not sure how to respond to that. Can you try rephrasing?" + reply_text = generate_reply( + prompt, + "I’m not sure how to respond to that. Can you try rephrasing?", + ) return jsonify({"reply": format_response(reply_text)}) @@ -219,17 +224,10 @@ def explain_mistake(): f"Correct Answer: {correct_answer}" ) - response = model.generate_content(prompt) - - reply_text = "" - if response and getattr(response, "candidates", None): - cand0 = response.candidates[0] if response.candidates else None - parts = getattr(getattr(cand0, "content", None), "parts", None) - if parts and len(parts) > 0: - reply_text = getattr(parts[0], "text", None) or "" - - if not reply_text: - reply_text = "I'm not sure how to explain this mistake. Let's review the main concept together." + reply_text = generate_reply( + prompt, + "I'm not sure how to explain this mistake. Let's review the main concept together.", + ) return jsonify({"reply": format_response(reply_text)}) @@ -243,5 +241,5 @@ def explain_mistake(): if __name__ == "__main__": debug_mode = os.environ.get("FLASK_DEBUG", "false").lower() == "true" - app.run(debug=debug_mode) + app.run(host="0.0.0.0", port=5000, debug=debug_mode) diff --git a/chatbotApi.js b/chatbotApi.js new file mode 100644 index 0000000..3f31222 --- /dev/null +++ b/chatbotApi.js @@ -0,0 +1,18 @@ +// chatbotApi.js — shared AI Tutor API helper used by home.js and review.js. +// Posts JSON to / and resolves with the parsed body. +// Throws an Error on HTTP errors so callers can show a friendly message. +window.chatbotPost = async function (path, payload) { + const response = await fetch(window.CHATBOT_BASE_URL + path, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + const data = await response.json().catch(() => ({})); + + if (!response.ok) { + throw new Error(data.error || `Server error: ${response.status}`); + } + + return data; +}; \ No newline at end of file diff --git a/chatbotConfig.js b/chatbotConfig.js new file mode 100644 index 0000000..8278d47 --- /dev/null +++ b/chatbotConfig.js @@ -0,0 +1,6 @@ +// chatbotConfig.js — where the AI Tutor backend lives. +// Default keeps the documented local workflow working (Flask on 127.0.0.1:5000). +// For same-origin deployments (Vite dev proxy or the nginx container proxy), +// set CHATBOT_BASE_URL before this script loads, or override it in the +// container build (see docker/chatbotConfig.js). +window.CHATBOT_BASE_URL = window.CHATBOT_BASE_URL || "http://127.0.0.1:5000"; \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 52ffd72..6ea675c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,3 +4,14 @@ services: build: . ports: - "8080:80" + depends_on: + - chatbot + + chatbot: + build: + context: . + dockerfile: Dockerfile.chatbot + environment: + # Set GEMINI_API_KEY in your .env file (see .env.example) + GEMINI_API_KEY: ${GEMINI_API_KEY:-} + GEMINI_MODEL: ${GEMINI_MODEL:-gemini-2.5-flash} \ No newline at end of file diff --git a/docker/chatbotConfig.js b/docker/chatbotConfig.js new file mode 100644 index 0000000..3b139f7 --- /dev/null +++ b/docker/chatbotConfig.js @@ -0,0 +1,4 @@ +// Container override for chatbotConfig.js — the browser must use the same +// origin as the nginx proxy (which forwards /chat and /explain_mistake to the +// chatbot container), NOT the client's own 127.0.0.1:5000. +window.CHATBOT_BASE_URL = window.CHATBOT_BASE_URL || ""; \ No newline at end of file diff --git a/home.html b/home.html index 529d26b..e70d9ae 100644 --- a/home.html +++ b/home.html @@ -400,6 +400,8 @@

🤖 Ask Your AI Tutor

+ + diff --git a/home.js b/home.js index 8f8a15e..4079a2b 100644 --- a/home.js +++ b/home.js @@ -306,41 +306,31 @@ function triggerMistakeExplanation(payload) { chatBox.appendChild(typingEl); chatBox.scrollTop = chatBox.scrollHeight; - fetch("http://127.0.0.1:5000/explain_mistake", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - question: question, - learnerAnswer: userAnswer, - correctAnswer: correctAnswer, - topic: topicId || "general" - }), - }) - .then((response) => { - if (!response.ok) { - throw new Error(`Server error: ${response.status}`); - } - return response.json(); + window.chatbotPost("/explain_mistake", { + question: question, + learnerAnswer: userAnswer, + correctAnswer: correctAnswer, + topic: topicId || "general", }) .then((data) => { - const typing = document.getElementById("typing-indicator"); - if (typing) typing.remove(); + const typing = document.getElementById("typing-indicator"); + if (typing) typing.remove(); - const reply = data.reply || "Sorry, I couldn't generate an explanation."; - appendMessage("Bot", reply, "bot-msg"); + const reply = data.reply || "Sorry, I couldn't generate an explanation."; + appendMessage("Bot", reply, "bot-msg"); - // Save last explanation per topic in localStorage for quick access - localStorage.setItem(`learnsphere_last_explanation_${topicId || 'general'}`, reply); + // Save last explanation per topic in localStorage for quick access + localStorage.setItem(`learnsphere_last_explanation_${topicId || 'general'}`, reply); }) .catch((error) => { - console.error("Explain mistake error:", error); - const typing = document.getElementById("typing-indicator"); - if (typing) typing.remove(); - appendMessage( - "Bot", - "⚠️ Unable to connect to the AI tutor. Please ensure the backend server is running.", - "bot-msg error-msg" - ); + console.error("Explain mistake error:", error); + const typing = document.getElementById("typing-indicator"); + if (typing) typing.remove(); + appendMessage( + "Bot", + "⚠️ Unable to connect to the AI tutor. Please ensure the backend server is running.", + "bot-msg error-msg" + ); }); } @@ -367,17 +357,7 @@ function sendMessage() { chatBox.appendChild(typingEl); chatBox.scrollTop = chatBox.scrollHeight; - fetch("http://127.0.0.1:5000/chat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ message: userInput }), - }) - .then((response) => { - if (!response.ok) { - throw new Error(`Server error: ${response.status}`); - } - return response.json(); - }) + window.chatbotPost("/chat", { message: userInput }) .then((data) => { const typing = document.getElementById("typing-indicator"); if (typing) typing.remove(); diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..502f3ca --- /dev/null +++ b/nginx.conf @@ -0,0 +1,22 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # Proxy AI Tutor API calls to the chatbot container (same-origin for the browser). + location = /chat { + proxy_pass http://chatbot:5000/chat; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + } + + location = /explain_mistake { + proxy_pass http://chatbot:5000/explain_mistake; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + } +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7a648da..ab9afc3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flask>=3.1.3 flask-cors>=4.0.0 -google-generativeai>=0.8.6 +google-genai>=1.0.0 python-dotenv>=1.0.0 diff --git a/review.html b/review.html index 7df51a0..d352de3 100644 --- a/review.html +++ b/review.html @@ -468,6 +468,8 @@

+ + diff --git a/review.js b/review.js index e8739dc..95b6cfd 100644 --- a/review.js +++ b/review.js @@ -750,12 +750,9 @@ askBtn.className = "action-btn"; askBtn.style.marginTop = "6px"; askBtn.onclick = () => { - fetch("/chat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ message: `Help me understand this question: ${item.q}` }) + window.chatbotPost("/chat", { + message: `Help me understand this question: ${item.q}` }) - .then(res => res.json()) .then(data => { alert(data.reply || "No reply"); }) diff --git a/review_mistakes.html b/review_mistakes.html index d959adf..4bd2886 100644 --- a/review_mistakes.html +++ b/review_mistakes.html @@ -276,6 +276,8 @@

Missed questions

+ + diff --git a/templates/index.html b/templates/index.html index e046bc1..cb978c4 100644 --- a/templates/index.html +++ b/templates/index.html @@ -10,38 +10,78 @@

Chatbot

- +
- - + +
- + \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index bc04200..ed1b055 100644 --- a/vite.config.js +++ b/vite.config.js @@ -45,5 +45,11 @@ export default defineConfig({ }, server: { open: true, + proxy: { + // Forward AI Tutor calls to the Flask backend during development, + // so a CHATBOT_BASE_URL of "" (same-origin) also works with `npm run dev`. + '/chat': 'http://127.0.0.1:5000', + '/explain_mistake': 'http://127.0.0.1:5000', + }, }, }); \ No newline at end of file