# Project export: SatChat

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: CruzHacks 2025
- Tagline: SatChat: AI Access Anywhere
- Devpost: https://devpost.com/software/satchat
- GitHub: https://github.com/preettank/SatChat
- Video: https://www.youtube.com/embed/ffCTw8Z_fz0?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner (Health Hacks)
- Team: 2 GitHub contributor(s) — tvaze (6 commits), Preet Tank (4 commits)

## Devpost submission (written by the team)

### Inspiration

We wanted to increase the accessibility of LLM technology when people don't have access to the internet.

### What it does

Imagine you are stranded or lost without wifi or cellular service and you need to answers to your questions. For example, let's say that you are camping in the woods and forgot how to light a fire. You can't connect to the internet or any AI but, you can use your phone that has satellite connection to send messages to our number which will return answers that Gemini creates. Imagine you are out in the field studying different plants in the amazon rainforest. SatChat has your back, with just a satellite connection you can learn more about specific plants or fauna.

### How we built it

We created a google extension that allows us to scrape google voice to get incoming messages from users. By doing this we were able to complete the project without using a SMS API simulator like Twilio which is something we initially had trouble with. When the google extension finds a new incoming message, it sends it to our Flask server which then sends the message to Gemini which returns a response back to our Flask server. We then use Google Voice to send that response back to the user automatically. You can now talk to a chatbot straight through your messages/texts.

### Challenges we ran into

While using Twilio, we ran into a lot of issues with setting up the phone numbers to make them work. There were many different permissions required that needed multiple days to get approved. We didn't have time to wait, so we had to pivot and change how we built SatChat. Google voice doesn't have an API, so we had to manually web scrape the page to get the content we needed. We also ran into some issues setting up a Google Chrome extension to work with our backend. It took many revisions to get it to work properly.

### Accomplishments we're proud of

We are proud that we created a useful hack that can help people in situations where they need information quickly and don't have access to great data or wifi. We are also proud that even though we hit a bottleneck that completely halted our progress, we were able to pivot and think of a way to finish building our project without using the "easier" method.

### What we learned

Before we even started working on the project, we started to look at our own lives and aimed to solve an issue that people face. With that, we learned to keep the users needs at the top priority throughout the development process. We also learned that it's important to be able to adapt and to keep momentum when working on a project.

### What's next

Our idea is meant to be simple, so the first thing we would like to do is move from the web extension to a real SMS API provider like Twilio. After that we would love to go public and provide this app for real life users that face the same problems.

## README (from the GitHub repository)

# SatChat

## Detected evidence (automated analysis)

Indexed codebase: 6 recognized source files, 30 KB.
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- Flask (technology) — claimed on Devpost, not found in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (10 of 10)

