# Project export: Navi

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: Navigate smarter: a co-pilot for your browser Navi streamlines your browsing experience with immediate assistance, voice navigation, and website recommendations based on predicted user intent.
- Devpost: https://devpost.com/software/navi-7sp5mz
- GitHub: https://github.com/danieltyx/navi-browser-agent
- Demo: https://navi-browser-agent.vercel.app/
- Video: https://www.youtube.com/embed/XXGyFPQvcPk?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner (Groq: Best on Groq Multimodal App Challenge ($1,000 of Groq Swag Store Credit))
- Team: 1 GitHub contributor(s) — Yuxuan Tian (5 commits)

## Devpost submission (written by the team)

### Inspiration

Given the substantial lack of accessibility resources and compliance in modern website UI—over 96% of websites fail to meet accessibility requirements—we wanted to develop an AI-driven copilot agent, Navi, for human browsing, in addition to creating an easier web infrastructure for AI models to interact with. 🌐🤖

### What it does

In short, Navi makes web browsing easier for human users, with accessible voice navigation 🎤, intuitive browsing with user intent prediction 🔮, instant feedback to questions 📝, and personalized further reading suggestions 📚. In terms of serving AI models, Navi extracts web pages more efficiently compared to traditional VLMs. ⚡

### How we built it

To integrate visual context, we captured screenshots of the user's navigation, which were then processed by visual language models (VLMs) to inform Navi's AI. This enabled Navi to understand and interpret website layouts in real time 🖼️. Simultaneously, we harnessed Context, a robust context fetching engine, to analyze user interactions, predicting browsing intent and tailoring recommendations. In the background, Scrapybara autonomously suggests related websites 🌐, while Groq converts audio commands into prompts for OpenAI's 4-o LLM, ensuring rapid and accurate responses ⚙️. Additionally, we leveraged Mistral for fast, simplified website renderings that highlight key information, making navigation even easier for the user 🏃‍♂️.

### Challenges we ran into

Balancing rapid AI responses with efficient web page extraction was just one layer of our challenge 🏔️. A deeper complexity arose from integrating multiple AI components into a unified, accessible system. Each component—whether it was the Context engine, VLMs, or voice processing with Groq—had its own processing speeds, data formats, and dependencies. Merging these disparate systems required careful orchestration to ensure they communicated seamlessly in real time ⏱️. We designed robust interfaces and error-handling protocols to bridge differences in performance and data structure ⚖️. This meant synchronizing outputs from slow VLMs with faster processing modules, ensuring that delays in one area wouldn’t disrupt the overall user experience.

### Accomplishments we're proud of

We're proud to have built Navi as a truly integrated, AI-driven copilot that enhances web accessibility and usability 🌟. Navi not only improves browsing efficiency through context-aware predictions and voice navigation but also serves as a high-performance data extractor for AI models. This project sets a new benchmark for creating accessible digital experiences and bridging the gap between human and AI interaction 🌉.

### What we learned

Throughout this project, we gained invaluable insights into prompt orchestration, context fetching, and the nuances of designing for accessibility. We learned how critical it is to balance technical sophistication with user-centric design, ensuring that advanced features translate into real-world usability for diverse audiences 🎓💡.

### What's next

Looking ahead, we plan to expand Navi's compatibility with more websites and enhance its voice and intent recognition capabilities 🔧. Our next steps include optimizing the system for even faster and more accurate responses ⏩, refining personalized recommendations, and exploring additional accessibility features to further empower users with disabilities ♿.

## README (from the GitHub repository)

# 🚀 Navi – Your AI-Powered Browsing Copilot

Navigate smarter. Navi streamlines your browsing experience with immediate assistance, voice navigation, and intelligent website recommendations based on predicted user intent.

## 🏆 Won TreeHack 2025: Best Multimodal App Challenge by Groq 

## 🛠️ What It Does

Navi revolutionizes browsing by offering: 

✅ Voice Navigation – Control your browser with hands-free commands 

✅ User Intent Prediction  – Understands where you’re headed before you do 

✅ Instant Q&A Feedback – Get real-time answers without searching manually 

✅ Personalized Reading Suggestions – Discover new, relevant content automatically 

✅ Optimized AI Web Extraction – Faster and more accurate page processing for AI models 

