# Project export: bluberri

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 12.0
- Tagline: Understand what people truly think
- Devpost: https://devpost.com/software/bluberri
- GitHub: https://github.com/njchen8/Cal-Hacks
- Video: https://www.youtube.com/embed/u4p5uxEIWqg?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 5 GitHub contributor(s) — Kevin Zhang (44 commits), aealni (20 commits), FlyingShire (15 commits), mmatlin2 (6 commits), Claude (5 commits)

## Devpost submission (written by the team)

### Inspiration

We wanted to create a platform that unifies opinions and reviews from across the internet — from Best Buy and Amazon to Reddit and beyond. It’s hard to get a full picture when feedback is scattered across multiple sites, so our goal with bluberri was to make it simple to explore, analyze, and understand what people really think about any product or topic — all in one place.

### What it does

bluberri collects and analyzes social media conversations and product reviews from multiple platforms. It uses AI-powered sentiment analysis to detect emotions and opinions, providing clear visual insights and downloadable CSV reports for any product, feature, or campaign. The frontend allows users to run analyses and view live results, while the backend manages scraping, data aggregation, and model inference.

### How we built it

We built bluberri with a Python backend using FastAPI, SQLAlchemy, and transformer models like BERT and RoBERTa, with Google Gemini handling summaries. The backend scrapes data from APIs and web sources, runs sentiment and emotion analysis, and streams progress and results to the frontend — built with Next.js and React. We designed a modular pipeline to integrate multiple data sources, handle rate limits, and provide a real-time analysis experience.

### Challenges we ran into

Getting transformer models to run efficiently and reliably Handling rate limits and API restrictions across multiple platforms Building a unified pipeline that merges data from different APIs and formats Keeping real-time updates smooth while streaming results to the frontend

### Accomplishments we're proud of

Integrated transformer models (BERT + fast sentiment models) for scalable analysis Created a modular, multi-source data pipeline Built a live, streaming frontend for progress and results Enabled AI-powered summaries and CSV exports for actionable insights

### What we learned

How to manage API rate limits and platform-specific restrictions Techniques for optimizing transformer model inference How to design robust error handling and streaming for a better user experience The importance of combining multiple data sources for richer insights

### What's next

Integrate more public APIs (e.g., Threads, Bluesky) Add web scrapers for platforms without public APIs Optimize model performance to reduce full-analysis time Expand support for more content types and review formats

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 32 recognized source files, 163 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- Hugging Face (technology) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- PyTorch (technology) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (43 of 43)

```
.gitignore
sentiment-event/.env.example
sentiment-event/.gitignore
sentiment-event/backend/app/__init__.py
sentiment-event/backend/app/api.py
sentiment-event/backend/app/config.py
sentiment-event/backend/app/database.py
sentiment-event/backend/app/models.py
sentiment-event/backend/app/pipeline.py
sentiment-event/backend/app/scraper_facebook.py
sentiment-event/backend/app/scraper_reddit.py
sentiment-event/backend/app/scraper.py
sentiment-event/backend/app/sentiment.py
sentiment-event/backend/app/summary.py
sentiment-event/backend/generate_report.py
sentiment-event/backend/lava_summary.py
sentiment-event/backend/main.py
sentiment-event/backend/reports/sentiment_iphone.csv
sentiment-event/backend/reports/sentiment_ray-ban_meta.csv
sentiment-event/backend/requirements.txt
sentiment-event/frontend/.eslintrc.json
sentiment-event/frontend/app/about/page.tsx
sentiment-event/frontend/app/analyze/page.tsx
sentiment-event/frontend/app/api/analyze/route.ts
sentiment-event/frontend/app/docs/page.tsx
sentiment-event/frontend/app/globals.css
sentiment-event/frontend/app/layout.tsx
sentiment-event/frontend/app/page.tsx
sentiment-event/frontend/components/BlueberryFarm.tsx
sentiment-event/frontend/components/BlueberryFarmScene.tsx
sentiment-event/frontend/components/Footer.tsx
sentiment-event/frontend/components/Navigation.tsx
sentiment-event/frontend/components/OrbitalBackground.tsx
sentiment-event/frontend/components/ReactionTicker.tsx
sentiment-event/frontend/components/SentimentSummary.tsx
sentiment-event/frontend/global-jsx.d.ts
sentiment-event/frontend/next-env.d.ts
sentiment-event/frontend/next.config.mjs
sentiment-event/frontend/package.json
sentiment-event/frontend/tsconfig.json
sentiment-event/frontend/types/sentiment.ts
sentiment-event/README.md
sentiment-event/vercel.json
```

