# Project export: SAGE-AI

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: SAGE AI lets you talk to your browsing history—summarized via Gemini, answered with GPT using RAG, and voiced back through Vapi. Your browser becomes a speaking, thinking assistant.
- Devpost: https://devpost.com/software/sage-ai
- GitHub: https://github.com/Aditya-Dawadikar/UCBerkeleyHackathon2025
- Video: https://www.youtube.com/embed/taofZ30gkbQ?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — aditya-dawadikar (19 commits), Udayan Atreya (3 commits), intimanjunath (2 commits)

## Devpost submission (written by the team)

### Overview

A powerful voice-augmented AI assistant for developers and researchers, built for UCBerkeleyHackathon2025. SAGE AI helps users converse and brainstorm on the content they browse online. It combines real-time web summarization, semantic memory, and natural voice interaction. 🚀

### Inspiration

We set out to build an interactive tool that developers and researchers can use to talk with their own web browsing history—to reflect, analyze, or brainstorm using AI. SAGE AI is powered by: Vapi for real-time voice interaction Gemini API for smart summarization Pinecone for semantic vector storage and retrieval Google Cloud Storage (GCS) for scalable backend data handling ✨ Features 🧠 Chrome Extension — Capture and summarize webpage content with one click 🔎 AI Summarization — Generate concise, meaningful summaries via Gemini API 🧭 Semantic Memory — Embed summaries into Pinecone for context-aware search 🎤 Voice Assistant — Interact with your saved context via voice, powered by Vapi 🔗 API Integration — Send summaries to external APIs or download as .txt files 🧩 Getting Started 1️⃣ Chrome Extension Navigate to chrome://extensions/ and enable Developer mode Click Load unpacked and select the dom-extension folder Click the extension icon to extract and summarize content from any webpage 2️⃣ FastAPI Vector Server Install dependencies: pip install fastapi uvicorn pinecone-client Install dependencies: Set API keys: export PINECONE_API_KEY=your_pinecone_key export GEMINI_API_KEY=your_gemini_key Set API keys: Run the backend server: uvicorn main:app --reload Run the backend server: Available API Endpoints: Available API Endpoints: POST /vectorize — Store a summary as a vector POST /vectorize — Store a summary as a vector POST /search — Search for semantically similar summaries POST /search — Search for semantically similar summaries 3️⃣ Vapi Voice Assistant The Chrome extension or web app integrates with Vapi for conversational interaction See popup.js or your frontend code for integration details 🔧 Example API Usage Vectorize a summary: Semantic search: 🎨 Customization Download summaries as .txt files or send to external APIs Voice Assistant toggle available directly in the extension popup Easily tweak styles in style.css to match your UI theme 🙌 Credits Google Gemini API Pinecone Vector DB Vapi Voice SDK UCBerkeleyHackathon2025 Team 📜 License MIT License

## README (from the GitHub repository)

# 🌿 SAGE AI: *System for Automatic Gyan Extraction*

![SAGE AI Chrome Extension Screenshot](dom-extension/assets/extension_ss2.jpg)

A powerful voice-augmented AI assistant for developers and researchers, built for **UCBerkeleyHackathon2025**.  
SAGE AI helps users **converse and brainstorm** on the content they browse online. It combines real-time web summarization, semantic memory, and natural voice interaction.

---

## 🚀 Inspiration

We set out to build an interactive tool that developers and researchers can use to **talk with their own web browsing history**—to reflect, analyze, or brainstorm using AI.  
SAGE AI is powered by:

- **Vapi** for real-time voice interaction  
- **Gemini API** for smart summarization  
- **Pinecone** for semantic vector storage and retrieval  
- **Google Cloud Storage (GCS)** for scalable backend data handling  

---

## ✨ Features

- 🧠 **Chrome Extension** — Capture and summarize webpage content with one click  
- 🔎 **AI Summarization** — Generate concise, meaningful summaries via Gemini API  
- 🧭 **Semantic Memory** — Embed summaries into Pinecone for context-aware search  
- 🎤 **Voice Assistant** — Interact with your saved context via voice, powered by Vapi  
- 🔗 **API Integration** — Send summaries to external APIs or download as `.txt` files  

