# Project export: Spotlight

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: A voice-guided web navigation assistant with visual guidance.
- Devpost: https://devpost.com/software/spotlight-12sk7z
- GitHub: https://github.com/zidaneho/spotlight
- Video: https://www.youtube.com/embed/VsMTXfaSB-c?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — zidaneho (21 commits), OrigamiStarz (5 commits)

## Devpost submission (written by the team)

### Inspiration

As tech-savvy CS majors, we are often bombarded by our relatives on tech-related topics, like how to renew a driver's license or how to schedule an appointment with their primary doctor.

### What it does

Spotlight is a voice-guided web navigation assistant that helps users browse websites using voice commands and visual guidance. Spotlight highlights exactly where to click, making web navigation intuitive and accessible for everyone, especially older users and those with cognitive or visual challenges.

### How we built it

We built a Google extension that harnesses the power of FishAudio and Claude. We first use FishAudio's Speech to Text to interpret a user's verbal request. Then, we put this into Claude alongside an image of the website to determine the correct answer as well as the positioning needed for the visuals. We then use FishAudio's Text to Speech to deliver the verbal answer to the user.

### Challenges we ran into

Some challenges we ran into include: computer vision scanning - being able to accurately find the correct position for visuals voice recognition (WIP) designing the user interface to be intuitive for our target audience

### Accomplishments we're proud of

We're happy to have a working demo by the end of the hackathon, especially since we had to fly in on Friday night (and so couldn't work on the project for a while).

### What we learned

How to build an extension in Chrome, as well as how to call APIs for it Knowledge of better methods for computer vision scanning Things really do break towards the end More detailed communication is very important for team collaboration

### What's next

Voice recognition Slight modification of UI to look more streamlined Adding more assistive tools (ex. ability to choose color theme or high contrast for color blind users) Fun features like customizable cursors!

## README (from the GitHub repository)

# Spotlight

A voice-guided web navigation assistant that helps users browse websites using voice commands and visual guidance. Spotlight highlights exactly where to click, making web navigation intuitive and accessible for everyone, especially elderly users and those with cognitive or visual challenges.

## Features

### Core Features
- 🧠 **Claude AI Reasoning**: Understands complex commands like "I want to make an appointment" and figures out what to click
- 🎤 **Natural Voice Commands**: Navigate websites hands-free with conversational language
- 💡 **Spotlight Panel**: Interactive assistant that minimizes to stay out of your way
- 🎯 **Smart Highlighting**: Multi-layer visual feedback (arrows, pulsing dots, tooltips, animated cursor)
- 👆 **Show, Don't Do**: Highlights where to click instead of auto-clicking, maintaining user control
- 🐟 **Fish Audio TTS**: Optional premium text-to-speech (auto-falls back to native browser TTS)
- 🖱️ **Enhanced Cursor**: Large, accessible cursor with animated guidance
- 📍 **Step Indicators**: Clear on-screen instructions for each action
- ♿ **Accessibility First**: Designed specifically for elderly and cognitive load-sensitive users

### Visual Enhancements
- **Pulsing Highlights**: Animated yellow outlines around target elements
- **Arrow Pointers**: Red arrows pointing to elements you need to click
- **Animated Cursor**: Guided cursor animation showing where to click
- **Tooltips**: Helpful text labels on interactive elements
- **Target Dots**: Pulsing red dots marking exact click locations
- **Large Cursor Mode**: Enlarged cursor for better visibility (enabled by default)
- **Smooth Animations**: All transitions use smooth, non-jarring animations

## How to Use

### Voice Commands

Spotlight uses Claude AI to understand both simple and complex commands:

**Simple Commands:**
- **"scroll down"** / **"scroll up"** - Navigate the page
- **"click login"** / **"click search"** - Find specific elements
- **"go back"** - Navigate back to the previous page
- **"repeat"** - Repeat the last action

