# Project export: The Fourth Branch

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: UC Berkeley AI Hackathon 2025
- Tagline: When journalism distorts, it stops informing — it controls. Our AI agents surface stories that you care about and the ones you're kept from — sourced, labeled, and engineered to break your bubble.
- Devpost: https://devpost.com/software/the-fourth-branch
- GitHub: https://github.com/fourthbranch/tfb-calhacks
- Demo: https://fourthbranch.vercel.app/
- Video: https://www.youtube.com/embed/l1h3zZ44u4o?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Zichen Zhang (53 commits), Yuchen Huang (16 commits), Jenny0229 (13 commits)

## Devpost submission (written by the team)

### Overview

When journalism censors, panders, and distorts, it’s not news. It’s control. They call it journalism. We call it a leash. A carefully constructed reality designed to keep you complacent, anxious, and clicking. Fourth Branch is the antidote. Our AI agents surface the stories that matter to you and the ones you're not supposed to see — sourced, labeled, and engineered to break your bubble wide open. This isn’t about showing you “the other side.” It’s about showing you the whole battlefield. Our Assembly Line of Truth Our platform uses a three-agent system to ensure quality and depth. Here’s the breakdown: Agent 1: The Lookout (Topic Generator): It all starts here. This agent scans news feeds and the web to identify crucial stories based on user interests and their personal information. It decides what topics demand a deeper look and generates one detailed line as the output. Agent 1: The Lookout (Topic Generator): It all starts here. This agent scans news feeds and the web to identify crucial stories based on user interests and their personal information. It decides what topics demand a deeper look and generates one detailed line as the output. Agent 2: The Investigator (Report Generator): Based on your chosen topic, our LangGraph engine runs a comprehensive web search, accumulating and analyzing results to build a research-backed dossier — with up to 15 raw and cited sources that represent diverse perspectives. Agent 2: The Investigator (Report Generator): Based on your chosen topic, our LangGraph engine runs a comprehensive web search, accumulating and analyzing results to build a research-backed dossier — with up to 15 raw and cited sources that represent diverse perspectives. Agent 3: The Wordsmith (Final Writer): The Investigator's factual dossier is handed to our final agent, which transforms the raw report into a sharp, coherent article formatted in the user's preferred writing style. It’s not just information; it's intelligence tailored for you. Agent 3: The Wordsmith (Final Writer): The Investigator's factual dossier is handed to our final agent, which transforms the raw report into a sharp, coherent article formatted in the user's preferred writing style. It’s not just information; it's intelligence tailored for you. 24 Hours of Chaos: Challenges & Fixes Agent Coordination: Getting three autonomous AIs to work in perfect sequence without going rogue was our biggest debugging challenge. AI Safety & Guardrails: We implemented strict protocols to keep the AI's output factual and prevent it from generating harmful content. No friendly fire. Deployment Under Fire: Shipping a full-stack, multi-agent app with a database and frontend in 24 hours is a trial by fire. We survived. Highlights We’re Hyped About Radical Transparency: We built live frontend streaming so you can watch our agents work. No black boxes. We show our work—always. An Intuitive Cockpit: We delivered a clean UX that makes navigating complex, debiased information feel intuitive, not overwhelming. The Truth, On Demand: We shipped with multimodal output. You can read the article or listen to the generated podcast. The truth is now portable. The Rebellion is Just Beginning. What we built in 24 hours is just the first shot. Here's where we're aiming next: Go Global: Expand our agents to research and report in multiple languages, breaking the anglo-centric news bubble. Trace the Process: Watch the agent work in real time — from query to conclusion — with interactive visualizations showing each step, source, and connection behind the story. Expose the Echo Chamber: Offer users a tool to analyze their own media habits and see the shape of the bubble others have built around them. This is Day One. Welcome to the rebellion.

## README (from the GitHub repository)

# The Fourth Branch

AI-powered personalized news with radical transparency.

## 🔍 Overview

Fourth Branch is an AI news platform that builds personalized, bias-labeled stories by sourcing, verifying, and visualizing information from across the web. It combines a frontend app, a LangGraph-based agent system, and a structured database.

## ⚙️ Tech Stack

