# Project export: Vigilante

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: Combat misinformation through flagged Tweets and grounded sources, and reference a realtime dashboard that visualizes trends on a global scale.
- Devpost: https://devpost.com/software/vigilante-xkf7s9
- GitHub: https://github.com/dav1dbai/Vigilante
- Demo: https://www.usevigilante.com/
- Video: https://www.youtube.com/embed/NQWxzOrpraY?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner (Otsuka Valuenex: VALUENEX Big Data Visualization Award ($1k Amazon Gift Card))
- Team: 4 GitHub contributor(s) — Cole Gawin (44 commits), David Bai (36 commits), Priyansh Bhatter (21 commits), Grace Li (14 commits)

## Devpost submission (written by the team)

### Inspiration

Today, misinformation spreads quicker than you can say “retweet.” Staying informed has never been more crucial—or more challenging. Social media platforms can be powerful tools for connection, but they’ve also become battlegrounds for accuracy, where misleading claims and manipulated information thrive. We built Vigilante to change that. In addition to being driven towards working on projects that solve real-world problems, each of our teammates are incredibly passionate about human-computer interaction—how technology and innovation overlaps with the human experience. We came across a paper published in the journal Science where researchers leveraged AI chatbots to combat belief perseverance (DOI: 10.1126/science.adq1814). As solution-centric engineers, we saw endeavors like these as validation that AI can be a powerful tool to fight entrenched misinformation and cognitive biases. We wanted to bring that power to the masses, leveraging AI to combat misinformation and improve media literacy in doing so. We chose to focus on misinformation on social media because it’s one of the defining challenges of our lives. Social media has the power to shape public discourse and individual belief systems. When placed in the wrong hands, it can be used as a tool for propaganda, sowing division and manipulating public opinion. For a demographic like ours, these points feel especially poignant and relevant. We were motivated by the idea of building a tool that could not only combat misinformation but also help foster media literacy—empowering users to think critically about what they read and engage with online.

### What it does

Vigilante provides users with a real-time fact-checking workflow that allows them to effortlessly identify potential misinformation. The user’s entire Twitter feed is anonymously ingested and, using agentic AI workflows, verifies in real-time every single post. Posts are stripped of evocative or emotional language and are broken down into their individual claims; these claims are then verified using Perplexity's Sonar API to ground our analysis in up-to-date sources. We do not flag every tweet you see as misinformation—rather, we carefully filter out opinions from statements, satire from misinformation. We also highlight when posts (and even specific claims) are, in fact, accurate and supported by evidence. Using Vigilante, in addition to staying informed through your feed, you also help contribute to a crowdsourced database of tweets, their metadata, and analyses. This data lives on a dashboard that visualizes ongoing trends in misinformation on social media using data science and NLP techniques. This publicly available dashboard provides its audience with insights into what topics to be wary of online, all the while helping to tune your media literacy and stay on top of misinformation.

### How we built it

Vigilante exists in 3 parts: a Chrome extension, the fact-checking engine, and a data visualization dashboard. The fact-checking engine, as well as routes for interfacing with our database stored in Supabase, is exposed via a FastAPI backend. The FastAPI backend supports data queries and analysis for the dashboard, and the model routing and plumbing for our verifier. We deploy and expose our backend for production on Koyeb. The Chrome Extension is built using Plasmo as a framework, with React/Tailwind for frontend. We implemented details like caching tweets client-side and using Plasmo messaging to bypass standard Chrome popup styling. The dashboard uses Next/React/Tailwing as scaffolding, and D3 for data visualizations. We also support real-time updates on the dashboard using a combination of technologies. For regular updates to overall statistics (i.e. number of tweets and claims analyzed), we leverage the Stale-While-Refresh technique through the useSWR hook in React. For live updates on new tweets flagged by our system, we loop in Supabase real-time messaging and listen for table insertions in the database. The fact-checking engine is built on top of multiple AI technologies. To ensure the best UX as possible, it was imperative the system exhibited the lowest amount of latency as possible. To accomplish this goal, we leveraged Groq’s blazing fast inference API. We use 3 different language models across the system, each of which excelled in different tasks: gemma2-9b-it, ​​llama-3.1-8b-instant, and llama-3.2-11b-vision-preview. We specifically use the vision model to endow the system with multimodal capabilities, allowing it to handle posts with text, images, and/or video, since each of these modalities are critical to a post’s core meaning. To evaluate a post’s claims, we leverage the new Perplexity Sonar Pro model to both search the web for evidence and provide an explanation for whether a claim is accurate or misleading. This process is also completely parallelized for each claim embedded within the text of a post, allowing for as fast of a system as possible.

