# Project export: Echo Rep

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: UC Berkeley AI Hackathon 2025
- Tagline: Talk your way to the Top
- Devpost: https://devpost.com/software/echo-rep
- GitHub: https://github.com/kevin1015wang/echoRep
- Demo: https://docs.google.com/presentation/d/1zP09Ex_dXeMbOv67fp9HBX-fsOImJ580njsMAyiDR7M/edit?usp=sharing
- Video: https://www.youtube.com/embed/aVyOwGGXBo8?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — Kevin Wang (5 commits), Vianeycodes (1 commits)

## Devpost submission (written by the team)

### Inspiration

The idea for Echo Rep came from all of us experiencing those awkward, uncomfortable customer interactions where we didn’t know what to say or do. Those situations we never expected and weren’t trained to handle. We realized that many people face these moments in real life without any preparation. That’s why we built this app: to make training for tough conversations accessible, realistic, and fun by turning it into a game.

### What it does

Echo Rep is an AI-powered call center training simulator that helps customer service representatives practice handling real-world conversations with challenging callers. Using voice interaction, it creates realistic scenarios where users must respond to different customer moods, ranging from confused to angry.

### How we built it

We built Echo Rep using Gemini AI, which powers the entire caller simulation. Gemini generates dynamic, emotionally realistic responses across different difficulty levels, ranging from a confused customer to a full-blown angry customer.

### Challenges we ran into

Finding the right conversation length was tough. Allowing more than 5 messages made calls feel too long and overwhelming, so we had to carefully limit interactions to keep training focused and engaging. We faced some technical issues with voice connection and initialization, where the AI voice sometimes failed to start or sync properly, which affected the flow and immersion of the training. Lastly, when displaying the feedback, the page rendered as raw code rather than clean text, which made it hard to present the results to the user clearly.

### Accomplishments we're proud of

As first-time hackathon participants, we’re proud of what we built with Echo Rep. We created a realistic, AI-powered training tool that keeps conversations short and engaging while giving instant feedback. This gamified approach makes learning customer service skills fun and effective.

### What we learned

We learned that focused sessions boost engagement and that designing AI to assess tone is challenging but crucial. Gamification really helps turn tough training into a positive experience.

### What's next

Next, we plan to add more customer types, improve voice reliability, and build tracking features to make Echo Rep even more impactful. For us, this project is a meaningful step toward helping people feel confident handling real customer calls.

## README (from the GitHub repository)

# Run and deploy your AI Studio app

This contains everything you need to run your app locally.

## Run Locally

**Prerequisites:**  Node.js


1. Install dependencies:
   `npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
   `npm run dev`


## Detected evidence (automated analysis)

Indexed codebase: 19 recognized source files, 60 KB.
- HTML (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (23 of 23)

```
.gitignore
App.tsx
components/Button.tsx
components/CallStartScreen.tsx
components/CallStatusIndicator.tsx
components/ChatInterface.tsx
components/ControlsHeader.tsx
components/Disclaimer.tsx
components/FeedbackDisplay.tsx
components/GuidancePanel.tsx
components/LoadingSpinner.tsx
components/MessageBubble.tsx
components/ScenarioSelectionScreen.tsx
constants.ts
index.html
index.tsx
metadata.json
package.json
README.md
services/geminiService.ts
tsconfig.json
types.ts
vite.config.ts
```

### Dependencies

- package.json: @google/genai@^1.6.0, @types/node@^22.14.0, react@^19.1.0, react-dom@^19.1.0, typescript@~5.7.2, vite@^6.2.0

### Recent commits (newest first)

- Update App.tsx
- Updated name of app
- Fixed final feedback
- Added gitignore and package-lock
- V1 of App
- Initial commit

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

### package.json

```
{
  "name": "call-center-representative-training-with-ai",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^19.1.0",
    "react-dom": "^19.1.0",
    "@google/genai": "^1.6.0"
  },
  "devDependencies": {
    "@types/node": "^22.14.0",
    "typescript": "~5.7.2",
    "vite": "^6.2.0"
  }
}

