# Project export: Aurora

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: Cal Hacks 12.0
- Tagline: Agentic Browsing for Blind people
- Devpost: https://devpost.com/software/aurora-xcsafk
- GitHub: https://github.com/theVedanta/calhacks
- Video: https://www.youtube.com/embed/aWuAMARpaSA?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Vedanta Somnathe (10 commits)

## Devpost submission (written by the team)

### Inspiration

Navigating the internet as a blind user is overwhelmingly difficult. Existing screen readers often feel rigid, slow, or detached from modern AI advancements. Aurora was inspired by the idea of a web that listens, understands, and acts — a browser where users describe what they need, and AI interprets, reads, and interacts on their behalf naturally.

### What it does

Aurora is an AI-driven browser designed specifically for blind and visually impaired people. It listens to spoken commands through LiveKit’s real-time speech interface, transcribes and understands intent with Groq Whisper 3, and automates browsing actions with Playwright MCP. Users can ask Aurora to “open my email,” “summarize this page,” or “read only the important links,” and it handles everything in real time — no mouse, no menus, no visual clutter

### How we built it

Aurora’s core pipeline combines: Playwright MCP (multi-code protocol) to drive browser automation, DOM traversal, and interaction with web elements. LiveKit for ultra-low-latency voice streaming and bidirectional speech support. Groq Whisper 3, the heart of Aurora’s speech-to-text layer, enabling lightning-fast and accurate command recognition on-device. Mem0 for persisting memory over sessions. Custom prompt orchestration to interpret user intent, trigger Playwright scripts, and return readable or spoken summaries using integrated generative models. The frontend was built in a minimal Electron environment to keep it lightweight and cross-platform, while backend operations run asynchronously to maintain responsiveness.

### Challenges we ran into

Voice intent parsing: Translating freeform, natural-language voice input into structured browser actions was harder than expected. Accessibility testing: Simulating blind-user workflows revealed subtle issues — for example, ARIA tags and page segmentation matter far more for spoken navigation than for visual browsers. Latency constraints: Ensuring that speech streaming through LiveKit stayed real-time even under network variation required careful threading and buffering control.

### Accomplishments we're proud of

Built a fully voice-driven browser from scratch using Playwright automation and real-time STT (speech-to-text). Achieved sub-150ms latency speech recognition with Groq Whisper 3 — fast enough for natural conversation. Successfully ran user tests with visually impaired participants who were able to independently browse and summarize live web content for the first time without external help.

### What we learned

Building for accessibility isn’t just about adding “voice support.” It’s about rethinking the interface entirely. Text, layout, and interaction flow matter differently for someone who experiences the web acoustically. We also learned that edge speech inference (like Groq’s) completely changes what “real-time” feels like.

### What's next

We’re expanding Aurora into a plugin-driven accessibility platform: Adding contextual web memory so users don't have to give context about their work daily. Building a community-driven dataset for blind-user web experiences to improve accuracy and personalization. Ultimately, Aurora aims to redefine what it means to browse the web — not by sight, but by sound and understanding.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 40 recognized source files, 302 KB.
- Anthropic (technology) — detected in the code
- Cohere (technology) — detected in the code
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- Hugging Face (technology) — detected in the code
- Mistral AI (technology) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (48 of 48)

```
server/.gitignore
server/.mcp.json
server/main.py
server/requirements.txt
web/.gitignore
web/app/globals.css
web/app/layout.tsx
web/app/page.tsx
web/components.json
web/components/insights-list.tsx
web/components/pdf-uploader.tsx
web/components/ui/alert.tsx
web/components/ui/aspect-ratio.tsx
web/components/ui/avatar.tsx
web/components/ui/badge.tsx
web/components/ui/breadcrumb.tsx
web/components/ui/button.tsx
web/components/ui/card.tsx
web/components/ui/carousel.tsx
web/components/ui/checkbox.tsx
web/components/ui/context-menu.tsx
web/components/ui/dialog.tsx
web/components/ui/drawer.tsx
web/components/ui/dropdown-menu.tsx
web/components/ui/input-otp.tsx
web/components/ui/input.tsx
web/components/ui/label.tsx
web/components/ui/navigation-menu.tsx
web/components/ui/pagination.tsx
web/components/ui/radio-group.tsx
web/components/ui/select.tsx
web/components/ui/separator.tsx
web/components/ui/sheet.tsx
web/components/ui/sidebar.tsx
web/components/ui/skeleton.tsx
web/components/ui/sonner.tsx
web/components/ui/switch.tsx
web/components/ui/tabs.tsx
web/components/ui/textarea.tsx
web/components/ui/tooltip.tsx
web/eslint.config.mjs
web/hooks/use-mobile.ts
web/lib/utils.ts
web/next.config.ts
web/package.json
web/postcss.config.mjs
web/README.md
web/tsconfig.json
```

