# Project export: Myst

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: deMystify your browsing
- Devpost: https://devpost.com/software/myst-p09ai2
- GitHub: https://github.com/matthewjguck/myst
- Team: 4 GitHub contributor(s) — Matthew (21 commits), aniketm3 (18 commits), tia-kun (9 commits), Janet Meng (2 commits)

## Devpost submission (written by the team)

### Inspiration

In an era of ever-increasing media consumption, individuals often fall into reinforcement bias—engaging only with content that aligns with their existing beliefs. Our project was born from the need to break this cycle and promote diverse perspectives in online media. By providing users with real-time insights into their content consumption habits, we empower them to make informed choices about what they engage with and how it shapes their worldview.

### What it does

We created a Chrome Extension that automatically captures screenshots of online activity upon tab changes and significant keystrokes and analyzes the content in real time, all while minimizing storage usage by updating a rolling classification vector instead of storing every screenshot. Using Vision-Language Models (VLMs), content classification, and sentiment analysis, we generate a numerical consumption vector that categorizes a user's browsing habits on progressive scales for various categories: motivational (generally optimistic vs. pessimistic content), educational (informative vs. brain rot content), political (conservative vs. liberal content), and economic (capitalist vs. social content), and whether or not the content is misinformation. Users are then given an easy to read summary of where their content consumption leans. By continuously updating rather than storing every screenshot, our system provides real-time insights without unnecessary data overhead. These insights are displayed dynamically, allowing users to track shifts in their media consumption and even receive recommendations for broadening their exposure.

### How we built it

We started by assembling a basic Chrome extension, learning the required structure for opening and deploying an extension on the Chrome Web Store. Next, we developed the core functionality and pipeline: capturing screenshots of the user's screen, extracting text using the OpenAI GPT-4o API, and classifying the content using Mistral AI. Finally, we performed sentiment analysis with our fine-tuned model on TogetherAI to generate meaningful insights about user consumption habits. The last stage was connecting these two components through the use of a Flask endpoint, enabling seamless communication between the extension and the backend. To make the insights more interactive, we used Luma Labs to visualize the classification data dynamically. Instead of simply displaying raw numerical values, we transformed the running classification vector into a blob-based 3D visualization, where the shape, size, and color of the blob evolve based on the user’s browsing trends. This provides an intuitive and engaging way for users to see how their content consumption habits shift over time.

### Challenges we ran into

We got stuck a few times when hosting the Flask backend. A lot of time was spent trying to debug the json outputs for the Mistral API realizing that formatting was creating a large amount of errors. After fixing this, debugging was difficult since understanding niche errors in database management and quicks of Flask took time to individually discover and address. We also have a hard time synchronizing data across the users as we added capabilities to maintain a user’s data across different devices. This meant sharing the json file on a backend hosted database.

### Accomplishments we're proud of

We are very proud that we were able to deliver an end-to-end extension that we had no idea how to do prior to the Hackathon start. Hosting on Flask was something we had never done, so getting an extension that fully integrates with user behavior and provide useful insights was incredibly rewarding. This extension is truly something that I would want to use in my everyday internet usage, for which, I am excited to continue building and fine-tuning to my usage patterns. The UI for this extension is gorgeous and came together with the metrics swimmingly. We are always excited to see what our unique "Blob" will look like!

### What we learned

Throughout this journey, we learned: How to fully build an end-to-end Chrome Extension—from background scripts to local storage synchronization. Leveraging Vision-Language Models (VLMs) for content classification and sentiment analysis. Integrating a Flask backend with a real-time classification model. Applying generative AI for visualizing user behavior in 3D using Luma Labs.

### What's next

If we were to continue developing Myst, we envision several key improvements and expansions to enhance its impact: Refining Classification Models: While our current approach effectively categorizes content, we aim to further fine-tune our model for better accuracy. This includes improving sentiment analysis, refining misinformation detection, and incorporating a broader range of training data to reduce biases. Displaying Global User Trends: To help users contextualize their browsing habits, we plan to introduce an aggregated, anonymized view of global consumption trends. Users will be able to compare their own consumption patterns against the average person's trends, seeing where they align or diverge in different categories and encouraging greater awareness of information bubbles. Personalized Recommendations: Beyond simply analyzing content, we want to guide users toward a more balanced information diet. Myst will offer recommendations based on gaps in their consumption, encouraging exposure to diverse perspectives while respecting user preferences.