```

### index.tsx

```typescript

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const rootElement = document.getElementById('root');
if (!rootElement) {
  throw new Error("Could not find root element to mount to");
}

const root = ReactDOM.createRoot(rootElement);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

```

### App.tsx

```typescript

import React, { useState, useEffect, useRef, useCallback } from 'react';
import { GoogleGenAI, Chat, GenerateContentResponse } from '@google/genai';
import { TrainingStage, Message, MessageSender, Scenario, Feedback, FeedbackResponse, MAX_USER_MESSAGES_BEFORE_FEEDBACK, ScenarioLevel } from './types';
import { Disclaimer } from './components/Disclaimer';
import { ScenarioSelectionScreen } from './components/ScenarioSelectionScreen';
import { ChatInterface } from './components/ChatInterface';
import { GuidancePanel } from './components/GuidancePanel';
import { ControlsHeader } from './components/ControlsHeader';
import { FeedbackDisplay } from './components/FeedbackDisplay';
import { GEMINI_MODEL_NAME, SCENARIOS, getAICallerSystemInstruction, AI_CALLER_NAME_OPTIONS, AI_ORDER_NUMBER_PREFIX } from './constants';
import { initializeChatWithSystemInstruction, sendMessageToAI } from './services/geminiService';

// --- Start of Web Speech API Type Definitions (remains the same s ) ---
interface SpeechRecognitionResultList { 
  readonly length: number;
  item(index: number): SpeechRecognitionResult;
  [index: number]: SpeechRecognitionResult;
}
interface SpeechRecognitionResult {
  readonly isFinal: boolean;
  readonly length: number;
  item(index: number): SpeechRecognitionAlternative;
  [index: number]: SpeechRecognitionAlternative;
}
interface SpeechRecognitionAlternative {
  readonly transcript: string;
  readonly confidence: number;
}
interface SpeechRecognitionEvent extends Event {
  readonly resultIndex: number;
  readonly results: SpeechRecognitionResultList;
}
interface SpeechRecognitionErrorEvent extends Event {
  readonly error: string;
  readonly message: string;
}
interface SpeechRecognitionStatic { new(): SpeechRecognition; }
interface SpeechRecognition extends EventTarget {
  grammars: any; lang: string; continuous: boolean; interimResults: boolean; maxAlternatives: number; serviceURI?: string;
  start(): void; stop(): void; abort(): void;
  onaudiostart: ((this: SpeechRecognition, ev: Event) => any) | null;
  onaudioend: ((this: SpeechRecognition, ev: Event) => any) | null;
  onend: ((this: SpeechRecognition, ev: Event) => any) | null;
  onerror: ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => any) | null;
  onnomatch: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => any) | null;
  onresult: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => any) | null;
  onsoundstart: ((this: SpeechRecognition, ev: Event) => any) | null;
  onsoundend: ((this: SpeechRecognition, ev: Event) => any) | null;
  onspeechstart: ((this: SpeechRecognition, ev: Event) => any) | null;
  onspeechend: ((this: SpeechRecognition, ev: Event) => any) | null;
  onstart: ((this: SpeechRecognition, ev: Event) => any) | null;
}
declare global {
  interface Window {
    SpeechRecognition?: SpeechRecognitionStatic;
    webkitSpeechRecognition?: SpeechRecognitionStatic;
  }
}
// --- End of Web Speech API Type Definitions ---