- **Frontend**: Next.js, Typescript, Tailwind CSS
- **Backend**: FastAPI, Python
- **Database**: Supabase(PostgreSQL)
- **LLMs**: LangGraph, Claude4, OpenAI, Gemini
<!-- 
## 📁 Project Structure

/
├── frontend/ # Next.js frontend interface
├── backend/ # API and LangGraph agents
│ ├── agent/ # Core AI pipeline (LangGraph)
│ └── app.py # Backend DB models and endpoints
└── db/ # SQL schema, seed scripts, and migrations -->

## 🧠 Key Folders
frontend/: Next.js-based UI with user interest input, main page and article view.

backend/agent/: Handles the entire news generation process using three AI agents.

db/: SQL scripts and schema for PostgreSQL.

## 💡 News Generation Workflow
<img src="./assets/news_generation_pipeline.png" width="600"/>

## 

UC Berkeley AI Hackathon 2025


## Detected evidence (automated analysis)

Indexed codebase: 57 recognized source files, 256 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- JavaScript (language) — detected in the code
- LangChain (technology) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Supabase (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code
- Vercel (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (67 of 67)

```
.gitignore
backend/__init__.py
backend/agent/__init__.py
backend/agent/configuration.py
backend/agent/final_writer_prompts.py
backend/agent/formats.py
backend/agent/graph.py
backend/agent/prompts.py
backend/agent/README.md
backend/agent/run.py
backend/agent/state.py
backend/agent/utils.py
backend/app.py
backend/db.py
backend/gemini_service.py
backend/main.py
backend/security.py
backend/VOICE_FEATURE.md
backend/voice_service.py
db/schema.sql
frontend/.gitignore
frontend/API_AUTH_SETUP.md
frontend/app/articles/[slug]/page.tsx
frontend/app/category/[category]/page.tsx
frontend/app/components/sections/ArticleGrid.tsx
frontend/app/components/sections/Chatbox.tsx
frontend/app/components/sections/Features.tsx
frontend/app/components/sections/Hero.tsx
frontend/app/components/sections/Newsletter.tsx
frontend/app/components/ui/ArticleCard.tsx
frontend/app/components/ui/Button.tsx
frontend/app/components/ui/Card.tsx
frontend/app/components/ui/Checkbox.tsx
frontend/app/components/ui/FeaturedArticle.tsx
frontend/app/components/ui/Footer.tsx
frontend/app/components/ui/Form.tsx
frontend/app/components/ui/Header.tsx
frontend/app/components/ui/Input.tsx
frontend/app/components/ui/Label.tsx
frontend/app/components/ui/LandingFooter.tsx
frontend/app/components/ui/LandingHeader.tsx
frontend/app/components/ui/NewsletterForm.tsx
frontend/app/components/ui/PodcastPlayer.tsx
frontend/app/components/ui/RadioGroup.tsx
frontend/app/components/ui/separator.tsx
frontend/app/components/ui/Textarea.tsx
frontend/app/globals.css
frontend/app/landing/page.tsx
frontend/app/layout.tsx
frontend/app/lib/api.ts
frontend/app/lib/articles.ts
frontend/app/lib/auth.ts
frontend/app/lib/utils.ts
frontend/app/onboarding/page.tsx
frontend/app/page.tsx
frontend/eslint.config.mjs
frontend/next.config.ts
frontend/package.json
frontend/postcss.config.mjs
frontend/README.md
frontend/tailwind.config.js
frontend/tsconfig.json
frontend/vercel.json
LICENSE
README.md
requirements.txt
vercel.json
```

### Dependencies

- frontend/package.json: @eslint/eslintrc@^3, @radix-ui/react-checkbox@^1.3.2, @radix-ui/react-label@^2.1.7, @radix-ui/react-radio-group@^1.3.7, @radix-ui/react-separator@^1.1.7, @types/node@^20, @types/react@^19, @types/react-dom@^19, autoprefixer@^10.4.20, class-variance-authority@^0.7.1, clsx@^2.1.1, eslint@^9, eslint-config-next@15.3.2, framer-motion@^12.18.1, lucide-react@^0.522.0, next@15.3.2, postcss@^8.4.49, react@^19.0.0, react-dom@^19.0.0, tailwind-merge@^3.3.1, tailwindcss@^3.4.17, typescript@^5
- requirements.txt: aiohttp@==3.11.18, beautifulsoup4@==4.13.4, exa_py@==1.14.10, fastapi@==0.115.13, google-generativeai@==0.8.3, httpx@==0.28.1, langchain@==0.3.26, langchain_anthropic@==0.3.15, langchain_community@==0.3.26, langchain_core@==0.3.66, langgraph@==0.4.8, linkup@==0.1.3, linkup_sdk@==0.2.5, markdownify@==1.1.0, openai@==1.90.0, pydantic@==2.11.7, python-dotenv@==1.1.0, supabase@==2.15.3, tavily_python@==0.7.2, uvicorn@==0.34.3

### Recent commits (newest first)

- Update CORS configuration to include new backend domain and remove unnecessary Vercel entries
- Fix CORS configuration to allow Vercel deployment domain
- Refactor podcast audio state initialization and clean up sidebar component structure
- Merge pull request #15 from fourthbranch/fix/deploy
- Merge remote-tracking branch 'origin/fix/deploy' into fix/deploy
- Add uvicorn to requirements.txt for server performance
- sidebar anchored
- Update requirements.txt to remove unused dependencies
- Update requirements and refactor search functions
- adding readme pic
- Merge remote-tracking branch 'origin/main' into fix/deploy
- adding readme pic and changing fonts
- Merge remote-tracking branch 'origin/feat/ui' into fix/deploy
- Update README.md
- ui fixed
- Update requirements to specify protobuf version and add uvicorn
- Update requirements and remove obsolete test files
- Add voice generation feature for personalized podcast audio
- Add Chatbox integration and enhance category handling in CategoryPage
- Add Gemini impact analysis feature and update dependencies

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

### frontend/API_AUTH_SETUP.md

```markdown
# Frontend API Authentication Setup

This document explains how to set up bearer token authentication for the frontend API calls.

## Environment Variable Setup

The frontend requires an API key to authenticate with the backend. You need to set the following environment variable:

### For Development

Create a `.env.local` file in the `frontend` directory:

```bash
NEXT_PUBLIC_API_KEY=your_api_key_here
NEXT_PUBLIC_API_URL=http://localhost:8000
NEXT_PUBLIC_API_BASE=http://localhost:8000
```

### For Production

Set the environment variables in your deployment platform:

```bash
NEXT_PUBLIC_API_KEY=your_api_key_here
NEXT_PUBLIC_API_URL=https://your-backend-domain.com
NEXT_PUBLIC_API_BASE=https://your-backend-domain.com
```

## API Key Requirements

- The API key must match the `API_KEY` environment variable set in your backend
- The key should be a secure, randomly generated string
- Never commit the actual API key to version control

## Authentication Implementation

The frontend now includes bearer token authentication for all API calls:

1. **Shared Authentication Utilities** (`app/lib/auth.ts`):

   - `getApiKey()`: Retrieves the API key from environment variables
   - `createAuthHeaders()`: Creates headers with bearer token authentication
   - `createAuthHeadersWithAccept()`: Creates headers with custom Accept header
   - `createAuthHeadersWithContentType()`: Creates headers with custom Content-Type

2. **Updated API Calls**:
   - All fetch requests now include the `Authorization: Bearer <api_key>` header
   - Authentication is handled automatically by the shared utilities

## Files Updated

The following files have been updated to include bearer token authentication:

- `app/lib/api.ts` - User management API calls
- `app/lib/articles.ts` - Article fetching API calls
- `app/components/sections/Chatbox.tsx` - News generation API calls
- `app/components/ui/NewsletterForm.tsx` - Newsletter subscription API calls
- `app/components/ui/Header.tsx` - Page views metrics API calls

## Error Handling

If the `NEXT_PUBLIC_API_KEY` environment variable is not set, the application will throw an error:

```
Error: API_KEY environment variable is not set
```

Make sure to set the environment variable before running the application.

```

### backend/VOICE_FEATURE.md

```markdown
# Voice AI Podcast Feature

This feature generates personalized podcast-style audio clips for news articles using OpenAI's text-to-speech API.

## Overview

The voice AI feature creates short (under 30 seconds) podcast clips that:

- Generate on-the-fly based on article content and user preferences
- Use a Daily Show-style conversational tone
- Include personalized context when user has `additional_info`
- Provide both audio playback and transcript display

## Components

### Backend

1. **`voice_service.py`** - Core voice generation service

   - `generate_podcast_script()` - Creates conversational scripts
   - `generate_podcast_audio()` - Generates audio using OpenAI TTS

2. **`main.py`** - API endpoint

   - `POST /articles/{article_id}/podcast-audio` - Generates podcast audio

3. **`test_voice.py`** - Test script to verify functionality

### Frontend

1. **`PodcastPlayer.tsx`** - Audio player component with:

   - Play/pause controls
   - Progress bar with seeking
   - Transcript display
   - Loading states

2. **`articles.ts`** - API client function

   - `getPodcastAudio()` - Fetches podcast audio from backend

3. **Article page** - Integrated podcast player display

## Setup

### Environment Variables

Add to your `.env` file:

```
OPENAI_API_KEY=your_openai_api_key_here
```

### Dependencies

The feature requires:

- `openai==1.52.0` - OpenAI API client
- `python-dotenv` - Environment variable loading

## Usage

### Testing

Run the test script to verify setup:

```bash
cd backend
python test_voice.py
```

This will:

- Check for OpenAI API key
- Generate a test script and audio
- Save test audio as `test_audio.mp3`

### API Usage

```javascript
// Frontend example
const audio = await getPodcastAudio(articleId, userEmail);
if (audio) {
  // audio.audio_data - Base64 encoded MP3
  // audio.transcript - Generated script
  // audio.duration_estimate - Estimated duration in seconds
}
```

### Backend API

```bash
POST /articles/{article_id}/podcast-audio
Content-Type: application/json
Authorization: Bearer {api_key}

{
  "user_email": "user@example.com"
}
```

Response:

```json
{
  "audio_data": "base64_encoded_mp3_data",
  "transcript": "Generated podcast script",
  "duration_estimate": 25.5
}
```

## Features

### Script Generation

- Uses GPT-4o-mini to create conversational scripts
- Optimized for ~30 seconds (under 75 words)
- Daily Show-style humor and engagement
- Personalized based on user's `additional_info`

### Audio Generation

- Uses OpenAI's TTS-1 model with "alloy" voice
- MP3 format with base64 encoding for easy transmission
- Configurable speed and voice options

### User Experience

- Automatic generation when viewing articles
- Loading states and error handling
- Full audio controls (play, pause, seek)
- Transcript display for accessibility

## Customization

### Voice Options

Available OpenAI TTS voices:

- `alloy` (default) - Balanced, professional
- `echo` - Warm, friendly
- `fable` - Storytelling
- `onyx` - Deep, auth
[truncated — 1127 more characters]
```

### requirements.txt

```
aiohttp==3.11.18
beautifulsoup4==4.13.4
exa_py==1.14.10
fastapi==0.115.13
google-generativeai==0.8.3
httpx==0.28.1
langchain==0.3.26
langchain_anthropic==0.3.15
langchain_community==0.3.26
langchain_core==0.3.66
langgraph==0.4.8
linkup==0.1.3
linkup_sdk==0.2.5
markdownify==1.1.0
openai==1.90.0
pydantic==2.11.7
python-dotenv==1.1.0
supabase==2.15.3
tavily_python==0.7.2
uvicorn==0.34.3

```

### frontend/package.json

```
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@radix-ui/react-checkbox": "^1.3.2",
    "@radix-ui/react-label": "^2.1.7",
    "@radix-ui/react-radio-group": "^1.3.7",
    "@radix-ui/react-separator": "^1.1.7",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "framer-motion": "^12.18.1",
    "lucide-react": "^0.522.0",
    "next": "15.3.2",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "tailwind-merge": "^3.3.1"
  },
  "devDependencies": {
    "@eslint/eslintrc": "^3",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "autoprefixer": "^10.4.20",
    "eslint": "^9",
    "eslint-config-next": "15.3.2",
    "postcss": "^8.4.49",
    "tailwindcss": "^3.4.17",
    "typescript": "^5"
  }
}

```

### backend/app.py

```python
# **************************************************************************
#  * Copyright (c) 2025 The Fourth Branch
#  * All Rights Reserved.
#  *
#  * This software contains proprietary and confidential information of The Fourth Branch.
#  * By using this software you agree to the terms of the associated License Agreement.
#  * Third party components are distributed under their respective licenses.
#  **************************************************************************

"""
This module handles FastAPI app initialization and configuration.
"""

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import os

app = FastAPI(title="The Fourth Branch API")

# Add CORS middleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "https://fourthbranch.vercel.app",
        "https://tfb-backend.onrender.com",
        "http://localhost:3000",
    ],
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD"],
    allow_headers=["*"],
    expose_headers=["*"],
)

```

### backend/main.py

```python
# **************************************************************************
#  * Copyright (c) 2025 The Fourth Branch
#  * All Rights Reserved.
#  *
#  * This software contains proprietary and confidential information of The Fourth Branch.
#  * By using this software you agree to the terms of the associated License Agreement.
#  * Third party components are distributed under their respective licenses.
#  **************************************************************************

"""
This module is the main entry point for the API.
"""

from fastapi import HTTPException, Depends, Request
from typing import List, Optional, Dict, Any
from pydantic import BaseModel
from fastapi.responses import StreamingResponse
import json

from backend.agent import topic_generator
from backend.agent.run import stream_report_generation
from backend.app import app
from backend.security import get_api_key
from backend.db import supabase
from backend.gemini_service import generate_impact_analysis
from backend.voice_service import generate_podcast_audio


@app.get("/")
def root() -> Dict[str, Any]:
    """Root endpoint for the API"""
    return {"message": "Hello, World!"}


@app.options("/{full_path:path}")
async def options_handler():
    """Handle OPTIONS requests for CORS preflight"""
    return {"message": "OK"}


@app.get("/cors-test")
def cors_test() -> Dict[str, Any]:
    """Test endpoint for CORS configuration"""
    return {"message": "CORS is working!", "status": "success"}


class ArticleListItem(BaseModel):
    id: int
    title: str
    summary: Optional[str] = None
    created_at: Optional[str] = None
    relevant_topics: Optional[list[str]] = None


class ArticlesResponse(BaseModel):
    user_preferred: List[ArticleListItem]
    explore: List[ArticleListItem]


class ArticleDetail(BaseModel):
    id: int
    title: str
    summary: Optional[str] = None
    content: str
    created_at: Optional[str] = None
    metadata: Optional[dict] = None
    relevant_topics: Optional[list[str]] = None
    bias: Optional[str] = None
    opposite_view: Optional[str] = None


class SubscribeRequest(BaseModel):
    email: str


class GenNewsWithRequestRequest(BaseModel):
    user_request: str
    user_email: Optional[str] = None  # Add user email for personalization


class UserCheckRequest(BaseModel):
    email: str


class UserCreateRequest(BaseModel):
    email: str
    preferred_topics: Optional[List[str]] = None
    locations: Optional[List[str]] = None
    political_leaning: Optional[str] = None
    additional_info: Optional[str] = None
    preferred_writing_style: Optional[List[str]] = None


class UserUpdateRequest(BaseModel):
    preferred_topics: Optional[List[str]] = None
    locations: Optional[List[str]] = None
    political_leaning: Optional[str] = None
    additional_info: Optional[str] = None
    preferred_writing_style: Optional[List[str]] = None


@app.get("/articles", response_model=ArticlesResponse)
def list_articles_display(request: Request, api_key: str = Depends(get_api_key)):
    # Get user email from request headers
    user_email = request.headers.get("user_email")
    print("reached server side, user email is:", user_email)
    if not user_email:
        raise HTTPException(
            status_code=401, detail="Unauthorized: Missing user email in headers")

    # Fetch user preferences first
    user_res = supabase.table("users").select(
        "preferred_topics, political_leaning, preferred_writing_style"
    ).eq("email", user_email).single().execute()

    if not user_res.data:
        raise HTTPException(status_code=404, detail="User not found")

    # Extract user preferences
    preferred_topics = user_res.data.get("preferred_topics", [])
    political_leaning = user_res.data.get("political_leaning", None)
    preferred_writing_style = user_res.data.get("preferred_writing_style", [])

    print("preferred_topics: ", preferred_topics)
    print("political_leaning: ", political_leaning)
    print("preferred_writing_style: ", preferred_writing_style)

    # Single optimized query with JOIN to get articles with created_at
    articles_res = supabase.table("articles_new").select(
        "id, title, summary, relevant_topics, topic_bias, opposite_view, report_id, preferred_writing_style, reports!inner(created_at)"
    ).execute()

    if not articles_res.data:
        return {"user_preferred": [], "explore": []}

    # Process the joined data
    articles_with_created_at = []
    for article in articles_res.data:
        # Extract created_at from the joined reports data
        created_at = article.get("reports", {}).get(
            "created_at") if article.get("reports") else None
        article_data = {
            "id": article["id"],
            "title": article["title"],
            "summary": article["summary"],
            "relevant_topics": article["relevant_topics"],
            "topic_bias": article["topic_bias"],
            "opposite_view": article["opposite_view"],
            "report_id": article["report_id"],
            "preferred_writing_style": article["preferred_writing_style"],
            "created_at": created_at
        }
        articles_with_created_at.append(article_data)

    # Sort articles by created_at in descending order
    articles_with_created_at.sort(key=lambda x: x["created_at"], reverse=True)

    # Define mapping for topic_bias to political_leaning
    bias_to_leaning = {
        "liberal": "left",
        "neutral": "neutral",
        "conservative": "right"
    }

    # Filter articles into user_preferred and explore
    user_preferred = []
    explore = []
    seen_report_ids = set()  # Track report_ids to avoid duplicates in explore

    for article in articles_with_created_at:
        # Translate topic_bias to political_leaning
        article_political_leaning = bias_to_leaning.get(
            article.get("topic_bias"))

        # Check if the article matches user preferences
        matches_topics = any(
            topic in preferred_top
[truncated — 11772 more characters]
```

### frontend/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "The Fourth Branch",
  description: "The world's first news company staffed entirely by AI agents",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
      </body>
    </html>
  );
}