### Dependencies

- server/requirements.txt: ag-ui-protocol@==0.1.9, aiofiles@==25.1.0, aiohappyeyeballs@==2.6.1, aiohttp@==3.13.1, aiosignal@==1.4.0, annotated-doc@==0.0.3, annotated-types@==0.7.0, anthropic@==0.71.0, anyio@==4.11.0, argcomplete@==3.6.3, attrs@==25.4.0, Authlib@==1.6.5, av@==15.1.0, beartype@==0.22.3, boto3@==1.40.59, botocore@==1.40.59, cachetools@==6.2.1, certifi@==2025.10.5, cffi@==2.0.0, charset-normalizer@==3.4.4, click@==8.3.0, cohere@==5.20.0, colorama@==0.4.6, coloredlogs@==15.0.1, cryptography@==46.0.3, cyclopts@==4.0.0, dataclasses-json@==0.6.7, diskcache@==5.6.3, distro@==1.9.0, dnspython@==2.8.0, docstring_parser@==0.17.0, docutils@==0.22.2, email-validator@==2.3.0, eval_type_backport@==0.2.2, exceptiongroup@==1.3.0, executing@==2.2.1, faiss-cpu@==1.12.0, fastapi@==0.120.0, fastavro@==1.12.1, fastmcp@==2.13.0, filelock@==3.20.0, flatbuffers@==25.9.23, frozenlist@==1.8.0, fsspec@==2025.9.0, genai-prices@==0.0.35, google-auth@==2.41.1, google-genai@==1.46.0, googleapis-common-protos@==1.71.0, griffe@==1.14.0, groq@==0.33.0, grpcio@==1.76.0, h11@==0.16.0, hf-xet@==1.2.0, httpcore@==1.0.9, httpx@==0.28.1, httpx-sse@==0.4.0, huggingface-hub@==0.36.0, humanfriendly@==10.0, idna@==3.11, importlib_metadata@==8.7.0, invoke@==2.2.1, isodate@==0.7.2, jaraco.classes@==3.4.0, jaraco.context@==6.0.1, jaraco.functools@==4.3.0, Jinja2@==3.1.6, jiter@==0.11.1, jmespath@==1.0.1, jsonpatch@==1.33, jsonpointer@==3.0.0, jsonschema@==4.25.1, jsonschema-path@==0.3.4, jsonschema-specifications@==2025.9.1, keyring@==25.6.0, langchain-classic@==1.0.0, langchain-community@==0.4, langchain-core@==1.0.1, langchain-openai@==1.0.1, langchain-text-splitters@==1.0.0, langsmith@==0.4.38, lazy-object-proxy@==1.12.0, livekit@==1.0.17, livekit-agents@==1.2.15, livekit-api@==1.0.7, livekit-blingfire@==1.0.0, livekit-plugins-deepgram@==1.2.15, livekit-plugins-groq@==1.2.15, livekit-plugins-openai@==1.2.15, livekit-plugins-silero@==1.2.15, livekit-plugins-turn-detector@==1.2.15, livekit-protocol@==1.0.8, logfire@==4.14.2, logfire-api@==4.14.2, markdown-it-py@==4.0.0, MarkupSafe@==3.0.3, marshmallow@==3.26.1, mcp@==1.19.0, mdurl@==0.1.2, mistralai@==1.9.11, more-itertools@==10.8.0, mpmath@==1.3.0, multidict@==6.7.0, mypy_extensions@==1.1.0, nest-asyncio@==1.6.0, nexus-rpc@==1.1.0, numpy@==2.3.4, onnxruntime@==1.23.2, openai@==2.6.1, openapi-core@==0.19.5, openapi-pydantic@==0.5.1, openapi-schema-validator@==0.6.3, openapi-spec-validator@==0.7.2, opentelemetry-api@==1.38.0, opentelemetry-exporter-otlp@==1.38.0, opentelemetry-exporter-otlp-proto-common@==1.38.0, opentelemetry-exporter-otlp-proto-grpc@==1.38.0, opentelemetry-exporter-otlp-proto-http@==1.38.0, opentelemetry-instrumentation@==0.59b0, opentelemetry-instrumentation-httpx@==0.59b0, opentelemetry-proto@==1.38.0, opentelemetry-sdk@==1.38.0, opentelemetry-semantic-conventions@==0.59b0, opentelemetry-util-http@==0.59b0, orjson@==3.11.4, packaging@==25.0, parse@==1.20.2, pathable@==0.4.4, pathvalidate@==3.3.1, pillow@==12.0.0, platformdirs@==4.5.0, prometheus_client@==0.23.1, prompt_toolkit@==3.0.52, propcache@==0.4.1, protobuf@==6.33.0, psutil@==7.1.2, py-key-value-aio@==0.2.8, py-key-value-shared@==0.2.8, pyasn1@==0.6.1, pyasn1_modules@==0.4.2, pycparser@==2.23, pydantic@==2.12.3, pydantic_core@==2.41.4, pydantic-ai@==1.6.0, pydantic-ai-slim@==1.6.0, pydantic-evals@==1.6.0, pydantic-graph@==1.6.0, pydantic-settings@==2.11.0, Pygments@==2.19.2, PyJWT@==2.10.1, pypdf@==6.1.3, pyperclip@==1.11.0, python-dateutil@==2.9.0.post0, python-dotenv@==1.1.1, python-multipart@==0.0.20, PyYAML@==6.0.3, referencing@==0.36.2, regex@==2025.10.23, requests@==2.32.5, requests-toolbelt@==1.0.0, rfc3339-validator@==0.1.4, rich@==14.2.0, rich-rst@==1.3.2, rpds-py@==0.28.0, rsa@==4.9.1, s3transfer@==0.14.0, safetensors@==0.6.2, six@==1.17.0, sniffio@==1.3.1, sounddevice@==0.5.3, SQLAlchemy@==2.0.44, sse-starlette@==3.0.2, starlette@==0.48.0, sympy@==1.14.0, temporalio@==1.18.0, tenacity@==9.1.2, tiktoken@==0.12.0, tokenizers@==0.22.1, tqdm@==4.67.1, transformers@==4.57.1, types-protobuf@==6.32.1.20250918, types-requests@==2.32.4.20250913, typing_extensions@==4.15.0, typing-inspect@==0.9.0, typing-inspection@==0.4.2, urllib3@==2.5.0, uvicorn@==0.38.0, watchfiles@==1.1.1, wcwidth@==0.2.14, websockets@==15.0.1, Werkzeug@==3.1.1, wrapt@==1.17.3, yarl@==1.22.0, zipp@==3.23.0, zstandard@==0.25.0
- web/package.json: @radix-ui/react-aspect-ratio@^1.1.7, @radix-ui/react-avatar@^1.1.10, @radix-ui/react-checkbox@^1.3.3, @radix-ui/react-context-menu@^2.2.16, @radix-ui/react-dialog@^1.1.15, @radix-ui/react-dropdown-menu@^2.1.16, @radix-ui/react-label@^2.1.7, @radix-ui/react-navigation-menu@^1.2.14, @radix-ui/react-radio-group@^1.3.8, @radix-ui/react-select@^2.2.6, @radix-ui/react-separator@^1.1.7, @radix-ui/react-slot@^1.2.3, @radix-ui/react-switch@^1.2.6, @radix-ui/react-tabs@^1.1.13, @radix-ui/react-tooltip@^1.2.8, @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, class-variance-authority@^0.7.1, clsx@^2.1.1, embla-carousel-react@^8.6.0, eslint@^9, eslint-config-next@16.0.0, input-otp@^1.4.2, lucide-react@^0.548.0, next@16.0.0, next-themes@^0.4.6, react@19.2.0, react-dom@19.2.0, react-dropzone@^14.3.8, sonner@^2.0.7, tailwind-merge@^3.3.1, tailwindcss@^4, tw-animate-css@^1.4.0, typescript@^5, vaul@^1.1.2

