# Project export: FactCheck

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: OpenAI Build Week
- Tagline: Verify news claims with trusted sources in one click.
- Devpost: https://devpost.com/software/factcheck-jztnas
- GitHub: https://github.com/alienqqe/fake-check-extension
- Video: https://www.youtube.com/embed/BWRAkM6YBSg?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Denys Sichka (20 commits), strongsasha (4 commits), hullss (2 commits)

## Devpost submission (written by the team)

### Inspiration

News articles often contain many factual claims, but a simple “true” or “false” label does not explain what is actually wrong. We wanted to build a tool that shows which specific claims require attention, what the available evidence says, and why a particular verdict was reached. What We Built FactCheck is a Chrome extension for checking news articles directly in the browser. It can analyze the text of an open article or check a single claim entered by the user. The system identifies up to three independently checkable claims, searches Google Fact Check Tools and Tavily for relevant evidence, ranks sources by relevance and credibility, compares claims with the retrieved evidence, and displays verdicts, explanations, confidence scores, citations, and source links. The project consists of: a React and TypeScript Chrome extension with a browser side panel; a Spring Boot backend that coordinates the fact-checking pipeline; a FastAPI model service using Qwen for claim extraction; DeBERTa for comparing claims with evidence; evidence retrieval through Google Fact Check Tools and Tavily. OpenAI Build Week We developed FactCheck during OpenAI Build Week. OpenAI Codex and ChatGPT supported our development process by helping us analyze the repository, design the API architecture, debug integration issues, create tests, and improve the project documentation. What We Learned We learned how to connect a browser extension with multiple backend services, design stable API contracts, process model responses, and handle uncertainty in automated fact-checking. We also learned that an AI-generated verdict should not rely only on a model’s internal knowledge. It should be connected to real, traceable sources and clearly show the evidence used to support or challenge a claim. Challenges The main challenge was coordinating several independent components: the Chrome extension, Spring Boot backend, external evidence providers, and AI inference service. We also had to handle slow model cold starts, unavailable external APIs, missing evidence, differences in source credibility, and invalid model responses. Another challenge was making the results understandable. Instead of returning only a final label, the system shows the exact claim, evidence excerpts, an explanation, a confidence score, and links to the original sources. Future Improvements Future versions could improve multilingual claim extraction, strengthen contextual manipulation detection, add more evidence providers, improve source verification, support analysis history, and provide more detailed explanations when the available evidence is conflicting or inconclusive.

## README (from the GitHub repository)

# FactCheck

FactCheck is a Chrome extension for checking claims in news articles. The
current repository contains three cooperating applications:

- a React/Vite Chrome extension that runs in the browser side panel;
- a Spring Boot backend that searches for evidence, ranks sources, and
  coordinates the fact-check;
- a FastAPI model service that uses Qwen for claim extraction and DeBERTa for
  claim/evidence comparison.

This is a working local prototype, not a production fact-checking authority.
Its verdicts depend on the claims extracted, the evidence returned by external
search providers, and domain-level source credibility heuristics. Always open
the cited sources before treating a result as conclusive.

## Current features

- **Article mode:** reads the active article and extracts up to three
  independently checkable claims.
- **Single-claim mode:** checks the exact statement entered by the user without
  splitting or rewriting it into subclaims.
- **Evidence retrieval:** searches Google Fact Check Tools and Tavily, then
  deduplicates and relevance-ranks up to three sources per claim. Transient
  Tavily failures are retried once, and either provider can work as a fallback
  when the other is unavailable.
- **Source evaluation:** weights model comparisons by a transparent
  domain-level credibility assessment, so a Reddit post is not treated like an
  official record or an established publisher.
- **Results:** shows a verdict, percentage, explanation, evidence excerpts,
  clickable citations, and a credibility label for each source.
- **Source tracing:** selects the earliest dated credible result when possible,
  or the best available search result, and labels it as the likely original
  source.
- **Distortion reporting:** automatically marks a factual contradiction when a
  refuted claim conflicts with the traced source. The backend and UI also
  support context-distortion results, although the current comparison logic
  does not generate manipulation verdicts automatically.
- **Article highlights:** highlights checked claims in the page and colors them
  by verdict: green for confirmed, red for refuted, orange for manipulation,
  and yellow for insufficient evidence. Clicking a highlight opens its
  evidence details.