```
.gitignore
extension/background.js
extension/content.js
extension/manifest.json
extension/popup.html
extension/popup.js
flasky.py
LICENSE
README.md
requirements.txt
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- working code
- sending response now
- added the prints for responses
- new
- removed the endpoint needed error
- hello
- populated files
- added extension scaffolding
- initial
- Initial commit

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

### requirements.txt

```
��a i o h a p p y e y e b a l l s = = 2 . 6 . 1  
 a i o h t t p = = 3 . 1 1 . 1 6  
 a i o h t t p - r e t r y = = 2 . 9 . 1  
 a i o s i g n a l = = 1 . 3 . 2  
 a n n o t a t e d - t y p e s = = 0 . 7 . 0  
 a n y i o = = 4 . 9 . 0  
 a s y n c - t i m e o u t = = 5 . 0 . 1  
 a t t r s = = 2 5 . 3 . 0  
 b l i n k e r = = 1 . 9 . 0  
 c a c h e t o o l s = = 5 . 5 . 2  
 c e r t i f i = = 2 0 2 5 . 1 . 3 1  
 c h a r s e t - n o r m a l i z e r = = 3 . 4 . 1  
 c l i c k = = 8 . 1 . 8  
 c o l o r a m a = = 0 . 4 . 6  
 d o t e n v = = 0 . 9 . 9  
 e x c e p t i o n g r o u p = = 1 . 2 . 2  
 F l a s k = = 3 . 1 . 0  
 f r o z e n l i s t = = 1 . 5 . 0  
 g o o g l e - a u t h = = 2 . 3 8 . 0  
 g o o g l e - g e n a i = = 1 . 1 0 . 0  
 h 1 1 = = 0 . 1 4 . 0  
 h t t p c o r e = = 1 . 0 . 8  
 h t t p x = = 0 . 2 8 . 1  
 i d n a = = 3 . 1 0  
 i t s d a n g e r o u s = = 2 . 2 . 0  
 J i n j a 2 = = 3 . 1 . 6  
 M a r k u p S a f e = = 3 . 0 . 2  
 m u l t i d i c t = = 6 . 4 . 3  
 p r o p c a c h e = = 0 . 3 . 1  
 p y a s n 1 = = 0 . 6 . 1  
 p y a s n 1 _ m o d u l e s = = 0 . 4 . 2  
 p y d a n t i c = = 2 . 1 1 . 3  
 p y d a n t i c _ c o r e = = 2 . 3 3 . 1  
 P y J W T = = 2 . 1 0 . 1  
 p y t h o n - d o t e n v = = 1 . 1 . 0  
 r e q u e s t s = = 2 . 3 2 . 3  
 r s a = = 4 . 9  
 s n i f f i o = = 1 . 3 . 1  
 t w i l i o = = 9 . 5 . 2  
 t y p i n g - i n s p e c t i o n = = 0 . 4 . 0  
 t y p i n g _ e x t e n s i o n s = = 4 . 1 3 . 2  
 u r l l i b 3 = = 2 . 4 . 0  
 w e b s o c k e t s = = 1 5 . 0 . 1  
 W e r k z e u g = = 3 . 1 . 3  
 y a r l = = 1 . 1 9 . 0  
 
```

### flasky.py

```python
from flask import Flask, request, jsonify
from google import genai
import os
from dotenv import load_dotenv

load_dotenv()

app = Flask(__name__)

# Set your Gemini API Key
client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))

@app.route("/", methods=["GET"])
def home():
    return "SatChat is live 🚀", 200

@app.route("/sms", methods=["POST"])
def sms_reply():
    data = request.get_json()

    print(data)
    incoming_msg = data.get("text", "")

    print(f"Incoming message: {incoming_msg}")

    if not incoming_msg:
        return jsonify({ "success": False, "error": "Missing text" }), 400

    try:
        response = client.models.generate_content(
            model="gemini-2.0-flash",
            contents=incoming_msg
        )

        return jsonify({
            "success": True,
            "reply": response.text 
        }), 200

    except Exception as e:
        return jsonify({ "success": False, "error": str(e) }), 500
    
@app.route("/test_gemini", methods=["GET"])
def test_gemini():
    prompt = request.args.get("q", "Say something cool about satellites")
    try:
        response = client.models.generate_content(
            model="gemini-2.0-flash",
            contents=prompt
        )
        return {"response": response.text}, 200
    except Exception as e:
        return {"error": str(e)}, 500
    
if __name__ == "__main__":
    app.run(port=5000, debug=True)