### Challenges we ran into

Our team is all super passionate about this problem space, but the initial hours of our work on this project was somewhat fragmented as we weren’t aligned on specific details. We went on a variety of fruitless goose chases, and didn’t settle on a specific vision for our project until we were deep in the weeds of it half-way through. (For instance, we toyed around with the idea of incorporating datasets of tweets from past election seasons and had even built out some visualizations, but ultimately chose not to pursue that path.) Ultimately though, we were able to rally and refocus on our vision—moving away from disorganized prototyping to a specific actionable gameplan. One specific issue we ran into was brainstorming visualizations for the dashboard. We all had different ideas for how we envisioned the dashboard, and we initially found ourselves working independently on designing visualizations and developing data processing pipelines. However, when we synced up, we realized our visions had all largely diverged from each others’. Though it isn’t inherently bad to have competing ideas, it was difficult for us to reconcile a common vision once we had all individually put so much thought into our own. Once we put our heads together, however, we developed a cohesive plan for not only this dashboard, but our project altogether. This taught us the importance of early alignment and continuous collaboration—syncing frequently and creating a shared vision early on can save time and reduce duplicated or divergent efforts. A more technical issue we encountered was not being overly skeptical of (what we thought were) good results from our system. The fact-checking system went through numerous rounds of iterations (archived as a Python notebook in our repo) and we had a working prototype that we implemented into our FastAPI backend. However, after performing a refactoring for additional features, we accidentally removed the call to the Perplexity API, a critical functionality, without us noticing. We took for granted that the system was still flagging tweets as “misleading” without actually checking to see whether its explanations were up to our standards. Thankfully we were able to catch this issue and resolve it in time before it got buried even further. We learned to not take surface level results for granted and to look deeper behind the scenes—which is quite fitting for this project altogether.

### Accomplishments we're proud of

One of our biggest accomplishments was our ability to iterate rapidly and improve upon multiple aspects of our system within a short timeframe. We took on significant technical challenges and addressed performance bottlenecks in our backend with creative, scalable solutions. Each iteration brought meaningful improvements, and we’re proud of how we managed to adapt and refine the system as we encountered new challenges. Another accomplishment were particularly proud of is that we were able to incorporate multimodal fact-checking capabilities. “An image is worth a thousand words” is particularly true in the context of social media, so it was important that our system wasn’t restricted to only text input. However, this was not a simple challenge to solve. We chose to integrate llama-3.2-11b-vision-preview into our system to analyze claims that extend beyond simple textual information. This is a feat we consider a major leap forward in combating misinformation in multimedia-driven social platforms. On another note of personal fulfillment, we’re proud of how we built a project that aims to address an incredibly pressing issue in today’s world. Navigating misinformation is shaping up to be one of the defining challenges of our lives; as social media platforms explore stripping away existing safeguards to prevent the spread of this sort of content, individuals have to be extremely vigilant with what they engage with online. Vigilante was built with this in mind, and we’re proud of how effective of an initial solution we’ve come up with to this problem.

### What's next

After we recover from our sleep-deprived past 36 hours, we plan to publish the Vigilante Chrome extension to the Chrome Webstore. We also aim to continue to maintain the crowdsourced dashboard to effectively present the anonymous data we collect to the general public. Beyond what we currently have built out, we plan to extend Vigilante’s functionality to mobile platforms. Apps for iOS and Android would provide fact-checking on-the-go and bring our technology to another form factor. Moreover, many users solely engage with social media on their phones, so it is important we can reach this audience as well. We are also very interested in how we can better leverage the data our users provide for personal and community benefit. Meta and others have begun to cancel their fact-checking programs, and users often report negative experiences with features like “community notes” on X/Twitter. We suspect that we can leverage the data we’re collecting to reveal larger trends in misinformation and hopefully begin to prevent it before it spreads. Altogether, we envision Vigilante as a tool that not only combats misinformation but also fosters a culture of critical thinking and informed engagement online. We’re excited to continue building after Treehacks, refining and scaling this platform to make a meaningful impact in the fight against misinformation.

## README (from the GitHub repository)