[![Demo](https://img.youtube.com/vi/XXGyFPQvcPk/0.jpg)](https://youtu.be/XXGyFPQvcPk)


## ⚙️ How We Built It

Navi integrates multiple AI-powered components:
	•	Visual Context Processing – Screenshots of user navigation are analyzed using Visual Language Models (VLMs) to interpret webpage layouts in real time.
	•	Context Fetching Engine – The Context framework predicts user browsing intent and generates tailored recommendations.
	•	Web Autonomy with Scrapybara – Automatically suggests related websites.
	•	AI-Powered Voice Commands – Groq processes user speech into structured prompts for OpenAI’s GPT-4o, ensuring accurate responses.
	•	Faster Page Rendering – Mistral extracts and simplifies web content for better readability and navigation.

💻 Tech Stack
	•	Frontend: Next.js, TypeScript
	•	AI Models: Groq, OpenAI GPT-4o, Mistral, Perplexity, Gemini
	•	Backend & Contextual Processing: Context, Windsurf
	•	Voice & Web Extraction: ElevenLabs

## 🤝 Contributing

Want to contribute to Navi? Here’s how you can help:

	1.	Fork the repository
 
	2.	Create a new branch (feature-branch)
 
	3.	Commit your changes
 
	4.	Push the branch and submit a PR

## 📜 License
This project is licensed under the MIT License – see the LICENSE file for details.


## Detected evidence (automated analysis)

Indexed codebase: 27 recognized source files, 75 KB.
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- Firebase (technology) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code
- Mistral AI (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (35 of 35)

```
.DS_Store
.gitignore
app/.gitignore
app/api/chat/route.ts
app/api/context/route.ts
app/api/scrapy/scrapy-link-explore.ts
app/api/screenshot/route.ts
app/api/simplify/route.ts
app/api/transcribe/route.ts
app/components/Chat.tsx
app/components/CircularText.tsx
app/components/LinkSuggester.tsx
app/components/RecordButton.tsx
app/components/VoiceRecorder.tsx
app/firebase/config.ts
app/globals.css
app/layout.tsx
app/page.tsx
app/simplified/CircularText.css
app/simplified/CircularText.js
app/simplified/page.tsx
app/tts.ts
audio.ts
bun.lockb
eslint.config.mjs
mic.d.ts
next-env.d.ts
next.config.js
next.config.ts
package.json
play-sound.d.ts
postcss.config.mjs
README.md
tailwind.config.ts
tsconfig.json
```

### Dependencies

- package.json: @eslint/eslintrc@^3, @firebase/storage@^0.13.6, @types/dotenv@^8.2.3, @types/node@^20, @types/react@^19, @types/react-dom@^19, axios@^1.7.9, cheerio@^1.0.0, dotenv@^16.4.7, elevenlabs@^1.51.0, eslint@^9, eslint-config-next@15.1.7, express@^4.21.2, firebase@^11.3.1, form-data@^4.0.2, framer-motion@^12.4.3, groq-sdk@^0.15.0, html2canvas@^1.4.1, install@^0.13.0, multer@^1.4.5-lts.1, next@15.1.7, openai@^4.85.1, postcss@^8, puppeteer@^24.2.1, react@^19.0.0, react-dom@^19.0.0, scrapybara@^2.2.2, tailwindcss@^3.4.1, typescript@5.7.3

### Recent commits (newest first)

- Update README.md
- Update README.md
- Update README.md
- Update page.tsx
- Update page.tsx
- Update route.ts
- fix build
- init

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

### package.json

```
{
  "name": "navi",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@firebase/storage": "^0.13.6",
    "@types/dotenv": "^8.2.3",
    "axios": "^1.7.9",
    "cheerio": "^1.0.0",
    "dotenv": "^16.4.7",
    "elevenlabs": "^1.51.0",
    "express": "^4.21.2",
    "firebase": "^11.3.1",
    "form-data": "^4.0.2",
    "framer-motion": "^12.4.3",
    "groq-sdk": "^0.15.0",
    "html2canvas": "^1.4.1",
    "install": "^0.13.0",
    "multer": "^1.4.5-lts.1",
    "next": "15.1.7",
    "openai": "^4.85.1",
    "puppeteer": "^24.2.1",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "scrapybara": "^2.2.2"
  },
  "devDependencies": {
    "@eslint/eslintrc": "^3",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "15.1.7",
    "postcss": "^8",
    "tailwindcss": "^3.4.1",
    "typescript": "5.7.3"
  }
}

```

### 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: "Navi",
  description: "co-pilot for your browser", 
};

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

```

### app/page.tsx

```typescript
'use client';
import { useState, useRef, useEffect } from "react";
import Image from "next/image";
// import html2canvas from 'html2canvas';
// import VoiceRecorder from './components/VoiceRecorder';
import RecordButton from './components/RecordButton';
import Link from 'next/link';

import { ElevenLabsClient } from 'elevenlabs';
// import axios from 'axios';


interface Screenshot {
  url: string;
  timestamp: number;
  analysis: string;
}

interface SuggestedQuestion {
  id: string;
  text: string;
}

export default function Home() {
  const [url, setUrl] = useState('');
  const [messages, setMessages] = useState<Array<{role: 'user' | 'assistant', content: string}>>([]);
  const [currentContext, setCurrentContext] = useState<string>('');
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [isCapturing, setIsCapturing] = useState(false);
  const iframeRef = useRef<HTMLIFrameElement>(null);
  const [screenshots, setScreenshots] = useState<Screenshot[]>([]);
  const streamRef = useRef<MediaStream | null>(null);
  const screenshotContainerRef = useRef<HTMLDivElement>(null);
  const [suggestedQuestions, setSuggestedQuestions] = useState<SuggestedQuestion[]>([]);
  const [userIntent, setUserIntent] = useState<string>('');

 

  const handleUrlSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!url) return;

    setIsLoading(true);
    setError(null);

    try {
      // Add protocol if missing
      const urlWithProtocol = url.startsWith('http') ? url : `https://${url}`;

      const response = await fetch('/api/screenshot', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ url: urlWithProtocol }),
      });

      if (!response.ok) {
        const errorText = await response.text();
        console.error('Server response:', errorText);
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const contentType = response.headers.get('content-type');
      if (!contentType || !contentType.includes('application/json')) {
        console.error('Invalid content type:', contentType);
        throw new Error('Server did not return JSON');
      }

      

      const data = await response.json();
      
      if (data.success) {
        setCurrentContext(data.analysis);
      } else {
        setError(data.error || 'Failed to capture screenshot');
      }
    } catch (error) {
      setError(`Error processing URL: ${error instanceof Error ? error.message : 'Unknown error'}`);
      console.error('Error:', error);
    } finally {
      setIsLoading(false);
    }
  };

  const initScreenShare = async () => {
    if (streamRef.current) return streamRef.current;
    
    try {
      const stream = await window.navigator.mediaDevices.getDisplayMedia({
        video: { displaySurface: "window" }
      });
      
      // Store stream in ref
      streamRef.current = stream;
      
      // Handle when user stops sharing
      stream.getVideoTracks()[0].onended = () => {
        streamRef.current = null;
      };
      
      return stream;
    } catch (error) {
      console.error('Failed to initialize screen sharing:', error);
      setError('Please allow screen sharing to enable automatic captures');
      return null;
    }
  };

  const queryUserIntent = async (context: string) => {
    try {
      const response = await fetch('/api/context', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ text: context }),
      });

      const data = await response.json();
      if (data.success && data.data && typeof data.data.response === 'string') {
        return data.data.response;
      }
      return null;
    } catch (error) {
      console.error('Error querying user intent:', error);
      return null;
    }
  };

  const captureIframe = async () => {
    if (!url || isCapturing || !iframeRef.current) return;
    
    setIsCapturing(true);
    try {
      let stream = streamRef.current;
      if (!stream) {
        stream = await initScreenShare();
        if (!stream) return;
      }

      const video = document.createElement('video');
      video.srcObject = stream;
      await video.play();

      const canvas = document.createElement('canvas');
      canvas.width = video.videoWidth;
      canvas.height = video.videoHeight;
      const ctx = canvas.getContext('2d')!;
      ctx.drawImage(video, 0, 0);
      video.srcObject = null;
      
      const screenshot = canvas.toDataURL('image/jpeg', 0.95);

      const response = await fetch('/api/screenshot', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ 
          screenshot,
          title: url
        }),
      });

      if (!response.ok) {
        const errorText = await response.text();
        console.error('Server response:', errorText);
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const contentType = response.headers.get('content-type');
      if (!contentType || !contentType.includes('application/json')) {
        console.error('Invalid content type:', contentType);
        throw new Error('Server did not return JSON');
      }

      const data = await response.json();
      
      if (data.success) {
        setCurrentContext(data.analysis);
        const intentResult = await queryUserIntent(data.analysis);
        if (intentResult) {
          setUserIntent(intentResult);
        }
        setScreenshots(prev => [{
          url: data.screenshotUrl,
          timestamp: Date.now(),
          analysis: data.analysis
        }, ...prev]);
      } else {
        setError(data.error || 'Failed to capture screenshot');
      }
    } catch (error) {
      console.error('Error capturing iframe:', error);
      streamRef.current = null;
    } finally {

[truncated — 16900 more characters]
```

### app/simplified/page.tsx

```typescript
'use client';
import { useEffect, useState, Suspense } from 'react';
import { useSearchParams } from 'next/navigation';
import Link from 'next/link';
import CircularText from './CircularText';
import Image from 'next/image';

function SimplifiedContent() {
  const searchParams = useSearchParams();
  const url = searchParams.get('url');
  const [content, setContent] = useState<string>('');
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchAndSimplify = async () => {
      if (!url) {
        setError('No URL provided');
        setLoading(false);
        return;
      }

      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), 60000);

      try {
        console.log('Attempting to simplify URL:', url);
        const response = await fetch('/api/simplify', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({ url }),
          signal: controller.signal
        });

        clearTimeout(timeout);

        if (!response.ok) {
          const errorData = await response.text();
          console.error('Simplify API error:', {
            status: response.status,
            statusText: response.statusText,
            error: errorData
          });
          
          if (response.status === 504) {
            throw new Error('The request timed out. The webpage might be too large or the server might be busy. Please try again with a smaller page or try later.');
          }
          
          throw new Error(`Failed to simplify content: ${response.status} ${errorData}`);
        }

        const data = await response.json();
        if (!data.content) {
          throw new Error('Received empty content from API');
        }
        setContent(data.content);
      } catch (err: unknown) {
        console.error('Simplification error:', err);
        if (err && typeof err === 'object' && 'name' in err && err.name === 'AbortError') {
          setError('Request timed out. The webpage might be too large or the server might be busy. Please try again with a smaller page or try later.');
        } else {
          setError(
            err instanceof Error ? 
              `Error: ${err.message}` : 
              'An unexpected error occurred while simplifying the content'
          );
        }
      } finally {
        clearTimeout(timeout);
        setLoading(false);
      }
    };

    fetchAndSimplify();
  }, [url]);

  return (
    <div className="min-h-screen bg-gradient-to-br from-gray-900 to-black">
      <div className="max-w-4xl mx-auto p-6">
        <div className="mb-6 flex items-center justify-between">
          <Link 
            href="/"
            className="text-sm px-4 py-2 bg-white/10 hover:bg-white/20 border border-white/20 rounded-full transition-all text-white/90"
          >
            ← Back to Navi
          </Link>
          <div className="text-xs px-2 py-1 bg-gradient-to-r from-blue-400/10 to-blue-500/10 border border-blue-400/20 rounded-full text-blue-200 flex items-center gap-2">
            <Image
              src="/mistral.png"
              alt="Mistral AI Logo"
              width={24}
              height={24}
              className="opacity-80"
            />
            Powered by Mistral AI
          </div>
        </div>
        {loading ? (
          <div className="flex flex-col items-center justify-center h-[60vh] gap-6">
            <CircularText
              text="------------"
              onHover="speedUp"
              spinDuration={20}
              className="mb-4"
            />
            <div className="text-white/80">Generating simplified content...</div>
          </div>
        ) : error ? (
          <div className="bg-red-500/10 border border-red-500/20 text-red-400 px-4 py-3 rounded-xl">
            {error}
          </div>
        ) : (
          <div className="bg-white/5 rounded-2xl backdrop-blur-md border border-white/10">
            <div className="p-8 text-white">
              <div 
                dangerouslySetInnerHTML={{ __html: content }}
                className="prose prose-invert max-w-none 
                  prose-h1:text-white prose-h1:text-3xl prose-h1:font-bold prose-h1:mb-6
                  prose-h2:text-white prose-h2:text-xl prose-h2:font-semibold prose-h2:mb-4
                  prose-p:text-white/90 prose-p:mb-4
                  prose-ul:list-disc prose-ul:pl-4 prose-ul:space-y-2
                  prose-li:text-white/90
                  prose-a:text-blue-400 hover:prose-a:text-blue-300 prose-a:underline
                  [&_section]:mb-8
                  [&_section:last-child]:mb-0
                  [&_section:last-child]:pt-8
                  [&_section:last-child]:border-t
                  [&_section:last-child]:border-white/10"
              />
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

export default function SimplifiedPage() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <SimplifiedContent />
    </Suspense>
  );
} 
```

### app/api/context/route.ts

```typescript
import { NextResponse } from 'next/server';
import axios from 'axios';

export async function POST(request: Request) {
  try {
    const { text } = await request.json();

    const response = await axios.post('https://playground.context.inc/api/context', {
      text: text,
      query: 'generate some user intention (like what they are trying to do)',
    }, {
      headers: {
        'x-api-key': process.env.NEXT_PUBLIC_CONTEXT_API_KEY,
        'Content-Type': 'application/json',
      },
    });

    // Ensure we're returning a string response
    const intentText = typeof response.data.response === 'string' 
      ? response.data.response 
      : JSON.stringify(response.data.response);

    return NextResponse.json({ 
      success: true, 
      data: { response: intentText }
    });
  } catch (error) {
    console.error('Context API error:', error);
    return NextResponse.json(
      { success: false, error: 'Failed to analyze context' },
      { status: 500 }
    );
  }
} 
```

### app/api/chat/route.ts

```typescript
import { NextResponse } from 'next/server';
import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

// Add OPTIONS handler
export async function OPTIONS() {
  return new NextResponse(null, {
    status: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type',
    },
  });
}

