# Project export: engagED

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2025
- Tagline: AI-powered classroom simulation for smarter teacher training 👩‍🎓👩🏾‍🎓👨🏻‍🎓
- Devpost: https://devpost.com/software/teacher-teacher
- GitHub: https://github.com/jasmine-dragons/engaged
- Video: https://www.youtube.com/embed/4tUIGxdDW_Q?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — Sean (73 commits), aaravbajaj012 (36 commits), Nishant Balaji (27 commits), Vaddala06 (27 commits)

## Devpost submission (written by the team)

### Overview

🌲 Made for TreeHacks 2025. Devpost · GitHub

### Inspiration

Every day, teachers step into classrooms filled with students who have different personalities, learning styles, and challenges. Some students are eager to participate, others are easily distracted, and a few might resist authority altogether. A teacher's ability to navigate these interactions can mean the difference between an engaging classroom and a chaotic one. Yet, there are few opportunities for educators to practice classroom management in a realistic, risk-free setting. So, we asked ourselves: What if teachers could train for the classroom the way pilots train in flight simulators? Could we create an AI-powered environment where educators could interact with dynamic student personalities, receive real-time feedback, and refine their teaching techniques so that they can accel in the classroom? That’s how we built engagED—an interactive classroom simulation that prepares educators for real-world teaching challenges. By leveraging AI-bots that simulate student behaviors and a performance analytics dashboard, engagED helps teachers develop strong communication skills, manage classroom dynamics, and build confidence—all in a safe, controlled environment. We believe that if engagED is implemented in teacher training programs and professional development workshops, it could revolutionize how educators prepare for the modern classroom—leading to more engaged students, less burnout, and stronger learning outcomes across the board.

### What it does

engagED simulates a classroom environment where teachers interact with AI-powered student personalities, each with unique behaviors, engagement levels, and challenges. The platform allows educators to practice managing real-world classroom dynamics—whether it’s handling a disruptive student, encouraging a quiet learner to participate, or maintaining engagement during a lesson. engagED responds to natural teacher interactions in a virtual classroom setting, adjusting student behaviors based on teaching strategies. Just like in a real class, students may ask unexpected questions, lose focus, or react differently depending on the teacher’s approach. At the end of each session, engagED provides a performance dashboard, offering insights into key performance metrics and providing feedback for improvement.

### How we built it

engagED was built by a mix of hackers from various backgrounds in frontend and backend roles. We began by wireframing and designing our user workflows, and then iterated upon these designs to create a seamless experience. We used a variety of cutting-edge technologies in this platform, which are outlined below. Design and Wireframing Engineering Our tech flow The frontend was build in React and Typescript using Next.js as our frontend framework in order to maintain a structured codebase and fast loading times. The backend server was built in Python and FastAPI, allowing us to utilize a variety APIs such as ElevenLabs, OpenAI, Groq, and LangChain. A websocket connection was also utilized between the frontend and backend in order to constantly stream audio data to the agents and LLMs, allowing for low latency. The database was created in MongoDB in order to store user sessions in a structured manner. Client logic flow Diving deeper into our client logic, there are a lot of moving parts that bring engagED together! We begin with the teacher user instructing the virtual class, and the AI students listening in. We then utilize OpenAI's Whisper API in concurrence with Groq in order to convert this instruction to a text transcript. We then feed this transcript back into the AI Agents in order to provide them context and prompt them to chime in if neccesary. Each agent was build with LangChain Agents and Groq to be able to respond to conversation with low latency. Initially, we ran each agent in parallel, but soon realized that this would cause them to talk over one another as they responded to the teacher. Thus, we implemented a round-robin algorithm between each agent and the teacher in order to minimize conflicts. Then, we utilize ElevenLabs' text to speech API in order to convert the agent's conversational input into speech, and played this in the virtual classroom setting. The speech from the teacher is now combined with the latest transcripts and messages from the AI Agents to create a master transcript. This is now used as the context provided to the AI Agents for them to respond to the teacher. Once the session is ended, the master transcript is sent to OpenAI to summarize and provide analytics data for teachers. This is then displayed to the teachers in the following page. The data is then stored with their unique session id in MongoDB for retrieval and visibility later. Our tech stack

### Challenges we ran into