const App: React.FC = () => {
  const [trainingStage, setTrainingStage] = useState<TrainingStage>(TrainingStage.SCENARIO_SELECTION);
  const [currentScenario, setCurrentScenario] = useState<Scenario | null>(null);
  const [messages, setMessages] = useState<Message[]>([]);
  const [userMessageCount, setUserMessageCount] = useState<number>(0);
  const [isLoading, setIsLoading] = useState<boolean>(false); // For AI responses
  const [error, setError] = useState<string | null>(null);
  const chatSessionRef = useRef<Chat | null>(null);
  const [apiKeyExists, setApiKeyExists] = useState<boolean>(true);
  const [feedback, setFeedback] = useState<Feedback | null>(null);

  // Voice features state
  const [isListening, setIsListening] = useState<boolean>(false);
  const [voiceOutputEnabled, setVoiceOutputEnabled] = useState<boolean>(true);
  const [microphoneError, setMicrophoneError] = useState<string | null>(null);
  const [speechSynthesisSupported, setSpeechSynthesisSupported] = useState<boolean>(false);
  const [speechRecognitionSupported, setSpeechRecognitionSupported] = useState<boolean>(false);
  const [liveTranscript, setLiveTranscript] = useState<string>('');
  const [typedMessage, setTypedMessage] = useState<string>('');
  
  const recognitionRef = useRef<SpeechRecognition | null>(null);
  const utteranceRef = useRef<SpeechSynthesisUtterance | null>(null);
  const manualStopInProgress = useRef<boolean>(false);
  const noSpeechErrorOccurredRef = useRef<boolean>(false);
  const hasAttemptedAutoRetryRef = useRef<boolean>(false);
  const currentAICallerName = useRef<string>('');
  const currentAIProblemDetails = useRef<string>('');


  useEffect(() => {
    if (!process.env.API_KEY) {
      setError("API_KEY environment variable is not set. This application requires an API key to function.");
      setApiKeyExists(false);
      setTrainingStage(TrainingStage.ERROR);
    }
    
    setSpeechSynthesisSupported('speechSynthesis' in window);
    const SpeechRecognitionAPI = window.SpeechRecognition || window.webkitSpeechRecognition;
    setSpeechRecognitionSupported(!!SpeechRecognitionAPI);

    if ('speechSynthesis' in window) {
      utteranceRef.current = new SpeechSynthesisUtterance();
      utteranceRef.current.lang = 'en-US';
      utteranceRef.current.rate = 1.0;
      return () => {
        window.speechSynthesis.cancel();
      };
    }
  }, []);

  const startListening = useCallback((isManualStart: boolean = false) => {
    if (!recognitionRef.current || isListening || !speechRecognitionSupported || isLoading || trainingStage !== TrainingStage.ACTIVE_SCENARIO) return;
    setMicrophoneError(null); 
    noSpeechErrorOccurredRef.current = false;
    setLiveTranscript('');
    if (isManualStart) {
      setTypedMessage(''); // Clear typed message ONLY on manual mic start by user
    }
    manualStopInProgress.current = false;
    try {
      recognitionRef.current.start();
      setIsListening(true);
    } catch (err) {
      console.error("Error starting speech rec
[truncated — 18057 more characters]
```

### vite.config.ts

```typescript
import path from 'path';
import { defineConfig, loadEnv } from 'vite';

export default defineConfig(({ mode }) => {
    const env = loadEnv(mode, '.', '');
    return {
      define: {
        'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
        'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
      },
      resolve: {
        alias: {
          '@': path.resolve(__dirname, '.'),
        }
      }
    };
});

```

### index.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Call Center Representative Training</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <link href="https://cdnjs.cloudflare.com/ajax/libs/heroicons/2.1.3/24/outline/heroicons.min.css" rel="stylesheet">
<script type="importmap">
{
  "imports": {
    "react/": "https://esm.sh/react@^19.1.0/",
    "react": "https://esm.sh/react@^19.1.0",
    "react-dom/": "https://esm.sh/react-dom@^19.1.0/",
    "@google/genai": "https://esm.sh/@google/genai@^1.6.0"
  }
}
</script>
<link rel="stylesheet" href="/index.css">
</head>
<body class="bg-slate-100">
  <div id="root"></div>
  <script type="module" src="/index.tsx"></script>
</body>
</html>
```

### types.ts

```typescript
export enum TrainingStage {
  SCENARIO_SELECTION = 'SCENARIO_SELECTION',
  BRIEFING = 'BRIEFING', // Optional: Show scenario details before starting
  ACTIVE_SCENARIO = 'ACTIVE_SCENARIO',
  GENERATING_FEEDBACK = 'GENERATING_FEEDBACK',
  FEEDBACK_DISPLAY = 'FEEDBACK_DISPLAY',
  ERROR = 'ERROR',
}

export enum MessageSender {
  USER_REPRESENTATIVE = 'USER_REPRESENTATIVE', // User is the call center rep
  AI_CALLER = 'AI_CALLER',           // Gemini is the customer
  SYSTEM = 'SYSTEM', // For system messages, e.g., errors in chat
}

export interface Message {
  sender: MessageSender;
  text: string;
  timestamp: Date;
}

export interface Scenario {
  id: string;
  level: ScenarioLevel;
  title: string;
  description: string; // Brief description for selection screen
  callerPersona: CallerPersona;
  callerProblem: string; // Detailed problem for the AI
  initialCallerMessage?: string; // If AI shouldn't generate the very first line from full prompt
}

export enum ScenarioLevel {
  LEVEL_1_CONFUSED = 'Confused',
  LEVEL_2_SLIGHTLY_ANNOYED = 'Slightly Annoyed',
  LEVEL_3_ANNOYED = 'Annoyed',
  LEVEL_4_ANGRY = 'Angry',
}

export enum CallerPersona {
  SARCASTIC_TEEN = 'Sarcastic Teen',
  TIRED_MOM = 'Tired Mom',
  STRESSED_WORKER = 'Stressed Worker',
  POLITE_ELDERLY = 'Polite Elderly Person',
  ENTITLED_CUSTOMER = 'Entitled Customer',
}

export interface Feedback {
  score: number;
  response_time_perception: string;
  tone_assessment: string;
  prioritization_and_info_gathering: string;
  problem_resolution_approach: string;
  overall_comment: string;
}

export interface FeedbackResponse {
  score: number;
  feedback: Feedback;
}

export const MAX_USER_MESSAGES_BEFORE_FEEDBACK = 5;

```

### constants.ts

```typescript
import { Scenario, ScenarioLevel, CallerPersona } from './types';

export const GEMINI_MODEL_NAME = 'gemini-2.5-flash-preview-04-17';

export const AI_CALLER_NAME_OPTIONS = ["Alex Smith", "Pat Jones", "Jamie Doe", "Chris Williams", "Jordan Brown"];
export const AI_ORDER_NUMBER_PREFIX = "ORD-";

// System instruction template for Gemini acting as the AI Caller
export const getAICallerSystemInstruction = (
  persona: CallerPersona,
  emotionalState: ScenarioLevel,
  problem: string,
  callerName: string,
  maxUserMessages: number
): string => `
You are a customer calling a call center.
Your name is ${callerName}.
Your assigned persona is: ${persona}.
Your emotional state is: ${emotionalState}.
The reason for your call is: ${problem}.

You will interact with the user, who is playing the role of a call center representative.
Initiate the conversation with your first line based on your persona, emotion, and scenario. If the problem statement includes an initial message, use that. Otherwise, create one.
Respond to the representative's messages naturally, staying in character.
Keep your responses concise, typically 1-3 sentences.

IMPORTANT: After the representative has sent exactly ${maxUserMessages} messages, your VERY NEXT response MUST be a JSON object detailing your feedback on their performance. Do not add any other text before or after this JSON object.

The JSON object must have this exact structure:
{
  "score": number (0-100, assess overall performance),
  "feedback": {
    "response_time_perception": "string (e.g., 'Responded promptly', 'Slight delay in responses', 'Noticeable pauses before responding')",
    "tone_assessment": "string (e.g., 'Professional and empathetic', 'Neutral, could be more engaging', 'Appeared dismissive or rushed', 'Sounded rude or unhelpful')",
    "prioritization_and_info_gathering": "string (e.g., 'Effectively gathered necessary information (name, order, issue)', 'Asked for some key details but missed others', 'Did not proactively seek essential information like name or order number', 'Focused on the wrong details initially')",
    "problem_resolution_approach": "string (e.g., 'Took clear steps towards resolution', 'Attempted to solve the issue but approach was unclear', 'Did not offer a clear path to resolution', 'Seemed unsure how to handle the problem')",
    "overall_comment": "string (A concise, constructive summary. Example: 'Good attempt at addressing the issue, but remember to verify the caller's identity early on.' or 'Excellent tone and empathy, made me feel heard.')"
  }
}

Base your feedback on:
- Response Time Perception: Implied by the flow; did the conversation feel like it moved efficiently or stalled? (You don't have actual timers, so this is a perception).
- Tone: Based on the representative's language – were they polite, empathetic, condescending, rude?
- Prioritization & Info Gathering: Did they ask for crucial information (e.g., name, account/order number, verify details) at appropriate times? Did they understand and address the core issue?
- Problem Resolution Approach: Did they guide the conversation towards a solution or offer appropriate next steps?

Do not break character or mention you are an AI until it's time to provide the JSON feedback.
Let's begin. You are ${persona} ${callerName}, and you are ${emotionalState} about your issue. Make your first statement to the call center representative.
`;


export const SCENARIOS: Scenario[] = [
  {
    id: 'level1-confused-order',
    level: ScenarioLevel.LEVEL_1_CONFUSED,
    title: 'Confused About Order Status',
    description: 'A customer is confused about their recent order and needs clarification.',
    callerPersona: CallerPersona.POLITE_ELDERLY,
    callerProblem: 'You are confused about an email you received regarding your recent order (Order # [ORDER_NUMBER]). You thought it was cancelled, but the email says it shipped. You need help understanding what is going on.',
    initialCallerMessage: "Oh, hello dear. I hope you can help me. I received an email about my order, [ORDER_NUMBER], and I'm a bit muddled. I thought I cancelled it, but now it says it's shipped?"
  },
  {
    id: 'level2-annoyed-delivery',
    level: ScenarioLevel.LEVEL_2_SLIGHTLY_ANNOYED,
    title: 'Late Delivery Inquiry',
    description: 'A customer is slightly annoyed because their package is late.',
    callerPersona: CallerPersona.TIRED_MOM,
    callerProblem: "Your package (Order # [ORDER_NUMBER]) was supposed to arrive three days ago, and you're juggling a lot with kids at home. You're looking for an update and are a bit frustrated with the delay.",
    initialCallerMessage: "Hi, I'm calling about my order, [ORDER_NUMBER]. It was supposed to be here three days ago, and frankly, with everything else I'm managing, this is just an extra headache I don't need. Can you tell me where it is?"
  },
  {
    id: 'level3-annoyed-billing',
    level: ScenarioLevel.LEVEL_3_ANNOYED,
    title: 'Incorrect Billing Charge',
    description: 'A customer is annoyed about an incorrect charge on their bill.',
    callerPersona: CallerPersona.STRESSED_WORKER,
    callerProblem: "You've been incorrectly charged on your last bill for a service you didn't subscribe to (Order related to account # [ACCOUNT_NUMBER]). You've been very busy at work, and this is an unwelcome surprise. You want it fixed immediately.",
    initialCallerMessage: "Yeah, hi. I've got a problem with my latest bill, account [ACCOUNT_NUMBER]. There's a charge on here for something I never signed up for, and I'm really not happy about it. I need this sorted out now."
  },
  {
    id: 'level4-angry-product',
    level: ScenarioLevel.LEVEL_4_ANGRY,
    title: 'Defective Product Received',
    description: 'A customer is angry because they received a defective product and had a bad experience.',
    callerPersona: CallerPersona.ENTITLED_CUSTOMER,
    callerProblem: "The expensive gadget you ordered (Order # [ORDER_NUMBER]) arrived broken. This is unacceptable, a
[truncated — 1629 more characters]
```

### components/LoadingSpinner.tsx

```typescript

import React from 'react';

interface LoadingSpinnerProps {
  size?: 'small' | 'medium' | 'large';
  color?: string;
  className?: string;
}

export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({ size = 'medium', color = 'currentColor', className = '' }) => {
  const sizeClasses = {
    small: 'w-4 h-4 border-2',
    medium: 'w-8 h-8 border-4',
    large: 'w-12 h-12 border-[6px]',
  };

  return (
    <div
      className={`animate-spin rounded-full ${sizeClasses[size]} border-solid border-t-transparent ${className}`}
      style={{ borderColor: color, borderTopColor: 'transparent' }}
      role="status"
      aria-label="Loading"
    ></div>
  );
};

```

### components/Disclaimer.tsx

```typescript
import React from 'react';

interface DisclaimerProps {
  appName?: string;
  emergencyNotice?: boolean;
}

export const Disclaimer: React.FC<DisclaimerProps> = ({ 
  appName = "Training Simulator", 
  emergencyNotice = true 
}) => {
  return (
    <div className="bg-sky-600 text-white p-3 text-center sticky top-0 z-50 shadow-md">
      <p className="font-semibold text-sm sm:text-base">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6 inline-block mr-2 align-text-bottom">
          <path strokeLinecap="round" strokeLinejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
        </svg>
        <strong>{appName.toUpperCase()}</strong>
      </p>
      {emergencyNotice && (
        <p className="text-xs sm:text-sm">
          <strong>IMPORTANT: This is for practice ONLY. DO NOT use for real emergencies.</strong>
        </p>
      )}
       <p className="text-xs sm:text-sm">
          This application uses AI and speech services for training purposes.
        </p>
    </div>
  );
};

```

### components/MessageBubble.tsx

```typescript
import React from 'react';
import { Message, MessageSender } from '../types';

interface MessageBubbleProps {
  message: Message;
}

export const MessageBubble: React.FC<MessageBubbleProps> = ({ message }) => {
  const isUserRep = message.sender === MessageSender.USER_REPRESENTATIVE;
  const isAICaller = message.sender === MessageSender.AI_CALLER;
  const isSystem = message.sender === MessageSender.SYSTEM;

  let bubbleClasses = '';
  let timeClasses = '';
  let alignment = '';

  if (isUserRep) {
    bubbleClasses = 'bg-sky-500 text-white self-end rounded-l-xl rounded-tr-xl';
    timeClasses = 'text-sky-200';
    alignment = 'items-end';
  } else if (isAICaller) {
    bubbleClasses = 'bg-slate-200 text-slate-800 self-start rounded-r-xl rounded-tl-xl';
    timeClasses = 'text-slate-500';
    alignment = 'items-start';
  } else if (isSystem) {
    bubbleClasses = 'bg-amber-100 text-amber-700 self-center text-center italic border border-amber-300 rounded-md text-xs sm:text-sm w-full sm:w-auto max-w-full';
    timeClasses = 'text-amber-500';
    alignment = 'items-center'; // Center system messages
  }


  return (
    <div className={`flex flex-col ${alignment} group w-full`}>
      <div className={`max-w-xs md:max-w-md lg:max-w-lg p-3 shadow ${bubbleClasses} ${isSystem ? 'mx-auto my-1 px-4' : ''}`}>
        <p className={`text-sm whitespace-pre-wrap ${isSystem ? 'text-center' : ''}`}>{message.text}</p>
      </div>
      {!isSystem && (
        <span className={`text-xs mt-1 px-1 ${timeClasses} opacity-0 group-hover:opacity-100 transition-opacity`}>
          {message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
        </span>
      )}
    </div>
  );
};

```

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