### Dependencies

- sentiment-event/backend/requirements.txt: fastapi@>=0.112.0, praw@>=7.7.1, python-dotenv@>=1.0.1, requests@>=2.32.0, SQLAlchemy@>=2.0.23, torch@>=2.2.0, tqdm@>=4.66.1, transformers@>=4.40.0, twikit@>=1.7.0, uvicorn[standard]@>=0.30.0
- sentiment-event/frontend/package.json: @react-three/drei@^10.7.6, @react-three/fiber@^9.4.0, @types/node@20.10.5, @types/react@18.2.45, @types/react-dom@18.2.17, eslint@8.56.0, eslint-config-next@14.1.0, framer-motion@10.16.4, next@^16.0.0, react@^19.2.0, react-dom@^19.2.0, react-markdown@^10.1.0, typescript@5.3.3

### Recent commits (newest first)

- readme
- formatting
- packages
- Merge remote-tracking branch 'refs/remotes/origin/main'
- booberyy 2.0
- format
- feature cards
- Merge remote-tracking branch 'refs/remotes/origin/main'
- Merge remote-tracking branch 'refs/remotes/origin/main'
- footer
- Revert "booberry"
- booberry
- Merge branch 'main' of https://github.com/njchen8/Cal-Hacks
- name
- loading bar
- readme
- formatting
- formatting
- footer
- Revert "frontend"

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

### sentiment-event/backend/requirements.txt

```
python-dotenv>=1.0.1
SQLAlchemy>=2.0.23
requests>=2.32.0
twikit>=1.7.0
transformers>=4.40.0
torch>=2.2.0
tqdm>=4.66.1
praw>=7.7.1
fastapi>=0.112.0
uvicorn[standard]>=0.30.0

```

### sentiment-event/frontend/package.json

```
{
  "name": "bluberri-frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@react-three/drei": "^10.7.6",
    "@react-three/fiber": "^9.4.0",
    "framer-motion": "10.16.4",
    "next": "^16.0.0",
    "react": "^19.2.0",
    "react-dom": "^19.2.0",
    "react-markdown": "^10.1.0"
  },
  "devDependencies": {
    "@types/node": "20.10.5",
    "@types/react": "18.2.45",
    "@types/react-dom": "18.2.17",
    "eslint": "8.56.0",
    "eslint-config-next": "14.1.0",
    "typescript": "5.3.3"
  }
}

```

### sentiment-event/backend/main.py