### Recent commits (newest first)

- h
- playwright persists and does well
- persist
- restruct
- mcp
- adding dynamic contexty
- test
- server + web
- web
- init

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

### web/package.json

```
{
  "name": "web",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@radix-ui/react-aspect-ratio": "^1.1.7",
    "@radix-ui/react-avatar": "^1.1.10",
    "@radix-ui/react-checkbox": "^1.3.3",
    "@radix-ui/react-context-menu": "^2.2.16",
    "@radix-ui/react-dialog": "^1.1.15",
    "@radix-ui/react-dropdown-menu": "^2.1.16",
    "@radix-ui/react-label": "^2.1.7",
    "@radix-ui/react-navigation-menu": "^1.2.14",
    "@radix-ui/react-radio-group": "^1.3.8",
    "@radix-ui/react-select": "^2.2.6",
    "@radix-ui/react-separator": "^1.1.7",
    "@radix-ui/react-slot": "^1.2.3",
    "@radix-ui/react-switch": "^1.2.6",
    "@radix-ui/react-tabs": "^1.1.13",
    "@radix-ui/react-tooltip": "^1.2.8",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "embla-carousel-react": "^8.6.0",
    "input-otp": "^1.4.2",
    "lucide-react": "^0.548.0",
    "next": "16.0.0",
    "next-themes": "^0.4.6",
    "react": "19.2.0",
    "react-dom": "19.2.0",
    "react-dropzone": "^14.3.8",
    "sonner": "^2.0.7",
    "tailwind-merge": "^3.3.1",
    "vaul": "^1.1.2"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.0.0",
    "tailwindcss": "^4",
    "tw-animate-css": "^1.4.0",
    "typescript": "^5"
  }
}

```

