diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..dbcd68c --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +# Environment variables template +# Copy this file to .env and fill in your actual values + +# MongoDB Connection +MONGO_URI=mongodb+srv://username:password@cluster.mongodb.net/database?retryWrites=true&w=majority + +# Google Gemini API +GEMINI_API_KEY=your_gemini_api_key_here + +# Node.js Environment +NODE_ENV=production + +# Next.js Environment +NEXT_PUBLIC_API_URL=http://localhost:8001 +NEXT_PUBLIC_FASTAPI_URL=http://localhost:8000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d91443 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.vscode +.idx +.github +.env +. \ No newline at end of file diff --git a/.vercelignore b/.vercelignore new file mode 100644 index 0000000..83e7546 --- /dev/null +++ b/.vercelignore @@ -0,0 +1,17 @@ +# Exclude other services from Vercel deployment +server/ +FastAPI/ +.github/ +docker-compose.yml +**/Dockerfile +**/.dockerignore +railway.yml + +# Standard ignores +node_modules +.next +.env.local +.env.development.local +.env.test.local +.env.production.local +*.log diff --git a/FastAPI/.dockerignore b/FastAPI/.dockerignore new file mode 100644 index 0000000..e990661 --- /dev/null +++ b/FastAPI/.dockerignore @@ -0,0 +1,16 @@ +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +.git +.gitignore +README.md +Dockerfile +.dockerignore +.env +.pytest_cache +.coverage +htmlcov/ +.venv +venv/ diff --git a/FastAPI/.gitignore b/FastAPI/.gitignore new file mode 100644 index 0000000..577cfe9 --- /dev/null +++ b/FastAPI/.gitignore @@ -0,0 +1,3 @@ +.env +client_secret.json +token.json \ No newline at end of file diff --git a/FastAPI/Dockerfile b/FastAPI/Dockerfile new file mode 100644 index 0000000..ccf0b8d --- /dev/null +++ b/FastAPI/Dockerfile @@ -0,0 +1,34 @@ +# Use Python 3.11 slim image +FROM python:3.11-slim + +# Set working directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + g++ \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first for better caching +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Create non-root user for security +RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app +USER appuser + +# Expose port +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +# Run the application +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/FastAPI/README.md b/FastAPI/README.md new file mode 100644 index 0000000..21dcc56 --- /dev/null +++ b/FastAPI/README.md @@ -0,0 +1,78 @@ +# Doubt Solver Agent + + +## Setup Instructions + +1. **Clone the repository:** + ```bash + git clone + cd + ``` + +2. **Install dependencies:** + ```bash + pip install -r requirements.txt + ``` + +3. **Create a `.env` file in the root directory and add your Gemini API key:** + ``` + GEMINI_API_KEY=your_api_key_here + MONGO_URI = "mongodb://localhost:27017" + ``` +4. **Paste Google Oauth2 file as `client_secret.json` and `token.json` in the root directory (Required while Google Meet Creation)** + +5. **Run the application locally:** + ```bash + uvicorn main:app --reload --port 5000 + ``` + +6. **Access the application at** `http://localhost:5000` + +## Project Structure + +``` +. +├── README.md +├── requirements.txt +├── .env +├── main.py +├── src/ +│ ├── db.py +│ ├── chat.py +│ ├── crud.py +│ ├── meet.py +│ └── models.py +``` + +## API Endpoints + +- `POST /api/query`: Submit a question to the tutoring system + - Request body: `{"query": "your question here"}` + - Response: `{"response": "system's response"}` + +## MongoDB Initialization + +To use this application, you must have MongoDB running locally. Follow these steps to ensure MongoDB is set up correctly: + +1. **Install MongoDB** + - Download and install MongoDB Community Edition from: https://www.mongodb.com/try/download/community + - Follow the installation instructions for your operating system. + +2. **Start the MongoDB server** + - On most systems, you can start MongoDB with: + ```bash + mongod + ``` + - By default, MongoDB will run on `mongodb://localhost:27017`. + +3. **Automatic Collection Creation** + - When you start the FastAPI server, the application will automatically check for and create the following collections if they do not exist: + - `teachers` + - `students` + - `chats` + - This logic is handled in `src/db.py`. + +4. **Verify Connection** + - Ensure your MongoDB server is running **before** starting the FastAPI server. If MongoDB is not running, the application will not be able to connect to the database. + +If you need to change the MongoDB connection URI, update the `MONGO_DETAILS` variable in `src/db.py`. \ No newline at end of file diff --git a/FastAPI/main.py b/FastAPI/main.py new file mode 100644 index 0000000..1fd0d75 --- /dev/null +++ b/FastAPI/main.py @@ -0,0 +1,89 @@ +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from typing import List +from src.models import TeacherModel, StudentModel, ChatModel, ChatEntry +from src.crud import ( + get_all_teachers, + get_all_students, + get_chat_by_roll, + update_chat_by_roll, + add_chat_entry +) +from pydantic import BaseModel +from src.chat import DoubtAgent +from dotenv import load_dotenv +load_dotenv() +import os + +GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") +app = FastAPI() + +# Health check endpoint +@app.get("/health") +async def health_check(): + return {"status": "healthy", "service": "NeuroGrade FastAPI"} + +# Add CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:3000", "http://127.0.0.1:3000"], # Add your frontend URL + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +@app.get("/") +def read_root(): + return {"message": "NeuroGrade Chat API is running"} + +@app.get("/teachers", response_model=List[TeacherModel]) +def read_teachers(): + return get_all_teachers() + +@app.get("/students", response_model=List[StudentModel]) +def read_students(): + return get_all_students() + +@app.get("/chats/{stud_roll}", response_model=ChatModel) +def read_chat(stud_roll: str): + chat = get_chat_by_roll(stud_roll) + if chat: + return chat + raise HTTPException(status_code=404, detail="Chat not found") + +@app.put("/chats/{stud_roll}", response_model=bool) +def update_chat(stud_roll: str, chats: List[ChatEntry]): + updated = update_chat_by_roll(stud_roll, chats) + if updated: + return True + raise HTTPException(status_code=404, detail="Chat not found or not updated") + +# Request model for agent endpoint +class AgentRequest(BaseModel): + query: str + student_roll: str + +@app.post("/agent/ask") +def agent_ask(request: AgentRequest): + # 1. Get history + chat_history_doc = get_chat_by_roll(request.student_roll) + chat_history = chat_history_doc.chats if chat_history_doc else [] + + # 2. Call agent with history + agent = DoubtAgent( + api_key=GEMINI_API_KEY, + student_roll=request.student_roll, + chat_history=chat_history + ) + response = agent.get_response(request.query) + + # 3. Store new entry + add_chat_entry(stud_roll=request.student_roll, query=request.query, answer=response) + + return {"response": response} + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=5000) + + diff --git a/FastAPI/requirements.txt b/FastAPI/requirements.txt new file mode 100644 index 0000000..6d966b3 --- /dev/null +++ b/FastAPI/requirements.txt @@ -0,0 +1,12 @@ +fastapi==0.115.14 +google_api_python_client==2.172.0 +google_auth_oauthlib==1.2.2 +langchain_core==0.3.68 +langchain_google_genai==2.1.6 +langgraph==0.5.1 +protobuf==6.31.1 +pydantic==2.11.7 +pymongo==4.13.2 +python-dotenv==1.1.1 +typing_extensions==4.14.1 +uvicorn==0.32.1 diff --git a/FastAPI/src/__init__.py b/FastAPI/src/__init__.py new file mode 100644 index 0000000..2216fbf --- /dev/null +++ b/FastAPI/src/__init__.py @@ -0,0 +1,3 @@ +""" +Multi-agent tutoring system agents package. +""" \ No newline at end of file diff --git a/FastAPI/src/__pycache__/__init__.cpython-310.pyc b/FastAPI/src/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..a2e2b70 Binary files /dev/null and b/FastAPI/src/__pycache__/__init__.cpython-310.pyc differ diff --git a/FastAPI/src/__pycache__/__init__.cpython-312.pyc b/FastAPI/src/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..e10355d Binary files /dev/null and b/FastAPI/src/__pycache__/__init__.cpython-312.pyc differ diff --git a/FastAPI/src/__pycache__/chat.cpython-310.pyc b/FastAPI/src/__pycache__/chat.cpython-310.pyc new file mode 100644 index 0000000..0e30f5d Binary files /dev/null and b/FastAPI/src/__pycache__/chat.cpython-310.pyc differ diff --git a/FastAPI/src/__pycache__/crud.cpython-310.pyc b/FastAPI/src/__pycache__/crud.cpython-310.pyc new file mode 100644 index 0000000..c5434f9 Binary files /dev/null and b/FastAPI/src/__pycache__/crud.cpython-310.pyc differ diff --git a/FastAPI/src/__pycache__/crud.cpython-312.pyc b/FastAPI/src/__pycache__/crud.cpython-312.pyc new file mode 100644 index 0000000..28f2fa1 Binary files /dev/null and b/FastAPI/src/__pycache__/crud.cpython-312.pyc differ diff --git a/FastAPI/src/__pycache__/db.cpython-310.pyc b/FastAPI/src/__pycache__/db.cpython-310.pyc new file mode 100644 index 0000000..44bd73d Binary files /dev/null and b/FastAPI/src/__pycache__/db.cpython-310.pyc differ diff --git a/FastAPI/src/__pycache__/db.cpython-312.pyc b/FastAPI/src/__pycache__/db.cpython-312.pyc new file mode 100644 index 0000000..df08f20 Binary files /dev/null and b/FastAPI/src/__pycache__/db.cpython-312.pyc differ diff --git a/FastAPI/src/__pycache__/meet.cpython-310.pyc b/FastAPI/src/__pycache__/meet.cpython-310.pyc new file mode 100644 index 0000000..ec485b8 Binary files /dev/null and b/FastAPI/src/__pycache__/meet.cpython-310.pyc differ diff --git a/FastAPI/src/__pycache__/models.cpython-310.pyc b/FastAPI/src/__pycache__/models.cpython-310.pyc new file mode 100644 index 0000000..801ea8e Binary files /dev/null and b/FastAPI/src/__pycache__/models.cpython-310.pyc differ diff --git a/FastAPI/src/__pycache__/models.cpython-312.pyc b/FastAPI/src/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..fd55d73 Binary files /dev/null and b/FastAPI/src/__pycache__/models.cpython-312.pyc differ diff --git a/FastAPI/src/chat.py b/FastAPI/src/chat.py new file mode 100644 index 0000000..22a7588 --- /dev/null +++ b/FastAPI/src/chat.py @@ -0,0 +1,200 @@ +from langchain_google_genai import ChatGoogleGenerativeAI +from .meet import create_meet_event +from .crud import get_all_subjects, get_teacher_by_subject, get_student_by_roll +from .models import ChatEntry +from typing import List +from langchain_core.messages import HumanMessage, AIMessage +from datetime import datetime + +# Debug: Print available subjects +try: + available_subjects = get_all_subjects() + print(f"[DEBUG] Available subjects from database: {available_subjects}") +except Exception as e: + print(f"[DEBUG] Error getting subjects: {e}") + available_subjects = [] + +# --- LANGGRAPH IMPORTS --- +from typing import Annotated +from typing_extensions import TypedDict +from langgraph.graph import StateGraph, END +from langgraph.graph.message import add_messages +from langchain_core.messages import BaseMessage, ToolMessage +from pydantic import BaseModel, Field +from langchain_core.tools import tool + +# --- STATE DEFINITION --- +class AgentState(TypedDict): + messages: Annotated[list, add_messages] + +class ScheduleMeetingArgs(BaseModel): + subject: str = Field(description="The subject for which to schedule the meeting") + student_roll: str = Field(description="The roll number of the student") + start_time: str = Field(description="The start time for the meeting (optional, format: YYYY-MM-DD HH:MM)", default=None) + end_time: str = Field(description="The end time for the meeting (optional, format: YYYY-MM-DD HH:MM)", default=None) + + +description = ( + "Use this tool to schedule a Google Meet between a student and a teacher. " + "If a student asks to schedule, arrange, or set up a meeting with a teacher for any subject, " + "you MUST use this tool. The tool requires the subject (from the list: {}). So enter the name of the subject as a string that's it." + "It will automatically find the correct teacher and student, and return a Google Meet link. " + "Always use this tool for any scheduling or meeting requests between students and teachers. " + "If you do not have enough information, ask the user for the subject, If the subject is not in the list, strictly say that the subject is not taught by any teacher. " + "Optional: If the student provides start_time and end_time, include them in the format YYYY-MM-DD HH:MM." +).format(", ".join(available_subjects) if available_subjects else "No subjects available") + +@tool(args_schema=ScheduleMeetingArgs,description=description) +def schedule_meeting(subject: str, student_roll: str, start_time: str = None, end_time: str = None) -> str: + """Schedule a Google Meet between a student and a teacher. + + Args: + subject: The subject for which to schedule the meeting. + student_roll: The roll number of the student. + start_time: Optional start time for the meeting (format: YYYY-MM-DD HH:MM). + end_time: Optional end time for the meeting (format: YYYY-MM-DD HH:MM). + """ + print(f"[schedule_meeting_tool] Called with subject: {subject}, student_roll: {student_roll}, start_time: {start_time}, end_time: {end_time}") + + teacher = get_teacher_by_subject(subject) + # print(f"[schedule_meeting_tool] Fetched teacher: {teacher}") + if not teacher: + return f"Could not find a teacher for the subject: {subject}" + + student = get_student_by_roll(student_roll) + # print(f"[schedule_meeting_tool] Fetched student: {student}") + if not student: + return f"Could not find a student with roll number: {student_roll}" + + attendee_emails = [teacher.email, student.email] + # print(f"[schedule_meeting_tool] Attendee emails: {attendee_emails}") + + # Prepare meeting parameters + meeting_params = {"attendee_emails": attendee_emails} + + # Convert string dates to datetime objects if provided + if start_time: + try: + start_datetime = datetime.strptime(start_time, "%Y-%m-%d %H:%M") + meeting_params["start_time"] = start_datetime + except ValueError as e: + return f"Invalid start_time format. Please use YYYY-MM-DD HH:MM format. Error: {e}" + + if end_time: + try: + end_datetime = datetime.strptime(end_time, "%Y-%m-%d %H:%M") + meeting_params["end_time"] = end_datetime + except ValueError as e: + return f"Invalid end_time format. Please use YYYY-MM-DD HH:MM format. Error: {e}" + + try: + meeting_details = create_meet_event(**meeting_params) + print(f"[schedule_meeting_tool] Meeting details: {meeting_details}") + return ( + f"A meeting has been successfully scheduled. The Google Meet link is: {meeting_details.get('hangoutLink')}. " + "You should now provide this link to the user as the final answer." + ) + except Exception as e: + print(f"[schedule_meeting_tool] Exception: {e}") + return f"Failed to create meeting: {e}" + +# --- DOUBT AGENT WITH LANGGRAPH --- +class DoubtAgent: + def __init__(self, api_key: str, student_roll: str, chat_history: List[ChatEntry]): + self.model = ChatGoogleGenerativeAI(api_key=api_key, model='models/gemini-1.5-flash') + self.student_roll = student_roll + self.chat_history = chat_history + + + self.tools = [schedule_meeting] + # Add a system prompt to instruct the model to act as a doubt solver + system_prompt = ( + "You are a helpful doubt solver for students. " + "Answer student questions clearly and concisely. " + "If a student asks to schedule, arrange, or set up a meeting with a teacher, use the provided tool. " + "Strictly don't reveal your identity as GEMINI" + "If the question is related to the Study You must answer it regardless whether the subject exists or not (remeber this )." + "For all other questions, answer as a knowledgeable tutor. " + "When scheduling meetings, use the student roll number provided in the conversation." + ) + self.model_with_tools = self.model.bind_tools(self.tools).with_config({"system_message": system_prompt}) + print(f"[LangGraph] Model with tools: {self.model_with_tools}") + # --- NODES --- + def llm_node(state: AgentState): + print("[LangGraph] Node: llm_node") + response = self.model_with_tools.invoke(state["messages"]) + print(f"[LangGraph] LLM response: {response}") + if hasattr(response, 'tool_calls'): + print(f"[LangGraph] LLM tool_calls: {response.tool_calls}") + return {"messages": [response]} + + def meet_tool_node(state: AgentState): + print("[LangGraph] Node: meet_tool_node") + messages = state["messages"] + last_message = messages[-1] + outputs = [] + for tool_call in getattr(last_message, "tool_calls", []): + print(f"[LangGraph] meet_tool_node tool_call: {tool_call}") + if tool_call["name"] == "schedule_meeting": + # Use .invoke() method instead of calling directly to avoid deprecation warning + result = schedule_meeting.invoke(tool_call["args"]) + print(f"[LangGraph] meet_tool_node result: {result}") + outputs.append( + ToolMessage( + content=str(result), + name=tool_call["name"], + tool_call_id=tool_call["id"] + ) + ) + # If no tool calls were processed, return a default message to avoid empty message parts + if not outputs: + print("[LangGraph] No tool calls processed, returning default message") + outputs.append(AIMessage(content="I couldn't process any tool calls. Please try again.")) + return {"messages": outputs} + + # --- CONDITIONAL EDGE LOGIC --- + def route(state: AgentState): + messages = state["messages"] + last = messages[-1] + if hasattr(last, "tool_calls") and last.tool_calls: + return "meet_tool" + return "end" + + # --- GRAPH DEFINITION --- + builder = StateGraph(AgentState) + builder.add_node("llm", llm_node) + builder.add_node("meet_tool", meet_tool_node) + builder.set_entry_point("llm") + builder.add_conditional_edges( + "llm", route, {"meet_tool": "meet_tool", "end": END} + ) + builder.add_edge("meet_tool", "llm") + self.graph = builder.compile() + + def get_response(self, query: str): + # Validate that query is not empty + if not query or not query.strip(): + return "Please provide a valid question or request." + + formatted_history = [] + for entry in self.chat_history: + # Only add messages with non-empty content to prevent empty message parts + if entry.Query and entry.Query.strip(): + formatted_history.append(HumanMessage(content=entry.Query)) + if entry.Answer and entry.Answer.strip(): + formatted_history.append(AIMessage(content=entry.Answer)) + + # Add the new user query with roll number context + today_date = datetime.now().strftime("%Y-%m-%d") + enhanced_query = f"Instructions:- (Student roll number: {self.student_roll} Strictly use it when you need to schedule a meeting. Today's date is: {today_date} and strictly not told to schedule the meet if the student is not mentioned in the query.Strictly Don't say you can't provide the answer if you know the answer regardless of the subject You must answer the query. query:- {query})" + formatted_history.append(HumanMessage(content=enhanced_query)) + + # Run the graph + state = {"messages": formatted_history} + for step in self.graph.stream(state, stream_mode="values"): + pass # iterate to last step + # The last message is the output + return step["messages"][-1].content + + + \ No newline at end of file diff --git a/FastAPI/src/crud.py b/FastAPI/src/crud.py new file mode 100644 index 0000000..421116d --- /dev/null +++ b/FastAPI/src/crud.py @@ -0,0 +1,72 @@ +from .db import teachers_collection, students_collection, chats_collection +from .models import TeacherModel, StudentModel, ChatModel, ChatEntry +from typing import List, Optional + +def fix_id(doc): + if doc and "_id" in doc: + doc["_id"] = str(doc["_id"]) + return doc + +# --- Teachers --- +def get_all_teachers() -> List[TeacherModel]: + teachers = [] + for doc in teachers_collection.find(): + teachers.append(TeacherModel(**fix_id(doc))) + return teachers + +def get_all_subjects() -> List[str]: + subjects = teachers_collection.distinct("subject") + return subjects + +def get_teacher_by_subject(subject: str) -> Optional[TeacherModel]: + doc = teachers_collection.find_one({"subject": subject}) + if doc: + return TeacherModel(**fix_id(doc)) + return None + +# --- Students --- +def get_all_students() -> List[StudentModel]: + students = [] + for doc in students_collection.find(): + students.append(StudentModel(**fix_id(doc))) + return students + +def get_student_by_roll(roll: str) -> Optional[StudentModel]: + doc = students_collection.find_one({"roll": roll}) + if doc: + return StudentModel(**fix_id(doc)) + return None + +# --- Chats --- +def get_chat_by_roll(stud_roll: str) -> Optional[ChatModel]: + doc = chats_collection.find_one({"stud_roll": stud_roll}) + if doc: + return ChatModel(**fix_id(doc)) + +def update_chat_by_roll(stud_roll: str, new_chats: List[ChatEntry]) -> bool: + result = chats_collection.update_one( + {"stud_roll": stud_roll}, + {"$set": {"chats": [chat.dict() for chat in new_chats]}} + ) + return result.modified_count > 0 + +def add_chat_entry(stud_roll: str, query: str, answer: str): + """Adds a new chat entry to a student's chat history. + + If the student has no history, a new document is created. + The history is capped at the last 10 entries. + """ + chat_entry = ChatEntry(Query=query, Answer=answer) + + chats_collection.update_one( + {"stud_roll": stud_roll}, + { + "$push": { + "chats": { + "$each": [chat_entry.model_dump()], + "$slice": -10 # Keeps the last 10 entries + } + } + }, + upsert=True # Creates the document if it doesn't exist + ) diff --git a/FastAPI/src/db.py b/FastAPI/src/db.py new file mode 100644 index 0000000..bf8b5bd --- /dev/null +++ b/FastAPI/src/db.py @@ -0,0 +1,29 @@ +from pymongo import MongoClient +from dotenv import load_dotenv +import os +import dns.resolver + +load_dotenv() + +# Configure DNS resolver to use Google DNS for better reliability +dns.resolver.default_resolver = dns.resolver.Resolver(configure=False) +dns.resolver.default_resolver.nameservers = ['8.8.8.8', '8.8.4.4'] + +MONGO_URI = os.getenv('MONGO_URI') + +client = MongoClient(MONGO_URI) +db = client.neurograde + +REQUIRED_COLLECTIONS = ["teachers", "students", "chats"] + +def ensure_collections(): + existing = db.list_collection_names() + for name in REQUIRED_COLLECTIONS: + if name not in existing: + db.create_collection(name) + +ensure_collections() + +teachers_collection = db.get_collection("teachers") +students_collection = db.get_collection("students") +chats_collection = db.get_collection("chats") \ No newline at end of file diff --git a/FastAPI/src/meet.py b/FastAPI/src/meet.py new file mode 100644 index 0000000..df832a3 --- /dev/null +++ b/FastAPI/src/meet.py @@ -0,0 +1,139 @@ +import datetime +import os.path +import uuid +from typing import Optional +from google.auth.transport.requests import Request +from google.oauth2.credentials import Credentials +from google_auth_oauthlib.flow import InstalledAppFlow +from googleapiclient.discovery import build + +# Scope: full calendar access +SCOPES = ['https://www.googleapis.com/auth/calendar'] + +# Step 1: Authorize user and store token +def get_calendar_service(): + """ + Authenticates with Google Calendar API and returns a service object. + + Returns: + googleapiclient.discovery.Resource: Authenticated Calendar service object + """ + creds = None + if os.path.exists('token.json'): + creds = Credentials.from_authorized_user_file('token.json', SCOPES) + # If no valid credentials, do OAuth flow + if not creds or not creds.valid: + if creds and creds.expired and creds.refresh_token: + creds.refresh(Request()) + else: + flow = InstalledAppFlow.from_client_secrets_file('client_secret.json', SCOPES) + creds = flow.run_local_server(port=0) + # Save token + with open('token.json', 'w') as token: + token.write(creds.to_json()) + return build('calendar', 'v3', credentials=creds) + +# Step 2: Create event with Meet link + guest invites +def create_meet_event( + attendee_emails: list[str], + start_time: Optional[datetime.datetime] = None, + end_time: Optional[datetime.datetime] = None +) -> dict: + """ + Creates a Google Calendar event with an automatically generated Google Meet link + and sends email invitations to specified attendees. + + Args: + attendee_emails (list[str]): A list of email addresses to invite to the meeting. + + start_time (Optional[datetime.datetime]): Start time of the meeting. + If not provided, defaults to current time. + Format: datetime.datetime object + Example: datetime.datetime(2025, 6, 18, 9, 0, 0) + + end_time (Optional[datetime.datetime]): End time of the meeting. + If not provided, defaults to 1 hour after start_time. + Format: datetime.datetime object + Example: datetime.datetime(2025, 6, 18, 10, 0, 0) + + Returns: + dict: A dictionary containing the created event details including: + - summary: Event title + - hangoutLink: Google Meet video conference link + - htmlLink: Google Calendar event link + - id: Unique event identifier + - start: Event start time + - end: Event end time + + Logic: + 1. Authenticates with Google Calendar API using OAuth2 + 2. Generates a unique requestId using uuid4() for the conference data + 3. Sets default start_time to current time if not provided + 4. Sets default end_time to 1 hour after start_time if not provided + 5. Creates event with Google Meet integration enabled + 6. Sends email invitations to all attendees + 7. Returns event details including the Meet link + + Example: + >>> create_meet_event(["user@example.com"]) + >>> create_meet_event(["user@example.com"], + ... start_time=datetime.datetime(2025, 6, 18, 9, 0, 0), + ... end_time=datetime.datetime(2025, 6, 18, 10, 30, 0)) + """ + print(f"[create_meet_event] Called with attendee_emails: {attendee_emails}, start_time: {start_time}, end_time: {end_time}") + service = get_calendar_service() + + # Set default start time to current time if not provided + if not start_time: + start_time = datetime.datetime.now() + + # Set default end time to 1 hour after start time if not provided + if not end_time: + end_time = start_time + datetime.timedelta(hours=1) + + # Generate unique requestId for conference data using uuid4 + unique_request_id = str(uuid.uuid4()) + + event = { + 'summary': 'Doubt solving meet', + 'description': 'Doubt solveing Meet Between Student and teacher', + 'start': { + 'dateTime': start_time.isoformat(), + 'timeZone': 'Asia/Kolkata', + }, + 'end': { + 'dateTime': end_time.isoformat(), + 'timeZone': 'Asia/Kolkata', + }, + 'attendees': [{'email': email} for email in attendee_emails], + 'conferenceData': { + 'createRequest': { + 'requestId': unique_request_id, # Unique identifier for each meeting + 'conferenceSolutionKey': {'type': 'hangoutsMeet'} + } + } + } + + created_event = service.events().insert( + calendarId='primary', + body=event, + conferenceDataVersion=1, + sendUpdates='all' # sends email invites + ).execute() + + print("Event created:") + print("Summary:", created_event['summary']) + print("Meet link:", created_event['hangoutLink']) + print("Event Link:", created_event['htmlLink']) + print(f"[create_meet_event] Event created: {created_event}") + + # Return the created event details for programmatic access + return { + 'summary': created_event['summary'], + 'hangoutLink': created_event['hangoutLink'], + 'htmlLink': created_event['htmlLink'], + 'id': created_event['id'], + 'start': created_event['start'], + 'end': created_event['end'] + } + diff --git a/FastAPI/src/models.py b/FastAPI/src/models.py new file mode 100644 index 0000000..d7b533e --- /dev/null +++ b/FastAPI/src/models.py @@ -0,0 +1,31 @@ +from pydantic import BaseModel, Field +from typing import List, Optional, Dict, Any + +class TeacherModel(BaseModel): + id: str = Field(..., alias="_id") + email: str + name: str + subject:str + students: Optional[list] = [] + salt: str + hash: str + __v: int + +class StudentModel(BaseModel): + id: str = Field(..., alias="_id") + email: str + name: str + roll: str + feedback: Optional[list] = [] + salt: str + hash: str + __v: int + +class ChatEntry(BaseModel): + Query: str + Answer: str + +class ChatModel(BaseModel): + id: str = Field(..., alias="_id") + stud_roll: str + chats: List[ChatEntry] diff --git a/FastAPI/static/style.css b/FastAPI/static/style.css new file mode 100644 index 0000000..fd0fcdc --- /dev/null +++ b/FastAPI/static/style.css @@ -0,0 +1,96 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #f0f2f5; +} + +.container { + max-width: 800px; + margin: 0 auto; + padding: 20px; +} + +h1 { + text-align: center; + color: #1a73e8; + margin-bottom: 30px; +} + +.chat-container { + background-color: white; + border-radius: 10px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + padding: 20px; + height: 600px; + display: flex; + flex-direction: column; +} + +#chat-messages { + flex-grow: 1; + overflow-y: auto; + padding: 10px; + margin-bottom: 20px; +} + +.message { + margin: 10px 0; + padding: 10px 15px; + border-radius: 15px; + max-width: 80%; + word-wrap: break-word; +} + +.user-message { + background-color: #e3f2fd; + margin-left: auto; + color: #1565c0; +} + +.tutor-message { + background-color: #f5f5f5; + margin-right: auto; + color: #333; +} + +.error-message { + background-color: #ffebee; + margin-right: auto; + color: #c62828; +} + +.input-container { + display: flex; + gap: 10px; + padding: 10px; + background-color: #f8f9fa; + border-radius: 5px; +} + +#query-input { + flex-grow: 1; + padding: 10px; + border: 1px solid #ddd; + border-radius: 5px; + font-size: 16px; +} + +button { + padding: 10px 20px; + background-color: #1a73e8; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 16px; + transition: background-color 0.2s; +} + +button:hover { + background-color: #1557b0; +} + +button:active { + background-color: #0d47a1; +} \ No newline at end of file diff --git a/client/.dockerignore b/client/.dockerignore new file mode 100644 index 0000000..0c72388 --- /dev/null +++ b/client/.dockerignore @@ -0,0 +1,12 @@ +node_modules +npm-debug.log +.next +.git +.gitignore +README.md +Dockerfile +.dockerignore +.env.local +.env.development.local +.env.test.local +.env.production.local diff --git a/client/Dockerfile b/client/Dockerfile new file mode 100644 index 0000000..2ab743d --- /dev/null +++ b/client/Dockerfile @@ -0,0 +1,50 @@ +# Multi-stage build for Next.js +FROM node:20-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Copy package files +COPY package*.json ./ +RUN npm ci --legacy-peer-deps + +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Build the application +RUN npm run build + +# Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV production + +# Create non-root user +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# Copy built application +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +# Expose port +EXPOSE 3000 + +ENV PORT 3000 +ENV HOSTNAME "0.0.0.0" + +# Health check +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000 || exit 1 + +# Run the application +CMD ["node", "server.js"] \ No newline at end of file diff --git a/client/next.config.mjs b/client/next.config.mjs index 4678774..e0bff65 100644 --- a/client/next.config.mjs +++ b/client/next.config.mjs @@ -1,4 +1,10 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + // Remove standalone output for Vercel deployment + // output: 'standalone', + experimental: { + outputFileTracingRoot: undefined, + }, +}; export default nextConfig; diff --git a/client/package-lock.json b/client/package-lock.json index 52dc38a..888f5d3 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -22,6 +22,7 @@ "next": "^15.1.6", "next-seo": "^6.6.0", "react": "^19.0.0", + "react-chat-widget": "^3.1.4", "react-dom": "^19.0.0", "react-icons": "^5.5.0" }, @@ -2133,6 +2134,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2169,11 +2182,22 @@ "version": "19.0.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz", "integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==", - "peer": true, "dependencies": { "csstype": "^3.0.2" } }, + "node_modules/@types/react-redux": { + "version": "7.1.34", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz", + "integrity": "sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==", + "license": "MIT", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.12", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", @@ -2981,6 +3005,12 @@ "node": ">= 6" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -3263,6 +3293,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -3424,6 +3470,12 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "license": "BSD-2-Clause" + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -5336,6 +5388,15 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/linkify-it": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", + "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", + "license": "MIT", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5396,6 +5457,49 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/markdown-it": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", + "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "entities": "~1.1.1", + "linkify-it": "^2.0.0", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it-link-attributes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/markdown-it-link-attributes/-/markdown-it-link-attributes-2.1.0.tgz", + "integrity": "sha512-NclmOF52k57idAZI93PREbPKbKPFyafwoJncWW9dKpkYGbO26oJGHa4bUoU27Lk8TeWLbPEzuKFyQGwuB2NbdA==", + "license": "MIT" + }, + "node_modules/markdown-it-sanitizer": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/markdown-it-sanitizer/-/markdown-it-sanitizer-0.4.3.tgz", + "integrity": "sha512-0Q2ua8+oDN7/3r5UXMnbVq8C+LRfT2pzVKA+h4nXTLEMBFQDwp7qJZOe7DkBa79C7V2cSBXJyScxJ7vYs9kE2w==", + "license": "MIT" + }, + "node_modules/markdown-it-sup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz", + "integrity": "sha512-E32m0nV9iyhRR7CrhnzL5msqic7rL1juWre6TQNxsnApg7Uf+F97JOKxUijg5YwXz86lZ0mqfOnutoryyNdntQ==", + "license": "MIT" + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5406,6 +5510,12 @@ "node": ">= 0.4" } }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6175,6 +6285,71 @@ "node": ">=0.10.0" } }, + "node_modules/react-chat-widget": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-chat-widget/-/react-chat-widget-3.1.4.tgz", + "integrity": "sha512-4mLcXCetlgW76JjdVL2IeA72z8unwMfHn1mwcxcgUP2kk8bf72EZFmaLIvQvqoNB8rrYIDX9BHiU4vRa+LGmFg==", + "license": "MIT", + "dependencies": { + "classnames": "^2.2.6", + "date-fns": "^2.11.1", + "emoji-mart": "^3.0.1", + "markdown-it": "^8.4.1", + "markdown-it-link-attributes": "^2.1.0", + "markdown-it-sanitizer": "^0.4.3", + "markdown-it-sup": "^1.0.0", + "react-redux": "^7.2.4", + "redux": "^4.1.0" + }, + "peerDependencies": { + "react": "^17.0.2", + "react-dom": "^17.0.2" + } + }, + "node_modules/react-chat-widget/node_modules/emoji-mart": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-3.0.1.tgz", + "integrity": "sha512-sxpmMKxqLvcscu6mFn9ITHeZNkGzIvD0BSNFE/LJESPbCA8s1jM6bCDPjWbV31xHq7JXaxgpHxLB54RCbBZSlg==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.0.0", + "prop-types": "^15.6.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0-0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/react-chat-widget/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT" + }, + "node_modules/react-chat-widget/node_modules/react-redux": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-dom": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", @@ -6239,6 +6414,15 @@ "node": ">=8.10.0" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -6689,6 +6873,12 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, "node_modules/stable-hash": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", @@ -7271,6 +7461,12 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "license": "MIT" + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", diff --git a/client/package.json b/client/package.json index 6f2670b..990f165 100644 --- a/client/package.json +++ b/client/package.json @@ -23,8 +23,9 @@ "lucide-react": "^0.479.0", "next": "^15.1.6", "next-seo": "^6.6.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", + "react": "^18.2.0", + "react-chat-widget": "^3.1.4", + "react-dom": "^18.2.0", "react-icons": "^5.5.0" }, "devDependencies": { diff --git a/client/src/TeamImages/Deep.png b/client/src/TeamImages/Deep.png new file mode 100644 index 0000000..cba337e Binary files /dev/null and b/client/src/TeamImages/Deep.png differ diff --git a/client/src/app/layout.js b/client/src/app/layout.js index 38a6bb3..09a89c7 100644 --- a/client/src/app/layout.js +++ b/client/src/app/layout.js @@ -3,6 +3,7 @@ import React, { useState, useEffect } from "react"; import { ChevronUp } from "lucide-react"; import { FiMessageSquare, FiMail, FiMapPin } from "react-icons/fi"; import Header from "../components/elements/Header"; +import Chatbot from "../components/Chatbot"; import "./globals.css"; import { AuthProvider, useAuth } from "@/context/AuthContext"; import Background from "@/components/Background"; @@ -230,6 +231,9 @@ function ThemedLayout({ children }) { )} + + {/* Chatbot Component */} + diff --git a/client/src/app/register/page.js b/client/src/app/register/page.js index 961fcd5..b19b706 100644 --- a/client/src/app/register/page.js +++ b/client/src/app/register/page.js @@ -10,6 +10,7 @@ export default function Register() { email: "", password: "", roll: "", + subject: "", }); const [role, setRole] = useState("teacher"); const [errors, setErrors] = useState({}); @@ -43,6 +44,9 @@ export default function Register() { if (role === "student" && !formData.roll) { newErrors.roll = "Roll number is required"; } + if (role === "teacher" && !formData.subject) { + newErrors.subject = "Subject is required"; + } return newErrors; }; @@ -70,7 +74,7 @@ export default function Register() { const payload = role === "student" ? { name: formData.name, email: formData.email, password: formData.password, roll: formData.roll } - : { name: formData.name, email: formData.email, password: formData.password }; + : { name: formData.name, email: formData.email, password: formData.password, subject: formData.subject }; try { const response = await fetch(endpoint, { @@ -183,6 +187,39 @@ export default function Register() { {errors.email &&