```python
"""Entry point CLI for the sentiment analysis backend.

Supports both Twitter (original) and Reddit (alternative) as data sources.
"""

from __future__ import annotations

import argparse
import json
import sys
import time
from typing import Any, Dict

from app import settings
from app.database import init_db
from app.pipeline import analyze_pending, scrape, scrape_reddit, scrape_facebook
from app.scraper import update_export_with_sentiment
from app.summary import summarize_keyword

ANALYZER_CHOICES = ("default", "fast")


def _configure_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Multi-source sentiment analysis backend (Twitter + Reddit)")
    subparsers = parser.add_subparsers(dest="command", required=True)

    scrape_parser = subparsers.add_parser("scrape", help="Scrape user content for a keyword")
    scrape_parser.add_argument("keyword", type=str, help="Keyword or search query")
    scrape_parser.add_argument("--limit", type=int, default=settings.scrape_limit, help="Number of items to fetch")
    scrape_parser.add_argument(
        "--ignore-cache",
        action="store_true",
        help="Bypass the cached export window and force a fresh scrape",
    )

    analyze_parser = subparsers.add_parser("analyze", help="Run sentiment analysis on stored content")
    analyze_parser.add_argument("--limit", type=int, default=None, help="Limit the number of items to analyze")
    analyze_parser.add_argument("--json", action="store_true", help="Print the analyzed content as JSON")
    analyze_parser.add_argument(
        "--engine",
        choices=ANALYZER_CHOICES,
        default="default",
        help="Select sentiment analyzer variant (default or fast).",
    )

    run_parser = subparsers.add_parser("run", help="Scrape and analyze in one step (Twitter)")
    run_parser.add_argument("keyword", type=str, help="Keyword or search query")
    run_parser.add_argument("--limit", type=int, default=settings.scrape_limit, help="Number of items to fetch")
    run_parser.add_argument(
        "--ignore-cache",
        action="store_true",
        help="Bypass the cached export window and force a fresh scrape",
    )
    run_parser.add_argument(
        "--engine",
        choices=ANALYZER_CHOICES,
        default="default",
        help="Select sentiment analyzer variant (default or fast).",
    )

    # ============================================================================
    # REDDIT COMMANDS (Alternative source)
    # ============================================================================
    scrape_reddit_parser = subparsers.add_parser("scrape-reddit", help="Scrape Reddit posts for a keyword")
    scrape_reddit_parser.add_argument("keyword", type=str, help="Keyword or search query")
    scrape_reddit_parser.add_argument("--limit", type=int, default=settings.scrape_limit, help="Number of posts to fetch")
    scrape_reddit_parser.add_argument("--subreddit", type=str, default="all", help="Subreddit to search (default: all)")

    run_reddit_parser = subparsers.add_parser("run-reddit", help="Scrape Reddit and analyze in one step")
    run_reddit_parser.add_argument("keyword", type=str, help="Keyword or search query")
    run_reddit_parser.add_argument("--limit", type=int, default=settings.scrape_limit, help="Number of posts to fetch")
    run_reddit_parser.add_argument("--subreddit", type=str, default="all", help="Subreddit to search (default: all)")
    run_reddit_parser.add_argument(
        "--engine",
        choices=ANALYZER_CHOICES,
        default="default",
        help="Select sentiment analyzer variant (default or fast).",
    )

    # =========================================================================
    # FACEBOOK COMMANDS (Alternative source)
    # =========================================================================
    scrape_facebook_parser = subparsers.add_parser("scrape-facebook", help="Scrape Facebook posts for a keyword")
    scrape_facebook_parser.add_argument("keyword", type=str, help="Keyword or search query")
    scrape_facebook_parser.add_argument("--limit", type=int, default=settings.scrape_limit, help="Number of posts to fetch")
    scrape_facebook_parser.add_argument("--page-id", type=str, default=None, help="Facebook Page ID to search (optional)")

    run_facebook_parser = subparsers.add_parser("run-facebook", help="Scrape Facebook and analyze in one step")
    run_facebook_parser.add_argument("keyword", type=str, help="Keyword or search query")
    run_facebook_parser.add_argument("--limit", type=int, default=settings.scrape_limit, help="Number of posts to fetch")
    run_facebook_parser.add_argument("--page-id", type=str, default=None, help="Facebook Page ID to search (optional)")
    run_facebook_parser.add_argument(
        "--engine",
        choices=ANALYZER_CHOICES,
        default="default",
        help="Select sentiment analyzer variant (default or fast).",
    )

    return parser


def _fetch_analyzed(limit: int | None = None) -> list[Dict[str, Any]]:
    from sqlalchemy import select

    from app.database import get_session
    from app.models import Tweet

    with get_session() as session:
        stmt = select(Tweet).where(Tweet.sentiment.is_not(None)).order_by(Tweet.created_at.desc())
        if limit:
            stmt = stmt.limit(limit)
        rows = session.scalars(stmt).all()
        return [
            {
                "tweet_id": row.tweet_id,
                "keyword": row.keyword,
                "username": row.username,
                "content": row.content,
                "created_at": row.created_at.isoformat(),
                "sentiment": row.sentiment,
            }
            for row in rows
        ]


def main(argv: list[str] | None = None) -> int:
    init_db()
    parser = _configure_parser()
    args = parser.parse_args(argv)

    if args.command == "scrape":
        result = scrape(args.keyword, limit=args.limit, ignore_cache=args.ignore_cache)
        print
[truncated — 11583 more characters]
```

### sentiment-event/frontend/app/layout.tsx