![logo](https://github.com/user-attachments/assets/5696d920-435f-4ce9-8c82-e1e529eb352c)
# Vigilante, built at Treehacks 2025
### Winner of the Otsuka Valuenex Data Visualization Award
Today, misinformation spreads quicker than you can say “retweet.” Staying informed has never been more crucial—or more challenging. Social media platforms can be powerful tools for connection, but they’ve also become battlegrounds for accuracy, where misleading claims and manipulated information thrive. We built Vigilante to change that.

Check out the devpost here! https://devpost.com/software/vigilante-xkf7s9?ref_content=user-portfolio&ref_feature=in_progress



## Detected evidence (automated analysis)

Indexed codebase: 76 recognized source files, 576 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Supabase (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- PostgreSQL (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (101 of 101)

```
.env.example
.gitignore
backend/.env.example
backend/.gitignore
backend/analytics/analytics.py
backend/analytics/author_leaderboard.py
backend/analytics/claims.py
backend/analytics/summary.py
backend/analytics/tweets_by_day.py
backend/Dockerfile
backend/fact_checking_pipeline.ipynb
backend/helpers/__init__.py
backend/helpers/claim.py
backend/helpers/post.py
backend/main.py
backend/requirements.txt
backend/tweet_analyzer.ipynb
backend/util/__init__.py
backend/util/llm.py
backend/util/prompts.py
backend/util/supabase.py
README.md
supabase/.gitignore
supabase/.vscode/settings.json
supabase/config.toml
supabase/functions/.env
supabase/functions/extract-keywords/.npmrc
supabase/functions/extract-keywords/deno.json
supabase/functions/extract-keywords/deno.lock
supabase/functions/extract-keywords/index.ts
vigilante-dashboard/.gitignore
vigilante-dashboard/app/api/generate-bubble-data.ts
vigilante-dashboard/app/api/generate-chord-data.ts
vigilante-dashboard/app/api/generate-stacked-bar-data.ts
vigilante-dashboard/app/api/generate-stats.ts
vigilante-dashboard/app/api/generate-top-three-keywords.ts
vigilante-dashboard/app/charts/bubble-diagram.tsx
vigilante-dashboard/app/charts/chord-diagram.tsx
vigilante-dashboard/app/charts/data-table.tsx
vigilante-dashboard/app/charts/stacked-bar-diagram.tsx
vigilante-dashboard/app/dashboard.tsx
vigilante-dashboard/app/globals.css
vigilante-dashboard/app/layout.tsx
vigilante-dashboard/app/live-tweets.tsx
vigilante-dashboard/app/most-misinformed-topics.tsx
vigilante-dashboard/app/page.tsx
vigilante-dashboard/app/stats.tsx
vigilante-dashboard/components.json
vigilante-dashboard/components/header.tsx
vigilante-dashboard/components/loading-view.tsx
vigilante-dashboard/components/misinformation-timeline.tsx
vigilante-dashboard/components/tweet-cascade.tsx
vigilante-dashboard/components/ui/accordion.tsx
vigilante-dashboard/components/ui/button.tsx
vigilante-dashboard/components/ui/card.tsx
vigilante-dashboard/components/ui/scroll-area.tsx
vigilante-dashboard/components/ui/skeleton.tsx
vigilante-dashboard/components/ui/table.tsx
vigilante-dashboard/components/ui/tabs.tsx
vigilante-dashboard/eslint.config.mjs
vigilante-dashboard/lib/groq.ts
vigilante-dashboard/lib/mock-data.ts
vigilante-dashboard/lib/queries/get-all-analyses-entries.ts
vigilante-dashboard/lib/queries/get-all-claims-entries.ts
vigilante-dashboard/lib/queries/get-all-keywords-entries.ts
vigilante-dashboard/lib/supabase.ts
vigilante-dashboard/lib/types/Keyword.ts
vigilante-dashboard/lib/utils.ts
vigilante-dashboard/next.config.ts
vigilante-dashboard/package.json
vigilante-dashboard/postcss.config.mjs
vigilante-dashboard/README.md
vigilante-dashboard/tailwind.config.ts
vigilante-dashboard/tsconfig.json
vigilante/.env
vigilante/.github/workflows/submit.yml
vigilante/.gitignore
vigilante/.prettierrc.mjs
vigilante/background/index.ts
vigilante/background/messages/analyze.ts
vigilante/background/messages/semantic.ts
vigilante/components.json
vigilante/components/FactCheckFlag.tsx
vigilante/components/SettingsPanel.tsx
vigilante/components/TextFormat.tsx
vigilante/components/ui/alert-dialog.tsx
vigilante/components/ui/alert.tsx
vigilante/components/ui/button.tsx
vigilante/components/ui/dialog.tsx
vigilante/components/ui/scroll-area.tsx
vigilante/contents/main.tsx
vigilante/index.css
vigilante/lib/utils.ts
vigilante/package.json
vigilante/popup.tsx
vigilante/postcss.config.js
vigilante/README.md
vigilante/tailwind.config.js
vigilante/tsconfig.json
vigilante/utils/cache.ts
vigilante/utils/extractTweetData.ts
```

### Dependencies

- backend/requirements.txt: aiohappyeyeballs@==2.4.6, aiohttp@==3.11.12, aiosignal@==1.3.2, annotated-types@==0.7.0, anyio@==4.8.0, appnope@==0.1.4, asttokens@==3.0.0, attrs@==25.1.0, certifi@==2025.1.31, charset-normalizer@==3.4.1, click@==8.1.8, comm@==0.2.2, debugpy@==1.8.12, decorator@==5.1.1, deprecation@==2.1.0, distro@==1.9.0, executing@==2.2.0, fastapi@==0.115.8, frozenlist@==1.5.0, gotrue@==2.11.3, groq@==0.18.0, h11@==0.14.0, h2@==4.2.0, hpack@==4.1.0, httpcore@==1.0.7, httpx@==0.28.1, hyperframe@==6.1.0, idna@==3.10, ipykernel@==6.29.5, ipython@==8.32.0, jedi@==0.19.2, jupyter_client@==8.6.3, jupyter_core@==5.7.2, matplotlib-inline@==0.1.7, multidict@==6.1.0, nest-asyncio@==1.6.0, numpy@==2.2.3, packaging@==24.2, pandas@==2.2.3, parso@==0.8.4, pexpect@==4.9.0, pillow@==11.1.0, platformdirs@==4.3.6, postgrest@==0.19.3, prompt_toolkit@==3.0.50, propcache@==0.2.1, psutil@==7.0.0, ptyprocess@==0.7.0, pure_eval@==0.2.3, pydantic@==2.10.6, pydantic_core@==2.27.2, Pygments@==2.19.1, python-dateutil@==2.9.0.post0, python-dotenv@==1.0.1, pytz@==2025.1, pyzmq@==26.2.1, realtime@==2.3.0, requests@==2.32.3, router@==0.1, six@==1.17.0, sniffio@==1.3.1, stack-data@==0.6.3, starlette@==0.45.3, storage3@==0.11.3, StrEnum@==0.4.15, supabase@==2.13.0, supafunc@==0.9.3, tornado@==6.4.2, traitlets@==5.14.3, typing_extensions@==4.12.2, tzdata@==2025.1, urllib3@==2.3.0, uvicorn@==0.34.0, wcwidth@==0.2.13, websockets@==14.2, yarl@==1.18.3
- vigilante-dashboard/package.json: @eslint/eslintrc@^3, @nivo/bar@^0.88.0, @nivo/bump@^0.88.0, @nivo/calendar@^0.88.0, @nivo/chord@^0.88.0, @nivo/circle-packing@^0.88.0, @nivo/heatmap@^0.88.0, @radix-ui/react-accordion@^1.2.3, @radix-ui/react-slot@^1.1.2, @radix-ui/react-tabs@^1.1.3, @supabase/supabase-js@^2.48.1, @types/d3@^7.4.3, @types/node@^20, @types/react@^19, @types/react-dom@^19, axios@^1.7.9, chart.js@^4.4.7, class-variance-authority@^0.7.1, clsx@^2.1.1, d3@^7.9.0, d3-scale@^4.0.2, eslint@^9, eslint-config-next@15.1.7, framer-motion@^12.4.3, groq-sdk@^0.15.0, lucide-react@^0.475.0, motion@^12.4.3, next@15.1.7, postcss@^8, react@^19.0.0, react-animated-numbers@^1.0.0, react-chartjs-2@^5.3.0, react-dom@^19.0.0, react-use@^17.6.0, swr@^2.3.2, tailwind-merge@^3.0.1, tailwindcss@^3.4.1, tailwindcss-animate@^1.0.7, typescript@^5
- vigilante/package.json: @ianvs/prettier-plugin-sort-imports@4.1.1, @plasmohq/messaging@^0.7.1, @plasmohq/storage@^1.15.0, @radix-ui/react-alert-dialog@^1.1.6, @radix-ui/react-dialog@^1.1.6, @radix-ui/react-scroll-area@^1.2.3, @radix-ui/react-slot@^1.1.2, @types/chrome@0.0.258, @types/node@20.11.5, @types/react@18.2.48, @types/react-dom@18.2.18, autoprefixer@^10.4.20, class-variance-authority@^0.7.1, clsx@^2.1.1, lucide-react@^0.475.0, plasmo@0.90.3, postcss@^8.1.0, prettier@3.2.4, react@18.2.0, react-dom@18.2.0, react-markdown@^7.1.2, remark-gfm@^2.0.0, tailwind-merge@^3.0.1, tailwindcss@3.4.14, tailwindcss-animate@^1.0.7, typescript@5.3.3

### Recent commits (newest first)

- Update README.md
- Update README.md
- Update README.md
- final fix for common topics
- live tweets updated
- updated package
- reverted back to 19
- Merge branch 'main' of https://github.com/dav1dbai/Vigilante
- Most Misinformed Topics card updated
- Merge branch 'main' of https://github.com/dav1dbai/Vigilante
- env updates
- remove verifier
- fixed navbar
- small updates to dash
- updated UI for live tweets
- fixed rendering + verified tag
- updated stats cards
- added perplexity back
- fix build issue
- Merge branch 'main' of https://github.com/dav1dbai/Vigilante

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

### backend/Dockerfile

```
# Use official Python image
FROM python:3.10

# Set the working directory inside the container
WORKDIR /app

# Copy backend files
COPY . .

# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt

# Expose the FastAPI default port
EXPOSE 8000

# Start FastAPI server
CMD uvicorn main:app --host 0.0.0.0 --port $PORT

```

### vigilante/package.json

```
{
  "name": "vigilante",
  "displayName": "Vigilante",
  "version": "0.0.1",
  "description": "A basic Plasmo extension.",
  "author": "Plasmo Corp. <foss@plasmo.com>",
  "scripts": {
    "dev": "plasmo dev",
    "build": "plasmo build",
    "package": "plasmo package"
  },
  "dependencies": {
    "@plasmohq/messaging": "^0.7.1",
    "@plasmohq/storage": "^1.15.0",
    "@radix-ui/react-alert-dialog": "^1.1.6",
    "@radix-ui/react-dialog": "^1.1.6",
    "@radix-ui/react-scroll-area": "^1.2.3",
    "@radix-ui/react-slot": "^1.1.2",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "lucide-react": "^0.475.0",
    "plasmo": "0.90.3",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-markdown": "^7.1.2",
    "remark-gfm": "^2.0.0",
    "tailwind-merge": "^3.0.1",
    "tailwindcss": "3.4.14",
    "tailwindcss-animate": "^1.0.7"
  },
  "devDependencies": {
    "@ianvs/prettier-plugin-sort-imports": "4.1.1",
    "@types/chrome": "0.0.258",
    "@types/node": "20.11.5",
    "@types/react": "18.2.48",
    "@types/react-dom": "18.2.18",
    "autoprefixer": "^10.4.20",
    "postcss": "^8.1.0",
    "prettier": "3.2.4",
    "typescript": "5.3.3"
  },
  "manifest": {
    "host_permissions": [
      "https://*/*"
    ]
  }
}

```

### backend/requirements.txt

```
aiohappyeyeballs==2.4.6
aiohttp==3.11.12
aiosignal==1.3.2
annotated-types==0.7.0
anyio==4.8.0
appnope==0.1.4
asttokens==3.0.0
attrs==25.1.0
certifi==2025.1.31
charset-normalizer==3.4.1
click==8.1.8
comm==0.2.2
debugpy==1.8.12
decorator==5.1.1
deprecation==2.1.0
distro==1.9.0
executing==2.2.0
fastapi==0.115.8
frozenlist==1.5.0
gotrue==2.11.3
groq==0.18.0
h11==0.14.0
h2==4.2.0
hpack==4.1.0
httpcore==1.0.7
httpx==0.28.1
hyperframe==6.1.0
idna==3.10
ipykernel==6.29.5
ipython==8.32.0
jedi==0.19.2
jupyter_client==8.6.3
jupyter_core==5.7.2
matplotlib-inline==0.1.7
multidict==6.1.0
nest-asyncio==1.6.0
numpy==2.2.3
packaging==24.2
pandas==2.2.3
parso==0.8.4
pexpect==4.9.0
pillow==11.1.0
platformdirs==4.3.6
postgrest==0.19.3
prompt_toolkit==3.0.50
propcache==0.2.1
psutil==7.0.0
ptyprocess==0.7.0
pure_eval==0.2.3
pydantic==2.10.6
pydantic_core==2.27.2
Pygments==2.19.1
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
pytz==2025.1
pyzmq==26.2.1
realtime==2.3.0
requests==2.32.3
router==0.1
six==1.17.0
sniffio==1.3.1
stack-data==0.6.3
starlette==0.45.3
storage3==0.11.3
StrEnum==0.4.15
supabase==2.13.0
supafunc==0.9.3
tornado==6.4.2
traitlets==5.14.3
typing_extensions==4.12.2
tzdata==2025.1
urllib3==2.3.0
uvicorn==0.34.0
wcwidth==0.2.13
websockets==14.2
yarl==1.18.3

```

### vigilante-dashboard/package.json

```
{
  "name": "vigilante-dashboard",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@nivo/bar": "^0.88.0",
    "@nivo/bump": "^0.88.0",
    "@nivo/calendar": "^0.88.0",
    "@nivo/chord": "^0.88.0",
    "@nivo/circle-packing": "^0.88.0",
    "@nivo/heatmap": "^0.88.0",
    "@radix-ui/react-accordion": "^1.2.3",
    "@radix-ui/react-slot": "^1.1.2",
    "@radix-ui/react-tabs": "^1.1.3",
    "@supabase/supabase-js": "^2.48.1",
    "axios": "^1.7.9",
    "chart.js": "^4.4.7",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "d3": "^7.9.0",
    "d3-scale": "^4.0.2",
    "framer-motion": "^12.4.3",
    "groq-sdk": "^0.15.0",
    "lucide-react": "^0.475.0",
    "motion": "^12.4.3",
    "next": "15.1.7",
    "react": "^19.0.0",
    "react-animated-numbers": "^1.0.0",
    "react-chartjs-2": "^5.3.0",
    "react-dom": "^19.0.0",
    "react-use": "^17.6.0",
    "swr": "^2.3.2",
    "tailwind-merge": "^3.0.1",
    "tailwindcss-animate": "^1.0.7"
  },
  "devDependencies": {
    "@eslint/eslintrc": "^3",
    "@types/d3": "^7.4.3",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "15.1.7",
    "postcss": "^8",
    "tailwindcss": "^3.4.1",
    "typescript": "^5"
  }
}

```

### backend/main.py

```python
from typing import Optional
from fastapi import FastAPI, Body
from fastapi.middleware.cors import CORSMiddleware
from helpers.post import analyze_post, check_semantic_relevance
from analytics.analytics import router as analytics_router  # Import analytics router
from pydantic import BaseModel


app = FastAPI()

# Configure CORS for localhost:3000
origins = [
    "http://localhost:3000",
]
app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(analytics_router, prefix="/analytics", tags=["analytics"])

class TweetData(BaseModel):
    tweet_id: str
    tweet_author: str
    tweet_text: str
    base64_image: Optional[str] = None
    timestamp: str

@app.post("/analyze_tweet")
async def analyze_tweet_endpoint(tweet_data: TweetData):
    # IMPORTANT: Await the analyze_post result so that we return the actual data
    result = await analyze_post(
        tweet_id=tweet_data.tweet_id,
        tweet_author=tweet_data.tweet_author,
        tweet_text=tweet_data.tweet_text,
        base64_image=tweet_data.base64_image,
        timestamp=tweet_data.timestamp,
        save_to_supabase=True,
    )
    return result


@app.post("/semantic_filter")
def semantic_filter(
    description: str = Body(..., embed=True),
    tweet_text: str = Body(..., embed=True)
):
  print(f"Checking semantic relevance for: {description}, {tweet_text}")
  return check_semantic_relevance(description, tweet_text)
```

### vigilante-dashboard/app/layout.tsx

```typescript
// app/layout.tsx
import type { Metadata } from "next";

import React from "react";
import "./globals.css";

export const metadata: Metadata = {
  title: "Vigilante Dashboard",
  description: "Misinformation Visualization System",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className="bg-background text-foreground flex flex-col">
        {children}
      </body>
    </html>
  );
}

```

### vigilante/background/index.ts

```typescript
// This keepAlive function periodically makes a call so that the background
// service worker remains active longer (during development/testing).
// const keepAlive = () => {
//   setInterval(() => {
//     chrome.runtime.getPlatformInfo((info) => {
//       console.log("KeepAlive: Platform Info", info);
//     });
//   }, 20e3); // every 20 seconds
// };

// chrome.runtime.onStartup.addListener(() => {
//   console.log("Extension starting up, initiating keepAlive");
//   keepAlive();
// });

// keepAlive(); // also invoke immediately on load

// chrome.runtime.onInstalled.addListener(() => {
//   console.log("Vigilante is installed");
// });


```

### vigilante-dashboard/app/page.tsx

```typescript
"use client";

import Header from "@/components/header";
import useSWR from "swr";
import getAllKeywordsEntries from "../lib/queries/get-all-keywords-entries";

import LoadingView from "@/components/loading-view";
import Dashboard from "./dashboard";

export default function Home() {
  // Define the fake data here
  //const data = await prepareData();

  const { data, isLoading, error } = useSWR("/", getAllKeywordsEntries, {
    refreshInterval: 5000,
    revalidateOnFocus: true,
  });

  console.log(data, isLoading, error);

  return (
    <>
      <Header />
      {isLoading && <LoadingView text={"Loading data..."} />}
      {data && <Dashboard data={data} />}
    </>
  );
}

```

### vigilante/contents/main.tsx

```typescript
import cssText from "data-text:~index.css"
import { useEffect, useState } from "react"
import { createRoot } from "react-dom/client"

import { sendToBackground } from "@plasmohq/messaging"
import { Storage } from "@plasmohq/storage"
import SettingsPanel from "~components/SettingsPanel"

import FactCheckFlag from "~components/FactCheckFlag"

import { getCachedAnalysis, setCachedAnalysis } from "../utils/cache"
import { extractTweetDataFromElement } from "../utils/extractTweetData"
import { useMessage } from "@plasmohq/messaging/hook"
import { useStorage } from "~node_modules/@plasmohq/storage/dist/hook"

function ContentScript() {
  useEffect(() => {
    if (window.location.hostname !== "x.com") return

    const style = document.createElement("style")
    style.textContent = cssText
    document.head.appendChild(style)
  }, [])

  const [isSettingsOpen, setIsSettingsOpen] = useState<boolean>(false)

  useMessage<boolean, boolean>(async (req, res) => {
    console.log("Received message:", req)
    if (req.name === 'toggle-settings-panel') {
      setIsSettingsOpen(!isSettingsOpen)
    }
    res.send(isSettingsOpen)
  })

  useEffect(() => {
    const storage = new Storage()
    const processedTweets = new WeakSet<Element>()
    const observerOptions = {
      root: null,
      rootMargin: "1200px",
      threshold: 0
    }

    const tweetObserver = new IntersectionObserver(
      async (entries, observer) => {
        // Get current settings
        const isEnabled = await storage.get<boolean>("vigilante-enabled")
        const semanticFilter = await storage.get<string>(
          "vigilante-semantic-filter"
        )
        const excludedKeywords = await storage.get<string>(
          "vigilante-excluded-keywords"
        )

        // Skip processing if extension is disabled
        if (!isEnabled) {
          return
        }

        entries.forEach((entry) => {
          if (entry.isIntersecting && !processedTweets.has(entry.target)) {
            const processTweet = async () => {
              // Extract tweet data
              const tweetData = extractTweetDataFromElement(entry.target)

              // Apply keyword exclusion filter
              if (excludedKeywords) {
                const keywords = excludedKeywords
                  .toLowerCase()
                  .split(",")
                  .map((k) => k.trim())
                const tweetText = tweetData.text.toLowerCase()
                if (keywords.some((keyword) => tweetText.includes(keyword))) {
                  console.log(
                    "⏭️ Skipping tweet due to excluded keywords ",
                    keywords,
                    tweetData.id
                  )
                  entry.target.remove()
                  return
                }
              }

              // Apply semantic filter (dummy implementation for now)
              // if (semanticFilter) {
              //   const isRelevant = await isSemanticallyRelevant(tweetData.text, semanticFilter)
              //   if (!isRelevant) {
              //     console.log("⏭️ Skipping tweet due to semantic filter:", semanticFilter, tweetData.id)
              //     entry.target.remove()
              //     return
              //   }
              // }

              console.log("�� Tweet Detection:", {
                id: tweetData.id,
                text: tweetData.text.substring(0, 50) + "...",
                hasMedia: tweetData.media.length > 0
              })

              let promise: Promise<any>

              // Skip video tweets with short text
              if (
                tweetData.media.some((media) =>
                  media.includes("video_thumb")
                ) &&
                tweetData.text.length < 20
              ) {
                console.log("⏭️ Skipping video tweet:", tweetData.id)
                return
              }

              console.log("🔄 Analyzing tweet:", {
                id: tweetData.id,
                timestamp: new Date().toISOString()
              })

              // Check cache first
              const cachedResult = await getCachedAnalysis(tweetData.id)
              if (cachedResult) {
                console.log("📦 Using cached analysis for tweet:", tweetData.id)
                promise = Promise.resolve(cachedResult)
              } else {
                promise = sendToBackground({
                  name: "analyze",
                  body: tweetData
                })
              }

              // Cache the result when promise resolves
              promise.then((result) => {
                setCachedAnalysis(tweetData.id, result)
              })

              // Find metrics bar and insert flag component
              const metricsBar = entry.target.querySelector('[role="group"]')
              if (metricsBar) {
                const mountPoint = document.createElement("div")
                metricsBar.parentNode?.insertBefore(mountPoint, metricsBar)

                createRoot(mountPoint).render(
                  <FactCheckFlag tweetId={tweetData.id} promise={promise} />
                )
              }

              processedTweets.add(entry.target)

              observer.unobserve(entry.target)
            }

            processTweet()
          } else {
            observer.unobserve(entry.target)
          }
        })
      },
      observerOptions
    )

    function observeExistingTweets() {
      const tweetArticles = document.querySelectorAll(
        'article[data-testid="tweet"]'
      )
      tweetArticles.forEach((article) => {
        if (!processedTweets.has(article)) {
          tweetObserver.observe(article)
        }
      })
    }

    observeExistingTweets()

    let mutationTimeout: number | null = null
    const mutationObserver = new MutationObserver(() => {
      if (mutationTimeout) clearTimeout(mutationTimeout)
      mutationTimeout = window.setTimeout(() => {
        observeExistingTweets()
      }, 500)
    })

    mutationObserv
[truncated — 845 more characters]
```

### supabase/functions/extract-keywords/index.ts

```typescript
import "jsr:@std/dotenv/load";
import "jsr:@supabase/functions-js/edge-runtime.d.ts";

import { createClient } from "jsr:@supabase/supabase-js@2";
import Groq from "npm:groq-sdk";

const PROMPT = `
Your role is to extract the most important keywords from each tweet.

Focus on nouns, verbs, and named entities.

The goal is to capture what the tweet is primarily about in a few keywords. 

## Examples

1. Tweet: "Excited to announce our new AI tool that simplifies content creation! 🚀 #AI #Productivity"
   AI,tool,content creation,productivity

2. Tweet: "Just finished an amazing workout at the new gym downtown. Feeling strong! 💪 #Fitness #Health"
   workout,gym,downtown,fitness,health

3. Tweet: "BREAKING: Launching our free course on Python programming for beginners! Sign up now. 🐍 #Python #Coding"
   programming,free course,beginners,python,coding

4. Tweet: "Apple just announced the new iPhone 15 with better battery life and an updated camera. 📱 #AppleEvent #Tech"
   Apple,iPhone 15,battery life,updated camera,Apple event,tech

5. Tweet: "Attended the Web3 summit today—exciting discussions on decentralized finance and the future of crypto. 🌐 #Web3 #Crypto"
   web3 summit,decentralized finance,future of crypto,web3,crypto


Separate keywords by commas. Only respond with one line.
`;

const groqClient = new Groq({
  apiKey: Deno.env.get("GROQ_API_KEY")!,
});

const supabaseClient = createClient(
  Deno.env.get("SUPABASE_URL")!,
  Deno.env.get("SUPABASE_ANON_KEY")!
);

Deno.serve(async (req) => {
  const { record } = await req.json();
  const { original_tweet_id: tweetId, text: tweetText } = record;

  if (!tweetText) {
    return new Response();
  }

  const chatCompletion = await groqClient.chat.completions.create({
    messages: [
      { role: "system", content: PROMPT },
      { role: "user", content: tweetText },
    ],
    model: "gemma2-9b-it",
  });

  const content = chatCompletion.choices[0].message.content?.trim();
  const rawKeywords = content?.split(",") || [];

  const keywords = rawKeywords.map((v) => v.trim());

  await supabaseClient
    .from("keywords")
    .insert([{ original_tweet_id: tweetId, keywords }]);

  return new Response(JSON.stringify(keywords), {
    headers: { "Content-Type": "application/json" },
  });
});

/* To invoke locally:

  1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start)
  2. Make an HTTP request:

  curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/extract-keywords' \
    --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
    --header 'Content-Type: application/json' \
    --data '{"name":"Functions"}'

*/

```

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