**Intelligent Complex Commands (powered by Claude):**
- **"I want to make an appointment for my driver's license"** - Claude analyzes the page and finds "Appointments", "Schedule", or "Book" buttons
- **"Help me find cheap flights to New York"** - Finds search or booking interfaces
- **"I need to update my address"** - Identifies account/profile settings
- **"Show me how to contact support"** - Finds contact or help links

Claude reads the page content and understands your intent, finding the right elements even if you don't know exactly what they're called!

### Spotlight Panel

The Spotlight panel provides a friendly chat interface for guidance:

1. **Open Spotlight**: Click the "💡 Open Spotlight" button in the extension popup
2. **Auto-Minimizes**: The panel starts minimized at the bottom-right and automatically minimizes when searching
3. **Ask Questions**: Type questions like "How do I login?" or "Find the search button"
4. **Get Visual Guidance**: Spotlight finds elements and highlights them with multiple visual cues
5. **Interactive Help**: Click "Help Guide" for a list of available commands

**Spotlight Panel Features**:
- **Draggable**: Click and drag the header to move the panel anywhere on screen
- **Auto-Minimize**: Minimizes during searches so you can see the highlighted elements
- **Black Text**: All text is in high-contrast black for better readability
- **Chat History**: All your questions and responses are saved during your session
- **Visual Feedback**: Coordinates with all visual guides (arrows, highlights, animated cursors)

## Setup Instructions

### 1. Get Claude API Key (Required)

Spotlight uses Claude AI to understand complex commands intelligently:

1. Visit [console.anthropic.com](https://console.anthropic.com/)
2. Sign up or log in to your account
3. Go to API Keys section
4. Create a new API key
5. Copy the key - you'll need it in step 3

### 2. Install the Extension

1. Open Chrome and navigate to `chrome://extensions/`
2. Enable "Developer mode" in the top-right corner
3. Click "Load unpacked"
4. Select the `help-nav` directory
5. The extension will now be installed

### 3. Configure Claude API

1. Click the extension icon → "Settings"
2. Paste your Claude API key in the "Claude AI Configuration" section
3. Click "Save Settings"

**You must configure Claude to use Spotlight's intelligent commands!**

### 4. Start Using

1. Click the extension icon
2. Click "🎤 Start listening" (allow microphone access when prompted)
3. Say an intelligent command:
   - Simple: "click search"
   - Complex: **"I want to make an appointment for my driver's license"**
4. Spotlight uses Claude to understand your intent and highlights where to click!

### 5. Configure Fish Audio (Optional)

For premium TTS voices:

1. Visit [fish.audio](https://fish.audio) and get an API key
2. Click the extension icon → "Settings"
3. Enter your Fish Audio API key
4. (Optional) Enter a Reference ID for a specific voice
5. Click "Save Settings"

Note: Without Fish Audio, Spotlight uses your browser's native text-to-speech.

## Technical Details

### File Structure

- `manifest.json` - Extension configuration and permissions
- `background.js` - Command processing and Fish Audio TTS (avoids CORS)
- `content.js` - DOM interaction, element finding, and visual coordination
- `assistant-panel.js` - Spotlight panel (chat interface and guidance system)
- `cursor-guide.js` - Enhanced cursor and visual guidance features
- `popup.html/js` - Extension popup interface with voice controls
- `options.html/js` - Settings page for Fish Audio API configuration

### Architecture

Spotlight uses a multi-layered AI-powered approach:

1. **AI Reasoning Layer** (Claude API):
   - Receives user command + simplified DOM snapshot
   - Understands user intent using natural language processing
   - Identifies the best element(s) to interact with
   - Returns structured action with reasoning

2. **Background Layer** (background.js):
   - Handles Claude API calls (no CORS restrictions)
   - Processes Fish Audio TTS API calls
   - Coordinates between popup and content scripts

3. **Content Layer** (content.js):
   - Generates DOM snapshots for Claude
   - Finds elements based on Claude's reasoning
   - Coordinates all visual elements and highlighting

4. **Visual Modules**:
   - `assistant-panel.js`: Spotlight panel (chat interface and conversational guidance)
   - `cursor-guide.js`: Manages cursor enhancements and visual pointers

5. **User Interface**: Popup for voice controls, Options page for API configuration

**Key Design Choices**:
- **Claude AI**: Replaces simple keyword matching with intelligent reasoning
- **DOM Context**: Sends page structure to Claude for accurate element identification
- **Show, Don't Click**: Highlights elements instead of auto-clicking to maintain user control and help users learn

### Claude AI Integration

Spotlight uses Anthropic's Claude 3.5 Sonnet for intelligent command understanding:

**How It Works:**
1. **User speaks**: "I want to make an appointment for my driver's license"
2. **DOM Snapshot**: Content script extracts interactive elements (buttons, links, inputs) with their text
3. **Claude Reasoning**: Background script sends command + DOM to Claude API
4. **Action Response**: Claude analyzes and returns:
   ```json
   {
     "reasoning": "User wants to schedule a driver's license appointment",
     "action": "click",
     "selector": "appointments",
     "speak": "I found the appointments button. Click it to schedule."
   }
   ```
5. **Element Highlighting**: Content script finds and highlights the element
6. **User Action**: User clicks the highlighted element themselves

**Why Claude?**
- **Natural Language**: Understands intent, not just keywords
- **Context Aware**: Reads the actual page content
- **Multi-Step Ready**: Can identify sequences of actions (future feature)
- **No Training**: Works on any website witho

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 16 recognized source files, 102 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Vercel (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (18 of 18)

```
.DS_Store
assistant-panel.css
assistant-panel.html
assistant-panel.js
background.js
button.css
content.js
cursor-guide.css
cursor-guide.js
fish-audio.js
manifest.json
options.css
options.html
options.js
popup.css
popup.html
popup.js
README.md
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- Edit README.md
- fixed chatbot responses
- Enhanced UI
- Merge branch 'main' into frontend-changes
- Added precise targeting
- fixed pr 2
- Remove broken theme colors section
- Modified assistant panel functions
- Styling changes for panel including fix for drag
- fixed pr
- percise targeting
- Replaced DOM with vision based reasoning
- fixed pr 2
- fixed pr
- modified DOM to Vision
- Added Claude API and DOM scanning
- fixed pr 2
- Add logo
- fixed pr
- fixed claude model

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

### button.css

```css
button {
  height: 30px;
  width: 30px;
  outline: none;
  margin: 10px;
  border: none;
  border-radius: 2px;
}

button.current {
  box-shadow: 0 0 0 2px white, 0 0 0 4px black;
}

```

### popup.html

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Spotlight</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="stylesheet" href="popup.css" />
  </head>
  <body>
    <div class="header">
      <div class="title">
        <img
          src="/images/logo.svg"
          alt="Spotlight"
          class="logo"
          id="logo-img"
        />
        <span>Spotlight</span>
      </div>
      <a href="#" id="settingsLink" class="settings-btn" aria-label="Settings"
        >⚙️</a
      >
    </div>

    <div class="content-body">
      <div class="panel">
        <div class="label">Voice Engine</div>
        <div id="voiceEngine">
          <span id="ttsStatus">Checking...</span>
        </div>
      </div>
    </div>
    <script src="popup.js"></script>
  </body>
</html>
```

### assistant-panel.html

```html
<div class="nav-assistant-collapsed" id="nav-assistant-collapsed">
  <img src="__LOGO_URL__" alt="Spotlight" />
</div>

<div class="nav-assistant-expanded" id="nav-assistant-expanded">
  <div class="nav-assistant-header">
    <div class="nav-assistant-title">
      <img src="__LOGO_URL__" alt="Spotlight" class="nav-assistant-logo" />
      <span>Spotlight</span>
    </div>
    <button
      id="nav-assistant-close"
      class="nav-assistant-btn-icon"
      aria-label="Close"
    >
      ×
    </button>
  </div>

  <div class="nav-assistant-body">
    <div class="nav-assistant-controls">
      <div class="voice-control-section">
        <label class="voice-toggle-label">
          <span class="voice-toggle-text">🎤 Voice Recognition</span>
          <div class="voice-toggle-switch">
            <input type="checkbox" id="voice-recognition-toggle" />
            <span class="voice-toggle-slider"></span>
          </div>
        </label>
        <div class="voice-status" id="voice-status">Ready</div>
      </div>
    </div>

    <div id="nav-assistant-messages" class="nav-assistant-messages"></div>

    <div class="nav-assistant-input-area">
      <input
        type="text"
        id="nav-assistant-input"
        placeholder="Ask me how to navigate..."
        aria-label="Chat input"
      />
      <button
        id="nav-assistant-send"
        class="nav-assistant-btn-primary"
        aria-label="Send"
      >
        Send
      </button>
    </div>
  </div>
</div>
```

### options.html

```html
<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="button.css" />
    <link rel="stylesheet" href="options.css" />
  </head>
  <body>
    <h1>Spotlight Settings</h1>

    <div class="section">
      <h2>Claude AI Configuration (Required)</h2>
      <label for="claudeApiKey">Claude API Key</label>
      <input
        type="password"
        id="claudeApiKey"
        placeholder="Enter your Anthropic Claude API key"
      />
      <p class="info">
        Get your API key from
        <a
          href="https://console.anthropic.com/"
          target="_blank"
          rel="noopener noreferrer"
          >console.anthropic.com</a
        >
      </p>
      <p class="info">
        <strong>Required for intelligent command understanding</strong> -
        Spotlight uses Claude to understand complex commands like "I want to
        make an appointment"
      </p>
    </div>

    <div class="section">
      <h2>Fish Audio TTS (Optional)</h2>
      <label for="apiKey">Fish Audio API Key</label>
      <input
        type="password"
        id="apiKey"
        placeholder="Enter your Fish Audio API key"
      />
      <p class="info">
        Get your API key from
        <a href="https://fish.audio" target="_blank" rel="noopener noreferrer"
          >fish.audio</a
        >
      </p>

      <label for="referenceId">Reference ID (Optional)</label>
      <input
        type="text"
        id="referenceId"
        placeholder="Enter voice model reference ID"
      />
      <p class="info">
        Leave empty to use the default voice model or browser's native TTS
      </p>

      <button id="saveBtn">Save Settings</button>
      <div id="status" class="status"></div>
    </div>

    <script src="options.js"></script>
  </body>
</html>

```

### options.css

```css
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial,
    sans-serif;
  max-width: 600px;
  margin: 20px auto;
  padding: 20px;
  padding-bottom: 40px;
  background: #f5f5f5;
  min-height: 100vh;
  box-sizing: border-box;
}
.section {
  background: white;
  padding: 24px;
  margin-bottom: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  overflow: visible;
}
h2 {
  margin-top: 0;
  color: #333;
  font-size: 18px;
}
label {
  display: block;
  margin-bottom: 8px;
  color: #555;
  font-weight: 500;
}
input[type="text"],
input[type="password"] {
  width: 100%;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 14px;
  box-sizing: border-box;
  margin-bottom: 10px;
}
button {
  padding: 12px 24px;
  background: #2563eb;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  margin-top: 15px;
  margin-bottom: 10px;
  min-width: 120px;
  display: inline-block;
}
button:hover {
  background: #1d4ed8;
}
button:disabled {
  background: #9ca3af;
  cursor: not-allowed;
}

.info {
  font-size: 13px;
  color: #666;
  margin-top: 5px;
}
a {
  color: #2563eb;
}

/* Status notifications */
.status {
  margin-top: 15px;
  padding: 12px 16px;
  border-radius: 6px;
  font-size: 14px;
  font-weight: 500;
  display: none;
  transition: all 0.3s ease;
}

.status.success {
  background: #d1fae5;
  color: #065f46;
  border: 1px solid #a7f3d0;
  display: block;
}

.status.error {
  background: #fee2e2;
  color: #991b1b;
  border: 1px solid #fca5a5;
  display: block;
}

.status.warning {
  background: #fef3c7;
  color: #92400e;
  border: 1px solid #fcd34d;
  display: block;
}

/* Loading state for save button */
button.loading {
  background: #9ca3af !important;
  cursor: not-allowed;
  position: relative;
}

button.loading:after {
  content: "";
  position: absolute;
  width: 16px;
  height: 16px;
  margin: auto;
  border: 2px solid transparent;
  border-top-color: #ffffff;
  border-radius: 50%;
  animation: button-loading-spinner 1s ease infinite;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
}

@keyframes button-loading-spinner {
  from {
    transform: rotate(0turn);
  }
  to {
    transform: rotate(1turn);
  }
}

```

### popup.css

```css
:root {
  --brand-gradient: linear-gradient(135deg, #7bb9ff 0%, #4b67a2 100%);
  --brand-primary: #667eea;
  --bg: #f8fafc;
  --panel-bg: #ffffff;
  --text-primary: #0f172a;
  --text-muted: #475569;
  --border-color: #e2e8f0;
  --ok: #16a34a;
  --warn: #ea580c;
}
* {
  box-sizing: border-box;
}
body {
  margin: 0;
  width: 320px;
  background: var(--bg);
  color: var(--text-primary);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial,
    sans-serif;
}

/* Header */
.header {
  background: var(--brand-gradient);
  color: white;
  padding: 16px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.title {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 600;
  font-size: 16px;
}
.logo {
  width: 24px;
  height: 24px;
}
.settings-btn {
  font-size: 20px;
  text-decoration: none;
  background: rgba(255, 255, 255, 0.2);
  color: white;
  width: 28px;
  height: 28px;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.2s;
}
.settings-btn:hover {
  background: rgba(255, 255, 255, 0.3);
}

/* Content Area */
.content-body {
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.panel {
  background: var(--panel-bg);
  border: 1px solid var(--border-color);
  border-radius: 12px;
  padding: 12px 14px;
}
.label {
  font-size: 12px;
  font-weight: 500;
  color: var(--text-muted);
  margin-bottom: 8px;
}
#hints {
  font-size: 13px;
  color: var(--text-primary);
  line-height: 1.4;
}
#voiceEngine {
  font-size: 13px;
}

/* Cursor Settings */
#cursor-settings {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
#cursor-settings.grid-2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px 12px;
}
#cursor-settings label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  cursor: pointer;
  -webkit-user-select: none;
  user-select: none;
}
#cursor-settings input[type="radio"] {
  accent-color: var(--brand-primary);
  margin: 0;
}
.cursor-preview {
  width: 24px;
  height: 24px;
  object-fit: contain;
  margin-left: 2px;
  /* Removed transform properties */
}
/* Removed .cursor-preview.default rule */

/* Utilities */
.tts-ok {
  color: var(--ok) !important;
  font-weight: 500;
}
.tts-warn {
  color: var(--warn) !important;
  font-weight: 500;
}
```

### popup.js

```javascript
// popup.js (Manifest V3)
const repeatBtn = document.getElementById("repeatBtn"); // Note: This ID is not in popup.html
const assistantBtn = document.getElementById("assistantBtn"); // Note: This ID is not in popup.html
const lastTranscriptEl = document.getElementById("lastTranscript"); // Note: This ID is not in popup.html
const ttsStatus = document.getElementById("ttsStatus");
const settingsLink = document.getElementById("settingsLink");
const logoImg = document.getElementById("logo-img");
// const cursorRadios = ... (REMOVED)

// --- All Speech Recognition Code Removed ---
// (No more startBtn, stopBtn, statusDot, recognition, isListening)
// (No more setListeningUI, initSpeechRecognition, startListening, stopListening)
// ---

// Set logo URL
if (logoImg) {
  logoImg.src = chrome.runtime.getURL("/images/logo.svg");
}

function sendMessage(msg) {
  chrome.runtime.sendMessage(msg);
}

// --- Wire buttons (These buttons are not in your provided popup.html) ---
if (repeatBtn) {
  repeatBtn.addEventListener("click", () => {
    sendMessage({ type: "REPEAT_LAST" });
  });
}

if (assistantBtn) {
  assistantBtn.addEventListener("click", () => {
    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
      if (tabs.length) {
        chrome.tabs.sendMessage(tabs[0].id, { type: "TOGGLE_ASSISTANT" });
      }
    });
  });
}
// --- End wiring buttons not in HTML ---

// Listen for background updates
chrome.runtime.onMessage.addListener((message) => {
  if (!message || typeof message !== "object") return;

  if (message.type === "STATUS_UPDATE") {
    // Update UI with status from background
    if (message.lastTranscript && lastTranscriptEl) {
      lastTranscriptEl.textContent = message.lastTranscript;
    }
  }
});

// Request current status from background
sendMessage({ type: "GET_STATUS" });

// Check configuration
chrome.storage.local.get(["claudeApiKey", "fishAudioApiKey"], (data) => {
  
  // Check Fish Audio (optional for TTS)
  if (data.fishAudioApiKey) {
    ttsStatus.textContent = "🐟 Fish Audio (Premium)";
    ttsStatus.classList.remove("tts-warn");
    ttsStatus.classList.add("tts-ok");
  } else {
    ttsStatus.textContent = "🔊 Native TTS (Basic)";
    ttsStatus.classList.remove("tts-ok");
    ttsStatus.classList.add("tts-warn");
  }

  // Load saved cursor style (REMOVED)
});

// Open settings when link is clicked
settingsLink.addEventListener("click", (e) => {
  e.preventDefault();
  chrome.runtime.openOptionsPage();
});


```

### cursor-guide.css

```css
/* --- Original Large Cursor Style --- */

/* 1. Large, accessible cursor */
.nav-large-cursor {
  cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="%23000" stroke="%23fff" stroke-width="2" d="M2 2 L2 28 L10 20 L14 28 L18 26 L14 18 L24 18 Z"/></svg>')
      0 0,
    auto !important;
}

/* --- Assistant Guidance Styles (from cursor-guide.js) --- */

/* Animated cursor pointer for guidance (the '👆') */
.nav-animated-cursor {
  position: fixed;
  width: 40px;
  height: 40px;
  pointer-events: none;
  z-index: 999999;
  transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

.nav-animated-cursor::before {
  content: "👆";
  position: absolute;
  font-size: 32px;
  animation: pointPulse 1.5s ease-in-out infinite;
}

@keyframes pointPulse {
  0%,
  100% {
    transform: scale(1) translateY(0);
  }
  50% {
    transform: scale(1.2) translateY(-5px);
  }
}

/* Enlarged clickable areas for accessibility */
.nav-enlarged-target {
  position: relative;
  transform: scale(1.15);
  transition: transform 0.2s ease;
  z-index: 10;
}

.nav-enlarged-target:hover {
  transform: scale(1.2);
}

/* Focus ring for keyboard navigation */
.nav-focus-ring {
  outline: 4px solid #667eea !important;
  outline-offset: 4px !important;
  box-shadow: 0 0 0 8px rgba(102, 126, 234, 0.2) !important;
}

/* Tooltip for guided elements */
.nav-tooltip {
  position: absolute;
  background: #1f2937;
  color: white;
  padding: 8px 12px;
  border-radius: 8px;
  font-size: 14px;
  font-weight: 500;
  white-space: nowrap;
  z-index: 999999;
  pointer-events: none;
  animation: tooltipFadeIn 0.3s ease;
}

.nav-tooltip::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 6px solid transparent;
  border-top-color: #1f2937;
}

@keyframes tooltipFadeIn {
  from {
    opacity: 0;
    transform: translateY(-5px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Pulsing dot on target */
.nav-target-dot {
  position: absolute;
  width: 20px;
  height: 20px;
  background: #ff6b6b;
  border-radius: 50%;
  border: 3px solid white;
  box-shadow: 0 0 0 4px rgba(255, 107, 107, 0.3);
  animation: dotPulse 1.5s ease-in-out infinite;
  z-index: 999998; /* Ensure it's on top */
}

@keyframes dotPulse {
  0%,
  100% {
    transform: scale(1);
    box-shadow: 0 0 0 4px rgba(255, 107, 107, 0.3);
  }
  50% {
    transform: scale(1.3);
    box-shadow: 0 0 0 8px rgba(255, 107, 107, 0.1);
  }
}
```

### options.js

```javascript
const selectedClassName = "current";

// API configuration
const claudeApiKeyInput = document.getElementById("claudeApiKey");
const apiKeyInput = document.getElementById("apiKey");
const referenceIdInput = document.getElementById("referenceId");
const showDebugMarkerInput = document.getElementById("showDebugMarker");
const saveBtn = document.getElementById("saveBtn");
const statusDiv = document.getElementById("status");

// Reacts to a button click by marking the selected button and saving
// the selection
function handleButtonClick(event) {
  // Remove styling from the previously selected color
  const current = event.target.parentElement.querySelector(
    `.${selectedClassName}`
  );
  if (current && current !== event.target) {
    current.classList.remove(selectedClassName);
  }

  // Mark the button as selected
  const color = event.target.dataset.color;
  event.target.classList.add(selectedClassName);
  chrome.storage.sync.set({ color });
}

// Load saved Fish Audio settings
chrome.storage.local.get(
  [
    "claudeApiKey",
    "fishAudioApiKey",
    "fishAudioReferenceId",
    "showDebugMarker",
  ],
  (data) => {
    if (data.claudeApiKey) {
      claudeApiKeyInput.value = data.claudeApiKey;
    }
    if (data.fishAudioApiKey) {
      apiKeyInput.value = data.fishAudioApiKey;
    }
    if (data.fishAudioReferenceId) {
      referenceIdInput.value = data.fishAudioReferenceId;
    }
    // Default to false if not set (only if element exists)
    if (showDebugMarkerInput) {
      showDebugMarkerInput.checked = data.showDebugMarker || false;
    }
  }
);

// Show status message with auto-hide
function showStatus(message, type = "success") {
  if (!statusDiv) return;

  statusDiv.textContent = message;
  statusDiv.className = `status ${type}`;

  // Auto-hide success messages after 4 seconds
  if (type === "success") {
    setTimeout(() => {
      statusDiv.className = "status";
    }, 4000);
  }
}

// Save settings with enhanced feedback
saveBtn.addEventListener("click", async () => {
  const claudeKey = claudeApiKeyInput.value.trim();
  const fishKey = apiKeyInput.value.trim();
  const referenceId = referenceIdInput.value.trim();
  const showDebugMarker = showDebugMarkerInput
    ? showDebugMarkerInput.checked
    : false;

  // Validation
  if (!claudeKey) {
    showStatus(
      "Please enter a Claude API key (required for intelligent commands)",
      "error"
    );
    claudeApiKeyInput.focus();
    return;
  }

  // Show loading state
  saveBtn.disabled = true;
  saveBtn.classList.add("loading");
  saveBtn.textContent = "Saving...";
  showStatus("Saving settings...", "warning");

  try {
    // Save to storage
    await new Promise((resolve, reject) => {
      chrome.storage.local.set(
        {
          claudeApiKey: claudeKey,
          fishAudioApiKey: fishKey,
          fishAudioReferenceId: referenceId,
          showDebugMarker: showDebugMarker,
        },
        () => {
          if (chrome.runtime.lastError) {
            reject(chrome.runtime.lastError);
          } else {
            resolve();
          }
        }
      );
    });

    // Notify background script to reload settings
    chrome.runtime.sendMessage({ type: "RELOAD_CONFIG" });

    // Show success message
    showStatus("Settings saved successfully!", "success");
  } catch (error) {
    console.error("Error saving settings:", error);
    showStatus("Failed to save settings. Please try again.", "error");
  } finally {
    // Reset button state
    saveBtn.disabled = false;
    saveBtn.classList.remove("loading");
    saveBtn.textContent = "Save Settings";
  }
});

```

### fish-audio.js

```javascript
// fish-audio.js
// Fish Audio API integration for text-to-speech

class FishAudioTTS {
  constructor() {
    this.apiKey = null;
    this.baseURL = 'https://api.fish.audio';
    this.audioContext = null;
    this.isPlaying = false;
    this.audioQueue = [];
  }

  // Initialize with API key from storage
  async init() {
    return new Promise((resolve) => {
      chrome.storage.local.get(['fishAudioApiKey', 'fishAudioReferenceId'], (data) => {
        this.apiKey = data.fishAudioApiKey || null;
        this.referenceId = data.fishAudioReferenceId || null;
        resolve(!!this.apiKey);
      });
    });
  }

  // Set API key
  setApiKey(key) {
    this.apiKey = key;
    chrome.storage.local.set({ fishAudioApiKey: key });
  }

  // Set reference ID (voice model)
  setReferenceId(id) {
    this.referenceId = id;
    chrome.storage.local.set({ fishAudioReferenceId: id });
  }

  // Check if API key is configured
  isConfigured() {
    return !!this.apiKey;
  }

  // Generate speech using Fish Audio API
  async generateSpeech(text) {
    if (!this.apiKey) {
      throw new Error('Fish Audio API key not configured');
    }

    if (!text || text.trim() === '') {
      return null;
    }

    const url = `${this.baseURL}/v1/tts`;
    
    const requestBody = {
      text: text,
      reference_id: this.referenceId || undefined,
      format: 'mp3',
      latency: 'normal'
    };

    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${this.apiKey}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(requestBody)
      });

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

      // Get audio data as blob
      const audioBlob = await response.blob();
      return audioBlob;
    } catch (error) {
      console.error('Error generating speech:', error);
      throw error;
    }
  }

  // Play audio from blob
  async playAudio(audioBlob) {
    if (!audioBlob) return;

    return new Promise((resolve, reject) => {
      const audioURL = URL.createObjectURL(audioBlob);
      const audio = new Audio(audioURL);
      
      audio.onended = () => {
        URL.revokeObjectURL(audioURL);
        this.isPlaying = false;
        resolve();
      };

      audio.onerror = (error) => {
        URL.revokeObjectURL(audioURL);
        this.isPlaying = false;
        reject(error);
      };

      this.isPlaying = true;
      audio.play().catch(error => {
        console.error('Error playing audio:', error);
        this.isPlaying = false;
        reject(error);
      });
    });
  }

  // Speak text using Fish Audio
  async speak(text, fallbackToNative = true) {
    try {
      const audioBlob = await this.generateSpeech(text);
      await this.playAudio(audioBlob);
    } catch (error) {
      console.error('Fish Audio TTS failed:', error);
      
      // Fallback to native speech synthesis if enabled
      if (fallbackToNative && window.speechSynthesis) {
        console.log('Falling back to native TTS');
        const utterance = new SpeechSynthesisUtterance(text);
        utterance.lang = 'en-US';
        utterance.rate = 0.95;
        speechSynthesis.speak(utterance);
      }
    }
  }

  // Stop current playback
  stop() {
    if (window.speechSynthesis) {
      window.speechSynthesis.cancel();
    }
    this.isPlaying = false;
  }
}

// Create singleton instance
const fishAudioTTS = new FishAudioTTS();

```

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