### server/requirements.txt

```
ag-ui-protocol==0.1.9
aiofiles==25.1.0
aiohappyeyeballs==2.6.1
aiohttp==3.13.1
aiosignal==1.4.0
annotated-doc==0.0.3
annotated-types==0.7.0
anthropic==0.71.0
anyio==4.11.0
argcomplete==3.6.3
attrs==25.4.0
Authlib==1.6.5
av==15.1.0
beartype==0.22.3
boto3==1.40.59
botocore==1.40.59
cachetools==6.2.1
certifi==2025.10.5
cffi==2.0.0
charset-normalizer==3.4.4
click==8.3.0
cohere==5.20.0
colorama==0.4.6
coloredlogs==15.0.1
cryptography==46.0.3
cyclopts==4.0.0
dataclasses-json==0.6.7
diskcache==5.6.3
distro==1.9.0
dnspython==2.8.0
docstring_parser==0.17.0
docutils==0.22.2
email-validator==2.3.0
eval_type_backport==0.2.2
exceptiongroup==1.3.0
executing==2.2.1
faiss-cpu==1.12.0
fastapi==0.120.0
fastavro==1.12.1
fastmcp==2.13.0
filelock==3.20.0
flatbuffers==25.9.23
frozenlist==1.8.0
fsspec==2025.9.0
genai-prices==0.0.35
google-auth==2.41.1
google-genai==1.46.0
googleapis-common-protos==1.71.0
griffe==1.14.0
groq==0.33.0
grpcio==1.76.0
h11==0.16.0
hf-xet==1.2.0
httpcore==1.0.9
httpx==0.28.1
httpx-sse==0.4.0
huggingface-hub==0.36.0
humanfriendly==10.0
idna==3.11
importlib_metadata==8.7.0
invoke==2.2.1
isodate==0.7.2
jaraco.classes==3.4.0
jaraco.context==6.0.1
jaraco.functools==4.3.0
Jinja2==3.1.6
jiter==0.11.1
jmespath==1.0.1
jsonpatch==1.33
jsonpointer==3.0.0
jsonschema==4.25.1
jsonschema-path==0.3.4
jsonschema-specifications==2025.9.1
keyring==25.6.0
langchain-classic==1.0.0
langchain-community==0.4
langchain-core==1.0.1
langchain-openai==1.0.1
langchain-text-splitters==1.0.0
langsmith==0.4.38
lazy-object-proxy==1.12.0
livekit==1.0.17
livekit-agents==1.2.15
livekit-api==1.0.7
livekit-blingfire==1.0.0
livekit-plugins-deepgram==1.2.15
livekit-plugins-groq==1.2.15
livekit-plugins-openai==1.2.15
livekit-plugins-silero==1.2.15
livekit-plugins-turn-detector==1.2.15
livekit-protocol==1.0.8
logfire==4.14.2
logfire-api==4.14.2
markdown-it-py==4.0.0
MarkupSafe==3.0.3
marshmallow==3.26.1
mcp==1.19.0
mdurl==0.1.2
mistralai==1.9.11
more-itertools==10.8.0
mpmath==1.3.0
multidict==6.7.0
mypy_extensions==1.1.0
nest-asyncio==1.6.0
nexus-rpc==1.1.0
numpy==2.3.4
onnxruntime==1.23.2
openai==2.6.1
openapi-core==0.19.5
openapi-pydantic==0.5.1
openapi-schema-validator==0.6.3
openapi-spec-validator==0.7.2
opentelemetry-api==1.38.0
opentelemetry-exporter-otlp==1.38.0
opentelemetry-exporter-otlp-proto-common==1.38.0
opentelemetry-exporter-otlp-proto-grpc==1.38.0
opentelemetry-exporter-otlp-proto-http==1.38.0
opentelemetry-instrumentation==0.59b0
opentelemetry-instrumentation-httpx==0.59b0
opentelemetry-proto==1.38.0
opentelemetry-sdk==1.38.0
opentelemetry-semantic-conventions==0.59b0
opentelemetry-util-http==0.59b0
orjson==3.11.4
packaging==25.0
parse==1.20.2
pathable==0.4.4
pathvalidate==3.3.1
pillow==12.0.0
platformdirs==4.5.0
prometheus_client==0.23.1
prompt_toolkit==3.0.52
propcache==0.4.1
protobuf==6.33.0
psutil==7.1.2
py-key-value-aio==0.2.8
py-key-value-shared==0.2.8
pyasn1==0.6.1
pyasn1_modules==0.4.2
pycparser==2.23
pydantic==2.12.3
pydantic-ai==1.6.0
pydantic-ai-slim==1.6.0
pydantic-evals==1.6.0
pydantic-graph==1.6.0
pydantic-settings==2.11.0
pydantic_core==2.41.4
Pygments==2.19.2
PyJWT==2.10.1
pypdf==6.1.3
pyperclip==1.11.0
python-dateutil==2.9.0.post0
python-dotenv==1.1.1
python-multipart==0.0.20
PyYAML==6.0.3
referencing==0.36.2
regex==2025.10.23
requests==2.32.5
requests-toolbelt==1.0.0
rfc3339-validator==0.1.4
rich==14.2.0
rich-rst==1.3.2
rpds-py==0.28.0
rsa==4.9.1
s3transfer==0.14.0
safetensors==0.6.2
six==1.17.0
sniffio==1.3.1
sounddevice==0.5.3
SQLAlchemy==2.0.44
sse-starlette==3.0.2
starlette==0.48.0
sympy==1.14.0
temporalio==1.18.0
tenacity==9.1.2
tiktoken==0.12.0
tokenizers==0.22.1
tqdm==4.67.1
transformers==4.57.1
types-protobuf==6.32.1.20250918
types-requests==2.32.4.20250913
typing-inspect==0.9.0
typing-inspection==0.4.2
typing_extensions==4.15.0
urllib3==2.5.0
uvicorn==0.38.0
watchfiles==1.1.1
wcwidth==0.2.14
websockets==15.0.1
Werkzeug==3.1.1
wrapt==1.17.3
yarl==1.22.0
zipp==3.23.0
zstandard==0.25.0

```

