# Project export: Context Click

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: Context on the latest stories is a click away.
- Devpost: https://devpost.com/software/context-click
- GitHub: https://github.com/bluekid2457/ContextClick
- Team: 1 GitHub contributor(s) — Bluekid (2 commits)

## Devpost submission (written by the team)

### Inspiration

Today news travels faster than ever and it is getting harder and harder to stay up to date but also make sure that the new that you are reading is accurate. We wanted to make determining the accuracy of news articles as easy as possible for users to get with a click of a button. When the news article is analyzed, key topics and phrases are highlighted. Users can then click on the highlighted text to read new and updated information on the topic.

### What it does

Our project is a Google Chrome extension that when activated highlights key quotes in the current tab's news article and provides further context on those topics. This analysis is done by Google Gemini's 1.5 Flash.

### How we built it

We used Javascript, Google Gemini API, HTML, and CSS. First we read the text on the webpage, sent it to Google Gemini, and then parsed Google Gemini's response with Javascript. Finally, we displayed everything on the webpage using HTML and CSS.

### Challenges we ran into

We initially were using another LLM API to analyze news articles but found the performance and formatting too inconsistent. We then switched to Google Gemini's 1.5 Flash and were much more satisfied with the performance.

### Accomplishments we're proud of

We are proud of completing a viable product and getting experience with a less popular area of web development.

### What we learned

We learned about the process of creating a Chrome extension, Javascript development, using HTML and CSS for essential formatting and styling, and prompt engineering APIs.

### What's next

We want to improve the accuracy of our LLM's quote background as well as the design and styling of our quote analysis boxes.

## README (from the GitHub repository)

# ContextClick
Context is just a click away


## Detected evidence (automated analysis)

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

## Codebase structure (from repository index)

### Files (7 of 7)

```
annotate.js
background.js
content.js
manifest.json
popup.html
popup.js
README.md
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- initial commit
- Initial commit

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

### content.js

```javascript
// Get all visible text from the page
const pageText = document.body.innerText;
console.log('Extracted page text:', pageText);
chrome.storage.local.set({ pageText: pageText }, () => {
  console.log('Page text stored');
});

