# Project export: pomibear!

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: productivity gone rogue - learning your patterns to reclaim time!
- Devpost: https://devpost.com/software/productivity-monster
- GitHub: https://github.com/mehdihdev/calhacks2025
- Video: https://www.youtube.com/embed/nB-xG8mhRBk?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Mehdi Hussain (8 commits), Ameenah (6 commits), Eric-KTH (3 commits)

## Devpost submission (written by the team)

### Inspiration

9 seconds. The average attention span of a goldfish. Human attention span? 8.25 seconds. In just 25 years, human attention span dropped from 12 seconds to 8.25 seconds. In a digital era where attention becomes increasingly commodified and AI barrels towards outperforming humans, practicing and retaining attention becomes crucial to maintain well-being, as a society and prevent further attention span decline. Current products fail to address user needs, blocking websites completely rather than learning from user behavior and adapting to their needs in order to accurately block the correct sites - this leads users to abandon such tools. Therefore, we’re building pomibear - a smart, user-first productivity extension that you won’t delete.

### What it does

pomibear helps users take control of their productivity by tracking whether the user’s current website is productive or not. If a website is deemed productive, it includes a pomodoro timer so that the user can track their productive time and more sustainably be productive. If the user visits an unproductive website when they aren’t supposed to, then they will get berated with popups and messages that push them to stop being unproductive.

### How we built it

pomibear was developed as a Chrome Extension and built using HTML/CSS, Javascript, and a Node JS backend. We did this because we realized that the number one place that people interact with on their computers was on their browsers, and by creating a chrome extension, it would be easily usable and allow for more options in terms of pushing our users to be more productive. We also made use of multiple libraries and api’s in order to enhance and expand the capabilities of our software including fish.audio for dynamic text-to-speech and Claude's LLM models to aid in classification.

### Challenges we ran into

Like most programming projects, ours ran into a couple of challenges. Firstly, we recognized that creating all the features was going to be a great challenge, so we paired up and worked on different features that more aligned with our skill sets in order to leverage our strengths to create a better Chrome Extension. Secondly, when finalizing our project, we realized that merging the code was a much more extensive and harder task than what was expected. In particular, github’s merge would delete vital parts of the project, move some parts into places where they shouldn’t have been, and overall made the project less comprehensible and unable to function. We ended up solving this by manually recreating the required features and using pair programming to more efficiently create the final product.

### Accomplishments we're proud of

One of the biggest things that we were proud of over the course of the development of pomibear was how we were able to adapt and learn new technologies that we were not familiar with prior to calhacks. At the start of this project, none of us had experience with things like Chrome Extensions and AI Voices, but throughout the development of this project, we were able to both learn these technologies and use them to create a final product we are all proud of.

### What we learned

This project taught us all many things in multiple disciplines - from teamwork to understanding web browser extension development. None of us had knowledge of how to develop Extensions at the start of this project, so we had to learn how to develop one from scratch. We ended up realizing that Extensions are like “mini-websites” and were able to use our knowledge of web development and apply them to this project. We also found value in using pair programming techniques to increase the efficiency and polish of the final product. Overall, we found this project to be extremely beneficial and fun, creating something that can help us personally in our day-to-day life.

### What's next

We know that pomibear can limit the decline in attention span and caters to users while our competitors do not. We’re certain that users will find us valuable; now, we just need to get the word out. By launching marketing campaigns, we can scale, drawing on the social features of our extension (e.g. the bear) to reel in and retain customers.

## README (from the GitHub repository)

# pomibear!
productivity gone rogue - learning your patterns to reclaim time!

## Inspiration
9 seconds. The average attention span of a goldfish. Human attention span? 8.25 seconds. In just 25 years, human attention span dropped from 12 seconds to 8.25 seconds.

In a digital era where attention becomes increasingly commodified and AI barrels towards outperforming humans, practicing and retaining attention becomes crucial to maintain well-being, as a society and prevent further attention span decline.