```typescript
'use client';

import { Nunito } from "next/font/google";
import "./globals.css";
import Navigation from "@/components/Navigation";
import Footer from "@/components/Footer";
import BlueberryFarm from "@/components/BlueberryFarm";
import { useEffect, useState } from "react";
import { usePathname } from "next/navigation";

const nunito = Nunito({
  subsets: ["latin"],
  variable: "--font-family-base",
  weight: ["300", "400", "500", "600", "700", "800"],
  display: "swap",
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  const [scrollProgress, setScrollProgress] = useState(0);
  const pathname = usePathname();
  const isHomePage = pathname === '/';

  useEffect(() => {
    const handleScroll = () => {
      const scrollY = window.scrollY;
      const maxScroll = 800;
      const progress = Math.min(scrollY / maxScroll, 1);
      setScrollProgress(progress);
    };

    window.addEventListener('scroll', handleScroll);
    handleScroll();
    
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);

  return (
    <html lang="en" className={nunito.variable}>
      <body>
        <div className="app-shell">
          <BlueberryFarm />
          <div 
            style={{
              opacity: isHomePage ? scrollProgress : 1,
              transition: 'opacity 0.3s ease',
              pointerEvents: (isHomePage && scrollProgress < 0.3) ? 'none' : 'auto',
            }}
          >
            <Navigation />
          </div>
          <main className="content-area">{children}</main>
          <Footer />
        </div>
      </body>
    </html>
  );
}

```

### sentiment-event/frontend/app/page.tsx

```typescript
'use client';

import Link from "next/link";
import ReactionTicker from "@/components/ReactionTicker";
import { useEffect, useState } from "react";

const featureItems = [
  {
    title: "Instant Sentiment Analysis",
    description:
      "Analyze any topic, product, or trend in minutes. Get comprehensive sentiment breakdowns with AI-powered summaries from Twitter, Reddit, and Facebook.",
  },
  {
    title: "Multi-Source Data Collection",
    description:
      "Gather authentic opinions from multiple social media platforms. Access real conversations happening across Twitter, Reddit, and Facebook communities.",
  },
  {
    title: "Emotion Intelligence",
    description:
      "Go beyond positive/negative sentiment. Understand nuanced emotions like joy, trust, desire, anger, and concern in every conversation.",
  },
  {
    title: "Educational & Research Ready",
    description:
      "Perfect for students, teachers, and researchers. Analyze public opinion, study trends, or teach data analysis and sentiment techniques.",
  },
  {
    title: "Product Intelligence",
    description:
      "Track how consumers feel about products, features, or brands. Make data-driven decisions with real-time sentiment insights.",
  },
  {
    title: "Trend Discovery",
    description:
      "Explore emerging topics and understand public discourse. Discover what people are excited, concerned, or talking about right now.",
  },
  {
    title: "Policy Making Impact",
    description:
      "Insights from sentiment analysis can inform policy decisions, helping leaders address public concerns and shape effective strategies.",
  },
  {
    title: "Social Impact & Welfare",
    description:
      "Understanding social sentiment enables organizations to better support community welfare, identify emerging issues, and drive positive change.",
  },
];

export default function HomePage() {
  const [scrollProgress, setScrollProgress] = useState(0);

  useEffect(() => {
    const handleScroll = () => {
      const scrollY = window.scrollY;
      const maxScroll = 800;
      const progress = Math.min(scrollY / maxScroll, 1);
      setScrollProgress(progress);
    };

    window.addEventListener('scroll', handleScroll);
    handleScroll();
    
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);

  return (
    <div className="page">
      <header
        style={{
          display: 'flex',
          justifyContent: 'center',
          alignItems: 'center',
          textAlign: 'center',
          width: '100%',
          padding: '1rem 0',
        }}
      >
        <h1
          className="hero-title fade-up"
          style={{
        display: 'inline-block',
        fontSize: 'clamp(2.0rem, 4vw, 3rem)',
        lineHeight: 1.05,
        fontWeight: 800,
        letterSpacing: '-0.02em',
        margin: 0,
        padding: '0.25rem 0.6rem',
        border: '2px solid #B7F5C1', // light green border
        borderRadius: '6px',
        backgroundColor: '#B7F5C1', // fill the border
        color: '#042a14', // dark text for contrast
          }}
        >
          bluberri
        </h1>
      </header>
      <section 
        className="main-hero"
        style={{
          opacity: Math.max(0, Math.min(1, (scrollProgress - 0.85) / 0.15)),
          transform: `translateY(${(1 - Math.max(0, Math.min(1, (scrollProgress - 0.85) / 0.15))) * 100}px)`,
          transition: 'opacity 0.3s ease, transform 0.3s ease',
        }}
      >
        <div className="hero-copy">
          <h1 className="hero-title fade-up">Understand what people really think</h1>
          <p className="fade-up delay-1">
            bluberri analyzes social media conversations to reveal authentic sentiment and emotions.
            Perfect for students, researchers, educators, businesses, and anyone curious about public opinion.
          </p>
          <div className="hero-actions fade-up delay-2">
            <Link href="/analyze" className="button-primary">
              Start analyzing
            </Link>
            <Link href="/docs" className="button-secondary">
              View documentation
            </Link>
          </div>
        </div>
        <ReactionTicker />
      </section>

      <section>
        <h2 className="section-heading">Features</h2>
        <p className="section-subtitle">
          Analyze anything from product launches to social trends. Get AI-powered insights from real conversations across Twitter, Reddit, and Facebook.
        </p>
        <div className="feature-grid">
          {featureItems.map((feature, index) => (
            <article
              key={feature.title}
              className="feature-card fade-up"
              style={{ animationDelay: `${0.08 * index + 0.1}s` }}
            >
              <h3>{feature.title}</h3>
              <p>{feature.description}</p>
            </article>
          ))}
        </div>
      </section>
    </div>
  );
}

```