```

### frontend/app/page.tsx

```typescript
"use client";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import Header from "./components/ui/Header";
import Footer from "./components/ui/Footer";
import ArticleGrid from "./components/sections/ArticleGrid";
import NewsletterForm from "./components/ui/NewsletterForm";
import { getArticlesForYou, type Article } from "./lib/articles";
import Chatbox from "./components/sections/Chatbox";

export default function Home() {
  const router = useRouter();
  const [hasEmail, setHasEmail] = useState<boolean | null>(null);
  const [articles, setarticles] = useState<Article[]>([]);

  useEffect(() => {
    const email =
      typeof window !== "undefined" ? localStorage.getItem("user_email") : null;
    if (!email) {
      router.replace("/landing");
    } else {
      setHasEmail(true);
      (async () => {
        setarticles(await getArticlesForYou(email));
      })();
    }
  }, [router]);

  if (hasEmail === null) return null;
  if (!hasEmail) return null;

  return (
    <div className="min-h-screen bg-white">
      <Header />
      <main className="container mx-auto px-4 py-8">
        <Chatbox />
        {articles.length > 0 && <ArticleGrid articles={articles} />}
        <NewsletterForm />
      </main>
      <Footer />
    </div>
  );
}

```

### frontend/app/landing/page.tsx

```typescript
"use client";
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import LandingHeader from "../components/ui/LandingHeader";
import LandingFooter from "../components/ui/LandingFooter";
import Button from "../components/ui/Button";
import { Input } from "../components/ui/Input";
import { api } from "../lib/api";
import { motion } from "framer-motion";
import { BarChart, BrainCircuit, Scale } from "lucide-react";