Current products fail to address user needs, blocking websites completely rather than learning from user behavior and adapting to their needs in order to accurately block the correct sites - this leads users to abandon such tools.

Therefore, we’re building pomibear - a smart, user-first productivity extension that you won’t delete.

## What it does
pomibear helps users take control of their productivity by tracking whether the user’s current website is productive or not. If a website is deemed productive, it includes a pomodoro timer so that the user can track their productive time and more sustainably be productive. If the user visits an unproductive website when they aren’t supposed to, then they will get berated with popups and messages that push them to stop being unproductive.

## How we built it
pomibear was developed as a Chrome Extension and built using HTML/CSS, Javascript, and a Node JS backend. We did this because we realized that the number one place that people interact with on their computers was on their browsers, and by creating a chrome extension, it would be easily usable and allow for more options in terms of pushing our users to be more productive. We also made use of multiple libraries and api’s in order to enhance and expand the capabilities of our software including fish.audio for dynamic text-to-speech and Claude's LLM models to aid in classification.

## Challenges we ran into
Like most programming projects, ours ran into a couple of challenges. Firstly, we recognized that creating all the features was going to be a great challenge, so we paired up and worked on different features that more aligned with our skill sets in order to leverage our strengths to create a better Chrome Extension.

Secondly, when finalizing our project, we realized that merging the code was a much more extensive and harder task than what was expected. In particular, github’s merge would delete vital parts of the project, move some parts into places where they shouldn’t have been, and overall made the project less comprehensible and unable to function. We ended up solving this by manually recreating the required features and using pair programming to more efficiently create the final product.

## Accomplishments that we're proud of
One of the biggest things that we were proud of over the course of the development of pomibear was how we were able to adapt and learn new technologies that we were not familiar with prior to calhacks. At the start of this project, none of us had experience with things like Chrome Extensions and AI Voices, but throughout the development of this project, we were able to both learn these technologies and use them to create a final product we are all proud of.

## What we learned
This project taught us all many things in multiple disciplines - from teamwork to understanding web browser extension development. None of us had knowledge of how to develop Extensions at the start of this project, so we had to learn how to develop one from scratch. We ended up realizing that Extensions are like “mini-websites” and were able to use our knowledge of web development and apply them to this project. We also found value in using pair programming techniques to increase the efficiency and polish of the final product. Overall, we found this project to be extremely beneficial and fun, creating something that can help us personally in our day-to-day life.

## What's next for pomibear!
We know that pomibear can limit the decline in attention span and caters to users while our competitors do not. We’re certain that users will find us valuable; now, we just need to get the word out. By launching marketing campaigns, we can scale, drawing on the social features of our extension (e.g. the bear) to reel in and retain customers.

## Detected evidence (automated analysis)