```

### extension/popup.html

```html
<!DOCTYPE html>
<html>
<head>
  <title>SMS Text Scraper</title>
  <style>
    body {
      width: 350px;
      font-family: Arial, sans-serif;
      padding: 15px;
    }
    h2 {
      color: #4285f4;
      margin-top: 0;
    }
    .section {
      margin-bottom: 15px;
      border-bottom: 1px solid #eee;
      padding-bottom: 15px;
    }
    label {
      display: block;
      margin-bottom: 5px;
      font-weight: bold;
    }
    select, input[type="text"] {
      width: 100%;
      padding: 8px;
      margin-bottom: 10px;
      border: 1px solid #ccc;
      border-radius: 4px;
      box-sizing: border-box;
    }
    button {
      background-color: #4285f4;
      color: white;
      border: none;
      padding: 10px 15px;
      border-radius: 4px;
      cursor: pointer;
      font-size: 14px;
    }
    button:hover {
      background-color: #357ae8;
    }
    #result {
      margin-top: 15px;
      padding: 10px;
      background-color: #f7f7f7;
      border-radius: 4px;
      max-height: 200px;
      overflow-y: auto;
      word-break: break-word;
    }
    .success {
      color: green;
    }
    .error {
      color: red;
    }
    #copyButton {
      background-color: #34a853;
      margin-top: 10px;
    }
    #copyButton:hover {
      background-color: #2e9549;
    }
    .selector-controls {
      display: flex;
      align-items: center;
      margin-bottom: 10px;
    }
    .selector-controls button {
      margin-left: 10px;
      padding: 8px;
    }
    #selectors {
      margin-bottom: 10px;
    }
    .selector-row {
      display: flex;
      align-items: center;
      margin-bottom: 5px;
    }
    .selector-row input {
      flex-grow: 1;
    }
    .selector-row button {
      margin-left: 5px;
      padding: 5px 8px;
      background-color: #ea4335;
    }
    .endpoint-section input {
      margin-bottom: 0;
    }
    
    /* Monitoring Toggle Styles */
    .toggle-container {
      display: flex;
      align-items: center;
      margin-bottom: 15px;
    }
    .toggle-switch {
      position: relative;
      display: inline-block;
      width: 50px;
      height: 24px;
      margin-right: 10px;
    }
    .toggle-switch input {
      opacity: 0;
      width: 0;
      height: 0;
    }
    .slider {
      position: absolute;
      cursor: pointer;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background-color: #ccc;
      transition: .4s;
      border-radius: 24px;
    }
    .slider:before {
      position: absolute;
      content: "";
      height: 16px;
      width: 16px;
      left: 4px;
      bottom: 4px;
      background-color: white;
      transition: .4s;
      border-radius: 50%;
    }
    input:checked + .slider {
      background-color: #34a853;
    }
    input:checked + .slider:before {
      transform: translateX(26px);
    }
    .monitoring-active {
      color: #34a853;
      font-weight: bold;
    }
    .monitoring-inactive {
      color: #757575;
    }
    .monitor-label {
      font-weight: normal;
      margin-left: 5px;
    }
  </style>
</head>
<body>
  <h2>SMS Text Scraper</h2>
  
  <div class="section">
    <label for="selectorMethod">Selection Method:</label>
    <select id="selectorMethod">
      <option value="css">CSS Selector</option>
      <option value="xpath">XPath</option>
      <option value="custom">Custom Selectors</option>
    </select>
    
    <div id="standardSelector">
      <label for="selector">Enter Selector:</label>
      <input type="text" id="selector" placeholder="e.g., .message-text or //div[@class='message']">
    </div>
    
    <div id="customSelectors" style="display:none;">
      <label>Custom Selectors:</label>
      <div id="selectors"></div>
      <div class="selector-controls">
        <input type="text" id="newSelector" placeholder="Add new selector...">
        <button id="addSelector">Add</button>
      </div>
    </div>
  </div>
  
  <div class="section endpoint-section">
    <label for="endpoint">API Endpoint (optional):</label>
    <input type="text" id="endpoint" placeholder="https://your-api.com/process">
    <small>If provided, scraped text will be sent to this endpoint</small>
  </div>
  
  <div class="section">
    <div class="toggle-container">
      <label class="toggle-switch">
        <input type="checkbox" id="monitorToggle">
        <span class="slider"></span>
      </label>
      <span id="monitorStatus" class="monitoring-inactive">Monitoring: Inactive</span>
    </div>
    <small>When enabled, continuously monitors for new text and automatically sends to the API</small>
  </div>
  
  <button id="scrapeButton">Scrape Text</button>
  
  <div id="result" style="display:none;">
    <h3>Scraped Text:</h3>
    <div id="scrapedText"></div>
    <button id="copyButton">Copy to Clipboard</button>
  </div>

  <script src="popup.js"></script>
</body>
</html>
```

### extension/background.js

```javascript
// Service worker for the Chrome extension
// This handles background tasks and ensures the extension remains active