export async function POST(request: Request) {
  try {
    const body = await request.json();
    console.log("Received message:", body.message);

    const { message, context } = body;

    const response = await openai.chat.completions.create({
      model: "gpt-4",
      messages: [
        {
          role: "system",
          content: `You are an assistant helping users understand a webpage. Current webpage context: ${context}`,
        },
        {
          role: "user",
          content: message,
        },
      ],
      temperature: 0.7,
    });

    const reply = response.choices[0]?.message?.content || '';

    console.log("Sending reply:", reply);
    return NextResponse.json({ reply: reply });
  } catch (error) {
    console.error("API error:", error);
    return NextResponse.json(
      { error: "Failed to process request" },
      { status: 500 }
    );
  }
} 
```

### app/api/transcribe/route.ts

```typescript
import { NextResponse } from 'next/server';
// import Groq from "groq-sdk";

// const groq = new Groq({
//   apiKey: process.env.GROQ_API_KEY,
// });

export async function POST(req: Request) {
  try {
    const formData = await req.formData();
    const file = formData.get('file') as Blob;

    if (!file) {
      return NextResponse.json(
        { error: 'No file provided' },
        { status: 400 }
      );
    }

    // Convert audio file to proper format
    const audioData = await file.arrayBuffer();
    const blob = new Blob([audioData], { type: 'audio/webm' });

    // Create form data for the API request
    const apiFormData = new FormData();
    apiFormData.append('file', blob, 'audio.webm');
    apiFormData.append('model', 'whisper-large-v3');

    // Use Groq's Whisper model for transcription
    const response = await fetch('https://api.groq.com/openai/v1/audio/transcriptions', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.GROQ_API_KEY}`,
      },
      body: apiFormData,
    });

    const data = await response.json();

    if (!response.ok) {
      throw new Error(data.error || 'Failed to transcribe audio');
    }

    return NextResponse.json({ text: data.text });
  } catch (error) {
    console.error('Transcription error:', error);
    return NextResponse.json(
      { error: 'Failed to transcribe audio' },
      { status: 500 }
    );
  }
} 
```

### app/api/screenshot/route.ts

```typescript
import { NextResponse } from 'next/server';
// import puppeteer from 'puppeteer';
import OpenAI from 'openai';
import { storage } from '@/app/firebase/config';
import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';

// Initialize OpenAI
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

// Add OPTIONS handler
export async function OPTIONS() {
  return new NextResponse(null, {
    status: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type',
    },
  });
}

export async function POST(req: Request) {
  const headers = {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type',
    'Content-Type': 'application/json',
  };

  try {
    const body = await req.text();
    const { screenshot, title } = JSON.parse(body);
    
    if (!screenshot) {
      return NextResponse.json({
        success: false,
        error: 'Screenshot is required'
      }, { status: 400, headers });
    }

    // Convert base64 to buffer
    const base64Data = screenshot.replace(/^data:image\/jpeg;base64,/, '');
    const imageBuffer = Buffer.from(base64Data, 'base64');

    console.log('Uploading to Firebase...');
    const timestamp = new Date().getTime();
    const storageRef = ref(storage, `screenshots/${timestamp}.jpg`);
    
    await uploadBytes(storageRef, imageBuffer);
    const downloadURL = await getDownloadURL(storageRef);

    console.log('Analyzing with OpenAI...');
    const response = await openai.chat.completions.create({
      model: "gpt-4o-mini",
      messages: [
        {
          role: "user",
          content: [
            {
              type: "text",
              text: `Analyze this webpage screenshot. Page title: ${title}. Provide a detailed context of the webpage content.`
            },
            {
              type: "image_url",
              image_url: {
                url: downloadURL,
                detail: "high"
              }
            }
          ],
        },
      ],
      max_tokens: 500,
    });

    const analysis = response.choices[0]?.message?.content || '';

    return NextResponse.json({
      success: true,
      analysis,
      screenshotUrl: downloadURL,
      pageTitle: title
    }, { headers });

  } catch (error) {
    console.error('Screenshot error:', error);
    return NextResponse.json({
      success: false,
      error: error instanceof Error ? error.message : 'Failed to process screenshot'
    }, { status: 500, headers });
  }
} 
```

### app/api/simplify/route.ts

```typescript
import { NextResponse } from 'next/server';
import axios from 'axios';
import * as cheerio from 'cheerio';

export async function POST(request: Request) {
  try {
    const { url } = await request.json();

    // Fetch the webpage content
    const response = await axios.get(url);
    const html = response.data;
    
    // Parse HTML and extract text content
    const $ = cheerio.load(html);
    
    // Remove unnecessary elements
    $('script, style, iframe, img').remove();
    
    // Get main content
    const text = $('body').text().trim();
    const links = $('a').map((_, el) => $(el).attr('href')).get();
    
    // Use Mistral to simplify the content
    const mistralResponse = await fetch('https://api.mistral.ai/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.NEXT_PUBLIC_MISTRAL_API_KEY}`,
      },
      body: JSON.stringify({
        model: 'mistral-medium',
        messages: [{
          role: 'user',
          content: `Create a clean, well-formatted HTML summary of this content. Follow these rules:
1. Use semantic HTML5 elements
2. Include a clear title at the top
3. Break content into logical sections with h2 headings
4. Use bullet points for key points
5. Style important text with <strong> tags
6. Include a "Key Takeaways" section
7. Add a "Related Links" section at the end
8. Keep paragraphs short and readable
9. Ensure all links are preserved and properly formatted

Content to summarize: ${text}

Response format:
<article class="content">
  <h1 class="text-white text-3xl font-bold mb-6">Title</h1>
  <section class="mb-8">
    <h2 class="text-white text-xl font-semibold mb-4">Overview</h2>
    <p class="text-white/90 mb-4">...</p>
  </section>
  <!-- More sections -->
  <section class="mb-8">
    <h2 class="text-white text-xl font-semibold mb-4">Key Takeaways</h2>
    <ul class="list-disc pl-4 space-y-2">
      <li class="text-white/90">...</li>
    </ul>
  </section>
  <section class="mt-8 pt-8 border-t border-white/10">
    <h2 class="text-white text-xl font-semibold mb-4">Related Links</h2>
    <ul class="list-disc pl-4 space-y-2">
      ${links.map(link => `<li><a href="${link}" class="text-blue-400 hover:text-blue-300 underline">${link}</a></li>`).join('\n')}
    </ul>
  </section>
</article>`
        }],
      }),
    });

    const simplifiedContent = await mistralResponse.json();
    
    return NextResponse.json({ 
      success: true, 
      content: simplifiedContent.choices[0].message.content 
    });
  } catch (error) {
    console.error('Simplification error:', error);
    return NextResponse.json(
      { success: false, error: 'Failed to simplify content' },
      { status: 500 }
    );
  }
} 
```

### play-sound.d.ts

```typescript
 
```

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