Zoom API Issues: We attempted to use the Zoom API to programmatically start meetings with Zoom bots. However, after numerous attempts and insights from Zoom mentors, we realized that the Meeting Bot approach was not viable due to API limitations and planned deprecation. We ended up building our own solution for streaming web and audio to the backend and adding having agents receive this context. Low-latency Interaction: We had to optimize AI interactions to maintain near-real-time responsiveness, ensuring that the virtual classroom felt immersive and natural. We attempted to achieve low-inference at every step possible using the Groq API to speed up Speech-To-Text and Agentic LLM reasoning. We found ElevenLabs API to be quite fast for Text-to-Speech. Agent Coordination: AI student agents needed to understand each other's context and avoid talking over one another. Synchronization and maintaining conversational order proved to be a complex challenge. We used a mixture probabilistic activations for each agent, a cooldown period specific to each agent, and locks to ensure a clean virtual classroom experience. Analytics APIs: We wanted to utilize a dedicated API in order to give users analytics on their sessions, but had a hard time finding options that were either sufficiently documented or free to use for this task. As such, we decided to utilize OpenAI's capabilities in order to analyze our transcripts. Audio Formats: Both the Web MediaRecorder API and the audio-generation services have limited information on how it streams "chunks" of audio. For example, one API returned base64-encoded data that consistently started with the same few octets, but looking them up as magic numbers turned up dry. We ultimately discovered that these chunks had to be concatenated into one single audio file—only the first chunk had the necessary header data needed to make the file playable. However, this was not feasible for generating instant responses and transcripts in response to the user's microphone audio. Our initial solution for this case was to accumulate chunks, but this led to large contexts for the AI models. Our breakthrough came when we used a deprecated method from the Web Audio API to collect raw audio samples, and manually formatted each chunk as a WAV file so that each chunk could be enjoyed in isolation.

### Accomplishments we're proud of

There are many things we are proud of: Developing Multi-Agent AI Personalities – We successfully implemented multiple AI-powered student personas that react uniquely to teacher input, creating a diverse and realistic classroom dynamic. Achieving Real-Time, Low-Latency AI Conversations – By leveraging Groq for rapid inference and optimizing our WebSocket connections, we were able to ensure smooth and natural interactions between teachers and AI students. Building an AI-Driven Performance Analytics Dashboard – We implemented a system that provides teachers with personalized feedback, including engagement scores, response effectiveness, and classroom management insights. Creating a Structured Data Storage System – By designing a scalable MongoDB database, we ensured that teachers can access their past sessions, review feedback, and track their progress over time. Creating an interactive UI/UX thats super aesthetically pleasing!

### What we learned

We learned many things: We learned that achieving real-time, natural AI interaction is challenging, especially when handling multiple AI agents in parallel. Implementing Groq for low-latency processing and refining our round-robin approach helped us create smoother conversations. Striking the right balance between accurate student simulations and efficient AI processing was crucial. Overloading the system with complex, simultaneous AI interactions led to chaotic responses, which we mitigated through probabilistic agent activations. Integrating Whisper AI for speech recognition and ElevenLabs for AI-generated speech gave us firsthand experience in handling continuous audio streams with minimal delay. We had to refine our approach to ensure clarity and prevent misinterpretations. Since each training session generates a large amount of structured and unstructured data, we learned best practices for storing, retrieving, and analyzing this data efficiently using MongoDB. While AI can provide powerful insights, we realized that clear, intuitive dashboards and actionable feedback are essential for users to gain value from the system. Simplifying the UI/UX to present feedback in a digestible format was a major learning experience.

### What's next

There are many aspects of engagED that we could improve: Using a real-world dataset of teacher-student interactions to fine-tune the LLM model would be beneficial to getting more realistic practice for teachers. Leveraging video analytics to understand expressions and hand-movements of the teacher would be useful for giving them more insightful feedback. Implementing visual-language models to understand content that is shared via "screenshare" could help provide useful context for the LLM when it is generating feedback. Creating a feature for teachers to customize the student persona. Having more agents in the meeting & figuring out how to better implement student-student interactions as well to best simulate the classroom enviorment.

## README (from the GitHub repository)

# _engagED_ - AI-Powered Classroom Simulation for Smarter Teacher Training 👩‍🎓👩🏾‍🎓👨🏻‍🎓