### server/main.py

```python
from dotenv import load_dotenv

from livekit.agents import UserInputTranscribedEvent
from livekit import agents
from livekit.agents import AgentSession, Agent, RoomInputOptions, ChatContext
from livekit.plugins import groq, silero
from livekit.agents import ChatMessage
from claude_agent_sdk import (
    ClaudeAgentOptions,
    ClaudeSDKClient,
    AssistantMessage,
    TextBlock,
)
from composio import Composio
import os

options = ClaudeAgentOptions(
    mcp_servers={
        "playwright": {
            "command": "pnpx",
            "args": ["@playwright/mcp@latest"],
        },
    },
    allowed_tools=[
        "mcp__playwright__browser_navigate",
        "mcp__playwright__browser_click",
        "mcp__playwright__browser_fill",
        "mcp__playwright__browser_screenshot",
        "mcp__playwright__browser_select",
        "mcp__playwright__browser_type",
        "mcp__playwright__browser_wait_for_element",
        "mcp__playwright__browser_wait_for_selector",
        "mcp__playwright__browser_get_text",
        "mcp__playwright__browser_get_attribute",
        "mcp__playwright__browser_scroll",
        "mcp__playwright__browser_hover",
        "mcp__playwright__browser_press",
        "mcp__playwright__browser_check",
        "mcp__playwright__browser_uncheck",
        "mcp__playwright__browser_evaluate",
        "mcp__playwright__browser_go_back",
        "mcp__playwright__browser_go_forward",
        "mcp__playwright__browser_reload",
        "mcp__playwright__browser_close",
    ],
    max_turns=20,
)


class Assistant(Agent):
    def __init__(self) -> None:
        super().__init__(
            instructions="""You are a helpful voice AI assistant.""",
        )
        self.claude_client = None
        self.first_q = True

    async def initialize_claude_client(self):
        """Initialize the Claude client (keeps browser alive)"""
        if self.claude_client is None:
            self.claude_client = ClaudeSDKClient(options)
            await self.claude_client.__aenter__()

    async def query_claude(self, prompt):
        """Query Claude with the persistent client"""
        if self.claude_client is None:
            await self.initialize_claude_client()

        if self.first_q:
            prompt += " using playwright. Reply super concisely and think very concisely as well, but tell your steps concisely still. No markdown format, try to normalize for speech"
            self.first_q = False

        # Send streaming input
        await self.claude_client.query(prompt)

        # Process responses
        response_text = ""
        async for message in self.claude_client.receive_response():
            if isinstance(message, AssistantMessage):
                for block in message.content:
                    if isinstance(block, TextBlock):
                        print(block.text)
                        await self.session.say(block.text)
                        response_text += block.text

        return response_text

    async def on_user_turn_completed(
        self,
        turn_ctx: ChatContext,
        new_message: ChatMessage,
    ) -> None:
        # RAG function definition omitted for brevity

        result = await self.query_claude(new_message.text_content)

        turn_ctx.add_message(
            role="assistant",
            content=result,
        )

    async def cleanup(self):
        """Cleanup the Claude client when done"""
        if self.claude_client is not None:
            await self.claude_client.__aexit__(None, None, None)
            self.claude_client = None


def process_transcript(text: str):
    # Example: send to another API, store in DB, or run analysis
    print("Transcript:", text)


async def entrypoint(ctx: agents.JobContext):
    session = AgentSession(
        stt=groq.STT(model="whisper-large-v3-turbo", language="en"),
        # llm="openai/gpt-4.1-mini",
        vad=silero.VAD.load(),
        tts="cartesia/sonic-2:9626c31c-bec5-4cca-baa8-f8ba9e84c8bc",
    )

    @session.on("user_input_transcribed")
    def on_user_input_transcribed(event: UserInputTranscribedEvent):
        process_transcript(event.transcript)

    await session.start(room=ctx.room, agent=Assistant())


if __name__ == "__main__":
    agents.cli.run_app(agents.WorkerOptions(entrypoint_fnc=entrypoint))

```