### sentiment-event/frontend/app/about/page.tsx

```typescript
import Link from "next/link";

const principles = [
  {
    title: "Multi-Platform Analysis",
    description:
      "Collect and analyze authentic conversations from Twitter, Reddit, and Facebook. Access diverse perspectives from multiple social media communities in one unified analysis.",
  },
  {
    title: "AI-Powered Insights",
    description:
      "Advanced sentiment analysis powered by machine learning models and Google Gemini AI. Get detailed emotion breakdowns including joy, trust, desire, anger, concern, and more.",
  },
  {
    title: "Real-Time Understanding",
    description:
      "Track sentiment as it evolves. Analyze trending topics, product launches, or any subject of interest with up-to-date data from social media platforms.",
  },
  {
    title: "Educational Tool",
    description:
      "Perfect for teaching data science, sentiment analysis, and social media research. Students and educators can explore real-world applications of natural language processing.",
  },
  {
    title: "Research Ready",
    description:
      "Export detailed CSV reports with full sentiment scores and metadata. Built for academic research, market analysis, and data-driven decision making.",
  },
  {
    title: "Open Source",
    description:
      "Built at Cal Hacks with transparency in mind. Review the methodology, understand the algorithms, and explore how sentiment analysis works under the hood.",
  },
];

export default function AboutPage() {
  return (
    <div className="page">
      <section className="analysis-panel">
        <header>
          <h1 className="section-heading hero-title fade-up">About bluberri</h1>
          <p className="section-subtitle fade-up delay-1">
            bluberri is a sentiment analysis platform that helps you understand what people really think about any topic.
            Built at Cal Hacks, it combines social media data from multiple platforms with AI-powered analysis to reveal
            authentic public sentiment and emotions. Whether you're a student, researcher, educator, or business professional,
            bluberri provides the tools to analyze and understand social media conversations at scale.
          </p>
        </header>

        <h2 className="section-heading fade-up" style={{ marginTop: "3rem", fontSize: "1.8rem" }}>
          What makes bluberri different
        </h2>

        <div className="feature-grid">
          {principles.map((item, index) => (
            <article
              key={item.title}
              className="feature-card fade-up"
              style={{ animationDelay: `${0.08 * index + 0.1}s` }}
            >
              <h3>{item.title}</h3>
              <p>{item.description}</p>
            </article>
          ))}
        </div>

        <div className="hero-actions fade-up delay-2" style={{ marginTop: "3rem" }}>
          <Link href="/analyze" className="button-primary">
            Start analyzing
          </Link>
          <Link href="/docs" className="button-secondary">
            View documentation
          </Link>
        </div>
      </section>
    </div>
  );
}

```

