From 8630dcd96d3e39c269348a4cd143e0ea4b87df19 Mon Sep 17 00:00:00 2001 From: Techhackontime999 Date: Fri, 11 Sep 2026 22:33:15 +0530 Subject: [PATCH 1/2] fix(chatbot): repair AI Tutor so it actually works MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Load .env via python-dotenv so 'cp .env.example .env' works (was exiting at startup) - Default to a current Gemini model (gemini-2.5-flash); document GEMINI_MODEL - Centralize the backend URL (chatbotConfig.js/chatbotApi.js) and use it from home.js and review.js — fixes hardcoded 127.0.0.1 URLs and the relative /chat 404 - Add a Vite dev proxy and nginx proxy for /chat and /explain_mistake - Add the Flask chatbot service to Docker (Dockerfile.chatbot + docker-compose) - Harden templates/index.html: response.ok handling, textContent, no onkeypress - Bind Flask to 0.0.0.0 so containers / remote hosts can reach it Closes #266 --- .env.example | 7 +++- Dockerfile | 4 ++ Dockerfile.chatbot | 13 ++++++ chatbot.py | 10 ++++- chatbotApi.js | 18 +++++++++ chatbotConfig.js | 6 +++ docker-compose.yml | 11 ++++++ docker/chatbotConfig.js | 4 ++ home.html | 2 + home.js | 60 ++++++++++------------------ nginx.conf | 22 +++++++++++ review.html | 2 + review.js | 7 +--- review_mistakes.html | 2 + templates/index.html | 88 ++++++++++++++++++++++++++++++----------- vite.config.js | 6 +++ 16 files changed, 190 insertions(+), 72 deletions(-) create mode 100644 Dockerfile.chatbot create mode 100644 chatbotApi.js create mode 100644 chatbotConfig.js create mode 100644 docker/chatbotConfig.js create mode 100644 nginx.conf 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..053f060 100644 --- a/chatbot.py +++ b/chatbot.py @@ -5,8 +5,13 @@ from flask import Flask, request, jsonify, render_template from flask_cors import CORS +from dotenv import load_dotenv import google.generativeai as 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 # ----------------------------- @@ -27,7 +32,8 @@ CORS(app) # Create the model once at startup (cheaper than per request) -MODEL_NAME = os.environ.get("GEMINI_MODEL", "gemini-1.5-pro") +# 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") model = genai.GenerativeModel(MODEL_NAME) @@ -243,5 +249,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/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 From 132da34e809896313d7c78135a6fd723e175ae4a Mon Sep 17 00:00:00 2001 From: Techhackontime999 Date: Fri, 11 Sep 2026 23:59:06 +0530 Subject: [PATCH 2/2] fix(chatbot): use google-genai REST SDK (no gRPC) google-generativeai depends on grpcio, whose native DLL is blocked by Windows Application Control (WDAC) policies on some systems (verified: 'DLL load failed ... Application Control policy has blocked this file'). google-genai talks to Gemini over HTTPS (httpx) with no gRPC dependency, so the AI Tutor runs on those machines too. It is also the SDK Google recommends going forward and supports current models like gemini-3.5-flash. --- chatbot.py | 46 +++++++++++++++++++--------------------------- requirements.txt | 2 +- 2 files changed, 20 insertions(+), 28 deletions(-) diff --git a/chatbot.py b/chatbot.py index 053f060..742dabf 100644 --- a/chatbot.py +++ b/chatbot.py @@ -6,7 +6,7 @@ from flask import Flask, request, jsonify, render_template from flask_cors import CORS from dotenv import load_dotenv -import google.generativeai as genai +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. @@ -23,18 +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) +# 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") -model = genai.GenerativeModel(MODEL_NAME) # ----------------------------- @@ -81,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. @@ -161,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)}) @@ -225,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)}) 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