### web/app/page.tsx

```typescript
import { PdfUploader } from "@/components/pdf-uploader"

export default function Home() {
  return (
    <main className="min-h-screen bg-background py-12 px-4">
      <div className="max-w-4xl mx-auto">
        <div className="text-center mb-12">
          <h1 className="text-4xl font-bold text-foreground mb-3 text-balance">PDF Insight Extractor</h1>
          <p className="text-lg text-muted-foreground text-balance">
            Upload any PDF document and get AI-powered key insights instantly
          </p>
        </div>
        <PdfUploader />
      </div>
    </main>
  )
}

```

### web/app/layout.tsx

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

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
      </body>
    </html>
  );
}

```

### web/next.config.ts

```typescript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
};

export default nextConfig;

```

### web/lib/utils.ts

```typescript
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

```

### web/hooks/use-mobile.ts

```typescript
import * as React from "react"

const MOBILE_BREAKPOINT = 768

export function useIsMobile() {
  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)

  React.useEffect(() => {
    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
    const onChange = () => {
      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
    }
    mql.addEventListener("change", onChange)
    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
    return () => mql.removeEventListener("change", onChange)
  }, [])

  return !!isMobile
}

```

### web/components/insights-list.tsx

```typescript
import { Card } from "@/components/ui/card";
import { Lightbulb } from "lucide-react";

interface Insight {
    title: string;
    description: string;
}

interface InsightsListProps {
    insights: Insight[];
}