// Set up initial default settings when the extension is installed
chrome.runtime.onInstalled.addListener(function(details) {
  if (details.reason === 'install') {
    // Set default settings
    chrome.storage.local.set({
      selectorMethod: 'css',
      selector: '.message, .text-message, .sms-message',
      customSelectors: ['.message', '.text-message', '.message-body'],
      endpoint: '',
      isMonitoring: false
    });
  }
});

// Listen for context menu clicks if you want to add right-click functionality
chrome.contextMenus?.create({
  id: 'scrapeSelection',
  title: 'Scrape this text',
  contexts: ['selection']
});

// Add a monitoring toggle to the context menu
chrome.contextMenus?.create({
  id: 'toggleMonitoring',
  title: 'Toggle auto-monitoring',
  contexts: ['page']
});

chrome.contextMenus?.onClicked.addListener((info, tab) => {
  console.log("Context Menu Clicked: Starting process"); // Basic logging
  
  if (info.menuItemId === 'scrapeSelection') {
    console.log("Scrape Selection Menu Item Selected");
    
    const selectedText = info.selectionText;
    console.log("Selected Text:", selectedText);
    
    // Retrieve endpoint from storage
    chrome.storage.local.get(['endpoint'], function(data) {
      console.log("Endpoint from storage:", data.endpoint);
      
      if (data.endpoint) {
        console.log("Preparing to send fetch request");
        
        fetch(data.endpoint, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            text: selectedText,
            source: tab.url,
            timestamp: new Date().toISOString(),
            method: 'context_menu_selection',
            endpoint: tab.url
          })
        })
        .then(response => {
          console.log("Fetch Response Received");
          console.log("Response Status:", response.status);
          return response.json();
        })
        .then(data => {
          console.log("Parsed Response Data:", data);
          
          // If there's a reply, send it to Google Voice
          if (data.reply) {
            chrome.tabs.query({url: '*://voice.google.com/*'}, (tabs) => {
              if (tabs.length > 0) {
                chrome.tabs.sendMessage(tabs[0].id, {
                  action: 'sendMessage',
                  text: data.reply
                });
              }
            });
          }
        })
        .catch(error => {
          console.error("Fetch Error:", error);
        });
      } else {
        console.warn("No endpoint configured in storage");
      }
    });
  }
  else if (info.menuItemId === 'toggleMonitoring') {
    // Toggle monitoring state
    chrome.storage.local.get(['isMonitoring'], function(data) {
      const newMonitoringState = !data.isMonitoring;
      
      // Update storage
      chrome.storage.local.set({ isMonitoring: newMonitoringState });
      
      // Get selectors and endpoint to pass to content script
      chrome.storage.local.get(['selectorMethod', 'selector', 'customSelectors', 'endpoint'], function(settings) {
        let selectors = [];
        
        if (settings.selectorMethod === 'custom' && Array.isArray(settings.customSelectors)) {
          settings.customSelectors.forEach(selector => {
            selectors.push({
              type: 'css',
              value: selector
            });
          });
        } else if (settings.selector) {
          selectors.push({
            type: settings.selectorMethod || 'css',
            value: settings.selector
          });
        }
        
        // Send message to content script
        chrome.tabs.sendMessage(tab.id, {
          action: newMonitoringState ? 'startMonitoring' : 'stopMonitoring',
          selectors: selectors,
          endpoint: settings.endpoint
        });
        
        // Show notification
        chrome.notifications.create({
          type: 'basic',
          iconUrl: 'icon48.png',
          title: 'Text Scraper',
          message: newMonitoringState ? 'Monitoring activated' : 'Monitoring deactivated'
        });
      });
    });
  }
});

// Listen for monitoring status changes
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === 'monitoringStatusChanged') {
    // Update badge to indicate monitoring status
    chrome.action.setBadgeText({ 
      text: request.isMonitoring ? 'ON' : '',
      tabId: sender.tab.id
    });
    
    // Set badge color
    chrome.action.setBadgeBackgroundColor({ 
      color: request.isMonitoring ? '#4CAF50' : '#F44336',
      tabId: sender.tab.id
    });
    
    // Store monitoring state
    chrome.storage.local.set({ isMonitoring: request.isMonitoring });
  }
});