// Helper for animations
const fadeIn = {
  initial: { opacity: 0, y: 20 },
  animate: { opacity: 1, y: 0 },
  transition: { duration: 0.6 },
};

const Feature = ({
  icon,
  title,
  children,
}: {
  icon: React.ReactNode;
  title: string;
  children: React.ReactNode;
}) => (
  <motion.div
    variants={fadeIn}
    className="bg-white/5 p-6 rounded-lg border border-white/10 text-center"
  >
    <div className="inline-block bg-white/10 p-3 rounded-full mb-4">{icon}</div>
    <h3 className="text-xl font-bold mb-2">{title}</h3>
    <p className="text-gray-400">{children}</p>
  </motion.div>
);

export default function LandingPage() {
  const router = useRouter();
  const [email, setEmail] = useState("");
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState("");

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!email) {
      setError("Please enter your email address.");
      return;
    }
    setIsLoading(true);
    setError("");

    try {
      const userCheck = await api.checkUser(email);
      localStorage.setItem("user_email", email);
      if (userCheck.exists && userCheck.onboarding_completed) {
        router.push("/");
      } else {
        router.push("/onboarding");
      }
    } catch (err) {
      setError("Failed to verify email. Please try again.");
      console.error(err);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="flex flex-col min-h-screen bg-black text-white overflow-x-hidden">
      <LandingHeader />
      <main className="flex-grow">
        {/* Hero Section */}
        <motion.section
          initial="initial"
          animate="animate"
          className="w-full text-center py-20 md:py-32 flex flex-col items-center justify-center"
          style={{
            backgroundImage: `radial-gradient(ellipse 80% 50% at 50% -20%,rgba(120,119,198,0.3), hsla(0, 0%, 100%, 0))`,
          }}
        >
          <motion.h1
            variants={fadeIn}
            className="text-5xl md:text-7xl font-bold tracking-tighter mb-6 text-transparent bg-clip-text bg-gradient-to-b from-white to-gray-400"
            style={{ fontFamily: "Times New Roman, serif",
              letterSpacing: "0.01em"
             }} // Apply Times New Roman font
          >
            Unsilence The Truth.
          </motion.h1>
          <motion.p
            variants={fadeIn}
            transition={{ delay: 0.2 }}
            className="text-lg md:text-xl text-gray-300 mb-8 max-w-2xl mx-auto"
          >
            Tired of biased narratives? Get AI-powered news that shows you every
            angle, personalized to your interests.
          </motion.p>
          <motion.div
            variants={fadeIn}
            transition={{ delay: 0.4 }}
            className="w-full max-w-md mx-auto"
          >
            <form
              onSubmit={handleSubmit}
              className="flex flex-col sm:flex-row gap-4"
            >
              <Input
                type="email"
                placeholder="Enter your email"
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                className="flex-grow bg-white/5 border-white/20 placeholder:text-gray-500 focus:ring-white/50 text-white"
                required
              />
              <Button
                type="submit"
                variant="secondary"
                size="lg"
                disabled={isLoading}
              >
                {isLoading ? "Checking..." : "Get Started"}
              </Button>
            </form>
            {error && <p className="text-red-400 mt-4">{error}</p>}
          </motion.div>
        </motion.section>

        {/* Features Section */}
        <motion.section
          initial="initial"
          whileInView="animate"
          variants={{ animate: { transition: { staggerChildren: 0.2 } } }}
          viewport={{ once: true, amount: 0.3 }}
          className="container mx-auto px-4 py-2 md:py-4"
        >
          <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
            <Feature
              icon={<BrainCircuit size={24} />}
              title="AI-Personalized Content"
            >
              Our AI learns what you care about and delivers news digests
              tailored specifically to your interests and reading style.
            </Feature>
            <Feature icon={<Scale size={24} />} title="Balanced Perspectives">
              We break you out of the echo chamber by presenting multiple
              viewpoints on the same issue, side-by-side.
            </Feature>
            <Feature icon={<BarChart size={24} />} title="In-Depth Analysis">
              Go beyond the headlines. Our AI agents provide comprehensive
              reports, not just sensationalized summaries.
            </Feature>
          </div>
        </motion.section>
      </main>
      <LandingFooter />
    </div>
  );
}