### sentiment-event/frontend/app/docs/page.tsx

```typescript
"use client";

export default function DocsPage() {
  return (
    <div className="page">
      <section className="analysis-panel">
        <header>
          <h1 className="section-heading fade-up">Documentation</h1>
          <p className="section-subtitle fade-up delay-1">
            Setup and usage instructions for the sentiment analysis backend
          </p>
        </header>

        <div className="instructions fade-up delay-2">
          <h2 className="section-heading" style={{ fontSize: "1.5rem", marginTop: "2rem" }}>
            Getting Started
          </h2>

          <h3 style={{ fontSize: "1.2rem", marginTop: "1.5rem", marginBottom: "0.5rem" }}>
            1. Start the Backend Server
          </h3>
          <p>
            Navigate to the backend directory and start the FastAPI server:
          </p>
          <pre style={{
            background: "rgba(255, 255, 255, 0.08)",
            padding: "1rem",
            borderRadius: "8px",
            overflow: "auto"
          }}>
            <code>cd backend{"\n"}uvicorn app.api:app --host 0.0.0.0 --port 8000 --reload</code>
          </pre>

          <h3 style={{ fontSize: "1.2rem", marginTop: "1.5rem", marginBottom: "0.5rem" }}>
            2. Scrape Data from Social Media
          </h3>
          <p>
            Run these commands from the <code>backend</code> directory to collect data:
          </p>

          <h4 style={{ fontSize: "1.1rem", marginTop: "1rem", marginBottom: "0.5rem" }}>
            Twitter (X)
          </h4>
          <pre style={{
            background: "rgba(255, 255, 255, 0.08)",
            padding: "1rem",
            borderRadius: "8px",
            overflow: "auto"
          }}>
            <code>python main.py run "your keyword"</code>
          </pre>

          <h4 style={{ fontSize: "1.1rem", marginTop: "1rem", marginBottom: "0.5rem" }}>
            Reddit
          </h4>
          <pre style={{
            background: "rgba(255, 255, 255, 0.08)",
            padding: "1rem",
            borderRadius: "8px",
            overflow: "auto"
          }}>
            <code>python main.py run-reddit "your keyword"</code>
          </pre>

          <h4 style={{ fontSize: "1.1rem", marginTop: "1rem", marginBottom: "0.5rem" }}>
            Facebook
          </h4>
          <pre style={{
            background: "rgba(255, 255, 255, 0.08)",
            padding: "1rem",
            borderRadius: "8px",
            overflow: "auto"
          }}>
            <code>python main.py run-facebook "your keyword" --page-id PAGE_ID</code>
          </pre>

          <h3 style={{ fontSize: "1.2rem", marginTop: "1.5rem", marginBottom: "0.5rem" }}>
            3. Optional: Fast Analysis Mode
          </h3>
          <p>
            Add <code>--engine fast</code> to any command for lightweight sentiment analysis:
          </p>
          <pre style={{
            background: "rgba(255, 255, 255, 0.08)",
            padding: "1rem",
            borderRadius: "8px",
            overflow: "auto"
          }}>
            <code>python main.py run "your keyword" --engine fast</code>
          </pre>

          <h2 className="section-heading" style={{ fontSize: "1.5rem", marginTop: "2.5rem" }}>
            Configuration
          </h2>

          <h3 style={{ fontSize: "1.2rem", marginTop: "1.5rem", marginBottom: "0.5rem" }}>
            Environment Variables
          </h3>
          <p>
            Create a <code>.env</code> file in the project root with these credentials:
          </p>
          <ul style={{ marginLeft: "1.5rem", lineHeight: "1.8" }}>
            <li><strong>Twitter:</strong> Cookie header from authenticated session</li>
            <li><strong>Reddit:</strong> Client ID and secret from reddit.com/prefs/apps</li>
            <li><strong>Facebook:</strong> App ID, secret, and access token from developers.facebook.com</li>
            <li><strong>Gemini:</strong> API key from makersuite.google.com/app/apikey</li>
          </ul>

          <h2 className="section-heading" style={{ fontSize: "1.5rem", marginTop: "2.5rem" }}>
            Using the Web Interface
          </h2>
          <p>
            Once the backend is running, use the <a href="/analyze" style={{ color: "var(--color-primary)", textDecoration: "underline" }}>Analyze</a> page to:
          </p>
          <ol style={{ marginLeft: "1.5rem", lineHeight: "1.8" }}>
            <li>Enter a product name or keyword</li>
            <li>Choose between full or fast sentiment analysis</li>
            <li>View AI-generated sentiment summaries</li>
            <li>Review detailed sentiment metrics and stored content</li>
          </ol>
        </div>
      </section>
    </div>
  );
}

```