function replaceAllDoubleQuotes() {
  const walker = document.createTreeWalker(
    document.body,
    NodeFilter.SHOW_TEXT,
    null,
    false
  );

  let node;
  while (node = walker.nextNode()) {
    // Replace all occurrences of " with '
    node.nodeValue = node.nodeValue.replace(/"/g, "'");
  }
}

replaceAllDoubleQuotes();
```

### popup.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>News Page Analyzer</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      margin: 0;
      padding: 0;
      background-color: transparent;
    }
    .container {
      background-color: #fff;
      border: 4px solid #28a745; 
      border-radius: 16px;
      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
      padding: 20px;
      width: 66%;
      max-width: 800px;
      min-width: 300px;
      aspect-ratio: 3 / 2;
      text-align: center;
      margin: 100px auto 0 auto;  
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
    }

    h2 {
      color: #333;
      margin-bottom: 20px;
    }

    label {
      display: block;
      margin-bottom: 10px;
      font-size: 1rem;
      color: #555;
    }

    button {
      background-color: #006D5B;
      color: #fff;
      border: none;
      border-radius: 4px;
      padding: 10px 20px;
      font-size: 1rem;
      cursor: pointer;
      transition: background-color 0.3s ease;
    }

    button:hover {
      background-color: #00544a;
    }

    textarea, input[type="text"] {
      width: 100%;
      padding: 10px;
      border: 1px solid #ccc;
      border-radius: 4px;
      margin-bottom: 15px;
      font-size: 1rem;
    }
  </style>
</head>
<body>
  <div class="container">
    <h2>Context Click</h2>
    <label for="question">Click below to get background on the news article!</label>
    <button id="askButton">Analyze!</button>
  </div>
</body>
</html>
<script src="popup.js"></script>

```

### annotate.js

```javascript
function annotateQuote(quote, noteText) {
    const iterator = document.createNodeIterator(
        document.body,
        NodeFilter.SHOW_TEXT,
        null,

    );
  
    let textNode;
    while ((textNode = iterator.nextNode())) {

        if (textNode.parentNode.nodeName === 'SCRIPT' || textNode.parentNode.nodeName === 'STYLE' || textNode.parentNode.classList.contains('highlighted-quote') || textNode.parentNode.classList.contains('quote-note')) {
            continue;
        }
  
        const pos = textNode.nodeValue.indexOf(quote);
        if (pos !== -1) {
            try {
                const range = document.createRange();
                range.setStart(textNode, pos);
                range.setEnd(textNode, pos + quote.length);
  
                const span = document.createElement('span');
                span.className = 'highlighted-quote';
   
                span.dataset.note = noteText;
                range.surroundContents(span); 
  
                const noteBox = document.createElement('div');
                noteBox.className = 'quote-note'; 
                noteBox.textContent = noteText;
  
                document.body.appendChild(noteBox);
                
                
  

span.addEventListener('click', function(event) {

    event.stopPropagation();

    const clickX = event.pageX;
    const clickY = event.pageY;
    
    noteBox.style.top = (clickY - 150) + "px";
    noteBox.style.left = (clickX + 5) + "px";
  

    noteBox.classList.toggle('visible');
  });

                break;
  
            } catch (e) {
                console.error("Error surrounding contents:", e, " Text node:", textNode.nodeValue, " Quote:", quote);

            }
        }
    }
  }
  
  function insertAnnotationStyles() {
    const styleId = 'annotation-styles';
    if (document.getElementById(styleId)) {
        return;
    }
    const style = document.createElement('style');
    style.id = styleId;
    style.textContent = `
      .highlighted-quote {
        background-color: yellow;
        cursor: pointer;
        /* position: relative; /* Only needed if positioning note absolutely */
      }
      .quote-note {
  position: absolute;  /* Removes the element from the document flow */
  display: none;       /* Hidden by default; toggle visibility on click */
  border: 2px solid #006D5B;
  background-color: rgba(240,240,240,0.9); /* slightly opaque background */
  padding: 5px;
  font-size: 90%;
  max-width: 250px;
  z-index: 1000;
  border-radius: 10px;
  box-shadow: 0px 0px 5px 2px rgba(70, 130, 118, 255);
}
.quote-note.visible {
  display: block; /* Show when toggled */
}

    `;
    document.head.appendChild(style);
  }
  insertAnnotationStyles();
  console.log("Annotate.js script is running"); 
//   annotateQuote("few weeks", 'Ensure it appears on top if overlapping Ensure it appears on top if overlapping Ensure it appears on top if overlapping Ensure it appears on top if overlapping\n Ensure it appears on top if overlapping');
  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
    console.log("Received message:", message); 
    if (message.type === "annotateQuote") {
        me = message.quote.trim();
        m = me.substring(1, me.length-1);;
        console.log("Quote to annotate:", m); 
      annotateQuote(m, message.noteText);
      sendResponse({ status: "annotated" });
    }
  });
  
```

### background.js

```javascript
chrome.runtime.onMessage.addListener((payload, sender, sendResponse) => {
  const { question, text } = payload;

  const GOOGLE_API_KEY = "Need to set";
  const model = "gemini-1.5-flash-latest"; // Or "gemini-1.5-flash", "gemini-1.5-pro-latest" etc.
  const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${GOOGLE_API_KEY}`;

  const prompt = `You will be given a news page. Your job is to find relevant information that may conflict with the messages in the article. Focus on specific details and see if they are accurate. The article will have either outdated or plainly wrong facts, your job is to find those. For example innacurate tariff numbers, search for current tarrif values.\
              Keep in mind that you are seeing all the text in a page and alot of stuff in the beggining and end may be ads and not related. \
    Quote the EXACT QUOTE (DO NOT CHANGE ANYTHING ABOUT THE QUOTE including adding ellipses (...) You dont need the full quote just a major section is fine) from the article and provide some alternate information from differnt sources but keep it very short 1-2 sentences each. The quote you are quotes doesnt actually have to be a quote just somethign exactly referenced from the text. Here is the strict format you are to use:\n\
    Topic: *Iran-US Nuclear Talks*\n\
    Quote: *Iran having to sit down with the US is a 'political win' for the Trump admin, says Joey Jones.*\n\
    Alternative Information: *Iran is reportedly considering an interim nuclear deal to avoid escalation and buy time for more comprehensive negotiations[1][3]. The success of these talks depends on both sides' willingness to compromise, particularly on issues like sanctions and nuclear enrichment[1][3].*\n\
    Sources: [1](https://www.axios.com/2025/04/10/iran-nuclear-deal-us-interim-agreement), [3](https://www.timesofisrael.com/iran-said-to-consider-proposing-interim-nuclear-deal-in-upcoming-talks-with-us/)\n\
    Topic: *Another Topic*\n\
    Quote: *Another line of text in the article*\n\
    Alternative Information: *Additional info here.*\n\
    Sources: [2](https://www.abc.def), [9](https://www.hdsf.com/that-is-really-cool//)\n\
    // (IMPORTANT, as a final note, do not deviate from this format for any reason) Give atleast 5 of these critiques.  \n Here is the page content:\n${text}`;

  const requestBody = {
    contents: [{
      parts: [{
        text: prompt
      }]
    }],
    generationConfig: {
      maxOutputTokens: 1500,
    },
 
  };

  fetch(apiUrl, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(requestBody)
  })
  .then(response => {
      if (!response.ok) {
          return response.json().then(errorData => {
              console.error('Gemini API Error Response:', errorData);
              throw new Error(`HTTP error ${response.status}: ${errorData?.error?.message || response.statusText}`);
          }).catch(parseError => {
              console.error('Failed to parse error response:', parseError);
              throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
          });
      }
      return response.json();
  })
  .then(data => {
    let answer = "Sorry, couldn't get a valid answer from Gemini."; // Default message

    try {
      if (data && data.candidates && data.candidates.length > 0) {
        const candidate = data.candidates[0];
        if (candidate.finishReason && candidate.finishReason !== "STOP" && candidate.finishReason !== "MAX_TOKENS") {
           console.warn(`Gemini generation finished due to ${candidate.finishReason}. Safety Ratings:`, candidate.safetyRatings);
           answer = `Generation stopped: ${candidate.finishReason}. Check safety ratings or try rephrasing.`;
        } else if (candidate.content && candidate.content.parts && candidate.content.parts.length > 0) {
           answer = candidate.content.parts[0].text;
        } else {
           console.warn('Gemini response candidate has no content parts:', candidate);
           answer = "Gemini returned a response, but it was empty.";
        }
      } else if (data && data.promptFeedback) {
          console.warn('Gemini prompt feedback indicated potential issues:', data.promptFeedback);
          answer = `The request was blocked. Reason: ${data.promptFeedback.blockReason || 'Unknown'}. Check safety ratings.`;
          if (data.promptFeedback.blockReasonMessage) {
              answer += ` Message: ${data.promptFeedback.blockReasonMessage}`;
          }
      }
       else {
        console.warn('Unexpected Gemini API response structure:', data);
      }
    } catch (extractionError) {
        console.error('Error processing Gemini response:', extractionError, 'Data:', data);
    }

    sendResponse({ answer: answer });
  })
  .catch(error => {
    console.error('Error communicating with Google Gemini API:', error);
    const errorMessage = error.message.startsWith('HTTP error')
      ? `Gemini API Error: ${error.message}`
      : "An error occurred while communicating with Google Gemini.";
    sendResponse({ answer: errorMessage });
  });

  return true;
});
```

### popup.js

```javascript
function parseTextToJSON(text) {
    console.log("Parsing text to JSON:", text); 

    const topics = text.split("Topic:").slice(1);
    
    const result = topics.map(topic => {
      console.log("Processing topic:", topic);
      const lines = topic.trim().split("\n");
      const title = lines[0].trim();
      console.log("Title:", title); 
  

      const quoteLine = lines.find(line => line.includes("Quote:"));
      const q = quoteLine 
        ? quoteLine.substring(quoteLine.indexOf("Quote:") + "Quote:".length).trim()
        : "";
      q2 = q.replace("*",''); 
      q3 = q2.replace('"', "'"); 
      const quote = q3.slice(3,-9);
      console.log("Quote:", quote);
  
      const altLine = lines.find(line => line.includes("Alternative Information:"));
      const alternativeInformation = altLine 
        ? altLine.substring(altLine.indexOf("Alternative Information:") + "Alternative Information:".length).trim()
        : "";
      console.log("Alternative Information:", alternativeInformation); // Debugging line
  

      const sourcesLine = lines.find(line => line.includes("Sources:")) || "";
      console.log("Sources Line:", sourcesLine); // Debugging line
      let sources = [];
      const sourceMatches = sourcesLine.match(/\[.*?\]\((.*?)\)/g);
      if (sourceMatches) {
        sources = sourceMatches.map(source => {
          const linkMatch = source.match(/\((.*?)\)/);
          return linkMatch ? linkMatch[1] : "";
        });
      }
      console.log("Sources:", sources);
  
      const ret = {
        topic: title,
        quote: quote,
        alternativeInformation: alternativeInformation,
        sources: sources
      };
      console.log("Parsed JSON object:", ret); 
      return ret;
    });
    
    return result;
  }
  
  // Example usage:
//   const textData = `
//   ## Topic: Trump's Proposal for Gaza
//   Quote: "U.S. President Donald Trump has proposed that the United States 'take over' and 'own' the Gaza Strip, suggesting long-term control after the ongoing conflict."
//   Alternative Information: This proposal has been met with widespread criticism, including from Amnesty International, which views it as a violation of international law and potentially a crime against humanity[3]. The Reform Leadership also condemned the plan, highlighting its potential to undermine Palestinian self-determination and regional stability[2].
//   Sources: [1](https://www.ajc.org/news/what-is-trumps-proposal-for-gaza), [2](https://urj.org/press-room/reform-leadership-responds-president-trumps-recent-comments-gaza), [3](https://www.amnesty.org/en/latest/news/2025/02/israel-opt-president-trumps-claim-that-us-will-take-over-gaza-and-forcibly-deport-palestinians-appalling-and-unlawful/)
  
//   ## Topic: Arab Leaders' Response
//   Quote: "Egypt, Jordan, and Saudi Arabia are on the forefront of the opposition."
//   Alternative Information: These countries have rejected Trump's plan due to concerns about regional stability and the rights of Palestinians. Egypt's rejection is also linked to preserving its peace treaty with Israel and maintaining national security[4]. Saudi Arabia has emphasized its support for a Palestinian state, which contrasts with Trump's proposals[4].
//   Sources: [4](https://carnegieendowment.org/emissary/2025/02/trump-gaza-plan-displacement-egypt-jordan-saudi-response?lang=en)
  