## README (from the GitHub repository)

# Myst - A Chrome Extension for Media Awareness

![image](https://github.com/user-attachments/assets/c9e61bc1-9b8b-421c-9bd2-9e799a9c83a7)


## Inspiration
In today's digital age, many individuals engage only with content that reinforces their existing beliefs. Myst was created to break this cycle by providing real-time insights into users' media consumption habits, empowering them to make informed choices.

## What It Does
Myst is a Chrome Extension that:
- Captures screenshots of online activity upon tab changes and keystrokes.
- Analyzes content in real time using Vision-Language Models (VLMs).
- Generates a dynamic consumption vector across multiple categories:
  - **Motivational** (Optimistic vs. Pessimistic)
  - **Educational** (Informative vs. Brain Rot)
  - **Political** (Conservative vs. Liberal)
  - **Economic** (Capitalist vs. Socialist)
  - **Misinformation Detection**
- Displays an interactive 3D visualization of browsing trends.

## How We Built It
- Developed a Chrome Extension with automated screenshot capturing.
- Used OpenAI's GPT-4o and Mistral AI for content classification.
- Integrated a Flask backend for real-time processing.
- Leveraged Luma Labs to generate 3D visualizations of user trends.

## Challenges
- Debugging Flask backend and JSON formatting errors.
- Synchronizing user data across devices via a cloud-hosted database.

## Accomplishments
- Successfully built and deployed an end-to-end extension.
- Designed an intuitive UI and interactive 3D visualization.
- Integrated real-time classification and sentiment analysis.

## What We Learned
- Building a full Chrome Extension with background scripts.
- Using Vision-Language Models for media classification.
- Hosting and integrating a Flask backend.
- Creating generative AI-driven 3D visualizations.

## Future Plans
- **Improve Classification Accuracy**: Refine sentiment analysis and misinformation detection.
- **Global User Trends**: Provide anonymized insights into broader media consumption patterns.
- **Personalized Recommendations**: Suggest diverse content to encourage balanced media exposure.

## Built With
- **Frontend**: HTML, CSS, JavaScript
- **Backend**: Flask, Python
- **AI/ML**: OpenAI GPT-4o, Mistral AI, TogetherAI
- **Visualization**: Luma Labs

---
🚀 *Break your content bubble with Myst!*


## Detected evidence (automated analysis)

Indexed codebase: 8 recognized source files, 36 KB.
- Flask (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- CSS (language) — claimed on Devpost, not found in the code
- Mistral AI (technology) — claimed on Devpost, not found in the code
- OpenAI (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (13 of 13)

```
.DS_Store
.gitignore
background.js
classification_model.py
luma_api.py
manifest.json
README.md
requirements.txt
running_average.json
scoring.py
sidepanel.html
sidepanel.js
simplify_classification.py
```

### Dependencies

- requirements.txt: flask, python-dotenv

### Recent commits (newest first)

- Create README.md
- 40 seconds cuz im poor
- cleaning up
- Cleaning up
- New UI Colors
- blob shuffle
- all 4
- commenting out
- Changed labels on 4 sliders to reflect categories
- 4 carsd
- testing event listening for sending info
- fixing the running average display in extension
- blobs
- prompt engineered more
- div zero fix?
- Merge remote-tracking branch 'refs/remotes/origin/main'
- fix for divide by 0
- prompt engineered for rankings
- running averages
- Working numbers

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

### requirements.txt

```
flask
python-dotenv
```

### luma_api.py

```python
import os
import requests
from dotenv import load_dotenv

load_dotenv()

LUMA_API_KEY = os.getenv("LUMA_API_KEY")
LUMA_API_URL = "https://api.lumalabs.ai/v1/upload"

def send_to_luma(blob_data):
    """Send the generated category blob to Luma AI for visualization."""
    if not LUMA_API_KEY:
        raise ValueError("Missing Luma API Key! Set it in a .env file.")
    
    headers = {
        "Authorization": f"Bearer {LUMA_API_KEY}",
        "Content-Type": "application/json"
    }

    payload = {
        "name": "User Browsing Insights",
        "blob": blob_data
    }

    response = requests.post(LUMA_API_URL, headers=headers, json=payload)

    if response.status_code == 200:
        result = response.json()
        return result.get("visualization_url", "No visualization generated.")
    else:
        return f"Luma AI Error: {response.json()}"

```

### scoring.py

```python
# update_file.py
import gpt4o

# Initialize the running totals for each category at 0.
RUNNING_SCORES = {
    "Motivational": 0,
    "Educational": 0,
    "Financial": 0,
    "Political": 0,
    "Other": 0
}

def update_scores():
    """
    Calls gpt4o.score() to get the latest score dictionary and updates
    the global RUNNING_SCORES by adding the new scores to the running totals.
    Returns a copy of the updated scores.
    """
    new_scores = gpt4o.score()
    for category, score in new_scores.items():
        if category in RUNNING_SCORES:
            RUNNING_SCORES[category] += score
        else:
            # If an unexpected category comes in, add it to 'Other'
            RUNNING_SCORES["Other"] += score
    return RUNNING_SCORES.copy()

def main():
    # For demonstration purposes, update the running scores a few times.
    # In practice, you could call update_scores() every time a new result is received.
    for i in range(5):  # Simulate 5 updates
        updated = update_scores()
        print(f"After update {i+1}:")
        for category, total in updated.items():
            print(f"  {category}: {total}")
        print("-" * 40)

if __name__ == '__main__':
    main()
```

### background.js

```javascript
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true })
  .catch((error) => console.error(error));

console.log("Background script loaded. Automatic screenshot classification enabled.");

// Automatically capture a screenshot when a tab updates
const cooldowns = new Map(); // Store cooldown timers per tab
const COOLDOWN_TIME = 5000; // Cooldown time in milliseconds (5 seconds)

setInterval(() => {
  console.log("Interval reached, capturing screenshot...");
  captureAndClassify();
}, 40000); // 40 seconds interval


// Function to capture a screenshot and classify it
function captureAndClassify() {
  console.log("Capturing screenshot and classifying...");
  chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
    if (tabs.length === 0) {
      console.error("No active tab found.");
      return;
    }

    chrome.tabs.captureVisibleTab(tabs[0].windowId, { format: "png" }, (dataUrl) => {
      if (chrome.runtime.lastError) {
        console.error("Error capturing screenshot:", chrome.runtime.lastError.message);
        return;
      }

      console.log("Screenshot captured. Sending for classification...");

      fetch("http://127.0.0.1:5002/analyze_screenshot", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ image: dataUrl }),
      })
        .then(response => response.json())
        .then(data => {
          if (data.error) {
        console.error("Error from classification API:", data.error);
          } else {
        console.log("Classification result:", data);
          }
        })
        .catch(error => console.error("Failed to send screenshot:", error));
    });
  });
}

// Automatically capture a screenshot when a tab updates (page load or navigation)
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  if (changeInfo.status === "complete") {
    console.log(`Tab updated: ${tab.url}`);
    captureAndClassify();
  }
});

// Manual trigger for capturing a screenshot
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === "captureScreenshot") {
    captureAndClassify();
    sendResponse({ message: "Screenshot captured and sent for classification." });
    return true;
  }

  if (message.action === "getClassificationVector") {
    chrome.storage.local.get("classification_vector", (result) => {
      sendResponse({ classification_vector: result.classification_vector || {} });
    });

    return true;
  }

  sendResponse({ error: "Invalid action." });
  return true;
});

```

### sidepanel.js

```javascript
document.addEventListener("DOMContentLoaded", () => {
  // Function to update insights display based on the fetched running average
  function updateInsightsDisplay() {
    fetch("http://127.0.0.1:5002/data")
      .then(response => response.json())
      .then(data => {
      RUNNING_AVERAGE = data;

      const politicalStrength = RUNNING_AVERAGE["Political"];
      const politicalPosition = 292 * politicalStrength / 2;

      if (politicalPosition > 146) {
        politicalPosition = 146;
      } else if (politicalPosition < -146) {
        politicalPosition = -146;
      }

      document.getElementById("political").style.left = `${politicalPosition + 146}px`;
    

      const economicStrength = RUNNING_AVERAGE["Financial"];
      const economicPosition = 292 * economicStrength / 2;

      if (economicPosition > 146) {
        economicPosition = 146;
      } else if (economicPosition < -146) {
        economicPosition = -146;
      }

      document.getElementById("economic").style.left = `${economicPosition + 146}px`;

      const motivationalStrength = RUNNING_AVERAGE["Motivational"];
      const motivationalPosition = 292 * motivationalStrength / 2;

      if (motivationalPosition > 146) {
        motivationalPosition = 146;
      } else if (motivationalPosition < -146) {
        motivationalPosition = -146;
      }

      document.getElementById("tonal").style.left = `${motivationalPosition + 146}px`;

      const educationalStrength = RUNNING_AVERAGE["Educational"];
      const educationalPosition = 292 * educationalStrength / 2;

      if (educationalPosition > 146) {
        educationalPosition = 146;
      } else if (educationalPosition < -146) {
        educationalPosition = -146;
      }

      document.getElementById("educational").style.left = `${educationalPosition + 146}px`;


      });
      
  }

  // Update insights every 5 seconds
  setInterval(updateInsightsDisplay, 5000);

  // Immediately update on page load
  updateInsightsDisplay();
});



document.addEventListener("DOMContentLoaded", () => {
  // Function to update insights display
  function updateInsightsDisplay() {


      // Update circle position based on category strengths
      const politicalStrength = RUNNING_AVERAGE["Political"];
      const circlePosition = 292 * politicalStrength / 2; // Assuming the bar width is 292px
      document.getElementById("political").style.left = `${circlePosition + 146}px`;

      // Update Luma Visualization if available
      if (visualizationUrl) {
        document.getElementById("lumaVisualization").innerHTML = `<iframe src="${visualizationUrl}" width="100%" height="400px"></iframe>`;
      }

  }

    let RUNNING_AVERAGE = {
      'Motivational': 0.0,
      'Educational': 0.0,
      'Financial': 0.0,
      'Political': 0.0
    };
  
    // Function to receive running average data
    function receiveRunningAverage(runningAverage) {
      RUNNING_AVERAGE = runningAverage;
      fetch("http://127.0.0.1:5002/update_running_average", {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify(runningAverage)
      })
      .then(response => {
        if (!response.ok) {
          return response.text().then(text => { throw new Error(text) });
        }
      })
      .catch(error => {
        console.error("Error sending running average to side panel:", error);
      });
    }
  
    // Update insights every 5 seconds
    setInterval(updateInsightsDisplay, 5000);
  
    // Immediately update on page load
    updateInsightsDisplay();
  });
  
  document.addEventListener("DOMContentLoaded", () => {
    // Array of blob image URLs. You might generate this list on the server
    // or hardcode it if it's fixed.
    const blobImages = [
      "blobs/image 72.png",
      "blobs/image 73.png",
      "blobs/image 74.png",
      "blobs/image 75.png",
      "blobs/image 76.png",
      "blobs/image 77.png",
      "blobs/image 78.png",
      // add more blob paths as needed
    ];
  
    let currentImageIndex = 0;
  
    function updateBlobImage() {
      // Get the image element where you want to display the blob
      const imageElement = document.getElementById("blobImage");
      if (!imageElement) return;
  
      // Set the src attribute to the current blob image
      imageElement.src = blobImages[currentImageIndex];
  
      // Move to the next image (loop back to start if needed)
      currentImageIndex = (currentImageIndex + 1) % blobImages.length;
    }
  
   
    setInterval(updateBlobImage, 60000);
  
    // Immediately update the image on page load
    updateBlobImage();
  });
  
```

### simplify_classification.py

```python
#!/usr/bin/env python3
import os
import json
import numpy as np
import requests
from flask import Flask, request, jsonify
from dotenv import load_dotenv
import re
import ast

# Load environment variables
load_dotenv()

app = Flask(__name__)

# Load Mistral API key
MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY")
if not MISTRAL_API_KEY:
    raise ValueError("Missing Mistral API Key! Set it in a .env file.")

# Mistral API Endpoint
MISTRAL_API_URL = "https://api.mistral.ai/v1/chat/completions"

# Reference categories for classification
CATEGORY_TEXTS = {
    "Motivational": "This content is about personal growth, inspiration, and motivation.",
    "Educational": "This content is focused on learning, courses, and tutorials.",
    "Financial": "This content is about money, business, and financial markets.",
    "Political": "This content covers government policies, elections, and social issues."
}

# File to store the running average and sample count
RUNNING_AVERAGE_FILE = "running_average.json"
CATEGORIES = list(CATEGORY_TEXTS.keys())

def get_mistral_embedding(text):
    """Fetch category similarity scores using Mistral."""
    headers = {"Authorization": f"Bearer {MISTRAL_API_KEY}", "Content-Type": "application/json"}
    
    # Create a structured prompt using the given text.
    prompt = (
        f"The following text is an insight extracted from a screenshot:\n\n"
        f"'{text}'\n\n"
        f"Rate the similarity of this insight to the following categories on a scale from 0 to 1:\n"
        f"- Motivational\n"
        f"- Educational\n"
        f"- Financial\n"
        f"- Political\n\n"
        f"Return your response as a valid JSON object with double-quoted keys and values only. "
        f"Example output: {{\"Motivational\": 0.8, \"Educational\": 0.6, \"Financial\": 0.1, \"Political\": 0.05}}"
    )
    
    payload = {
        "model": "mistral-small",
        "messages": [
            {"role": "system", "content": "You are a model that classifies text into predefined categories and outputs valid JSON."},
            {"role": "user", "content": prompt}
        ]
    }
    
    response = requests.post(MISTRAL_API_URL, headers=headers, json=payload)
    
    if response.status_code == 200:
        result = response.json()
        category_scores_text = result["choices"][0]["message"]["content"].strip()
        
        try:
            category_scores = json.loads(category_scores_text)
        except json.JSONDecodeError:
            try:
                category_scores = ast.literal_eval(category_scores_text)
            except (SyntaxError, ValueError):
                # Fallback: extract key-value pairs via regex.
                pattern = r'"([^"]+)"\s*:\s*([\d.]+)'
                matches = re.findall(pattern, category_scores_text)
                category_scores = {key: float(val) for key, val in matches}
        
        return category_scores
    else:
        raise Exception(f"Mistral API Error: {response.json()}")

def load_running_average():
    """Load the running average and count from the file, or initialize if not present."""
    if os.path.exists(RUNNING_AVERAGE_FILE):
        with open(RUNNING_AVERAGE_FILE, "r") as f:
            data = json.load(f)
            return data.get("running_average", {cat: 0.0 for cat in CATEGORIES}), data.get("count", 0)
    else:
        return {cat: 0.0 for cat in CATEGORIES}, 0

def update_running_average(new_scores, running_avg, count):
    """
    Update the running average with new_scores.
    new_avg = (old_total * count + new_score) / (count + 1)
    """
    new_count = count + 1
    updated_avg = {}
    for cat in CATEGORIES:
        updated_avg[cat] = (running_avg.get(cat, 0.0) * count + new_scores.get(cat, 0.0)) / new_count
    return updated_avg, new_count

def save_running_average(running_avg, count):
    """Save the running average and sample count to a file."""
    with open(RUNNING_AVERAGE_FILE, "w") as f:
        json.dump({"running_average": running_avg, "count": count}, f, indent=2)

@app.route('/simplify_classification', methods=['POST'])
def simplify_classification():
    data = request.json
    if 'image' not in data:
        return jsonify({'error': 'No image provided'}), 400

    # Here, image data is provided in base64 format.
    # (1) In a real implementation, you’d generate a text description from the image (e.g., via OCR or image captioning).
    # For now, we simulate by using a fixed description.
    description_text = "This screenshot shows various political ads and news headlines."

    # (2) Get classification scores using the description.
    scores = get_mistral_embedding(description_text)
    total_score = sum(scores.values())
    normalized_scores = {cat: round(scores.get(cat, 0.0) / total_score, 2) for cat in scores}

    # (3) Update the running average.
    current_avg, count = load_running_average()
    updated_avg, new_count = update_running_average(normalized_scores, current_avg, count)
    save_running_average(updated_avg, new_count)

    # (4) Return the results.
    return jsonify({
        "description": description_text,
        "normalized_scores": normalized_scores,
        "updated_running_average": updated_avg,
        "sample_count": new_count
    })

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5002, debug=True)

```

### classification_model.py

```python
from flask import Flask, request, jsonify
import os
import json
import base64
import numpy as np
import requests
from dotenv import load_dotenv
from luma_api import send_to_luma
from io import BytesIO
from PIL import Image
from mistralai import Mistral

# Load environment variables
load_dotenv()

app = Flask(__name__)

# Load Mistral API key
MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")

OPENAI_API_URL = "https://api.openai.com/v1/chat/completions"

if not MISTRAL_API_KEY:
    raise ValueError("Missing Mistral API Key! Set it in a .env file.")

# Mistral API Endpoint
MISTRAL_API_URL = "https://api.mistral.ai/v1/chat/completions"

# Reference categories for classification
CATEGORY_TEXTS = {
    "Motivational": "This content is about personal growth, inspiration, and motivation.",
    "Educational": "This content is focused on learning, courses, and tutorials.",
    "Financial": "This content is about money, business, and financial markets.",
    "Political": "This content covers government policies, elections, and social issues."
}

USER_EMBEDDINGS_FILE = "user_embeddings.json"  # Store embeddings locally

import re
import ast


mistralerror = 0

def get_mistral_embedding(text):
    print("get_mistral_embedding")
    """Fetch category similarity scores using Mistral."""
    headers = {"Authorization": f"Bearer {MISTRAL_API_KEY}", "Content-Type": "application/json"}

    # Create a structured prompt
    prompt = (
        f"The following text is an insight extracted from a screenshot:\n\n"
        f"'{text}'\n\n"
        f"Rate the similarity of this insight to the following categories on a scale from -1 to 1:\n"
        f"- Motivational (-1 = strongly pessimistic, depressing, scary, 1 = strongly optimistic/inspiring)\n"
        f"- Educational (-1 = highly uneducational/brainrot/racist or misleading, 1 = highly educational/factual)\n"
        f"- Financial (-1 = highly capitalist/profit-driven, 1 = highly socialist/equity-driven)\n"
        f"- Political (-1 = strongly conservative/traditionalist/racist, 1 = strongly liberal/progressive)\n\n"
        f"**Guidelines for scoring:**\n"
        f"- **Values closer to -1 or 1** indicate **strong opinions** or **clear alignment** with one side.\n"
        f"- **Values closer to 0** indicate **weak alignment or neutrality** in the category.\n"
        f"Return your response as a valid JSON object with double-quoted keys and values only."
        f"Example output 1: {{\"Motivational\": -0.934, \"Educational\": 0.642, \"Financial\": -0.582, \"Political\": 0.357}}"
        f"Example output 2: {{\"Motivational\": 0.866, \"Educational\": 0.220, \"Political\": 0.2}}"
    )

    payload = {
        "model": "mistral-large-latest",
        "messages": [
            {"role": "system", "content": "You are a model that classifies text into predefined categories and outputs valid JSON."},
            {"role": "user", "content": prompt}
        ]
    } 

    global mistralerror

    if mistralerror == 1:
        payload = {
            "model": "gpt-4o",
            "messages": [
                {"role": "system", "content": "You are a model that classifies text into predefined categories and outputs valid JSON."},
                {"role": "user", "content": prompt}
            ]
        }
        response = requests.post(OPENAI_API_URL, headers=headers, json=payload)

        

    response = requests.post(MISTRAL_API_URL, headers=headers, json=payload)

    if response.status_code != 200:
        mistralerror = 1

    if response.status_code == 200:
        result = response.json()
        category_scores_text = result["choices"][0]["message"]["content"].strip()

        # Ensure the response is a valid JSON string
        try:
            # Clean up the response to ensure it is valid JSON
            category_scores_text = category_scores_text.split('```json')[1].split('```')[0].strip()
            category_scores = json.loads(category_scores_text)
        except (json.JSONDecodeError, IndexError):
            raise ValueError(f"Invalid JSON response from Mistral: {category_scores_text}")

        
        return category_scores
    else:
        mistralerror = 1
        raise Exception(f"Mistral API Error: {response.json()}")

def generate_blob(category_scores):
    """Convert category scores into a structured binary format (Blob)."""
    # Convert dictionary values to a NumPy array
    category_values = np.array(list(category_scores.values()), dtype=np.float32)

    # Encode as base64 for easy storage and transmission
    blob_data = base64.b64encode(category_values.tobytes()).decode('utf-8')

    return blob_data

# Generate embeddings for predefined categories
CATEGORY_EMBEDDINGS = {
    category: get_mistral_embedding(text)
    for category, text in CATEGORY_TEXTS.items()
}

def cosine_similarity(vec1, vec2):
    """Compute the cosine similarity between two vectors."""
    return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))

RUNNING_AVERAGE = {"Motivational": 0.0, "Educational": 0.0, "Financial": 0.0, "Political": 0.0}
RUNNING_COUNT = {"Motivational": 0, "Educational": 0, "Financial": 0, "Political": 0}

def update_running_average(new_scores):
    """
    Update the global rolling averages based on new_scores, which is a dictionary of category scores.
    Uses the formula:
        new_average = old_average + (new_score - old_average) / (count + 1)
    """
    global RUNNING_AVERAGE, RUNNING_COUNT
    for category, new_value in new_scores.items():
        # If the category isn't one of our predefined ones, assign it to "Other"
        if category not in RUNNING_AVERAGE:
            print(f"Unknown category: {category}")
            continue

        if new_value == 0.0:
            continue
        
        count = RUNNING_COUNT[category]
        old_avg = RUNNING_AVERAGE[category]
        # Update using incremental averaging formula
        new_avg = old_avg + (new_value - old_avg) /
[truncated — 3002 more characters]
```

### sidepanel.html

```html
<!DOCTYPE html>
<html>
<head>
  <style>
    body { width: 360px; display: flex-col; align-items: center; padding: 0px; font-family: Arial, sans-serif; background: linear-gradient(118deg, rgba(27.70, 35.93, 98.22, 0.40) 0%, rgba(3.23, 11.58, 74.79, 0.40) 100%); 
        backdrop-filter: blur(14px)}
    button { margin: 5px; padding: 8px; width: 100%; cursor: pointer; }
    #latestScreenshot { max-width: 100%; margin-top: 10px; }
    .screenshot-item { margin-bottom: 10px; }
    .screenshot-item img { max-width: 100%; }
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    html, body {
      overflow-x: hidden;
      width: 100%;
      height: 100%;
    }

    body {
      background: url('background.png'); /* Replace with your image URL */
      background-size: cover;
      background-size: auto 100%;
      /* background-position: center;
      position: relative; */
      animation: scrollBackground 40s linear infinite;
    }

    @keyframes scrollBackground {
    from {
        background-position: 0 0;
    }
    to {
        background-position: 100% 100%;
    }
    }

    .bobbing {
            display: block;
            width: 200px; /* Adjust as needed */
            height: auto;
            animation: bobbing 2s infinite ease-in-out;
        }

        @keyframes bobbing {
            0%, 100% {
                transform: translateY(0);
            }
            50% {
                transform: translateY(-10px); /* Adjust height of bobbing */
            }
        }

    /* Gradient Overlay */
    .gradient-overlay {
      position: absolute;
      top: 0;
      left: 0;
      width: 100vw;
      height: 100vh;                    
      /* Edit colors live? */
      background: linear-gradient(118deg, rgba(118, 104, 243, 0.4) 20%, rgba(3, 11, 75, 0.40) 100%);
      z-index: -1; /* Ensures it's above the background but below everything else */
    }
  </style>
</head>
<body>

    <!-- Gradient Overlay -->
    <div class="gradient-overlay"></div>

    <div style="display: flex; direction: column; justify-content: center; width: 100%;">
        <img style="width: 120px; height: auto; margin-top: 10px;" src="/Myst.png"/>
    </div>
    <div style="display: flex; direction: column; justify-content: center; width: 100%;">
        <img id="blobImage" style="width: 180px; margin-top: 40px; height: auto;" src="/Blob.png" class="bobbing"/>
    </div>

     <div style="text-align: center; margin-top: 40px;"><span style="color: white; font-size: 20px; font-family: SF Pro Display; font-weight: 500; text-transform: capitalize; line-height: 24.16px; word-wrap: break-word">
        Today's Summary<br/>
      </span>
    </div>
  
  
  <!-- <button id="capture">Capture Screenshot</button>
  <button id="viewLatest">View Latest Screenshot</button>
  <button id="viewAll">View All Screenshots</button> -->

    <!-- Info Cards -->
    <div style="display: flex; direction: column; justify-content: center; width: 100%; margin-top: 30px;">

        <!-- Political Alignment Card-->
        <div style="width: 300x; height: 50px; padding-top: 5px; padding-bottom: 5px; padding-left: 14px; padding-right: 14px; background: rgba(0, 0, 0, 0.18); border-radius: 10px; flex-direction: column; justify-content: center; align-items: flex-start; gap: 6.58px; display: inline-flex">
            
            <div style="height: 30px; flex-direction: column; justify-content: flex-start; align-items: flex-start; display: flex">

                <!-- Title -->
                <div style="text-align: center; color: white; font-size: 10px; font-family: SF Pro Display; font-weight: 350; text-transform: capitalize; word-wrap: break-word; margin-bottom: 1px;">Political Alignment</div>

                <div style="width: 292px; height: 8px; position: relative">
                    <!-- Bar -->
                    <div style="width: 292px; height: 3px; left: 0px; top: 3px; position: absolute; background: linear-gradient(90deg, #FF9AB3 0%, #7F98FF 100%); border-radius: 4px"></div>

                    <!-- Circle -->
                    <div id="political" style="width: 9px; height: 9px; left: 201.46px; top: 0px; position: absolute; background: #A299EA; border-radius: 9999px; border: 1.5px white solid"></div>
                    <!-- Circle -->
                </div>
                <div style="height: 7px; position: relative">
                    <div style="width: 50px; left: 0px; top: 0px; position: absolute; text-align: left; color: white; font-size: 8px; font-family: SF Pro Display; font-weight: 200; text-transform: capitalize; word-wrap: break-word">Conservative</div>
                    <div style="width: 50px; right: -292px; top: 0px; position: absolute; text-align: right; color: white; font-size: 7px; font-family: SF Pro Display; font-weight: 200; text-transform: capitalize; word-wrap: break-word">Liberal</div>
                </div>
            </div>
    
          </div>
  
    </div>

    <div style="display: flex; direction: column; justify-content: center; width: 100%; margin-top: 20px;">

      <!-- Economic Alignment Card-->
      <div style="width: 300x; height: 50px; padding-top: 5px; padding-bottom: 5px; padding-left: 14px; padding-right: 14px; background: rgba(0, 0, 0, 0.18); border-radius: 10px; flex-direction: column; justify-content: center; align-items: flex-start; gap: 6.58px; display: inline-flex">
          
          <div style="height: 30px; flex-direction: column; justify-content: flex-start; align-items: flex-start; display: flex">

              <!-- Title -->
              <div style="text-align: center; color: white; font-size: 10px; font-family: SF Pro Display; font-weight: 350; text-transform: capitalize; word-wrap: break-word; margin-bottom: 1px;">Economic Alignment</div>

              <div style="width: 292px; height: 8px; position: relative">
                  <!-- Bar -->
                  <div style="width: 292px; height: 3px; left: 0px; top: 3px; position: a
[truncated — 5334 more characters]
```