```

### frontend/app/onboarding/page.tsx

```typescript
"use client";
import React, { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import Button from "../components/ui/Button";
import { Input } from "../components/ui/Input";
import { Label } from "../components/ui/Label";
import { RadioGroup, RadioGroupItem } from "../components/ui/RadioGroup";
import { Textarea } from "../components/ui/Textarea";
import LandingHeader from "../components/ui/LandingHeader";
import LandingFooter from "../components/ui/LandingFooter";
import { api } from "../lib/api";
import { Separator } from "../components/ui/separator";

const topics = [
  "Technology",
  "Politics",
  "Finance",
  "Sports",
  "Entertainment",
  "Local News",
  "World News",
];

export default function OnboardingPage() {
  const router = useRouter();
  const [email, setEmail] = useState("");
  const [selectedTopics, setSelectedTopics] = useState<string[]>([]);
  const [locations, setLocations] = useState<string[]>([]);
  const [currentLocation, setCurrentLocation] = useState("");
  const [politicalLeaning, setPoliticalLeaning] = useState("neutral");
  const [articleLength, setArticleLength] = useState("short");
  const [writingTone, setWritingTone] = useState("informal");
  const [contentStyle, setContentStyle] = useState("straight");
  const [additionalInfo, setAdditionalInfo] = useState("");
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState("");
  const [additionalInfoError, setAdditionalInfoError] = useState("");

  useEffect(() => {
    const storedEmail = localStorage.getItem("user_email");
    if (storedEmail) {
      setEmail(storedEmail);
    } else {
      router.push("/landing");
    }
  }, [router]);

  const handleAddLocation = () => {
    if (currentLocation && !locations.includes(currentLocation)) {
      setLocations([...locations, currentLocation]);
      setCurrentLocation("");
    }
  };

  const handleRemoveLocation = (index: number) => {
    setLocations(locations.filter((_, i) => i !== index));
  };

  const validateAdditionalInfo = (text: string) => {
    if (!text.trim()) {
      return "Please tell us more about yourself to personalize your news experience.";
    }

    const wordCount = text.trim().split(/\s+/).length;
    if (wordCount < 10) {
      return `Please provide at least 10 words. You currently have ${wordCount} word${
        wordCount !== 1 ? "s" : ""
      }.`;
    }

    return "";
  };

  const handleAdditionalInfoChange = (
    e: React.ChangeEvent<HTMLTextAreaElement>
  ) => {
    const text = e.target.value;
    setAdditionalInfo(text);
    setAdditionalInfoError(validateAdditionalInfo(text));
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsLoading(true);
    setError("");

    // Validate additional info
    const additionalInfoValidation = validateAdditionalInfo(additionalInfo);
    if (additionalInfoValidation) {
      setAdditionalInfoError(additionalInfoValidation);
      setIsLoading(false);
      return;
    }

    const writingStyle = [articleLength, writingTone, contentStyle];

    try {
      const userCheck = await api.checkUser(email);

      if (userCheck.exists && userCheck.user_id) {
        await api.updateUser(userCheck.user_id, {
          preferred_topics: selectedTopics,
          locations: locations,
          political_leaning: politicalLeaning,
          preferred_writing_style: writingStyle,
          additional_info: additionalInfo,
        });
      } else {
        await api.createUser({
          email: email,
          preferred_topics: selectedTopics,
          locations: locations,
          political_leaning: politicalLeaning,
          preferred_writing_style: writingStyle,
          additional_info: additionalInfo,
        });
      }

      localStorage.setItem("onboarding_completed", "true");
      router.push("/");
    } catch (err) {
      console.error(err);
      setError("An unexpected error occurred. Please try again.");
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="min-h-screen bg-gray-50">
      <LandingHeader variant="light" />
      <main className="container mx-auto px-4 py-12 sm:py-16 mt-20">
        <div className="max-w-4xl mx-auto">
          <div className="text-center mb-12">
            <h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl">
              Personalize Your News Feed
            </h1>
            <p className="mt-4 text-lg text-gray-600">
              This will help us tailor the news to your exact preferences.
            </p>
          </div>

          <div className="bg-white p-8 sm:p-12 rounded-2xl shadow-lg border border-gray-200">
            <form onSubmit={handleSubmit} className="space-y-10">
              {/* Section 1: Topics */}
              <div className="space-y-4">
                <h3 className="text-xl font-semibold text-gray-800">
                  1. What topics are you interested in?
                </h3>
                <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
                  {topics.map((topic) => (
                    <div
                      key={topic}
                      onClick={() => {
                        setSelectedTopics((prev) =>
                          prev.includes(topic)
                            ? prev.filter((t) => t !== topic)
                            : [...prev, topic]
                        );
                      }}
                      className={`py-3 px-2 rounded-lg text-center cursor-pointer transition-all duration-200 border text-sm font-medium
                        ${
                          selectedTopics.includes(topic)
                            ? "bg-blue-600 border-blue-600 text-white shadow-md"
                            : "bg-white border-gray-300 text-gray-700 hover:bg-gray-100 hover:border-gray-400"
                        }`}
                    >
                      {topic}
           
[truncated — 8957 more characters]
```

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