{errors.email}

} + {role === "teacher" && ( +
+ +
+ + {errors.subject && !} +
+ {errors.subject &&

{errors.subject}

} +
+ )} + {role === "student" && (
diff --git a/client/src/components/Chatbot.jsx b/client/src/components/Chatbot.jsx new file mode 100644 index 0000000..4cc9656 --- /dev/null +++ b/client/src/components/Chatbot.jsx @@ -0,0 +1,178 @@ +'use client'; + +import { useState, useRef, useEffect } from 'react'; +import { useUser } from '@/context/UserContext'; + +const Chatbot = () => { + const [isOpen, setIsOpen] = useState(false); + const [messages, setMessages] = useState([ + { text: "Hello! I'm your NeuroGrade assistant. How can I help you today?", sender: 'bot' } + ]); + const [inputMessage, setInputMessage] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const messagesEndRef = useRef(null); + const { user } = useUser(); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + const sendMessage = async () => { + if (!inputMessage.trim()) return; + + const userMessage = { text: inputMessage, sender: 'user' }; + setMessages(prev => [...prev, userMessage]); + setInputMessage(''); + setIsLoading(true); + + try { + // Get student roll from user context + const studentRoll = user?.roll || user?.rollNo || user?.studentRoll || "guest_user"; + + // Debug logging + console.log("User object:", user); + console.log("Student roll being sent:", studentRoll); + + const response = await fetch(`${process.env.NEXT_PUBLIC_CHAT_API_URL}/agent/ask`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ + query: inputMessage, + student_roll: studentRoll + }), + }); + + if (response.ok) { + const data = await response.json(); + const botMessage = { text: data.response || 'I apologize, but I couldn\'t process your request.', sender: 'bot' }; + setMessages(prev => [...prev, botMessage]); + } else { + throw new Error('Failed to get response'); + } + } catch (error) { + console.error('Chat error:', error); + const errorMessage = { text: 'Sorry, I\'m having trouble connecting. Please try again later.', sender: 'bot' }; + setMessages(prev => [...prev, errorMessage]); + } finally { + setIsLoading(false); + } + }; + + const handleKeyPress = (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + sendMessage(); + } + }; + + // Only show chatbot for students + if (!user || user.role === 'teacher') { + return null; + } + + return ( + <> + {/* Chat Widget Button */} +
+ +
+ + {/* Chat Window */} + {isOpen && ( +
+ {/* Header */} +
+

NeuroGrade Assistant

+ +
+ + {/* Messages */} +
+ {messages.map((message, index) => ( +
+
+

{message.text}

+
+
+ ))} + {isLoading && ( +
+
+
+
+
+
+
+
+
+ )} +
+
+ + {/* Input */} +
+
+