Skip to content
Open
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
64 changes: 58 additions & 6 deletions flask_app/app.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
from flask import Flask, request, jsonify
from flask_cors import CORS
from flask_app.chatbot import vectorize_and_store, get_response
import datetime

from flask_app.add_sample_users import insert_sample_users

app = Flask(__name__)
CORS(app, origins="*", methods=["GET", "POST", "OPTIONS"], allow_headers=["Content-Type", "Authorization"])


FILE_TYPES = [
"application/pdf", # PDF
Expand All @@ -17,40 +23,86 @@ def root():
})


@app.route("/upload", methods=["POST"])
@app.route("/api/upload", methods=["POST"])
def upload_file():
"""Handles user file uploads."""
file = request.files.get("file")
# course = request.json.get("course") # could be form or json dependng on how frontend sends request
# TODO: add other useful file metadata
course = request.form.get("course")

if not file:
return jsonify({"error": "No file provided"}), 400

if not course:
return jsonify({"error": "No course provided"}), 400

if file.content_type not in FILE_TYPES:
return jsonify({"error": "Unsupported file type"}), 400

# Process the file and store in ChromaDB (for chatbot)
vectorize_and_store(file)
vectorize_and_store(file, course)

# TODO: insert to database


return jsonify({"message": "File uploaded & processed successfully."})


@app.route("/chat", methods=["POST"])
@app.route("/api/chat", methods=["POST"])
def generate_response():
"""Generate a response to user queries."""
query = request.json.get("query")
course = request.json.get("course")

if not query:
return jsonify({"error": "No query provided"}), 400

response = get_response(query)
response = get_response(query, course)

return jsonify({'response': response})


@app.route('/api/users', methods=['POST'])
def create_user():
# Get the JSON data from request
user_data = request.get_json()

# Validate required fields
required_fields = ['googleUid', 'email']
for field in required_fields:
if not user_data.get(field):
return jsonify({
"error": f"Missing required field: {field}"
}), 400

try:
# Format the user data for MongoDB
user_to_insert = [{
"googleUid": user_data.get('googleUid'),
"email": user_data.get('email'),
"displayName": user_data.get('displayName'),
"photoURL": user_data.get('photoURL'),
"createdAt": datetime.datetime.now(datetime.timezone.utc),
"lastLoginAt": datetime.datetime.now(datetime.timezone.utc)
}]

# Insert the user into MongoDB
inserted_ids = insert_sample_users(user_to_insert, "User")

if not inserted_ids:
return jsonify({
"error": "Failed to create user in database"
}), 500

return jsonify({
"message": "User created successfully",
"userId": str(inserted_ids[0])
}), 201

except Exception as e:
return jsonify({
"error": f"Server error: {str(e)}"
}), 500


if __name__ == '__main__':
app.run(debug=True)
20 changes: 10 additions & 10 deletions flask_app/chatbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,22 @@
HISTORY = [] # Store conversation history in-memory for the purpose of hackathon


def vectorize_and_store(file):
def vectorize_and_store(file, course):
text = extract_text(file)
embedding = get_embedding(text)
documents.add(ids=str(uuid.uuid4()), documents=text, embeddings=embedding)
documents.add(ids=str(uuid.uuid4()), documents=text, embeddings=embedding, metadatas={"course": course})


def get_embedding(text):
return embedding_model.encode(text)
return embedding_model.encode(text).tolist()


def query_documents(query_embedding, top_k=3):
results = documents.query(query_embeddings=query_embedding, n_results=top_k)
def query_documents(query_embedding, course, top_k=3):
results = documents.query(query_embeddings=query_embedding, n_results=top_k, where={"course": course})
return results["documents"], results["metadatas"]


def get_prompt(query, context):
def get_prompt(query, course, context):
prompt = f"""
Conversation History:
{"\n".join([f"User: {exchange['user']}\nBot: {exchange['bot']}" for exchange in HISTORY])}
Expand All @@ -42,7 +42,7 @@ def get_prompt(query, context):
{context}

Instructions:
You are an AI assistant designed to answer user questions based on the provided context. Focus on providing a clear and relevant answer based on the context and the conversation history if applicable. Do not mention the process of retrieving or accessing data, and keep the conversation natural and focused on the user's needs. If a piece of information is missing, simply state that you don't know it, but avoid referencing how or why you lack that information.
You are an AI assistant designed to answer user questions about their {course} course, based on the provided context. Focus on providing a clear and relevant answer based on the context and the conversation history if applicable. Do not mention the process of retrieving or accessing data, and keep the conversation natural and focused on the user's needs. If a piece of information is missing, simply state that you don't know it, but avoid referencing how or why you lack that information.

Answer:
"""
Expand All @@ -51,13 +51,13 @@ def get_prompt(query, context):



def get_response(query):
def get_response(query, course):
query_embedding = get_embedding(query)
documents, metadatas = query_documents(query_embedding)
documents, _ = query_documents(query_embedding, course)
context = "\n".join([doc for doc in documents[0]])

model = genai.GenerativeModel(model_name='gemini-2.0-flash')
response = model.generate_content(get_prompt(query, context))
response = model.generate_content(get_prompt(query, course, context))

HISTORY.append({"user": query, "bot": response})

Expand Down
4 changes: 3 additions & 1 deletion flask_app/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ google-generativeai
chromadb
sentence-transformers
uuid
pdfplumber
pdfplumber
flask-cors
pymongo
1 change: 1 addition & 0 deletions front_end/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ docs/_build/
.env
.DS_Store
privateinfobutidc.json
node_modules/