// Handle scraping text message
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === 'scrapeText') {
    // Forward the scraping request to the content script of the active tab
    chrome.tabs.query({url: '*://voice.google.com/*', active: true}, (tabs) => {
      if (tabs.length > 0) {
        chrome.tabs.sendMessage(tabs[0].id, {
          action: 'scrapeText',
          text: request.text,
          endpoint: request.endpoint
        }, (response) => {
          sendResponse(response);
        });
      }
    });
    return true; // Enable async response
  }
  
  if (request.action === 'sendToGoogleVoice') {
    // Find the active Google Voice tab
    chrome.tabs.query({url: '*://voice.google.com/*'}, (tabs) => {
      if (tabs.length > 0) {
        // Send message to the first Google Voice tab
        chrome.tabs.sendMessage(tabs[0].id, {
          action: 'sendMessage',
          text: request.text
        }, (response) => {
          console.log('Send message response:', response);
          sendResponse(response);
        });
      } else {
   
[truncated — 183 more characters]
```

### extension/popup.js

```javascript
document.addEventListener('DOMContentLoaded', function() {
  // DOM elements
  const selectorMethod = document.getElementById('selectorMethod');
  const standardSelector = document.getElementById('standardSelector');
  const customSelectors = document.getElementById('customSelectors');
  const selectorsContainer = document.getElementById('selectors');
  const newSelectorInput = document.getElementById('newSelector');
  const addSelectorButton = document.getElementById('addSelector');
  const scrapeButton = document.getElementById('scrapeButton');
  const monitorToggle = document.getElementById('monitorToggle');
  const monitorStatus = document.getElementById('monitorStatus');
  const result = document.getElementById('result');
  const scrapedText = document.getElementById('scrapedText');
  const copyButton = document.getElementById('copyButton');
  const endpointInput = document.getElementById('endpoint');
  
  // Load saved settings
  chrome.storage.local.get(['selectorMethod', 'selector', 'customSelectors', 'endpoint', 'isMonitoring'], function(data) {
    if (data.selectorMethod) {
      selectorMethod.value = data.selectorMethod;
      toggleSelectorMethod(data.selectorMethod);
    }
    
    if (data.selector) {
      document.getElementById('selector').value = data.selector;
    }
    
    if (data.customSelectors && Array.isArray(data.customSelectors)) {
      data.customSelectors.forEach(selector => addSelectorRow(selector));
    }
    
    if (data.endpoint) {
      endpointInput.value = data.endpoint;
    }
    
    // Update monitoring toggle state
    if (data.isMonitoring) {
      monitorToggle.checked = true;
      monitorStatus.textContent = 'Monitoring: Active';
      monitorStatus.className = 'monitoring-active';
    } else {
      monitorToggle.checked = false;
      monitorStatus.textContent = 'Monitoring: Inactive';
      monitorStatus.className = 'monitoring-inactive';
    }
  });
  
  // Selection method change handler
  selectorMethod.addEventListener('change', function() {
    const method = this.value;
    toggleSelectorMethod(method);
    saveSettings();
  });
  
  function toggleSelectorMethod(method) {
    if (method === 'custom') {
      standardSelector.style.display = 'none';
      customSelectors.style.display = 'block';
    } else {
      standardSelector.style.display = 'block';
      customSelectors.style.display = 'none';
    }
  }
  
  // Add selector button handler
  addSelectorButton.addEventListener('click', function() {
    const newSelector = newSelectorInput.value.trim();
    if (newSelector) {
      addSelectorRow(newSelector);
      newSelectorInput.value = '';
      saveSettings();
    }
  });
  
  // Add selector when pressing Enter
  newSelectorInput.addEventListener('keyup', function(event) {
    if (event.key === 'Enter') {
      addSelectorButton.click();
    }
  });
  
  // Function to add a new selector row
  function addSelectorRow(selector) {
    const row = document.createElement('div');
    row.className = 'selector-row';
    
    const input = document.createElement('input');
    input.type = 'text';
    input.className = 'custom-selector';
    input.value = selector;
    input.addEventListener('change', saveSettings);
    
    const removeButton = document.createElement('button');
    removeButton.textContent = 'X';
    removeButton.addEventListener('click', function() {
      row.remove();
      saveSettings();
    });
    
    row.appendChild(input);
    row.appendChild(removeButton);
    selectorsContainer.appendChild(row);
  }
  
  // Save settings to storage
  function saveSettings() {
    const method = selectorMethod.value;
    const selector = document.getElementById('selector').value;
    const endpoint = endpointInput.value;
    
    let customSelectorsArray = [];
    if (method === 'custom') {
      const selectorInputs = document.querySelectorAll('.custom-selector');
      selectorInputs.forEach(input => {
        if (input.value.trim()) {
          customSelectorsArray.push(input.value.trim());
        }
      });
    }
    
    chrome.storage.local.set({
      selectorMethod: method,
      selector: selector,
      customSelectors: customSelectorsArray,
      endpoint: endpoint
    });
  }
  
  // Monitoring toggle handler
  monitorToggle.addEventListener('change', function() {
    const isMonitoring = this.checked;
    
    if (isMonitoring) {
      monitorStatus.textContent = 'Monitoring: Active';
      monitorStatus.className = 'monitoring-active';
    } else {
      monitorStatus.textContent = 'Monitoring: Inactive';
      monitorStatus.className = 'monitoring-inactive';
    }
    
    // Save monitoring state
    chrome.storage.local.set({ isMonitoring: isMonitoring });
    
    // Send message to content script to start/stop monitoring
    chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
      chrome.tabs.sendMessage(
        tabs[0].id,
        {
          action: isMonitoring ? 'startMonitoring' : 'stopMonitoring',
          selectors: getSelectors(),
          endpoint: endpointInput.value.trim()
        }
      );
    });
  });
  
  // Get current selectors based on settings
  function getSelectors() {
    const method = selectorMethod.value;
    let selectors = [];
    
    if (method === 'custom') {
      const selectorInputs = document.querySelectorAll('.custom-selector');
      selectorInputs.forEach(input => {
        if (input.value.trim()) {
          selectors.push({
            type: 'css',
            value: input.value.trim()
          });
        }
      });
    } else {
      const selectorValue = document.getElementById('selector').value.trim();
      if (selectorValue) {
        selectors.push({
          type: method,
          value: selectorValue
        });
      }
    }
    
    return selectors;
  }
  
  // Scrape button handler
  scrapeButton.addEventListener('click', function() {
    const selectors = getSelectors();
    
    if (selectors.length === 0) {
     
[truncated — 1789 more characters]
```

### extension/content.js

```javascript
let lastScrapedText = null;
let monitoringInterval = null;
let currentSelectors = [];
let currentEndpoint = '';

// Listen for messages from the popup
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
  if (request.action === 'scrapeText') {
    const result = scrapeTextFromPage(request.selectors);
    
    // Store the last scraped text for comparison during monitoring
    if (result.success) {
      lastScrapedText = result.text;
    }
    
    // If endpoint is provided, send the data to the endpoint
    if (request.endpoint && result.success) {
      sendToEndpoint(result.text, request.endpoint, result.phone)
        .then(response => {
          // Handle auto-reply if available
          if (response.reply) {
            chrome.runtime.sendMessage({
              action: 'sendToGoogleVoice', 
              text: response.reply 
            }, (response) => {
              console.log('Google Voice message send result:', response);
            });
          }
          
          sendResponse({ 
            success: true, 
            text: result.text,
            phone: result.phone,
            apiResponse: response
          });
        })
        .catch(error => {
          sendResponse({ 
            success: true, 
            text: result.text,
            phone: result.phone,
            apiError: `Failed to send to API: ${error.message}`
          });
        });
      return true; // Keep the messaging channel open for the async response
    }
    
    // Otherwise just return the scraped text
    sendResponse(result);
  }
  else if (request.action === 'startMonitoring') {
    startTextMonitoring(request.selectors, request.endpoint);
    sendResponse({ success: true, message: 'Monitoring started' });
  }
  else if (request.action === 'stopMonitoring') {
    stopTextMonitoring();
    sendResponse({ success: true, message: 'Monitoring stopped' });
  }
  else if (request.action === 'sendMessage') {
    sendGoogleVoiceMessage(request.text)
      .then(success => {
        sendResponse({ 
          success: success,
          message: success ? 'Message sent successfully' : 'Failed to send message'
        });
      })
      .catch(error => {
        sendResponse({ 
          success: false, 
          message: `Error: ${error.message}`
        });
      });
    
    return true; // Required for async sendResponse
  }
  
  return true; // Enable async response
});

function startTextMonitoring(selectors, endpoint) {
  // Store the current selectors and endpoint for use in the monitoring function
  currentSelectors = selectors;
  currentEndpoint = endpoint;
  
  // Stop any existing monitoring
  stopTextMonitoring();
  
  console.log('Starting text monitoring with selectors:', selectors);
  
  // Start the monitoring interval
  monitoringInterval = setInterval(() => {
    const result = scrapeTextFromPage(currentSelectors);
    
    if (result.success) {
      // If the text has changed and isn't empty
      if (result.text && result.text !== lastScrapedText) {
        console.log('Text change detected:', result.text);
        
        // Send to endpoint if provided
        if (currentEndpoint) {
          sendToEndpoint(result.text, currentEndpoint, result.phone)
            .then(response => {
              console.log('API Response:', response);
              
              // Auto-reply if available
              if (response.reply) {
                chrome.runtime.sendMessage({
                  action: 'sendToGoogleVoice', 
                  text: response.reply 
                }, (sendResponse) => {
                  console.log('Google Voice message send result:', sendResponse);
                });
              }
            })
            .catch(error => {
              console.error('API Error:', error);
            });
        }
        
        // Update the last scraped text
        lastScrapedText = result.text;
      }
    }
  }, 3000); // Check every 3 seconds - adjust as needed
  
  // Notify background script that monitoring is active
  chrome.runtime.sendMessage({ action: 'monitoringStatusChanged', isMonitoring: true });
}

function stopTextMonitoring() {
  if (monitoringInterval) {
    console.log('Stopping text monitoring');
    clearInterval(monitoringInterval);
    monitoringInterval = null;
    
    // Notify background script that monitoring is inactive
    chrome.runtime.sendMessage({ action: 'monitoringStatusChanged', isMonitoring: false });
  }
}

function scrapeTextFromPage(selectors) {
  try {
    let results = [];
    let phoneNumber = null;
    
    // Select all incoming messages
    const incomingMessageSelector = '.message-row:not(.outgoing)';
    const messageElements = document.querySelectorAll(incomingMessageSelector);
    
    // Get the last incoming message
    if (messageElements.length > 0) {
      const lastMessage = messageElements[messageElements.length - 1];
      const messageContent = lastMessage.querySelector('.subject-content-container .content');
      
      if (messageContent) {
        const text = messageContent.textContent.trim();
        if (text) {
          results.push(text);
        }
      }
    }
    
    // Extract phone number
    const phoneNumberElement = document.querySelector('.secondary-text');
    if (phoneNumberElement) {
      phoneNumber = phoneNumberElement.textContent.trim().replace(/[^\d]/g, '');
    }
    
    if (results.length === 0) {
      return {
        success: false,
        error: 'No incoming messages found'
      };
    }
    
    return {
      success: true,
      text: results[0], // Just the last message
      phone: phoneNumber
    };
  } catch (error) {
    return {
      success: false,
      error: `Error scraping text: ${error.message}`
    };
  }
}

async function sendToEndpoint(text, endpoint, phoneNumber = null) {
  console.log('Sending to endpoint:', {
    endpoint: endpoint,
    text: text,
    phone: phoneNumber,
    source: window.location.h
[truncated — 4270 more characters]
```