🌲 Made for TreeHacks 2025. [Devpost](https://devpost.com/software/teacher-teacher) · [GitHub](https://github.com/jasmine-dragons/treehacks-2025/)

## Inspiration

Every day, teachers step into classrooms filled with students who have different personalities, learning styles, and challenges. Some students are eager to participate, others are easily distracted, and a few might resist authority altogether. A teacher's ability to navigate these interactions can mean the difference between an engaging classroom and a chaotic one.

Yet, there are few opportunities for educators to practice classroom management in a realistic, risk-free setting. So, we asked ourselves: What if teachers could train for the classroom the way pilots train in flight simulators? Could we create an AI-powered environment where educators could interact with dynamic student personalities, receive real-time feedback, and refine their teaching techniques so that they can accel in the classroom?

That’s how we built _engagED_—an interactive classroom simulation that prepares educators for real-world teaching challenges. By leveraging AI-bots that simulate student behaviors and a performance analytics dashboard, _engagED_ helps teachers develop strong communication skills, manage classroom dynamics, and build confidence—all in a safe, controlled environment.

We believe that if _engagED_ is implemented in teacher training programs and professional development workshops, it could revolutionize how educators prepare for the modern classroom—leading to more engaged students, less burnout, and stronger learning outcomes across the board.

## What it does

_engagED_ simulates a classroom environment where teachers interact with AI-powered student personalities, each with unique behaviors, engagement levels, and challenges. The platform allows educators to practice managing real-world classroom dynamics—whether it’s handling a disruptive student, encouraging a quiet learner to participate, or maintaining engagement during a lesson.

_engagED_ responds to natural teacher interactions in a virtual classroom setting, adjusting student behaviors based on teaching strategies. Just like in a real class, students may ask unexpected questions, lose focus, or react differently depending on the teacher’s approach. At the end of each session, _engagED_ provides a performance dashboard, offering insights into key performance metrics and providing feedback for improvement.

## How we built it

_engagED_ was built by a mix of hackers from various backgrounds in frontend and backend roles. We began by wireframing and designing our user workflows, and then iterated upon these designs to create a seamless experience. We used a variety of cutting-edge technologies in this platform, which are outlined below.

### Design and Wireframing

<img src="https://github.com/user-attachments/assets/dc7576bc-e58f-4868-b769-94d4d9266c97" alt="drawing" width="500"/>
<img src="https://github.com/user-attachments/assets/6c10abff-5699-42a9-800c-e3b1f50f03ae" alt="drawing" width="500"/>
<img src="https://github.com/user-attachments/assets/53af2792-a776-4cf8-9a06-de68f87da23d" alt="drawing" width="500"/>
<img src="https://github.com/user-attachments/assets/f597a2bd-fd5a-44c1-b101-8c8d948f9edc" alt="drawing" width="500"/>

### Engineering

![image](https://github.com/user-attachments/assets/e406cdbf-61e2-4f4f-95de-2e75e09ab974)
_Our tech flow_

The frontend was build in [React](https://react.dev/) and [Typescript](https://www.typescriptlang.org/) using [Next.js](https://nextjs.org/) as our frontend framework in order to maintain a structured codebase and fast loading times. The backend server was built in [Python](https://www.python.org/) and [FastAPI](https://fastapi.tiangolo.com/), allowing us to utilize a variety APIs such as [ElevenLabs](https://elevenlabs.io/), [OpenAI](https://openai.com/), [Groq](https://groq.com/), and [LangChain](https://www.langchain.com/). A websocket connection was also utilized between the frontend and backend in order to constantly stream audio data to the agents and LLMs, allowing for low latency. The database was created in [MongoDB](https://www.mongodb.com/) in order to store user sessions in a structured manner.

![image](https://github.com/user-attachments/assets/678ddf3b-1be3-4b51-9f38-f948ea1f00a8)
_Client logic flow_

Diving deeper into our client logic, there are a lot of moving parts that bring _engagED_ together! We begin with the **teacher** user instructing the virtual class, and the **AI students** listening in. We then utilize OpenAI's [Whisper API](https://platform.openai.com/docs/guides/speech-to-text) in concurrence with Groq in order to convert this instruction to a text transcript. We then feed this transcript back into the AI Agents in order to provide them context and prompt them to chime in if neccesary. Each agent was build with [LangChain Agents](https://python.langchain.com/v0.1/docs/modules/agents/) and Groq to be able to respond to conversation with low latency. Initially, we ran each agent in parallel, but soon realized that this would cause them to talk over one another as they responded to the teacher. Thus, we implemented a round-robin algorithm between each agent and the teacher in order to minimize conflicts.

Then, we utilize ElevenLabs' [text to speech API](https://elevenlabs.io/docs/api-reference/text-to-speech/convert) in order to convert the agent's conversational input into speech, and played this in the virtual classroom setting. The speech from the teacher is now combined with the latest transcripts and messages from the AI Agents to create a master transcript. This is now used as the context provided to the AI Agents for them to respond to the teacher.

Once the session is ended, the master transcript is sent to OpenAI to summarize and provide analytics data for teachers. This is then displayed to the teachers in the following page. The data is then stored with their unique session id in MongoDB for retrieval and visibility later.

![image](https://github.com/user-attachments/assets/1756c555-4835-47e7-bdb4-fc44dc4b5589)
_Our tech stack_

## Challenges we ran into

Zoom API Issues: We attempted to use the Zoom API to programmatically start meetings with Zoom bots. However, after numerous attempts and insights from Zoom mentors, we realized that the Meeting Bot approach was not viable due to API limitations and planned deprecation. We ended up building our own solution for streaming web and audio to the backend and adding having agents receive this context.

Low-latency Interaction: We had to optimize AI interactions to maintain near-real-time responsiveness, ensuring that the virtual classroom felt immersive and natural. We attempted to achieve low-inference at every step possible using the Groq API to speed up Speech-To-Text and Agentic LLM reasoning. We found ElevenLabs API to be quite fast for Text-to-Speech.

Agent Coordination: AI student agents needed to understand each other's context and avoid talking over one another. Synchronization and maintaining conversational order proved to be a complex challenge. We used a mixture probabilistic activations for each agent, a cooldown period specific to each agent, and locks to ensure a clean virtual classroom experience.

Analytics APIs: We wanted to utilize a dedicated API in order to give users analytics on their sessions, but had a hard time finding options that were either sufficiently documented or free to use for this task. As such, we decided to utilize OpenAI's capabilities in order to analyze our transcripts.

Audio Formats: Both the [Web MediaRecorder API](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream_Recording_API) and the audio-generation services have limited information on how it streams "chunks" of audio. For example, one API returned base64-encoded data that consistently started wi

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 38 recognized source files, 93 KB.
- CSS (language) — detected in the code
- LangChain (technology) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code
- JavaScript (language) — claimed on Devpost, not found in the code
- MongoDB (technology) — claimed on Devpost, not found in the code
- OpenAI (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (46 of 46)

```
backend/.env.example
backend/.gitignore
backend/analytics.py
backend/audio_processor.py
backend/emotionanalysis.py
backend/main.py
backend/requirements.txt
backend/server.py
backend/student_bots.py
backend/texttospeech.py
client/.env
client/.gitignore
client/eslint.config.mjs
client/next.config.ts
client/package.json
client/README.md
client/src/app/globals.css
client/src/app/history/page.tsx
client/src/app/layout.tsx
client/src/app/login/page.module.css
client/src/app/login/page.tsx
client/src/app/page.module.css
client/src/app/page.tsx
client/src/app/results/[simId]/page.module.css
client/src/app/results/[simId]/page.tsx
client/src/app/setup/actions.ts
client/src/app/setup/page.module.css
client/src/app/setup/page.tsx
client/src/app/start/page.module.css
client/src/app/start/page.tsx
client/src/components/Classroom/index.module.css
client/src/components/Classroom/index.tsx
client/src/components/HistoryItem/index.module.css
client/src/components/HistoryItem/index.tsx
client/src/components/Stat/index.module.css
client/src/components/Stat/index.tsx
client/src/components/Student/index.module.css
client/src/components/Student/index.tsx
client/src/lib/api.ts
client/src/lib/fmt.ts
client/src/lib/MeetingManager.ts
client/src/lib/students.ts
client/src/lib/types.ts
client/src/lib/wav-encoder.ts
client/tsconfig.json
README.md
```

### Dependencies

- backend/requirements.txt: elevenlabs@==1.51.0, email-validator@>=2.0.0, fastapi[standard]@>=0.103.2,<0.104.0, groq@==0.18.0, httpx@>=0.24.0, itsdangerous@>=2.0.0, jinja2@>=3.0.0, langchain@==0.3.18, langchain-community@==0.3.17, langchain-groq@==0.2.4, langchain-openai@==0.3.6, librosa@==0.10.2.post1, pydantic@>=2.0.0,<3.0.0, pymongo@==4.11.1, python-dotenv@==1.0.1, python-multipart@>=0.0.6, ujson@>=5.8.0, uvicorn[standard]@>=0.23.2,<0.24.0
- client/package.json: @eslint/eslintrc@^3, @types/audioworklet@^0.0.70, @types/dom-speech-recognition@^0.0.4, @types/node@^20.17.19, @types/react@^19, @types/react-dom@^19, eslint@^9, eslint-config-next@15.1.7, next@15.1.7, react@^19.0.0, react-dom@^19.0.0, typescript@^5

### Recent commits (newest first)

- Update README.md
- Update README.md
- add screenshots
- Add challenge about audio formats
- Merge branch 'main' of https://github.com/jasmine-dragons/treehacks-2025
- tune for experiments
- feat: show summary, hide two useless stats
- Merge branch 'main' of https://github.com/jasmine-dragons/treehacks-2025
- fix: right align talk speed better
- summary in analytics
- Merge branch 'main' of https://github.com/jasmine-dragons/treehacks-2025
- feat: retry class, show who was in your class simulation
- Update README.md
- finetuning bots
- Merge branch 'main' of https://github.com/jasmine-dragons/treehacks-2025
- Update README.md
- fix merge conflicts
- Merge branch 'main' of https://github.com/jasmine-dragons/treehacks-2025
- fix: wav seems to work!! holy shit
- home link on nav

## Key source files (fetched from GitHub, selected and truncated for size)

### backend/requirements.txt

```
langchain==0.3.18
langchain-groq==0.2.4
langchain-openai==0.3.6
langchain-community==0.3.17
python-dotenv==1.0.1
fastapi[standard]>=0.103.2,<0.104.0
uvicorn[standard]>=0.23.2,<0.24.0
python-multipart>=0.0.6
pydantic>=2.0.0,<3.0.0
email-validator>=2.0.0
httpx>=0.24.0
itsdangerous>=2.0.0
jinja2>=3.0.0
ujson>=5.8.0
groq==0.18.0
elevenlabs==1.51.0
pymongo==4.11.1
librosa==0.10.2.post1
```

### client/package.json

```
{
  "name": "treehacks-2025",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@types/audioworklet": "^0.0.70",
    "@types/dom-speech-recognition": "^0.0.4",
    "next": "15.1.7",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },
  "devDependencies": {
    "@eslint/eslintrc": "^3",
    "@types/node": "^20.17.19",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "15.1.7",
    "typescript": "^5"
  }
}

```

### backend/server.py

```python
import json
from fastapi import FastAPI, WebSocket
from fastapi.middleware.cors import CORSMiddleware
from datetime import datetime
import os
from dotenv import load_dotenv
from audio_processor import AudioProcessor
from student_bots import StudentBotManager
from texttospeech import text_to_speech
from analytics import SpeechAnalyzer
from typing import Dict, List
import base64

# Load environment variables
load_dotenv()

app = FastAPI()

# Add CORS middleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

class SimulationSession:
    def __init__(self):
        self.audio_processor = AudioProcessor()
        self.student_bot_manager = StudentBotManager()
        self.start_time = None
        self.transcript: List[Dict] = []
        
    async def handle_event(self, websocket: WebSocket, event: str, payload: dict) -> None:
        if event == "START-SIM":
            await self.handle_start_sim(websocket, payload)
        elif event == "AUDIO-CHUNK":
            await self.handle_audio_chunk(websocket, payload)
        elif event == "END-SIM":
            await self.handle_end_sim(websocket)
            
    async def handle_start_sim(self, websocket: WebSocket, payload: dict) -> None:
        """Handle simulation start event"""
        student_personalities = payload.get("studentPersonalities", [])
        if not student_personalities:
            await websocket.send_json({
                "event": "ERROR",
                "payload": {"message": "No student personalities provided"}
            })
            return
            
        self.student_bot_manager.initialize_students(student_personalities)
        self.start_time = datetime.now()
        
        await websocket.send_json({
            "event": "SIM-STARTED",
            "payload": {"message": "Simulation started successfully"}
        })
        
    async def handle_audio_chunk(self, websocket: WebSocket, payload: dict) -> None:
        """Handle incoming audio chunk"""
        try:
            # Decode base64 audio data
            audio_bytes = base64.b64decode(payload["audio"])
            
            # Process and transcribe audio
            self.audio_processor.process_chunk(audio_bytes)
            transcription = await self.audio_processor.transcribe_latest()
            
            if transcription:
                # Add to transcript
                self.transcript.append({
                    "text": transcription,
                    "speaker": "teacher",
                    "timestamp": datetime.now().isoformat()
                })
                
                # Get student response
                response = await self.student_bot_manager.process_teacher_input(self.transcript)
                
                if response:
                    # Add student response to transcript
                    self.transcript.append({
                        "text": response["text"],
                        "speaker": response["speaker"],
                        "timestamp": datetime.now().isoformat()
                    })
                    
                    # Generate audio response
                    audio_stream = text_to_speech(response["text"], response["voice_id"])
                    if audio_stream:
                        # Convert audio to base64
                        audio_base64 = base64.b64encode(audio_stream).decode('utf-8')
                        
                        # Send audio response
                        await websocket.send_json({
                            "event": "STUDENT-AUDIO",
                            "payload": {
                                "audio": audio_base64,
                                "text": response["text"],
                                "speaker": response["speaker"]
                            }
                        })
                        
        except Exception as e:
            await websocket.send_json({
                "event": "ERROR",
                "payload": {"message": f"Error processing audio: {str(e)}"}
            })
            
    async def handle_end_sim(self, websocket: WebSocket) -> None:
        """Handle simulation end event"""
        try:
            end_time = datetime.now()
            duration = end_time - self.start_time if self.start_time else None
            
            # Generate analytics
            analyzer = SpeechAnalyzer(os.getenv("OPENAI_API_KEY"))
            analysis = analyzer.analyze(self.transcript, duration)
            
            # Send final analytics
            await websocket.send_json({
                "event": "SIM-ENDED",
                "payload": {
                    "transcript": self.transcript,
                    "analytics": analysis,
                    "duration": str(duration) if duration else None
                }
            })
            
        except Exception as e:
            await websocket.send_json({
                "event": "ERROR",
                "payload": {"message": f"Error ending simulation: {str(e)}"}
            })
            
        finally:
            await websocket.close()

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    session = SimulationSession()
    
    try:
        while True:
            # Receive JSON message
            message = await websocket.receive_json()
            event = message.get("event")
            payload = message.get("payload", {})
            
            if not event:
                await websocket.send_json({
                    "event": "ERROR",
                    "payload": {"message": "No event specified"}
                })
                continue
                
            await session.handle_event(websocket, event, payload)
            
    except Exception as e:
        print(f"WebSocket error: {e}")
        try:
            await websocket.send_j
[truncated — 346 more characters]
```

### backend/main.py

```python
import asyncio
from doctest import master
from typing import Dict
from fastapi import FastAPI, Request, WebSocket
from datetime import datetime
from fastapi.encoders import jsonable_encoder
import uvicorn
from typing import Dict, List
import os
from dotenv import load_dotenv
from groq import Groq
from student_bots import StudentBotManager
from texttospeech import text_to_speech
from audio_processor import AudioProcessor
from fastapi import FastAPI, WebSocket
import json
from pymongo import MongoClient
from typing import Dict
import requests
from fastapi import FastAPI, Request
import uvicorn
from typing import Dict, List
from analytics import SpeechAnalyzer

load_dotenv()

# set up database
MONGO_URL = os.getenv("MONGO_URL")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")

mongo_client = MongoClient(MONGO_URL)

database = mongo_client.get_database("treehacks-2025")
sessions = database.get_collection("user-sessions")

user_id = 2
simulation_id = 1

VOICEGAIN_API_KEY = os.getenv("VOICEGAIN_API_KEY")

# Initialize Groq client
groq_client = Groq(api_key=os.getenv("GROQ_API_KEY"))

master_transcript : List[Dict[str, str]] = []

student_bot_manager = StudentBotManager()
audio_processor = AudioProcessor()

app = FastAPI()

async def handle_audio_chunk(websocket: WebSocket, audio_chunk: bytes):
    global running
    if not running:
        return
    audio_processor.process_chunk(audio_chunk)
    transcription = await audio_processor.transcribe_latest()

    print("Transcription: ", transcription)

    master_transcript.append({
        "text": transcription,
        "speaker": "teacher",
        "timestamp": datetime.now()
    })

    if not running:
        return
    response = await student_bot_manager.process_teacher_input(master_transcript)
    if response:
        master_transcript.append({
            "text": response["text"],
            "speaker": response["speaker"],
            "timestamp": response["timestamp"],
        })
        audio_stream = text_to_speech(response["text"], response["voice_id"])
        if not running:
            return
        await websocket.send_json({"type": "about-to-speak", "studentName": response["speaker"]})
        await websocket.send_bytes(audio_stream)

tasks: list[asyncio.Task] = []
running = False

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    global master_transcript
    global duration
    global tasks, running

    await websocket.accept()
    start_time = datetime.now()
    tasks = []
    running = True
    await websocket.send_json({"type": "students", "students": [[x.name, x.type, x.personality] for x in student_bot_manager.students]})
    try:
        while True:
            print(master_transcript)
            audio_chunk = await websocket.receive_bytes()
            tasks.append(asyncio.create_task(handle_audio_chunk(websocket, audio_chunk)))
            
    except Exception as e:
        print(f"WebSocket error: {e}")

    finally:
        running = False
        # Save the final combined audio to WebM file
        final_audio_path = audio_processor.get_full_audio("final_output.webm")
        if final_audio_path:
            print(f"Final audio saved as {final_audio_path}")
            end_time = datetime.now()
            duration = end_time - start_time
        else:
            print("No audio was recorded")

        await websocket.close()

@app.post("/start-sim")
async def start_sim(request: Request):
    """Start the simulation."""
    global master_transcript
    master_transcript = []

    data = await request.json()

    student_personalities = data.get("studentPersonalities")
    if not student_personalities:
        return {"message": "No student personalities provided"}

    # Start the simulation
    try:
        student_bot_manager.initialize_students(student_personalities)        

    except Exception as e:
        return {"message": f"Error starting simulation: {e}"}

    return {"message": "Simulation started"}

@app.get("/history/{user_id}")
async def get_history(user_id: int):
    """Get the session history from MongoDB."""
    history = list(sessions.find({"user_id": user_id}, {'_id': 0}))
    return {"data": [{**d, "simulation_id_str": str(d["simulation_id"])} for d in history]}

@app.get("/sim/{simulation_id}")
async def get_sim(simulation_id: int):
    """Get the simulation by ID from MongoDB."""
    simulation = list(sessions.find({"simulation_id": simulation_id}, {"_id": 0}))
    return {"data": simulation[0] if len(simulation) > 0 else False}

@app.post("/analytics")
async def get_analytics(): 
    import random

    global simulation_id
    global tasks


    await asyncio.gather(*tasks)
    tasks = []
    
    analyzer = SpeechAnalyzer(OPENAI_API_KEY)

#     duration= 1000
#     master_transcript = [
#     {"text": "Today so we will discuss the solar system.", 
#      "speaker": "Teacher", 
#      "timestamp": "00:00:00"},
#     {"text": "Okay!", 
#      "speaker": "Student", 
#      "timestamp": "00:00:10"},
#     {"text": "The sun is at the center, and planets orbit around it.", 
#      "speaker": "Teacher", 
#      "timestamp": "00:00:20"},
#     {"text": "Oh, I see lol!", 
#      "speaker": "Student", 
#      "timestamp": "00:00:30"}
# ]


    analysis = analyzer.analyze_teacher_speech(master_transcript, duration)

    configs = [x.personality for x in student_bot_manager.students]

    simulation_id = random.randint(0, (2**63) - 1)

    sim_id = simulation_id
    new_object = {
            "user_id": user_id,
            "transcript": master_transcript,
            "simulation_id": simulation_id,
            "analytics": analysis, 
            # "audio": encoding,
            "config": configs,
            "personalities": [x.type for x in student_bot_manager.students],
            "timestamp": datetime.now(),
        }
    print('INSERT', new_object)
    sessions.insert_one(new_object)

    simulation_id += 1

    return {"simId": str(sim_id), "analysis": analysis}

    # 
[truncated — 1467 more characters]
```

### client/src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Geist } from "next/font/google";
import "./globals.css";
import Link from "next/link";

const inter = Geist({
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "EngagED",
  description: "Made with 🌲 for TreeHacks 2025 🥰",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <div className="blob-wrapper">
          <div className="blob"></div>
        </div>
        <nav className="nav">
          <Link href="/">Home</Link>
          <Link href="/setup">Get Started</Link>
          <Link href="/history">History</Link>
        </nav>
        {children}
      </body>
    </html>
  );
}

```

### client/src/app/page.tsx

```typescript
import styles from "./page.module.css";
import Link from "next/link";
import PersonAtDeskZoom from "@/../public/remote learning.svg";
import Whiteboard from "@/../public/teaching.svg";
import PersonPcWindow from "@/../public/videolearning.svg";
import Image from "next/image";

export default async function Home() {
  return (
    <div className={styles.page}>
      <nav className={styles.nav}>
        <Link href="/login" className={`button ${styles.login}`}>
          Log In
        </Link>
      </nav>
      <div className={styles.hero}>
        <div className={styles.content}>
          <h1 className={styles.title}>
            engag<strong className={styles.ed}>ed</strong>
          </h1>
          <p className={styles.tagline}>
            An AI-powered classroom simulator that helps teachers practice
            real-world classroom management in a risk-free environment. Interact
            with dynamic student personalities, refine your teaching strategies,
            and receive real-time feedback—all to build confidence and improve
            student engagement.
          </p>
          <Image
            src={PersonAtDeskZoom}
            alt="A person sits at a desk holding paper while a different person waits on their computer screen"
            height={300}
            className={styles.imageBottom}
          />
        </div>
        <div className={styles.blobWrapper}>
          <div className={styles.blob} />
          <Image
            src={PersonPcWindow}
            alt="A person in a desktop window does a presentation while two people look at her"
            className={styles.imageRight}
          />
        </div>
      </div>
    </div>
  );
}

```

### client/src/app/login/page.tsx

```typescript
import styles from "./page.module.css";

export default function Login() {
  return (
    <form className={styles.page} action="/setup">
      <h1 className="heading">Welcome.</h1>
      <label className={styles.label}>
        <span>Email</span>
        <input type="email" placeholder="you@example.com" />
      </label>
      <label className={styles.label}>
        <span>Password</span>
        <input type="password" placeholder="••••••••" />
      </label>
      <button type="submit" className="button">
        Log In
      </button>
    </form>
  );
}

```

### client/src/app/history/page.tsx

```typescript
import { HistoryItem } from "@/components/HistoryItem";
import { getHistory } from "@/lib/api";

const USER_ID = "2";

export default async function History() {
  const { data } = await getHistory(USER_ID);
  return (
    <div className="container">
      <h1 className="heading">Past Sessions</h1>
      {data
        .toSorted((a, b) => b.timestamp.localeCompare(a.timestamp))
        .map((anal, i) => (
          <HistoryItem
            analysis={anal}
            key={anal.timestamp}
            style={{ animationDelay: `${i * 50}ms` }}
          />
        ))}
    </div>
  );
}

```

### client/src/components/Student/index.tsx

```typescript
import Image, { StaticImageData } from "next/image";
import styles from "./index.module.css";

export type StudentProps = {
  name: string;
  description: string;
  image: string | StaticImageData;
  count: number;
  onCount: (count: number) => void;
};
export function Student({
  name,
  description,
  image,
  count,
  onCount,
}: StudentProps) {
  return (
    <div className={styles.card}>
      <Image
        src={image}
        alt="child"
        width={80}
        height={80}
        className={styles.pfp}
      />
      <p className={styles.name}>{name}</p>
      <p className={styles.description}>{description}</p>
      <div className={styles.counter}>
        <button
          className={styles.btn}
          onClick={() => onCount(count - 1)}
          disabled={count <= 0}
        >
          &minus;
        </button>
        <div className={styles.count}>{count}</div>
        <button className={styles.btn} onClick={() => onCount(count + 1)}>
          +
        </button>
      </div>
    </div>
  );
}

```

### client/src/components/Classroom/index.tsx

```typescript
import Image, { StaticImageData } from "next/image";
import styles from "./index.module.css";
import classImage from "@/../public/classroom.png";

export type ClassroomProps = {
  name: string;
  studentImages: { src: string | StaticImageData; name: string }[];
  onClick: () => void;
  selected?: boolean;
};
export function Classroom({
  name,
  studentImages,
  onClick,
  selected,
}: ClassroomProps) {
  return (
    <button
      className={`${styles.card} ${selected ? styles.selected : ""}`}
      onClick={onClick}
    >
      <div className={styles.imageWrapper}>
        <Image src={classImage} alt="classroom" fill className={styles.image} />
      </div>
      <div className={styles.contents}>
        <p className={styles.name}>{name}</p>
        <div className={styles.children}>
          {studentImages.map(({ src, name }, i) => (
            <Image
              src={src}
              alt={name}
              width={40}
              height={40}
              className={styles.child}
              key={i}
              style={{ zIndex: studentImages.length - i }}
            />
          ))}
          <p className={styles.total}>{studentImages.length} students</p>
        </div>
      </div>
    </button>
  );
}

```

[28 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]