## How We Used OpenAI Codex

We used OpenAI Codex throughout development as an engineering and product
design assistant. Codex helped us analyze the existing repository, shape the
extension and backend architecture, and divide the implementation into clear
modules for the team.

For the frontend, Codex supported the side-panel layout, responsive states,
article and single-claim flows, loading and error states, and the presentation
of verdicts, confidence scores, evidence excerpts, citations, and source
credibility. It also helped us refine the visual hierarchy and make the result
cards easier to scan.

For the backend and model integration, Codex helped implement and debug the
REST API, validation, CORS, caching, AI client abstraction, evidence pipeline,
remote model API integration, and error handling. It also helped create tests,
diagnose Windows and PowerShell setup issues, verify service communication, and
keep the local and hosted run instructions up to date.

Codex was used as a development assistant; the runtime fact-checking pipeline
uses Qwen for claim extraction and DeBERTa for claim/evidence comparison.

## Architecture

```mermaid
flowchart LR
    Page["Open article or manual claim"] --> Extension["Chrome side panel"]
    Extension --> Backend["Spring Boot API :8080"]
    Backend --> Qwen["Qwen claim extraction"]
    Backend --> Search["Evidence search and ranking"]
    Search --> Google["Google Fact Check Tools"]
    Search --> Tavily["Tavily"]
    Backend --> DeBERTa["DeBERTa comparison via FastAPI :7860"]
    Backend --> Extension
    Extension --> Highlights["Colored, clickable page highlights"]
```

For an article request, the backend asks Qwen for at most three claims. For a
manual request, it skips extraction and preserves the submitted claim. Each
claim is searched independently and compared with the returned evidence using
`MoritzLaurer/DeBERTa-v3-base-mnli-fever-anli`.

The backend multiplies each comparison score by the source credibility weight
before deciding a verdict. The strongest adjusted support and contradiction
scores are compared with a `0.55` decision threshold.

| Source category                  | Weight | Examples                                              |
| -------------------------------- | -----: | ----------------------------------------------------- |
| Professional fact-check result   | `1.00` | Result returned by Google Fact Check Tools            |
| Official source                  | `0.98` | Government, court, university, EU, UN, or WHO domains |
| Established fact-check publisher | `0.95` | FactCheck.org, Full Fact, PolitiFact, Snopes          |
| Established news publisher       | `0.85` | AP, BBC, Reuters, and the maintained news-domain list |
| Unrated site                     | `0.55` | A domain without a configured rating                  |
| Community or social platform     | `0.15` | Reddit, Facebook, X, YouTube, and similar platforms   |
| Satire publisher                 | `0.05` | The Onion, Babylon Bee                                |

These weights are heuristics for source provenance, not a judgment that every
page from a listed domain is correct. If credible evidence strongly supports
and contradicts the same claim, the backend returns
`INSUFFICIENT_EVIDENCE` for human review.

The source trace is also intentionally cautious. It chooses among sources
returned by the configured search providers; it does not prove that the chosen
page was the first publication anywhere on the web.

Completed responses with evidence for every claim are cached in memory for ten
minutes, up to 100 entries. Empty-evidence responses are not cached, so a
temporary search-provider failure can recover on the next request.

## Run with the hosted services (recommended)

The intended way to run FactCheck is to build only the Chrome extension and
use the already deployed backend and model API:

| Service | Public URL |
| --- | --- |
| Spring backend | `https://fake-check-extension.onrender.com` |
| Model API | `https://sickadenis--facttrace-ai-api.modal.run` |

The extension sends fact-check requests to the Render backend, and the backend
calls the Modal model API. You do not need to run Java or Python locally for
this setup.

From the repository root, install and build the extension against the hosted
backend:

```bash
npm install --prefix extension
VITE_FACTCHECK_API_URL=https://fake-check-extension.onrender.com npm run build
```

To keep the hosted URL for future Vite builds, create
`extension/.env.local` containing:

```text
VITE_FACTCHECK_API_URL=https://fake-check-extension.onrender.com
```

Load the built extension in Chrome:

1. Open `chrome://extensions`.
2. Enable **Developer mode**.
3. Select **Load unpacked**.
4. Choose `extension/dist`.
5. Click the **FactCheck** toolbar icon to open the side panel.

After rebuilding, click the extension reload button on
`chrome://extensions`. Chrome asks for optional access to a website the first
time FactCheck reads it; browser-internal pages cannot be inspected.