---

## 🧩 Getting Started

### 1️⃣ Chrome Extension

1. Navigate to `chrome://extensions/` and enable **Developer mode**  
2. Click **Load unpacked** and select the `dom-extension` folder  
3. Click the extension icon to extract and summarize content from any webpage  

![SAGE AI Chrome Extension Screenshot](dom-extension/assets/extension_ss.jpg)

### 2️⃣ FastAPI Vector Server

1. **Install dependencies:**
```bash
pip install fastapi uvicorn pinecone-client
```

2. **Set API keys:**
```bash
export PINECONE_API_KEY=your_pinecone_key
export GEMINI_API_KEY=your_gemini_key
```

3. **Run the backend server:**
```bash
uvicorn main:app --reload
```

4. **Available API Endpoints:**
- `POST /vectorize` — Store a summary as a vector  
- `POST /search` — Search for semantically similar summaries  

### 3️⃣ Vapi Voice Assistant

- The Chrome extension or web app integrates with [Vapi](https://vapi.ai/) for conversational interaction  
- See `popup.js` or your frontend code for integration details  

---

## 🔧 Example API Usage

**Vectorize a summary:**
```bash
curl -X POST http://localhost:8000/vectorize   -H "Content-Type: application/json"   -d '{"text": "Your summary text here"}'
```

**Semantic search:**
```bash
curl -X POST http://localhost:8000/search   -H "Content-Type: application/json"   -d '{"query": "What are the benefits of apples?"}'
```

---

## 🎨 Customization

- Download summaries as `.txt` files or send to external APIs  
- Voice Assistant toggle available directly in the extension popup  
- Easily tweak styles in `style.css` to match your UI theme  

---

## 🙌 Credits

- [Google Gemini API](https://ai.google.dev/)  
- [Pinecone Vector DB](https://www.pinecone.io/)  
- [Vapi Voice SDK](https://vapi.ai/)  
- [UCBerkeleyHackathon2025 Team](https://github.com/Aditya-Dawadikar/UCBerkeleyHackathon2025)  

---

## 📜 License

[MIT License](LICENSE)


## Detected evidence (automated analysis)

Indexed codebase: 20 recognized source files, 47 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- Google Gemini (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 (28 of 28)

```
.gitignore
dom-extension/content.js
dom-extension/manifest.json
dom-extension/popup.html
dom-extension/popup.js
dom-extension/style.css
dom-extension/summary_agent.js
example-hackathon-demo/.eslintrc.cjs
example-hackathon-demo/.gitignore
example-hackathon-demo/env.example
example-hackathon-demo/index.html
example-hackathon-demo/package.json
example-hackathon-demo/postcss.config.js
example-hackathon-demo/prompt-examples.md
example-hackathon-demo/QUICK_START.md
example-hackathon-demo/README.md
example-hackathon-demo/src/App.jsx
example-hackathon-demo/src/assistants/studyBuddy.assistant.js
example-hackathon-demo/src/index.css
example-hackathon-demo/src/main.jsx
example-hackathon-demo/src/vapi.js
example-hackathon-demo/tailwind.config.js
example-hackathon-demo/TEST_YOUR_SETUP.md
example-hackathon-demo/vite.config.js
README.md
vector-server/Dockerfile
vector-server/main.py
vector-server/requirements.txt
```

### Dependencies

- example-hackathon-demo/package.json: @types/react@^18.2.43, @types/react-dom@^18.2.17, @vapi-ai/web@^2.3.6, @vitejs/plugin-react@^4.2.1, autoprefixer@^10.4.16, clsx@^2.1.1, eslint@^8.55.0, eslint-plugin-react@^7.33.2, eslint-plugin-react-hooks@^4.6.0, eslint-plugin-react-refresh@^0.4.5, framer-motion@^12.18.1, lucide-react@^0.294.0, postcss@^8.4.32, react@^18.2.0, react-dom@^18.2.0, tailwind-merge@^3.3.1, tailwindcss@^3.3.0, vite@^5.0.8
- vector-server/requirements.txt: fastapi, pinecone, pydantic, uuid, uvicorn

### Recent commits (newest first)

- Merge pull request #9 from Aditya-Dawadikar/rag_enhancement
- fix: ingest chunk wise data to pinecone
- update readme.md
- readme.md
- Merge pull request #8 from Aditya-Dawadikar/app_ui
- enhance: app name rendering in browser
- fix: improved prompt to preserve details after retrieval from vector db
- fix: update vector server url in extension
- Merge pull request #7 from Aditya-Dawadikar/vector_server_enhancement
- feat: fall back strategy for rag output and response formatting
- Merge pull request #6 from Aditya-Dawadikar/manju
- updated with retro black theam
- Merge pull request #5 from Aditya-Dawadikar/extension_aesthetic
- feat: make the extension UI have retro terminal look
- ✨ Enhanced SAGE UI: Dark theme, glassmorphism, animations & improved UX
- Merge pull request #4 from Aditya-Dawadikar/extention_vector_server_integration
- feat: Integrated extension with gcp server.
- Merge pull request #3 from Aditya-Dawadikar/vapi_integration
- feat: add demo vapi ui
- Merge pull request #2 from Aditya-Dawadikar/vapi_integration

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

### example-hackathon-demo/TEST_YOUR_SETUP.md

```markdown
# 🧪 Test Your Setup

Follow these steps to ensure everything is working correctly:

## 1. Check Node.js Installation

```bash
node --version
# Should show v16.0.0 or higher
```

## 2. Check npm Installation

```bash
npm --version
# Should show 7.0.0 or higher
```

## 3. Install Dependencies

```bash
npm install
# Should complete without errors
```

## 4. Set Up Your API Key

```bash
# Copy the example environment file
cp env.example .env

# Edit .env and add your Vapi key
# VITE_VAPI_PUBLIC_KEY=pk_your_actual_key_here
```

## 5. Start the Development Server

```bash
npm run dev
# Should open browser at http://localhost:3000
```

## 6. Test the Application

1. **Visual Check**:
   - Beautiful gradient background ✓
   - White card with Study Buddy title ✓
   - Start Conversation button ✓
   - Feature list visible ✓

2. **Console Check**:
   - Open browser DevTools (F12)
   - Check Console tab for errors
   - Should see "Vapi initialized" or similar

3. **API Key Check**:
   - If you see "Please add your Vapi API key"
   - Make sure .env file exists
   - Make sure key starts with `pk_`
   - Restart dev server after adding key

4. **Microphone Check**:
   - Click "Start Conversation"
   - Browser should ask for microphone permission
   - Allow microphone access

## ✅ Success Indicators

- No red error messages in the UI
- Button changes to "Connecting..." when clicked
- Browser asks for microphone permission
- Status changes to "Connected!" after allowing mic

## ❌ Common Issues

| Problem | Solution |
|---------|----------|
| "Command not found: npm" | Install Node.js from nodejs.org |
| "Cannot find module" | Run `npm install` again |
| "Port 3000 in use" | Kill the process or use different port |
| Blank white screen | Check browser console for errors |
| Tailwind styles not working | Restart dev server |

## 🎉 Ready for Workshop

If all tests pass, you're ready to build your AI Study Buddy!

```

### example-hackathon-demo/prompt-examples.md

```markdown
# 🎨 Vapi Prompt Examples

Use these prompts as inspiration for your hackathon projects!

## 📚 Educational Assistants

### Language Learning Partner

```
You are a friendly language learning partner helping users practice [Spanish/French/etc]. 
- Speak slowly and clearly
- Correct mistakes gently
- Suggest better ways to phrase things
- Keep conversations at beginner/intermediate level
- Use common everyday topics
```

### Math Tutor

```
You are a patient math tutor specializing in helping students understand concepts, not just get answers.
- Ask guiding questions instead of giving direct answers
- Break down complex problems into steps
- Celebrate small victories
- If student is stuck, provide hints progressively
- Focus on understanding over speed
```

## 🏥 Healthcare Assistants

### Appointment Scheduler

```
You are a medical appointment scheduling assistant for [Clinic Name].
- Be professional yet warm
- Collect: name, reason for visit, preferred dates/times
- Mention available slots
- Confirm all details before ending
- If emergency, advise to call 911 or visit ER
```

### Mental Health Check-in

```
You are a supportive mental wellness assistant (NOT a therapist).
- Use empathetic, non-judgmental language
- Ask open-ended questions about their day
- Suggest simple wellness activities (breathing, walks)
- If user expresses serious concerns, provide crisis hotline info
- Keep tone gentle and supportive
```

## 💼 Business Assistants

### Customer Support Bot

```
You are a helpful customer support agent for [Company].
- Greet warmly and ask how you can help
- Common issues: order status, returns, product questions
- If you don't know, offer to connect with human agent
- Always confirm you've resolved their issue
- End with satisfaction check
```

### Sales Qualifier

```
You are a friendly sales assistant qualifying leads for [Product/Service].
- Introduce yourself and company briefly
- Ask about their current solution/pain points
- Gauge budget range and timeline
- Determine decision-making process
- If qualified, offer to schedule demo
```

## 🎮 Fun & Creative Assistants

### Dungeon Master

```
You are an enthusiastic Dungeon Master for a voice-based D&D adventure.
- Set scene vividly but concisely
- Give 2-3 clear action options
- React dramatically to player choices
- Track basic health/inventory
- Keep adventures short (5-10 minutes)
```

### Joke Buddy

```
You are a cheerful joke-telling companion.
- Start with "Want to hear a joke?"
- Tell age-appropriate, clean jokes
- React positively to user's jokes
- Can do joke categories (dad jokes, puns, etc.)
- Keep energy high and playful
```

## 🏃 Fitness & Wellness

### Workout Coach

```
You are an energetic fitness coach leading voice-guided workouts.
- Ask about fitness level first
- Give clear, timed instructions
- Count reps enthusiastically
- Provide modifications for exercises
- Celebrate completion
```

### Meditation Guide

```
You are a calm meditation guide.
- Speak slowly
[truncated — 1540 more characters]
```

### vector-server/requirements.txt

```
fastapi
uvicorn
pinecone
uuid
pydantic
```

### vector-server/Dockerfile

```
# Use official Python base image
FROM python:3.11-slim

# Set working directory
WORKDIR /app

# Install system dependencies
RUN apt-get update && apt-get install -y build-essential

# Copy requirements and install
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy app code
COPY . .

# Expose port
EXPOSE 8000

# Start FastAPI server
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

```

### example-hackathon-demo/package.json

```
{
  "name": "vapi-workshop",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "description": "AI Study Buddy - Vapi Voice AI Workshop for Hackathons",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0"
  },
  "keywords": [
    "vapi",
    "voice-ai",
    "workshop",
    "hackathon",
    "education",
    "react"
  ],
  "author": "",
  "license": "MIT",
  "dependencies": {
    "@vapi-ai/web": "^2.3.6",
    "clsx": "^2.1.1",
    "framer-motion": "^12.18.1",
    "lucide-react": "^0.294.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "tailwind-merge": "^3.3.1"
  },
  "devDependencies": {
    "@types/react": "^18.2.43",
    "@types/react-dom": "^18.2.17",
    "@vitejs/plugin-react": "^4.2.1",
    "autoprefixer": "^10.4.16",
    "eslint": "^8.55.0",
    "eslint-plugin-react": "^7.33.2",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.5",
    "postcss": "^8.4.32",
    "tailwindcss": "^3.3.0",
    "vite": "^5.0.8"
  }
}

```

### vector-server/main.py

```python
from fastapi import FastAPI, Query
from pinecone import Pinecone
import requests
import json
import re
from pydantic import BaseModel
from uuid import uuid4
import os


DESIRED_DIM = 384

def extract_floats(raw_text, expected_dim=384):
    # Extract floats using regex
    floats = re.findall(r"[-+]?\d*\.\d+|\d+", raw_text)
    vec = [float(x) for x in floats]

    # Adjust to exactly expected_dim length
    if len(vec) > expected_dim:
        return vec[:expected_dim]
    elif len(vec) < expected_dim:
        vec += [0.0] * (expected_dim - len(vec))
    return vec

PINECONE_API_KEY = os.getenv("PINECONE_API_KEY", "pcsk_41cTQL_UwaZpN75s2h6kDLnxKQJtMkYoV17Z2bGpScQPnyiFbNzbmoL8DKNqhZ9SFytjQD")
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "AIzaSyBV3uUh9KkOdupuyPTM9iOdITJK601utmM")

if not PINECONE_API_KEY or not GEMINI_API_KEY:
    raise EnvironmentError("Missing required environment variables.")

# Setup
pc = Pinecone(api_key=PINECONE_API_KEY)
index = pc.Index("sage-index")

app = FastAPI()


def embed_with_gemini(text: str):
    url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={GEMINI_API_KEY}"
    headers = { "Content-Type": "application/json" }
    payload = {
        "contents": [
            {
                "parts": [
                    {
                        "text": f"Generate exactly a {DESIRED_DIM}-dimensional float embedding array for this sentence. Output only the numbers in a JSON-like format, no explanation:\n\n{text}"
                    }
                ]
            }
        ]
    }

    response = requests.post(url, headers=headers, json=payload)
    data = response.json()

    try:
        raw_text = data['candidates'][0]['content']['parts'][0]['text'].strip()

        # Clean up markdown if any
        raw_text = re.sub(r"^```(?:json)?", "", raw_text)
        raw_text = re.sub(r"```$", "", raw_text)

        return extract_floats(raw_text, expected_dim=DESIRED_DIM)

    except Exception as e:
        raise RuntimeError(f"Failed to parse Gemini embedding.\nError: {e}\nRaw response: {raw_text}\nFull data: {data}")

SCORE_THRESHOLD = 0.01  # Minimum confidence for Pinecone match to be used

async def query_gemini_directly(query: str) -> str:
    prompt = f"""
You are a helpful assistant. Answer the following user query with general knowledge, phrased naturally for speech synthesis. If you don't know the answer, say so.

User Query:
{query}
"""

    url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={GEMINI_API_KEY}"
    headers = {"Content-Type": "application/json"}
    payload = {
        "contents": [{"parts": [{"text": prompt}]}]
    }

    response = requests.post(url, headers=headers, json=payload)
    data = response.json()

    try:
        # return data['candidates'][0]['content']['parts'][0]['text'].strip()
        answer = data['candidates'][0]['content']['parts'][0]['text'].strip()
        if answer:
            return f"Since no relevant history was found, here’s a general answer based on available knowledge: \n{answer}"
        return ""
    except Exception:
        return ""

async def stitch_matches_with_gemini(query: str, matches: list[dict]) -> str:
    # raw_texts = [m["metadata"].get("raw_text", "") for m in matches if m.get("score", 0.0) >= SCORE_THRESHOLD]
    raw_texts = [m["metadata"].get("raw_text", "") for m in matches]

    if not raw_texts:
        return ""

    prompt = f"""
You are my memory assistant. Use the following retrieved information to answer the user's query while **preserving factual accuracy, URLs, and original phrasing where appropriate**.

**Do:**
- Extract and retain important names, definitions, quotes, and URLs.
- Quote original language when it’s meaningful (e.g. definitions or historical facts).
- Highlight or cite key links using markdown (e.g. [label](url)).
- Summarize clearly but with source fidelity.

**Don't:**
- Paraphrase essential facts unless clarity requires it.
- Remove links or references.
- Invent or hallucinate anything.

---

User Query:
{query}

Retrieved Context:
{chr(10).join([f'- {txt}' for txt in raw_texts if txt.strip()])}

Now respond in a clear and friendly tone, suitable for speech, but include original factual structure and references.
"""

    url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={GEMINI_API_KEY}"
    headers = {"Content-Type": "application/json"}
    payload = {
        "contents": [{"parts": [{"text": prompt}]}]
    }

    response = requests.post(url, headers=headers, json=payload)
    data = response.json()

    print("stitch_matches_with_gemini data:", data)

    try:
        return data['candidates'][0]['content']['parts'][0]['text'].strip()
    except Exception:
        return ""

class TextPayload(BaseModel):
    text: str

class QueryPayload(BaseModel):
    query: str

import textwrap

def chunk_text(text, max_words=80):
    sentences = re.split(r'(?<=[.?!])\s+', text)
    chunks = []
    chunk = ""
    for sentence in sentences:
        if len((chunk + " " + sentence).split()) > max_words:
            chunks.append(chunk.strip())
            chunk = sentence
        else:
            chunk += " " + sentence
    if chunk:
        chunks.append(chunk.strip())
    return chunks

# @app.post("/vectorize")
# async def vectorize(payload: TextPayload):
#     vector_id = str(uuid4())
#     vec = embed_with_gemini(payload.text)
    
#     index.upsert(
#         vectors=[
#             {
#                 "id": vector_id,
#                 "values": vec,
#                 "metadata": {
#                     "category": "general",
#                     "raw_text": payload.text
#                 }
#             }
#         ],
#         namespace="example-namespace"
#     )
#     return {"message": f"Vector inserted", "id": vector_id}

@app.post("/vectorize")
async def vectorize(payload: TextPayload):
    text_chunks = chunk_text(p
[truncated — 2627 more characters]
```

### example-hackathon-demo/src/main.jsx

```javascript
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
) 
```

### example-hackathon-demo/src/App.jsx

```javascript
import { useState, useEffect, useRef } from 'react'
import { vapi } from './vapi'

const VAPI_PUBLIC_KEY = import.meta.env.VITE_VAPI_PUBLIC_KEY
const VAPI_AGENT_ID = import.meta.env.VITE_VAPI_AGENT_ID

const CALL_STATUS = {
  INACTIVE: 'inactive',
  ACTIVE: 'active',
  LOADING: 'loading'
}

function App() {
  const [callStatus, setCallStatus] = useState(CALL_STATUS.INACTIVE)
  const [assistantIsSpeaking, setAssistantIsSpeaking] = useState(false)
  const [error, setError] = useState('')
  const [volumeLevel, setVolumeLevel] = useState(0)
  const [isMuted, setIsMuted] = useState(false)
  const [messages, setMessages] = useState([])
  const [activeTranscript, setActiveTranscript] = useState(null)
  const messagesEndRef = useRef(null)

  const isLoading = callStatus === CALL_STATUS.LOADING
  const isActive = callStatus === CALL_STATUS.ACTIVE

  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
  }, [messages, activeTranscript])

  useEffect(() => {
    if (!VAPI_PUBLIC_KEY || VAPI_PUBLIC_KEY === 'your_vapi_public_key_here') {
      setError('Please add your Vapi API key to .env file')
      return
    }

    const onCallStart = () => {
      setCallStatus(CALL_STATUS.ACTIVE)
      setError('')
      addMessage('system', '📞 Call connected')
    }

    const onCallEnd = () => {
      setCallStatus(CALL_STATUS.INACTIVE)
      setAssistantIsSpeaking(false)
      setVolumeLevel(0)
      setActiveTranscript(null)
      addMessage('system', '📴 Call ended')
    }

    const onSpeechStart = () => setAssistantIsSpeaking(true)
    const onSpeechEnd = () => setAssistantIsSpeaking(false)
    const onVolumeLevel = (vol) => setVolumeLevel(vol)

    const onMessage = (msg) => {
      if (msg.type === 'transcript') {
        if (msg.transcriptType === 'partial') {
          setActiveTranscript({
            role: msg.role,
            text: msg.transcript,
            timestamp: Date.now()
          })
        } else {
          setActiveTranscript(null)
          addMessage(msg.role, msg.transcript)
        }
      } else if (msg.type === 'function-call') {
        addMessage('system', `🔧 Function called: ${msg.functionCall.name}`)
      } else if (msg.type === 'hang') {
        addMessage('system', '👋 Call ended by assistant')
      }
    }

    const onError = (err) => {
      setError(err.message || 'An error occurred')
      setCallStatus(CALL_STATUS.INACTIVE)
      addMessage('system', `❌ Error: ${err.message || err}`)
    }

    vapi.on('call-start', onCallStart)
    vapi.on('call-end', onCallEnd)
    vapi.on('speech-start', onSpeechStart)
    vapi.on('speech-end', onSpeechEnd)
    vapi.on('volume-level', onVolumeLevel)
    vapi.on('message', onMessage)
    vapi.on('error', onError)

    return () => {
      vapi.off('call-start', onCallStart)
      vapi.off('call-end', onCallEnd)
      vapi.off('speech-start', onSpeechStart)
      vapi.off('speech-end', onSpeechEnd)
      vapi.off('volume-level', onVolumeLevel)
      vapi.off('message', onMessage)
      vapi.off('error', onError)
    }
  }, [])

  const addMessage = (type, content) => {
    const newMsg = {
      id: Date.now(),
      type,
      content,
      time: new Date().toLocaleTimeString('en-US', {
        hour: '2-digit',
        minute: '2-digit'
      })
    }
    setMessages(prev => [...prev, newMsg])
  }

  const toggleCall = async () => {
    if (isActive) {
      setCallStatus(CALL_STATUS.LOADING)
      vapi.stop()
    } else if (!isLoading) {
      setCallStatus(CALL_STATUS.LOADING)
      setError('')
      addMessage('system', '🔄 Starting call...')
      
      try {
        await vapi.start(VAPI_AGENT_ID)
      } catch (error) {
        console.error('Failed to start call:', error)
        setError('Failed to connect. Please check your API key and internet connection.')
        setCallStatus(CALL_STATUS.INACTIVE)
      }
    }
  }

  return (
    <div className="bg-black text-cyan-300 font-[VT323] text-xl p-2 min-h-screen flex flex-col items-center justify-start">
      {/* ASCII Banner */}
      <pre className="text-cyan-400 text-lg leading-tight font-bold tracking-wider">

{`+-----------------------------------------------------------------+
|     ____       _       ____   _____              _    ___       |
|    / ___|     / \\     / ___| | ____|            / \\  |_ _|      |
|    \\___ \\    / _ \\   | |  _  |  _|    _____    / _ \\  | |       |
|     ___) |  / ___ \\  | |_| | | |___  |_____|  / ___ \\ | |       |
|    |____/  /_/   \\_\\  \\____| |_____|         /_/   \\_\\___|      |
+-----------------------------------------------------------------+`}
      </pre>

      {/* Subtitles */}
      <p className="subtext text-cyan-300">SAGE: System for Automatic Gyan Extraction</p>
      <p className="sysinfo text-cyan-500">[v1.0.1] - [ {isLoading ? 'Connecting' : isActive ? 'Session Live' : 'Ready to Connect'} ]</p>
      <p className="cursor text-cyan-400">&gt;</p>

      {/* Message Log Area */}
      <div className="w-[700px] h-[250px] overflow-y-scroll scrollbar-hide border border-cyan-700 p-4 rounded-md bg-black/80 space-y-2 mt-4">
        {messages.length === 0 && !activeTranscript ? (
          <div className="text-center text-cyan-600 mt-24">
            <p className="mb-2">[ SYSTEM READY ]</p>
            <p>Press START VOICE CHAT to begin...</p>
          </div>
        ) : (
          <>
            {messages.map((m) => (
              <div key={m.id} className="whitespace-pre-wrap">
                <span className={m.type=='user'?"text-green-500":"text-cyan-500"}>[{m.time}]</span>
                <span className={m.type=='user'?"text-green-500 font-bold ml-2":"text-cyan-400 font-bold ml-2"}>
                  {m.type === 'user' ? 'YOU' : m.type === 'assistant' ? 'SAGE' : 'SYSTEM'}:
                </span>
                <span className={m.type=='user'?"ml-2 text-green-500":"ml-2 text-cyan-300"}>{m.content}</span>
              </div>
            ))}
            {activeTranscri
[truncated — 1134 more characters]
```

### example-hackathon-demo/postcss.config.js

```javascript
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
} 
```

### example-hackathon-demo/vite.config.js

```javascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
    port: 3000,
    open: true
  }
}) 
```

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