### sentiment-event/frontend/app/analyze/page.tsx

```typescript
"use client";

import { FormEvent, useState } from "react";
import ReactMarkdown from "react-markdown";
import type { StoredContentResponse } from "@/types/sentiment";

export default function AnalyzePage() {
  const [text, setText] = useState("");
  const [result, setResult] = useState<StoredContentResponse | null>(null);
  const [logs, setLogs] = useState<string[]>([]);
  const [progressStep, setProgressStep] = useState(0);
  const [totalSteps, setTotalSteps] = useState(25);
  const [insightSummary, setInsightSummary] = useState<string | null>(null);
  const [insightMetadata, setInsightMetadata] = useState<{ keyword: string; csvPath?: string; summaryPath?: string } | null>(
    null,
  );
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [activeEngine, setActiveEngine] = useState<"default" | "fast">("default");

  const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
  const nativeEvent = event.nativeEvent as Event & { submitter?: EventTarget | null };
  const submitter = (nativeEvent?.submitter ?? null) as HTMLElement | null;
    const engineAttr = submitter?.getAttribute("data-engine");
    const engine: "default" | "fast" = engineAttr === "fast" ? "fast" : "default";

    const input = text.trim();
    if (!input) {
      setError("Please describe a product, feature, or campaign to analyze.");
      setResult(null);
      return;
    }

    setIsLoading(true);
    setActiveEngine(engine);
    setError(null);
    setLogs([]);
    setProgressStep(0);
  setInsightSummary(null);
  setResult(null);
  setInsightMetadata(null);

    try {
      const response = await fetch("/api/analyze", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ keyword: input, engine }),
      });

      if (!response.ok || !response.body) {
        const fallback = await response.text();
        throw new Error(fallback || "Unable to fetch sentiment analysis.");
      }

      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let buffer = "";
  let summaryPayload: StoredContentResponse | null = null;
  let fallbackSummary: Partial<StoredContentResponse> | null = null;

      const yieldToBrowser = async () =>
        new Promise<void>((resolve) => {
          if (typeof window !== "undefined" && typeof window.requestAnimationFrame === "function") {
            window.requestAnimationFrame(() => resolve());
          } else {
            setTimeout(() => resolve(), 0);
          }
        });

      const updateFallbackSummary = (message: string) => {
        const storedMatch = message.match(/^([0-9]+)\s+content entries currently stored for '(.+)'\.?$/i);
        if (storedMatch) {
          const [, total, keywordFromLog] = storedMatch;
          fallbackSummary = {
            ...(fallbackSummary ?? {}),
            storedContent: Number(total),
            keyword: keywordFromLog,
            message,
          };
        }

        const sampleMatch = message.match(/Most recent summary used\s+([0-9]+)\s+content entries/i);
        if (sampleMatch) {
          fallbackSummary = {
            ...(fallbackSummary ?? {}),
            sampleSize: Number(sampleMatch[1]),
          };
        }

        const latestMatch = message.match(/Latest content entry recorded at\s+(.+)\.?$/i);
        if (latestMatch) {
          fallbackSummary = {
            ...(fallbackSummary ?? {}),
            latestContentAt: latestMatch[1],
          };
        }
      };

      const handleEventLine = (rawLine: string): boolean => {
        try {
          const event = JSON.parse(rawLine) as {
            type: string;
            message?: string | { text: string; keyword: string; csvPath?: string; summaryPath?: string };
            payload?: StoredContentResponse;
          };
          if (event.type === "log" && typeof event.message === "string") {
            const message = event.message;
            setLogs((prev) => [...prev, message]);
            setProgressStep((prev) => Math.min(prev + 1, totalSteps));
            updateFallbackSummary(message);
            return true;
          }
          if (event.type === "lava" || event.type === "gemini") {
            if (typeof event.message === "string") {
              setInsightSummary(event.message.trim());
              return true;
            }

            if (event.message && typeof event.message === "object") {
              const { text, keyword: kw, csvPath, summaryPath } = event.message;
              if (typeof text === "string") {
                setInsightSummary(text.trim());
              }
              if (typeof kw === "string" || csvPath || summaryPath) {
                setInsightMetadata({
                  keyword: typeof kw === "string" ? kw : input,
                  csvPath: typeof csvPath === "string" ? csvPath : undefined,
                  summaryPath: typeof summaryPath === "string" ? summaryPath : undefined,
                });
              }
              return true;
            }
            return true;
          }
          if (event.type === "summary" && event.payload) {
            summaryPayload = event.payload;
          } else if (event.type === "error" && typeof event.message === "string") {
            throw new Error(event.message);
          }
        } catch (parseError) {
          // Ignore partial JSON fragments; they will be retried once more data arrives.
        }
        return false;
      };

      while (true) {
        const { value, done } = await reader.read();
        if (done) {
          break;
        }
        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split("\n");
        buffer = lines.pop() ?? "";

        let appendedLog = false;

        for (const line of lines) {
          const trimmed = line.trim();
          if (!trimmed) {
            continue;
       
[truncated — 7095 more characters]
```