//   ## Topic: Impact on Regional Stability
//   Quote: "Displacement plans in Gaza and annexation plans in the West Bank will only create more violence, instability, and human suffering in the Middle East."
//   Alternative Information: The proposed displacement could exacerbate economic and security challenges in countries like Jordan, which already faces significant refugee pressures and economic strain[4]. Additionally, such plans could undermine U.S. alliances with Arab states by appearing to support Israeli annexation efforts[4].
//   Sources: [4](https://carnegieendowment.org/emissary/2025/02/trump-gaza-plan-displacement-egypt-jordan-saudi-response?lang=en)
//   `;
  
// console.log(JSON.stringify(parseTextToJSON(textData), null, 2));
document.addEventListener('DOMContentLoaded', () => {
    const debugDiv = document.createElement('div');
    debugDiv.id = 'debug';
    debugDiv.style = 'margin-top: 20px; font-size: 12px; color: gray; white-space: pre-wrap;';
    document.body.appendChild(debugDiv);

    const  logToPopup = (message) => {
        const debugElement = document.getElementById('debug');
        debugElement.textContent += `${message}\n`;
    };
    console.log('popup.js script is running'); 

    annotations = {};
    document.getElementById('askButton').addEventListener('click', async () => {
        // logToPopup('Button clicked'); // Debugging line to check if the button is clicked
        console.log('Button clicked'); // Console log for debugging
        // Retrieve the user question

        // Retrieve stored page text from the content script
        // logToPopup('Attempting to retrieve pageText from chrome.storage.local');
        chrome.storage.local.get('pageText', async (result) => {
            const pageText = result.pageText ?? "No text found on page.";

            // Prepare the payload for Gemini
            const payload = {
                question: "NA",
                text: pageText
            };
            // Send a message to the background script to call the API
            chrome.runtime.sendMessage(payload, (response) => {
                const answer = response.answer || 'No answer received.';
                const sources = response.sources || 'No sources available.';
    
                annotations = parseTextToJSON(answer);
                console.log(JSON.stringify(annotations, null, 2));
              
        console.log("Done doing stuff gonna annotate now");

        chrome.tabs.query({ active: true, currentWindow
[truncated — 2041 more characters]
```