You can verify the hosted services with:

```bash
curl https://fake-check-extension.onrender.com/api/health
curl https://sickadenis--facttrace-ai-api.modal.run/health
```

Both hosts can have a cold start after being idle, so the first request can
take longer than later requests.

## Self-hosting fallback

Use the following setup if a hosted service is unavailable or if you need to
develop the backend or model service locally.

### Configure environment variables

Create the local environment file from the committed template:

```bash
cp .env.example .env
```

The complete evidence pipeline uses:

- `GOOGLE_FACTCHECK_API_KEY` for
  [Google Fact Check Tools](https://console.cloud.goo

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 84 recognized source files, 264 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- Hugging Face (technology) — detected in the code
- Java (language) — 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
- Docker (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (105 of 105)

```
.env.example
.gitignore
ai-service/.dockerignore
ai-service/app/__init__.py
ai-service/app/config.py
ai-service/app/inference.py
ai-service/app/main.py
ai-service/app/parsing.py
ai-service/app/schemas.py
ai-service/app/service.py
ai-service/Dockerfile
ai-service/modal_app.py
ai-service/README.md
ai-service/requirements-dev.txt
ai-service/requirements.txt
ai-service/tests/test_inference.py
ai-service/tests/test_service.py
backend/.dockerignore
backend/.mvn/wrapper/maven-wrapper.properties
backend/Dockerfile
backend/mvnw
backend/mvnw.cmd
backend/pom.xml
backend/src/main/java/com/facttrace/common/config/CorsConfiguration.java
backend/src/main/java/com/facttrace/common/config/RestClientConfiguration.java
backend/src/main/java/com/facttrace/common/error/GlobalExceptionHandler.java
backend/src/main/java/com/facttrace/evidence/EvidenceController.java
backend/src/main/java/com/facttrace/evidence/EvidenceOrigin.java
backend/src/main/java/com/facttrace/evidence/EvidenceResult.java
backend/src/main/java/com/facttrace/evidence/EvidenceSearchRequest.java
backend/src/main/java/com/facttrace/evidence/EvidenceSearchResponse.java
backend/src/main/java/com/facttrace/evidence/EvidenceService.java
backend/src/main/java/com/facttrace/evidence/SourceCredibility.java
backend/src/main/java/com/facttrace/factcheck/AiClient.java
backend/src/main/java/com/facttrace/factcheck/AiResult.java
backend/src/main/java/com/facttrace/factcheck/AiServiceException.java
backend/src/main/java/com/facttrace/factcheck/ClaimDistortion.java
backend/src/main/java/com/facttrace/factcheck/ClaimResult.java
backend/src/main/java/com/facttrace/factcheck/EvidenceCitation.java
backend/src/main/java/com/facttrace/factcheck/FactCheckClient.java
backend/src/main/java/com/facttrace/factcheck/FactCheckClientConfiguration.java
backend/src/main/java/com/facttrace/factcheck/FactCheckController.java
backend/src/main/java/com/facttrace/factcheck/FactCheckException.java
backend/src/main/java/com/facttrace/factcheck/FactCheckRequest.java
backend/src/main/java/com/facttrace/factcheck/FactCheckResponse.java
backend/src/main/java/com/facttrace/factcheck/FactCheckResult.java
backend/src/main/java/com/facttrace/factcheck/FactCheckService.java
backend/src/main/java/com/facttrace/factcheck/MockAiClient.java
backend/src/main/java/com/facttrace/factcheck/ModelApiAiClient.java
backend/src/main/java/com/facttrace/factcheck/SourceTrace.java
backend/src/main/java/com/facttrace/factcheck/Verdict.java
backend/src/main/java/com/facttrace/FactCheckApplication.java
backend/src/main/java/com/facttrace/health/HealthController.java
backend/src/main/java/com/facttrace/health/HealthResponse.java
backend/src/main/java/com/facttrace/websearch/WebSearchClient.java
backend/src/main/java/com/facttrace/websearch/WebSearchClientConfiguration.java
backend/src/main/java/com/facttrace/websearch/WebSearchController.java
backend/src/main/java/com/facttrace/websearch/WebSearchException.java
backend/src/main/java/com/facttrace/websearch/WebSearchRequest.java
backend/src/main/java/com/facttrace/websearch/WebSearchResult.java
backend/src/main/resources/application.properties
backend/src/test/java/com/facttrace/evidence/EvidenceControllerTest.java
backend/src/test/java/com/facttrace/evidence/EvidenceServiceTest.java
backend/src/test/java/com/facttrace/evidence/SourceCredibilityTest.java
backend/src/test/java/com/facttrace/factcheck/FactCheckClientTest.java
backend/src/test/java/com/facttrace/factcheck/FactCheckControllerTest.java
backend/src/test/java/com/facttrace/factcheck/FactCheckServiceTest.java
backend/src/test/java/com/facttrace/factcheck/ModelApiAiClientTest.java
backend/src/test/java/com/facttrace/factcheck/RemoteAiConfigurationTest.java
backend/src/test/java/com/facttrace/health/HealthControllerTest.java
backend/src/test/java/com/facttrace/websearch/WebSearchClientTest.java
backend/src/test/java/com/facttrace/websearch/WebSearchControllerTest.java
extension/package.json
extension/public/manifest.json
extension/scripts/generate-icons.mjs
extension/sidepanel.html
extension/src/api/fact-check.test.ts
extension/src/api/fact-check.ts
extension/src/article/extract-article.test.ts
extension/src/article/extract-article.ts
extension/src/article/highlight-claims.ts
extension/src/background/service-worker.ts
extension/src/sidepanel/App.css
extension/src/sidepanel/App.tsx
extension/src/sidepanel/components/ArticleCheckForm.tsx
extension/src/sidepanel/components/ClaimResultCard.tsx
extension/src/sidepanel/components/EvidenceList.tsx
extension/src/sidepanel/components/ResultsPanel.tsx
extension/src/sidepanel/components/SidePanelHeader.tsx
extension/src/sidepanel/components/SingleClaimForm.tsx
extension/src/sidepanel/components/SourceTracePanel.tsx
extension/src/sidepanel/hooks/useFactChecks.ts
extension/src/sidepanel/main.tsx
extension/src/sidepanel/result-formatters.ts
extension/src/sidepanel/types.ts
extension/src/types/health.ts
extension/src/vite-env.d.ts
extension/tsconfig.app.json
extension/tsconfig.json
extension/tsconfig.node.json
extension/vite.config.ts
LICENSE
package.json
README.md
sh
```

### Dependencies

- ai-service/requirements.txt: accelerate@==1.8.1, fastapi@==0.116.1, pydantic@==2.11.7, sentencepiece@==0.2.0, torch@==2.7.1, transformers@==4.53.2, uvicorn[standard]@==0.35.0
- extension/package.json: @types/chrome@^0.1.38, @types/node@^24.10.1, @types/react@^19.2.14, @types/react-dom@^19.2.3, @vitejs/plugin-react@^6.0.1, react@^19.2.4, react-dom@^19.2.4, typescript@^5.9.3, vite@^8.0.0, vitest@^4.1.0

### Recent commits (newest first)

- Document usage of OpenAI Codex in development
- bug fix
- fact check improvement
- added more context handling
- evidence search bug fix x2
- evidence search fix
- bug fix
- readme update
- readme + docker for hosting
- readme update
- original source tracing
- frontend code refactor
- Strongsasha evidence relevance (#6)
- Add claim highlighting with popup, fix verbatim extraction via sentence indices
- nli + ollama integration
- Merge pull request #3 from alienqqe/strongsasha
- Merge branch 'master' into strongsasha
- Checkpoint/backend fact check (#2)
- Merge pull request #1 from alienqqe/extension-frontend
- Search, ranking , evidence

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

### package.json

```
{
  "name": "factcheck",
  "version": "0.1.0",
  "private": true,
  "description": "FactCheck is a Chrome extension, Spring Boot API, and model service for evidence-based fact-checking of news articles.",
  "scripts": {
    "dev": "npm --prefix extension run dev",
    "build": "npm --prefix extension run build",
    "test": "npm --prefix extension run test",
    "typecheck": "npm --prefix extension run typecheck",
    "backend:run": "sh -c 'set -a; if [ -f .env ]; then . ./.env; fi; set +a; cd backend && exec ./mvnw spring-boot:run'",
    "backend:test": "cd backend && ./mvnw test",
    "backend:package": "cd backend && ./mvnw clean package",
    "ai:run": "sh -c 'set -a; if [ -f .env ]; then . ./.env; fi; set +a; cd ai-service; if [ -x .venv/bin/uvicorn ]; then exec .venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 7860; else exec uvicorn app.main:app --host 0.0.0.0 --port 7860; fi'",
    "ai:test": "sh -c 'cd ai-service; export PYTHONPATH=.; if [ -x .venv/bin/pytest ]; then exec .venv/bin/pytest; else exec pytest; fi'"
  },
  "license": "MIT"
}

```

### ai-service/requirements.txt

```
accelerate==1.8.1
fastapi==0.116.1
pydantic==2.11.7
sentencepiece==0.2.0
torch==2.7.1
transformers==4.53.2
uvicorn[standard]==0.35.0

```

### ai-service/Dockerfile

```
FROM python:3.11-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    HF_HOME=/home/user/.cache/huggingface

RUN useradd --create-home --uid 1000 user
WORKDIR /home/user/app

COPY --chown=user:user requirements.txt ./
RUN pip install --no-cache-dir --upgrade pip \
    && pip install --no-cache-dir -r requirements.txt

COPY --chown=user:user app ./app
USER user

EXPOSE 7860
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]

```

### extension/package.json

```
{
  "name": "factcheck-extension",
  "private": true,
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "dev": "vite build --watch --mode development",
    "build": "tsc -b && vite build",
    "test": "vitest run",
    "typecheck": "tsc -b --pretty false"
  },
  "dependencies": {
    "react": "^19.2.4",
    "react-dom": "^19.2.4"
  },
  "devDependencies": {
    "@types/chrome": "^0.1.38",
    "@types/node": "^24.10.1",
    "@types/react": "^19.2.14",
    "@types/react-dom": "^19.2.3",
    "@vitejs/plugin-react": "^6.0.1",
    "typescript": "^5.9.3",
    "vite": "^8.0.0",
    "vitest": "^4.1.0"
  }
}

```

### backend/Dockerfile

```
FROM maven:3.9-eclipse-temurin-21-alpine AS build

WORKDIR /workspace

# Download dependencies in a separate layer so source-only changes rebuild faster.
COPY pom.xml ./
RUN mvn --batch-mode --no-transfer-progress dependency:go-offline

COPY src ./src
RUN mvn --batch-mode --no-transfer-progress -DskipTests package

FROM eclipse-temurin:21-jre-alpine AS runtime

RUN addgroup -S factcheck \
    && adduser -S factcheck -G factcheck

WORKDIR /app

COPY --from=build --chown=factcheck:factcheck \
    /workspace/target/factcheck-backend-*.jar \
    /app/app.jar

USER factcheck

EXPOSE 8080

ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-XX:+ExitOnOutOfMemoryError", "-jar", "/app/app.jar"]

```

### ai-service/app/main.py

```python
from __future__ import annotations

import logging

from fastapi import FastAPI, HTTPException

from .inference import (
    get_claim_extractor,
    get_evidence_comparator,
    get_settings,
)
from .parsing import InferenceError
from .schemas import (
    AnalyzeRequest,
    AnalyzeResponse,
    ClaimResult,
    ClaimExtractionRequest,
    ClaimExtractionResponse,
    ComparisonRequest,
    ComparisonResponse,
)
from .service import FactCheckPipeline, make_claim_result

logging.basicConfig(level=logging.INFO)
LOGGER = logging.getLogger(__name__)

app = FastAPI(
    title="FactCheck AI",
    version="0.1.0",
    description="Claim extraction and evidence comparison for FactCheck.",
)


def _model_unavailable(exception: Exception) -> HTTPException:
    LOGGER.exception("Model service failed", exc_info=exception)
    return HTTPException(
        status_code=503,
        detail="A model could not be loaded or executed. Check the service logs.",
    )


@app.get("/health")
def health() -> dict[str, str]:
    return {"status": "UP", "service": "factcheck-ai"}


@app.post("/v1/claims", response_model=ClaimExtractionResponse)
def extract_claims(request: ClaimExtractionRequest) -> ClaimExtractionResponse:
    settings = get_settings()
    try:
        claims = get_claim_extractor().extract(request.text, request.max_claims)
    except InferenceError as exception:
        raise HTTPException(status_code=502, detail=str(exception)) from exception
    except (OSError, RuntimeError) as exception:
        raise _model_unavailable(exception) from exception
    return ClaimExtractionResponse(claims=claims, model=settings.claim_model_id)


@app.post("/v1/compare", response_model=ComparisonResponse)
def compare_evidence(request: ComparisonRequest) -> ComparisonResponse:
    settings = get_settings()
    try:
        scores = get_evidence_comparator().compare(request.claim, request.evidence)
    except InferenceError as exception:
        raise HTTPException(status_code=502, detail=str(exception)) from exception
    except (OSError, RuntimeError) as exception:
        raise _model_unavailable(exception) from exception
    return ComparisonResponse(
        claim=request.claim,
        evidence=scores,
        model=settings.nli_model_id,
    )


@app.post("/v1/verify", response_model=ClaimResult)
def verify_claim(request: ComparisonRequest) -> ClaimResult:
    settings = get_settings()
    try:
        scores = get_evidence_comparator().compare(request.claim, request.evidence)
        return make_claim_result(request.claim, scores, settings)
    except InferenceError as exception:
        raise HTTPException(status_code=502, detail=str(exception)) from exception
    except (OSError, RuntimeError) as exception:
        raise _model_unavailable(exception) from exception


@app.post("/v1/analyze", response_model=AnalyzeResponse)
def analyze(request: AnalyzeRequest) -> AnalyzeResponse:
    settings = get_settings()
    try:
        pipeline = FactCheckPipeline(
            settings=settings,
            claim_extractor=get_claim_extractor(),
            evidence_comparator=(get_evidence_comparator() if request.evidence else None),
        )
        claims = pipeline.analyze(request.text, request.evidence, request.max_claims)
    except InferenceError as exception:
        raise HTTPException(status_code=502, detail=str(exception)) from exception
    except (OSError, RuntimeError) as exception:
        raise _model_unavailable(exception) from exception
    return AnalyzeResponse(claims=claims)

```

### extension/src/sidepanel/main.tsx

```typescript
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";

const sidePanelPort = chrome.runtime.connect({ name: "factcheck-side-panel" });
void sidePanelPort;

const rootElement = document.getElementById("root");

if (!rootElement) {
  throw new Error("Root element was not found");
}

createRoot(rootElement).render(
  <StrictMode>
    <App />
  </StrictMode>
);

```

### extension/src/sidepanel/App.tsx

```typescript
import "./App.css";
import { ArticleCheckForm } from "./components/ArticleCheckForm";
import { ResultsPanel } from "./components/ResultsPanel";
import { SidePanelHeader } from "./components/SidePanelHeader";
import { SingleClaimForm } from "./components/SingleClaimForm";
import { useFactChecks } from "./hooks/useFactChecks";

export default function App() {
  const {
    mode,
    switchMode,
    articleCheck,
    articleBusy,
    checkArticle,
    manualClaim,
    setManualClaim,
    claimCheck,
    claimBusy,
    checkManualClaim
  } = useFactChecks();

  return (
    <main className="app-shell">
      <SidePanelHeader mode={mode} onSwitchMode={switchMode} />

      {mode === "article" ? (
        <>
          <ArticleCheckForm busy={articleBusy} onCheck={checkArticle} />
          <ResultsPanel state={articleCheck} sourceLabel="Captured article" />
        </>
      ) : (
        <>
          <SingleClaimForm
            claim={manualClaim}
            busy={claimBusy}
            onClaimChange={setManualClaim}
            onCheck={checkManualClaim}
          />
          <ResultsPanel state={claimCheck} sourceLabel="Manual claim" />
        </>
      )}
    </main>
  );
}

```

### extension/sidepanel.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="color-scheme" content="light dark" />
    <title>FactCheck</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/sidepanel/main.tsx"></script>
  </body>
</html>

```

### extension/vite.config.ts

```typescript
import { resolve } from "node:path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  build: {
    emptyOutDir: true,
    rollupOptions: {
      input: {
        sidepanel: resolve(__dirname, "sidepanel.html"),
        "service-worker": resolve(__dirname, "src/background/service-worker.ts")
      },
      output: {
        entryFileNames: (chunkInfo) =>
          chunkInfo.name === "service-worker"
            ? "service-worker.js"
            : "assets/[name]-[hash].js",
        chunkFileNames: "assets/[name]-[hash].js",
        assetFileNames: "assets/[name]-[hash][extname]"
      }
    }
  }
});


```

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