### sentiment-event/frontend/app/api/analyze/route.ts

```typescript
import { NextRequest, NextResponse } from "next/server";

const DEFAULT_BACKEND_URL = "http://localhost:8000";

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const keyword = typeof body?.keyword === "string" ? body.keyword.trim() : "";
    const limit =
      typeof body?.limit === "number" && Number.isFinite(body.limit) ? Math.max(1, Math.floor(body.limit)) : undefined;
  const refresh = body?.refresh !== undefined ? Boolean(body.refresh) : true;
  const requestedEngine = typeof body?.engine === "string" ? body.engine.toLowerCase() : "default";
  const engine = requestedEngine === "fast" ? "fast" : "default";

    if (!keyword) {
      return NextResponse.json({ error: "The 'keyword' field is required." }, { status: 400 });
    }

    const envBackend = process.env.BACKEND_API_URL?.trim() || DEFAULT_BACKEND_URL;
    let backendUrl = envBackend.replace(/\/$/, "");
    if (backendUrl.startsWith("http://0.0.0.0")) {
      backendUrl = backendUrl.replace("0.0.0.0", "127.0.0.1");
    } else if (backendUrl.startsWith("https://0.0.0.0")) {
      backendUrl = backendUrl.replace("0.0.0.0", "127.0.0.1");
    }

    const target = `${backendUrl}/analyze/stream`;

    const backendResponse = await fetch(target, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ keyword, limit, refresh, engine }),
      cache: "no-store",
    });

    if (!backendResponse.ok || !backendResponse.body) {
      const message = await safeReadError(backendResponse);
      return NextResponse.json({ error: message }, { status: backendResponse.status });
    }

    const stream = new ReadableStream({
      async start(controller) {
        const reader = backendResponse.body!.getReader();
        try {
          while (true) {
            const { value, done } = await reader.read();
            if (done) {
              break;
            }
            if (value) {
              controller.enqueue(value);
            }
          }
        } finally {
          controller.close();
        }
      },
    });

    const contentType = backendResponse.headers.get("content-type") ?? "application/x-ndjson";
    return new NextResponse(stream, {
      status: 200,
      headers: {
        "Content-Type": contentType,
        "Cache-Control": "no-store",
      },
    });
  } catch (error) {
    const message = error instanceof Error ? error.message : "Unexpected error";
    return NextResponse.json({ error: message }, { status: 500 });
  }
}

async function safeReadError(response: Response): Promise<string> {
  try {
    const data = await response.json();
    if (typeof data?.error === "string") {
      return data.error;
    }
    if (typeof data?.detail === "string") {
      return data.detail;
    }
    return `Backend request failed with status ${response.status}`;
  } catch (err) {
    return `Backend request failed with status ${response.status}`;
  }
}

```

### sentiment-event/frontend/global-jsx.d.ts

```typescript
declare namespace JSX {
  interface IntrinsicElements {
    [elemName: string]: any;
  }
}

```

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