Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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;"]
13 changes: 13 additions & 0 deletions Dockerfile.chatbot
Original file line number Diff line number Diff line change
@@ -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"]
56 changes: 27 additions & 29 deletions chatbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")


# -----------------------------
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)})

Expand Down Expand Up @@ -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)})

Expand All @@ -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)

18 changes: 18 additions & 0 deletions chatbotApi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// chatbotApi.js — shared AI Tutor API helper used by home.js and review.js.
// Posts JSON to <CHATBOT_BASE_URL>/<path> 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;
};
6 changes: 6 additions & 0 deletions chatbotConfig.js
Original file line number Diff line number Diff line change
@@ -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";
11 changes: 11 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
4 changes: 4 additions & 0 deletions docker/chatbotConfig.js
Original file line number Diff line number Diff line change
@@ -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 || "";
2 changes: 2 additions & 0 deletions home.html
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,8 @@ <h2 id="chatbot-heading">🤖 Ask Your AI Tutor</h2>
</div>

<script src="progress.js"></script>
<script src="chatbotConfig.js"></script>
<script src="chatbotApi.js"></script>
<script src="review.js"></script>
<script src="quizProgress.js"></script>
<script src="badges.js"></script>
Expand Down
60 changes: 20 additions & 40 deletions home.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
});
}

Expand All @@ -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();
Expand Down
22 changes: 22 additions & 0 deletions nginx.conf
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions review.html
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,8 @@ <h2 id="reviewModalTitle" style="margin:0; font-size:1.3rem; color:var(--text-co

<!-- Scripts -->
<script src="progress.js"></script>
<script src="chatbotConfig.js"></script>
<script src="chatbotApi.js"></script>
<script src="review.js"></script>
<script src="navbar.js"></script>
<script src="accessibility.js" defer></script>
Expand Down
7 changes: 2 additions & 5 deletions review.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
})
Expand Down
2 changes: 2 additions & 0 deletions review_mistakes.html
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ <h2>Missed questions</h2>

<script src="progress.js"></script>
<script src="quizProgress.js"></script>
<script src="chatbotConfig.js"></script>
<script src="chatbotApi.js"></script>
<script src="review.js"></script>
<script src="review_mistakes.js"></script>
<script src="navbar.js"></script>
Expand Down
Loading