chroma_db
36 changes: 36 additions & 0 deletions front_end/src/api/api.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export const fetchAssistantResponse = async (userMessage: string) => {
const response = await fetch('http://127.0.0.1:5000/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: userMessage, course: "course2" }),
});

if (!response.ok) {
throw new Error('Failed to fetch AI response');
}

const data = await response.json();
return data;
};


export const insertUser = async (userData: object) => {
const response = await fetch('http://127.0.0.1:5000/api/users', {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(userData)
});

if (!response.ok) {
throw new Error('Failed to insert user to database');
}

const data = await response.json();
return data;

};
4 changes: 1 addition & 3 deletions front_end/src/components/AssessmentSection.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import React, { useState } from 'react';
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
Expand Down Expand Up @@ -61,8 +60,7 @@ const AssessmentSection: React.FC = () => {

const handleUploadFile = (file: File) => {
console.log("Assessment file uploaded:", file.name);
toast({
title: "File Uploaded",
toast.success("File Uploaded", {
description: `${file.name} has been uploaded successfully.`,
duration: 3000,
});
Expand Down
22 changes: 10 additions & 12 deletions front_end/src/components/ChatSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Send } from 'lucide-react';
import { fetchAssistantResponse } from '@/api/api';

interface Message {
id: string;
Expand Down Expand Up @@ -43,7 +44,7 @@ const ChatSection: React.FC = () => {
}
};

const handleSendMessage = () => {
const handleSendMessage = async () => {
if (inputValue.trim() === '') return;

// Add user message
Expand All @@ -57,17 +58,14 @@ const ChatSection: React.FC = () => {
setMessages(prev => [...prev, userMessage]);
setInputValue('');

// Simulate assistant response
setTimeout(() => {
const assistantMessage: Message = {
id: (Date.now() + 1).toString(),
text: "I've received your message. In a real implementation, this would connect to an AI assistant to provide relevant responses about your course materials.",
sender: 'assistant',
timestamp: new Date()
};

setMessages(prev => [...prev, assistantMessage]);
}, 1000);
const { response } = await fetchAssistantResponse(inputValue);
const assistantMessage: Message = {
id: (Date.now() + 1).toString(),
text: response,
sender: 'assistant',
timestamp: new Date()
};
setMessages(prev => [...prev, assistantMessage]);
};

const formatTime = (date: Date) => {
Expand Down
29 changes: 27 additions & 2 deletions front_end/src/components/CourseSettingsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import React, { useState, useEffect } from 'react';
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
Expand Down Expand Up @@ -36,6 +35,7 @@ const CourseSettingsModal: React.FC<CourseSettingsModalProps> = ({
const [courseName, setCourseName] = useState('');
const [instructor, setInstructor] = useState('');
const [syllabus, setSyllabus] = useState<File | null>(null);
const [showRemoveConfirmation, setShowRemoveConfirmation] = useState(false);

// Parse current schedule and initialize form values
useEffect(() => {
Expand Down Expand Up @@ -121,6 +121,12 @@ const CourseSettingsModal: React.FC<CourseSettingsModalProps> = ({
onClose();
};

const handleConfirmRemove = () => {
onRemoveCourse();
setShowRemoveConfirmation(false);
onClose();
};

return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[800px] sm:max-h-[90vh]">
Expand Down Expand Up @@ -182,7 +188,7 @@ const CourseSettingsModal: React.FC<CourseSettingsModalProps> = ({
<h3 className="text-sm font-medium text-destructive mb-2">Danger Zone</h3>
<Button
variant="destructive"
onClick={onRemoveCourse}
onClick={() => setShowRemoveConfirmation(true)}
className="w-full justify-center"
>
<Trash2 className="h-4 w-4 mr-2" />
Expand Down Expand Up @@ -211,6 +217,25 @@ const CourseSettingsModal: React.FC<CourseSettingsModalProps> = ({
<Button onClick={handleSave}>Save Changes</Button>
</DialogFooter>
</DialogContent>

{/* Confirmation Dialog */}
<Dialog open={showRemoveConfirmation} onOpenChange={setShowRemoveConfirmation}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Confirm Removal</DialogTitle>
</DialogHeader>
<div className="py-4">
<p>Are you sure you want to remove "{courseName}"? This action cannot be undone.</p>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowRemoveConfirmation(false)}>Cancel</Button>
<Button variant="destructive" onClick={handleConfirmRemove}>
<Trash2 className="h-4 w-4 mr-2" />
Remove Course
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Dialog>
);
};
Expand Down
34 changes: 31 additions & 3 deletions front_end/src/lib/auth-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
onAuthStateChanged,
User
} from 'firebase/auth';
import { insertUser } from '@/api/api'
import { auth } from './firebase';

interface AuthContextType {
Expand All @@ -30,10 +31,37 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
return () => unsubscribe();
}, []);

const signInWithGoogle = async () => {
const signInWithGoogle = async () => {
const provider = new GoogleAuthProvider();
try {
await signInWithPopup(auth, provider);
const result = await signInWithPopup(auth, provider);
const user = result.user;

// Create the user data object
const userData = {
googleUid: user.uid,
email: user.email,
displayName: user.displayName,
photoURL: user.photoURL,
};

// Send POST request to Flask backend API
try {
const data = await insertUser(userData);

if (data.userId) {
console.log('User created successfully:', data);
// You can store the userId in local storage or state if needed
localStorage.setItem('userId', data.userId);
} else {
throw new Error('Invalid response from server');
}

} catch (error: any) {
console.error('Error creating user:', error.data?.error || error.message);
throw new Error('Failed to create user in database');
}

} catch (error) {
console.error('Error signing in with Google:', error);
throw error;
Expand Down Expand Up @@ -69,4 +97,4 @@ export function useAuth() {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
}