export function InsightsList({ insights }: InsightsListProps) {
    return (
        <Card className="p-8">
            <div className="flex items-center gap-3 mb-6">
                <div className="rounded-full bg-accent/10 p-2">
                    <Lightbulb className="h-6 w-6 text-accent" />
                </div>
                <h2 className="text-2xl font-bold text-foreground">
                    Key Insights
                </h2>
            </div>

            <div className="space-y-6">
                {insights.map((insight, index) => (
                    <div
                        key={index}
                        className="border-l-4 border-primary pl-6 py-2 space-y-2"
                    >
                        <h3 className="text-lg font-semibold text-foreground">
                            {insight.title}
                        </h3>
                        <p className="text-muted-foreground leading-relaxed">
                            {insight.description}
                        </p>
                    </div>
                ))}
            </div>
        </Card>
    );
}

```

### web/app/globals.css

```css
@import "tailwindcss";
@import "tw-animate-css";

@custom-variant dark (&:is(.dark *));

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --font-sans: var(--font-geist-sans);
  --font-mono: var(--font-geist-mono);
  --color-sidebar-ring: var(--sidebar-ring);
  --color-sidebar-border: var(--sidebar-border);
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
  --color-sidebar-accent: var(--sidebar-accent);
  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
  --color-sidebar-primary: var(--sidebar-primary);
  --color-sidebar-foreground: var(--sidebar-foreground);
  --color-sidebar: var(--sidebar);
  --color-chart-5: var(--chart-5);
  --color-chart-4: var(--chart-4);
  --color-chart-3: var(--chart-3);
  --color-chart-2: var(--chart-2);
  --color-chart-1: var(--chart-1);
  --color-ring: var(--ring);
  --color-input: var(--input);
  --color-border: var(--border);
  --color-destructive: var(--destructive);
  --color-accent-foreground: var(--accent-foreground);
  --color-accent: var(--accent);
  --color-muted-foreground: var(--muted-foreground);
  --color-muted: var(--muted);
  --color-secondary-foreground: var(--secondary-foreground);
  --color-secondary: var(--secondary);
  --color-primary-foreground: var(--primary-foreground);
  --color-primary: var(--primary);
  --color-popover-foreground: var(--popover-foreground);
  --color-popover: var(--popover);
  --color-card-foreground: var(--card-foreground);
  --color-card: var(--card);
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);
}

:root {
  --radius: 0.625rem;
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  --popover: oklch(1 0 0);
  --popover-foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);
  --accent: oklch(0.97 0 0);
  --accent-foreground: oklch(0.205 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);
  --chart-1: oklch(0.646 0.222 41.116);
  --chart-2: oklch(0.6 0.118 184.704);
  --chart-3: oklch(0.398 0.07 227.392);
  --chart-4: oklch(0.828 0.189 84.429);
  --chart-5: oklch(0.769 0.188 70.08);
  --sidebar: oklch(0.985 0 0);
  --sidebar-foreground: oklch(0.145 0 0);
  --sidebar-primary: oklch(0.205 0 0);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.97 0 0);
  --sidebar-accent-foreground: oklch(0.205 0 0);
  --sidebar-border: oklch(0.922 0 0);
  --sidebar-ring: oklch(0.708 0 0);
}

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.205 0 0);
  --card-foreground: oklch(0.985 0 0);
  --popover: oklch(0.205 0 0);
  --popover-foreground: oklch(0.985 0 0);
  --primary: oklch(0.922 0 0);
  --primary-foreground: oklch(0.205 0 0);
  --secondary: oklch(0.269 0 0);
  --secondary-foreground: oklch(0.985 0 0);
  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);
  --accent: oklch(0.269 0 0);
  --accent-foreground: oklch(0.985 0 0);
  --destructive: oklch(0.704 0.191 22.216);
  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 15%);
  --ring: oklch(0.556 0 0);
  --chart-1: oklch(0.488 0.243 264.376);
  --chart-2: oklch(0.696 0.17 162.48);
  --chart-3: oklch(0.769 0.188 70.08);
  --chart-4: oklch(0.627 0.265 303.9);
  --chart-5: oklch(0.645 0.246 16.439);
  --sidebar: oklch(0.205 0 0);
  --sidebar-foreground: oklch(0.985 0 0);
  --sidebar-primary: oklch(0.488 0.243 264.376);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.269 0 0);
  --sidebar-accent-foreground: oklch(0.985 0 0);
  --sidebar-border: oklch(1 0 0 / 10%);
  --sidebar-ring: oklch(0.556 0 0);
}

@layer base {
  * {
    @apply border-border outline-ring/50;
  }
  body {
    @apply bg-background text-foreground;
  }
}

```

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