Indexed codebase: 7 recognized source files, 153 KB.
- Anthropic (technology) — detected in the code
- Express (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- CSS (language) — claimed on Devpost, not found in the code
- Node.js (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (11 of 11)

```
.gitignore
backend/package.json
backend/server.js
content.js
manifest.json
package.json
popup.html
popup.js
README.md
server.js
service-worker.js
```

### Dependencies

- backend/package.json: @anthropic-ai/sdk@^0.67.0, cors@^2.8.5, dotenv@^16.6.1, express@^4.21.2, lru-cache@^10.4.3, morgan@^1.10.1, zod@^3.25.76
- package.json: @anthropic-ai/sdk@^0.67.0, cors@^2.8.5, dotenv@^16.6.1, express@^4.21.2, lru-cache@^10.4.3, morgan@^1.10.1, zod@^3.25.76

### Recent commits (newest first)

- fix pomibear
- add readme
- Rename extension to 'Pomibear - Your Productivity Tracker'
- new logo yayy
- Replace walking bear with token notification bear system
- Add token deduction system with large distraction popup
- Enhanced Pomodoro Timer: Added draggable/resizable functionality, dynamic text scaling, pause/resume controls, and improved positioning
- 🐟 Enhanced Fish productivity enforcement system
- Fix Fish Audio implementation with comprehensive debugging and error handling
- Implement Fish Audio berating system with media pausing and visual alerts
- Delete backend/.env
- Add .env file with configuration variables
- new logo yay
- fix lru
- Merge remote-tracking branch 'refs/remotes/origin/main'
- new .env scheme
- Delete backend/.env
- add base stuff to test if the title of the tab is productive thru claude
- base chrome extensions

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

### package.json

```
{
  "name": "productivity-backend",
  "version": "0.1.0",
  "type": "module",
  "main": "server.js",
  "scripts": {
    "dev": "node server.js"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.67.0",
    "cors": "^2.8.5",
    "dotenv": "^16.6.1",
    "express": "^4.21.2",
    "lru-cache": "^10.4.3",
    "morgan": "^1.10.1",
    "zod": "^3.25.76"
  }
}

```

### backend/package.json

```
{
  "name": "productivity-backend",
  "version": "0.1.0",
  "type": "module",
  "main": "server.js",
  "scripts": {
    "dev": "node server.js"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.67.0",
    "cors": "^2.8.5",
    "dotenv": "^16.6.1",
    "express": "^4.21.2",
    "lru-cache": "^10.4.3",
    "morgan": "^1.10.1",
    "zod": "^3.25.76"
  }
}

```

### server.js

```javascript
// server.js (ESM)

console.log("server.js path:", new URL(import.meta.url).pathname);
console.log("cwd:", process.cwd());

import "dotenv/config";
import express from "express";
import morgan from "morgan";
import cors from "cors";
import Anthropic from "@anthropic-ai/sdk";
import { LRUCache } from "lru-cache";
import { z } from "zod";

const app = express();

// middleware
app.use(cors({ origin: "*" }));            // relax for local dev; tighten later
app.use(express.json({ limit: "256kb" }));
app.use(morgan("tiny"));

// optional bearer auth for local dev if INTERNAL_API_KEY is set
app.use((req, res, next) => {
  const expected = process.env.INTERNAL_API_KEY;
  if (!expected) return next();
  const auth = req.headers.authorization || "";
  if (!auth.startsWith("Bearer ")) {
    return res
      .status(401)
      .json({ message: "Invalid `Authorization` header format. Must be `Bearer {apikey}`" });
  }
  const token = auth.slice(7);
  if (token !== expected) return res.status(401).json({ message: "Invalid API key" });
  next();
});

// Anthropic client (do not crash if missing)
const anthropic = process.env.ANTHROPIC_API_KEY
  ? new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
  : null;

const CLAUDE_MODEL = process.env.CLAUDE_MODEL || "claude-3-5-haiku-latest";

// input schema
const classifyInput = z.object({
  url: z.string().url().optional().default(""),
  title: z.string().optional().default(""),
});

// cache by domain and simplified title
const cache = new LRUCache({
  max: 2000,
  ttl: 1000 * 60 * 60 * 24, // 24 hours
});

// policy prompt
const PRODUCTIVITY_POLICY = `
You are a strict productivity classifier for browsing activity.

Return ONLY valid JSON with keys:
- productive: boolean
- reason: string
- category: string // one of: "work","learning","utilities","communication","news","social","entertainment","shopping","adult","unknown"

Heuristics:
- Productive: docs, IDEs, coding Q&A, academic papers, LMS, email (work), calendar, dashboards, notes, task tools, StackOverflow, GitHub, Jupyter, LeetCode.
- Maybe: YouTube only if title implies tutorials or lectures; news only if researching a task.
- Not productive: social feeds, generic entertainment, shopping without clear work context, gaming, adult.
- Utilities (search engine home, blank new tab) = unknown.

If unsure, set productive=false.
`;

// helpers
function keyFrom(url, title) {
  try {
    const u = new URL(url || "http://unknown/");
    const domain = u.hostname.replace(/^www\./, "");
    const t = (title || "").toLowerCase().replace(/\s+/g, " ").trim();
    return `${domain}::${t || "(untitled)"}`;
  } catch {
    const t = (title || "").toLowerCase().replace(/\s+/g, " ").trim();
    return `unknown::${t || "(untitled)"}`;
  }
}

// routes
app.get("/", (req, res) => {
  res.json({ 
    message: "Productivity Classifier API is running!",
    endpoints: {
      health: "/health",
      classify: "POST /classify",
      event: "POST /event"
    }
  });
});

app.get("/health", (_req, res) => res.json({ ok: true }));
app.get("/healthz", (_req, res) => res.json({ ok: true }));

app.post("/classify", async (req, res) => {
  const parsed = classifyInput.safeParse(req.body);
  if (!parsed.success) return res.status(400).json({ error: "Bad input" });

  const { url, title } = parsed.data;
  const cacheKey = keyFrom(url, title);
  const cached = cache.get(cacheKey);
  if (cached) return res.json(cached);

  if (!anthropic) {
    return res
      .status(500)
      .json({ error: "Server missing ANTHROPIC_API_KEY; set it in backend/.env" });
  }

  const domain = (() => {
    try {
      return new URL(url).hostname.replace(/^www\./, "");
    } catch {
      return "unknown";
    }
  })();

  const prompt = `URL: ${url}
Domain: ${domain}
Page Title: ${title}

Classify this visit. Return JSON only.`;

  try {
    const msg = await anthropic.messages.create({
      model: CLAUDE_MODEL,
      max_tokens: 200,
      temperature: 0,
      system: PRODUCTIVITY_POLICY,
      messages: [{ role: "user", content: prompt }],
    });

    const text = msg?.content?.[0]?.text || "";
    let parsedJson;
    try {
      parsedJson = JSON.parse(text);
    } catch {
      const match = text.match(/\{[\s\S]*\}/);
      if (!match) throw new Error("No JSON in Claude response");
      parsedJson = JSON.parse(match[0]);
    }

    const shape = z.object({
      productive: z.boolean(),
      reason: z.string(),
      category: z.string(),
    });

    const result = shape.parse(parsedJson);
    cache.set(cacheKey, result);
    return res.json(result);
  } catch (e) {
    console.error(e);
    return res.status(500).json({ error: "Classification failed" });
  }
});

app.post("/event", (_req, res) => res.json({ ok: true }));

// Fish Audio TTS endpoint for berating messages
app.post("/berate", async (req, res) => {
  const { message, category } = req.body;
  
  if (!message) {
    return res.status(400).json({ error: "Message is required" });
  }

  // Generate berating messages based on category
  const beratingMessages = {
    social: [
      "Stop wasting time on social media! Get back to work!",
      "You're supposed to be productive, not scrolling through feeds!",
      "Social media can wait! Focus on your goals!",
      "Put down the phone and pick up your work!"
    ],
    entertainment: [
      "This isn't helping you achieve your goals!",
      "Entertainment can wait until you're done with work!",
      "You're procrastinating! Get back to productive tasks!",
      "Focus! This video isn't going anywhere!"
    ],
    shopping: [
      "Stop shopping and start working!",
      "Your wallet and productivity will thank you later!",
      "Shopping can wait! Focus on earning first!",
      "Put the cart down and pick up your tasks!"
    ],
    news: [
      "News can wait! Your work can't!",
      "Stop doomscrolling and start working!",
      "The news will still be there when you're done!",
      "Focus on what you can control 
[truncated — 1051 more characters]
```

### backend/server.js

```javascript
// server.js (ESM)

console.log("server.js path:", new URL(import.meta.url).pathname);
console.log("cwd:", process.cwd());

import "dotenv/config";
import express from "express";
import morgan from "morgan";
import cors from "cors";
import Anthropic from "@anthropic-ai/sdk";
import { LRUCache } from "lru-cache";
import { z } from "zod";

const app = express();

// middleware
app.use(cors({ origin: "*" }));            // relax for local dev; tighten later
app.use(express.json({ limit: "256kb" }));
app.use(morgan("tiny"));

// optional bearer auth for local dev if INTERNAL_API_KEY is set
app.use((req, res, next) => {
  const expected = process.env.INTERNAL_API_KEY;
  if (!expected) return next();
  const auth = req.headers.authorization || "";
  if (!auth.startsWith("Bearer ")) {
    return res
      .status(401)
      .json({ message: "Invalid `Authorization` header format. Must be `Bearer {apikey}`" });
  }
  const token = auth.slice(7);
  if (token !== expected) return res.status(401).json({ message: "Invalid API key" });
  next();
});

// Anthropic client (do not crash if missing)
const anthropic = process.env.ANTHROPIC_API_KEY
  ? new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
  : null;

const CLAUDE_MODEL = process.env.CLAUDE_MODEL || "claude-3-5-haiku-latest";

// input schema
const classifyInput = z.object({
  url: z.string().url().optional().default(""),
  title: z.string().optional().default(""),
});

// cache by domain and simplified title
const cache = new LRUCache({
  max: 2000,
  ttl: 1000 * 60 * 60 * 24, // 24 hours
});

// policy prompt
const PRODUCTIVITY_POLICY = `
You are a strict productivity classifier for browsing activity.

Return ONLY valid JSON with keys:
- productive: boolean
- reason: string
- category: string // one of: "work","learning","utilities","communication","news","social","entertainment","shopping","adult","unknown"

Heuristics:
- Productive: docs, IDEs, coding Q&A, academic papers, LMS, email (work), calendar, dashboards, notes, task tools, StackOverflow, GitHub, Jupyter, LeetCode.
- Maybe: YouTube only if title implies tutorials or lectures; news only if researching a task.
- Not productive: social feeds, generic entertainment, shopping without clear work context, gaming, adult.
- Utilities (search engine home, blank new tab) = unknown.

If unsure, set productive=false.
`;

// helpers
function keyFrom(url, title) {
  try {
    const u = new URL(url || "http://unknown/");
    const domain = u.hostname.replace(/^www\./, "");
    const t = (title || "").toLowerCase().replace(/\s+/g, " ").trim();
    return `${domain}::${t || "(untitled)"}`;
  } catch {
    const t = (title || "").toLowerCase().replace(/\s+/g, " ").trim();
    return `unknown::${t || "(untitled)"}`;
  }
}

// routes
app.get("/", (req, res) => {
  res.json({ 
    message: "Productivity Classifier API is running!",
    endpoints: {
      health: "/health",
      classify: "POST /classify",
      event: "POST /event"
    }
  });
});

app.get("/health", (_req, res) => res.json({ ok: true }));
app.get("/healthz", (_req, res) => res.json({ ok: true }));

app.post("/classify", async (req, res) => {
  const parsed = classifyInput.safeParse(req.body);
  if (!parsed.success) return res.status(400).json({ error: "Bad input" });

  const { url, title } = parsed.data;
  const cacheKey = keyFrom(url, title);
  const cached = cache.get(cacheKey);
  if (cached) return res.json(cached);

  if (!anthropic) {
    return res
      .status(500)
      .json({ error: "Server missing ANTHROPIC_API_KEY; set it in backend/.env" });
  }

  const domain = (() => {
    try {
      return new URL(url).hostname.replace(/^www\./, "");
    } catch {
      return "unknown";
    }
  })();

  const prompt = `URL: ${url}
Domain: ${domain}
Page Title: ${title}

Classify this visit. Return JSON only.`;

  try {
    const msg = await anthropic.messages.create({
      model: CLAUDE_MODEL,
      max_tokens: 200,
      temperature: 0,
      system: PRODUCTIVITY_POLICY,
      messages: [{ role: "user", content: prompt }],
    });

    const text = msg?.content?.[0]?.text || "";
    let parsedJson;
    try {
      parsedJson = JSON.parse(text);
    } catch {
      const match = text.match(/\{[\s\S]*\}/);
      if (!match) throw new Error("No JSON in Claude response");
      parsedJson = JSON.parse(match[0]);
    }

    const shape = z.object({
      productive: z.boolean(),
      reason: z.string(),
      category: z.string(),
    });

    const result = shape.parse(parsedJson);
    cache.set(cacheKey, result);
    return res.json(result);
  } catch (e) {
    console.error(e);
    return res.status(500).json({ error: "Classification failed" });
  }
});

app.post("/event", (_req, res) => res.json({ ok: true }));

// Fish Audio TTS endpoint for berating messages
app.post("/berate", async (req, res) => {
  const { message, category } = req.body;

  if (!message) {
    return res.status(400).json({ error: "Message is required" });
  }

  try {
    // Generate short, punchy insults using Claude AI
    const prompt = `You are Fish, a menacing productivity enforcer. Generate a SHORT, punchy, memorable insult for someone wasting time on ${category || 'entertainment'}.

Requirements:
- MAXIMUM 8-10 words
- Short and punchy
- Memorable and repeatable
- Funny but menacing
- NO asterisks or stage directions
- NO voice descriptions
- Just the insult itself

Examples:
- "Your productivity is dryer than a desert!"
- "Sloths work harder than you!"
- "Your goals are crying!"
- "Get back to work, slacker!"

Generate ONE short insult now:`;

    const msg = await anthropic.messages.create({
      model: CLAUDE_MODEL,
      max_tokens: 100,
      temperature: 0.9, // High creativity
      messages: [{ role: "user", content: prompt }],
    });

    const aiMessage = msg?.content?.[0]?.text || "You're procrastinating! Get back to productive tasks!";
    console.log(`AI-generated berating message: ${aiMessage}`);

    // Return the AI-generated m
[truncated — 2693 more characters]
```

### popup.html

```html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <style>
      body { font-family: ui-sans-serif, system-ui, -apple-system; margin: 12px; width: 280px; }
      .pill { display:inline-block; padding:4px 8px; border-radius:999px; font-size:12px; }
      .prod { background:#e6ffed; border:1px solid #00a341; color:#006d28; }
      .unprod { background:#fff5f5; border:1px solid #e11d48; color:#9f1239; }
      .muted { color:#666; font-size:12px; }
      label { display:block; font-size:12px; margin-top:8px; color:#444; }
      input { width:100%; padding:6px; border:1px solid #ccc; border-radius:8px; }
      button { margin-top:8px; width:100%; padding:8px; border:none; border-radius:8px; background:#111827; color:white; cursor:pointer; }
    </style>
  </head>
  <body>
    <div id="status">Loading…</div>
    <div style="margin-top:12px">
      <label>Backend URL</label>
      <input id="backend" placeholder="http://localhost:4000" />
      <button id="save">Save</button>
      <div class="muted">Your Claude API key stays on the backend only.</div>
    </div>
    
    <div style="margin-top:16px; border-top: 1px solid #e5e7eb; padding-top: 12px;">
      <div style="font-size: 14px; font-weight: bold; margin-bottom: 8px; color: #374151;">🍅 Pomodoro Timer</div>
             <div style="display: flex; gap: 8px;">
               <button id="start-work" style="flex: 1; background: #dc2626; font-size: 12px; padding: 6px;">Start Work (25m)</button>
               <button id="start-break" style="flex: 1; background: #059669; font-size: 12px; padding: 6px;">Start Break (5m)</button>
             </div>
             <div style="display: flex; gap: 8px; margin-top: 6px;">
               <button id="pause-timer" style="flex: 1; background: #f59e0b; font-size: 12px; padding: 6px;">Pause Timer</button>
               <button id="resume-timer" style="flex: 1; background: #10b981; font-size: 12px; padding: 6px;">Resume Timer</button>
             </div>
    </div>
    
    <div style="margin-top:16px; border-top: 1px solid #e5e7eb; padding-top: 12px;">
      <div style="font-size: 14px; font-weight: bold; margin-bottom: 8px; color: #374151;">🧸 <span id="bear-name">Virtual Closet</span></div>
      <button id="open-closet" style="background: #8B4513; font-size: 12px; padding: 6px;">Open Bear Closet</button>
      <div id="token-count" class="muted" style="margin-top: 4px;">Tokens: Loading...</div>
    </div>
    
    <script src="popup.js"></script>
  </body>
</html>

```

### content.js

```javascript
// Debounced send to background when URL/title changes.
let lastSent = { url: null, title: null };
let timer = null;
let lastClassification = null;

function sendSnapshot() {
  const payload = { url: location.href, title: document.title || "" };
  if (payload.url === lastSent.url && payload.title === lastSent.title) return;
  lastSent = payload;
  chrome.runtime.sendMessage({ type: "SNAPSHOT", payload }, (response) => {
    if (chrome.runtime.lastError) {
      console.error('SNAPSHOT error:', chrome.runtime.lastError);
      return;
    }
    
    console.log('Content script received response:', response);
    
    // Check if this is an unproductive site and we haven't already played audio
    if (response && response.result && !response.result.productive && 
        lastClassification !== response.result.url) {
      lastClassification = response.result.url;
      console.log('Unproductive site detected, triggering audio and popup');
      
      // Wait 3 seconds for video to start playing, then berate the user
      setTimeout(() => {
        // Trigger audio berating
        chrome.runtime.sendMessage({ 
          type: "PLAY_BERATING_AUDIO", 
          category: response.result.category 
        }, (audioResponse) => {
          if (chrome.runtime.lastError) {
            console.error('PLAY_BERATING_AUDIO error:', chrome.runtime.lastError);
          } else {
            console.log('Audio berating triggered successfully');
          }
        });
        
        // Trigger popup to open
        chrome.runtime.sendMessage({ 
          type: "OPEN_POPUP" 
        }, (popupResponse) => {
          if (chrome.runtime.lastError) {
            console.error('OPEN_POPUP error:', chrome.runtime.lastError);
          } else {
            console.log('Popup notification triggered successfully');
          }
        });
      }, 3000); // 3 second delay
    }
  });
}

// Trigger on common navigation/title-change events
const observeTitle = () => {
  const titleEl = document.querySelector("title");
  if (!titleEl) return;
  const obs = new MutationObserver(() => {
    clearTimeout(timer);
    timer = setTimeout(sendSnapshot, 300);
  });
  obs.observe(titleEl, { subtree: true, childList: true, characterData: true });
};

window.addEventListener("load", () => {
  observeTitle();
  sendSnapshot();
});
window.addEventListener("popstate", () => {
  clearTimeout(timer);
  timer = setTimeout(sendSnapshot, 200);
});
document.addEventListener("visibilitychange", () => {
  if (!document.hidden) sendSnapshot();
});

```

### popup.js

```javascript
const statusEl = document.getElementById("status");
const backendEl = document.getElementById("backend");
const saveBtn = document.getElementById("save");
const startWorkBtn = document.getElementById("start-work");
const startBreakBtn = document.getElementById("start-break");
const pauseTimerBtn = document.getElementById("pause-timer");
const resumeTimerBtn = document.getElementById("resume-timer");
const openClosetBtn = document.getElementById("open-closet");
const tokenCountEl = document.getElementById("token-count");
const bearNameEl = document.getElementById("bear-name");

chrome.storage.sync.get(["backendUrl"], ({ backendUrl }) => {
  backendEl.value = backendUrl || "http://localhost:4000";
});

saveBtn.addEventListener("click", async () => {
  await chrome.storage.sync.set({ backendUrl: backendEl.value.trim() });
  saveBtn.textContent = "Saved!";
  setTimeout(() => (saveBtn.textContent = "Save"), 800);
});

// Pomodoro timer event listeners
startWorkBtn.addEventListener("click", () => {
  chrome.runtime.sendMessage({ type: "START_POMODORO", isWorkSession: true }, (response) => {
    if (response && response.ok) {
      startWorkBtn.textContent = "Started!";
      setTimeout(() => (startWorkBtn.textContent = "Start Work (25m)"), 1000);
    }
  });
});

startBreakBtn.addEventListener("click", () => {
  chrome.runtime.sendMessage({ type: "START_POMODORO", isWorkSession: false }, (response) => {
    if (response && response.ok) {
      startBreakBtn.textContent = "Started!";
      setTimeout(() => (startBreakBtn.textContent = "Start Break (5m)"), 1000);
    } else if (response && !response.ok) {
      // Security system blocked the break
      startBreakBtn.textContent = "Blocked!";
      startBreakBtn.style.background = "#dc2626"; // Red background
      setTimeout(() => {
        startBreakBtn.textContent = "Start Break (5m)";
        startBreakBtn.style.background = "#059669"; // Reset to green
      }, 2000);
    }
  });
});

pauseTimerBtn.addEventListener("click", () => {
  chrome.runtime.sendMessage({ type: "PAUSE_POMODORO" }, (response) => {
    if (response && response.ok) {
      pauseTimerBtn.textContent = "Paused!";
      setTimeout(() => (pauseTimerBtn.textContent = "Pause Timer"), 1000);
    }
  });
});

resumeTimerBtn.addEventListener("click", () => {
  chrome.runtime.sendMessage({ type: "RESUME_POMODORO" }, (response) => {
    if (response && response.ok) {
      resumeTimerBtn.textContent = "Resumed!";
      setTimeout(() => (resumeTimerBtn.textContent = "Resume Timer"), 1000);
    }
  });
});

// Virtual closet event listener
openClosetBtn.addEventListener("click", () => {
  chrome.runtime.sendMessage({ type: "OPEN_CLOSET" }, (response) => {
    if (response && response.ok) {
      openClosetBtn.textContent = "Opening...";
      setTimeout(() => (openClosetBtn.textContent = "Open Bear Closet"), 1000);
    }
  });
});

// Update bear name
function updateBearName() {
  chrome.runtime.sendMessage({ type: "GET_BEAR_NAME" }, (response) => {
    if (response && response.bearName) {
      bearNameEl.textContent = response.bearName;
    } else {
      bearNameEl.textContent = "Virtual Closet";
    }
  });
}

// Update token count
function updateTokenCount() {
  chrome.runtime.sendMessage({ type: "GET_TOKEN_COUNT" }, (response) => {
    if (response && response.tokens !== undefined) {
      tokenCountEl.textContent = `🪙 Tokens: ${response.tokens}`;
    } else {
      tokenCountEl.textContent = "🪙 Tokens: Loading...";
    }
  });
}

function renderStatus(s) {
  if (!s) {
    statusEl.textContent = "No classification yet for this tab.";
    return;
  }
  statusEl.innerHTML = `
    <div style="display:flex; gap:8px; align-items:center;">
      <span class="pill ${s.productive ? "prod" : "unprod"}">
        ${s.productive ? "Productive" : "Not Productive"}
      </span>
      <span class="muted">${s.category || "uncategorized"}</span>
    </div>
    <div style="margin-top:8px; font-weight:600;">${s.title}</div>
    <div class="muted" style="word-break:break-all;">${s.url}</div>
    <div style="margin-top:8px;">${s.reason || ""}</div>
  `;
}

function loading(msg = "Classifying…") {
  statusEl.textContent = msg;
}

// Immediately classify the active tab on popup open
document.addEventListener("DOMContentLoaded", () => {
  loading();
  updateTokenCount(); // Update token count on popup open
  updateBearName(); // Update bear name on popup open
  
  chrome.runtime.sendMessage({ type: "CLASSIFY_ACTIVE" }, (resp) => {
    if (!resp || resp.ok === false) {
      // Try to at least show the last-known status if present
      chrome.runtime.sendMessage({ type: "GET_STATUS" }, ({ status }) => {
        if (!resp || resp.error) {
          statusEl.textContent = resp?.error || "Classification failed.";
        }
        if (status) renderStatus(status);
      });
      return;
    }
    renderStatus(resp.status || null);
  });
});
```