From 5d742c17479e3d142d3f0a692609b7ebfef2296d Mon Sep 17 00:00:00 2001 From: azizu06 Date: Sun, 12 Jul 2026 15:08:44 -0400 Subject: [PATCH] Preserve CapCheck as a read-only portfolio demo --- .env.example | 6 ++++ README.md | 36 ++++++++++++++----- src/app/analyze/page.tsx | 10 ++++++ src/app/globals.css | 22 ++++++++++++ src/app/page.tsx | 9 ++++- src/components/portfolio-demo-notice.test.tsx | 19 ++++++++++ src/components/portfolio-demo-notice.tsx | 32 +++++++++++++++++ src/lib/portfolio-mode.test.ts | 20 +++++++++++ src/lib/portfolio-mode.ts | 11 ++++++ 9 files changed, 156 insertions(+), 9 deletions(-) create mode 100644 src/components/portfolio-demo-notice.test.tsx create mode 100644 src/components/portfolio-demo-notice.tsx create mode 100644 src/lib/portfolio-mode.test.ts create mode 100644 src/lib/portfolio-mode.ts diff --git a/.env.example b/.env.example index 84ec646..9f8f095 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,12 @@ CAPCHECK_ANALYSIS_MODE=fixture +CAPCHECK_FEED_MODE=fixture # Live mode is selected when CAPCHECK_ANALYSIS_MODE is not "fixture". # GEMINI_API_KEY= # FINNHUB_KEY= # Server-only key for Verified Feed refresh (YouTube Data API v3 discovery). # YOUTUBE_API_KEY= +# Supabase-backed feed configuration. The anon key is public and remains +# protected by Row Level Security; never expose the server-only secret key. +# NEXT_PUBLIC_SUPABASE_URL= +# NEXT_PUBLIC_SUPABASE_ANON_KEY= +# SUPABASE_SERVICE_ROLE_KEY= diff --git a/README.md b/README.md index a16c1e5..2d6d0ec 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,25 @@ scorecard. A user can paste a video URL or upload a video, follow the analysis stages, and inspect the evidence behind each supported, contradicted, or unverifiable claim. -This repository contains the Bloomberg Hackathon 2026 application. Local and -browser tests use deterministic fixtures, while live mode streams the real -analysis pipelines through the same runtime-validated contracts. +Built for BloomKnights 2026, CapCheck combines Gemini video understanding, +Google Search grounding, Finnhub market data, and a Supabase-backed Verified +Feed. + +> **Project status:** Complete and preserved as a read-only portfolio showcase. +> Browse the deployed app at [capcheck-sigma.vercel.app](https://capcheck-sigma.vercel.app/). +> The persisted feed and evidence pages remain live; paid/private analysis and +> refresh integrations were retired after the hackathon. + +## What shipped + +- URL and upload intake with streamed analysis progress. +- Gemini claim extraction, grounded verification, and deterministic Cap Scores. +- Finnhub function calling for quantitative market claims. +- A searchable, category-filtered catalog of CapCheck-vetted YouTube videos. +- Supabase persistence, idempotent refresh runs, reliability gating, and safe + failure behavior. +- Responsive, accessible feed, detail, and scorecard experiences backed by + deterministic unit and Playwright coverage. ## Local setup @@ -71,11 +87,10 @@ The shared seam is the Zod-validated `AnalysisEvent` contract in completion, and error events that a live adapter must produce. The UI consumes only the SSE response from `/api/analyze`; it does not import either adapter. -Lane A can introduce the live yt-dlp, Gemini, and evidence-grounding adapter by -selecting it on the server while preserving the route and `AnalysisEvent` -stream. Lane B can continue working against fixtures. Contract changes require -a coordinated update to schemas, fixtures, adapter tests, parser tests, and UI -tests before either lane depends on them. +The production implementation selects the live yt-dlp, Gemini, Search +grounding, and Finnhub adapters while preserving the same `AnalysisEvent` +stream used by deterministic fixtures. Contract changes require coordinated +updates to schemas, fixtures, adapter tests, parser tests, and UI tests. The live claim-extraction entry point is `createNodeClaimExtractionPipeline` in @@ -163,6 +178,11 @@ duplicate) with no credentials. Live mode additionally requires a server-only `YOUTUBE_API_KEY` (plus `GEMINI_API_KEY`, `FINNHUB_KEY`, and `SUPABASE_SERVICE_ROLE_KEY`). +The hosted portfolio deployment intentionally omits those private credentials. +It reads the persisted catalog through the public Supabase anon key under RLS, +while live refresh and analysis render an archived-demo state instead of +calling external providers. + Opt-in live smoke test (never part of CI): ```bash diff --git a/src/app/analyze/page.tsx b/src/app/analyze/page.tsx index e565754..2712b35 100644 --- a/src/app/analyze/page.tsx +++ b/src/app/analyze/page.tsx @@ -1,6 +1,8 @@ import type { Metadata } from "next"; import { CapCheckApp } from "@/components/capcheck-app"; +import { PortfolioDemoNotice } from "@/components/portfolio-demo-notice"; +import { isPortfolioDemoMode } from "@/lib/portfolio-mode"; export const metadata: Metadata = { title: "Analyze — CapCheck", @@ -8,5 +10,13 @@ export const metadata: Metadata = { }; export default function AnalyzePage() { + if (isPortfolioDemoMode()) { + return ( +
+ +
+ ); + } + return ; } diff --git a/src/app/globals.css b/src/app/globals.css index 34dfd4b..fe6d938 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1453,6 +1453,28 @@ input:disabled { font-size: 0.92rem; } +.portfolio-demo-notice { + margin-top: 24px; +} + +.portfolio-demo-notice > svg { + color: var(--ink); +} + +.portfolio-demo-notice .kicker { + margin-bottom: 6px; +} + +.portfolio-demo-notice h2 { + margin: 0 0 6px; + color: var(--ink); + font: 600 clamp(1.2rem, 2.5vw, 1.6rem)/1.2 var(--display); +} + +.portfolio-demo-page { + max-width: 760px; +} + .feed-state-action { display: inline-flex; align-items: center; diff --git a/src/app/page.tsx b/src/app/page.tsx index 7994825..e5e4da0 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,8 +2,10 @@ import { TriangleAlert } from "lucide-react"; import Link from "next/link"; import { FeedExplorer } from "@/components/feed/feed-explorer"; +import { PortfolioDemoNotice } from "@/components/portfolio-demo-notice"; import { RefreshFeedButton } from "@/components/refresh-feed-button"; import type { CatalogItem } from "@/domain/feed"; +import { isPortfolioDemoMode } from "@/lib/portfolio-mode"; import { getCatalogRepository } from "@/server/feed/catalog-repository"; export const dynamic = "force-dynamic"; @@ -15,6 +17,7 @@ export default async function FeedHome({ }) { let items: CatalogItem[] = []; let failed = false; + const portfolioDemo = isPortfolioDemoMode(); const { feedState } = await searchParams; const fixtureState = process.env.NODE_ENV !== "production" && @@ -55,7 +58,11 @@ export default async function FeedHome({ only if its claims held up. Open one to see the Cap Score, the evidence, and the citations behind it.

- + {portfolioDemo ? ( + + ) : ( + + )} {failed ? ( diff --git a/src/components/portfolio-demo-notice.test.tsx b/src/components/portfolio-demo-notice.test.tsx new file mode 100644 index 0000000..74d7493 --- /dev/null +++ b/src/components/portfolio-demo-notice.test.tsx @@ -0,0 +1,19 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { PortfolioDemoNotice } from "./portfolio-demo-notice"; + +describe("PortfolioDemoNotice", () => { + it("keeps the archived project useful without offering retired live actions", () => { + render(); + + expect( + screen.getByRole("heading", { name: "Live analysis is retired" }), + ).toBeVisible(); + expect(screen.getByRole("link", { name: "Browse the verified feed" })).toHaveAttribute( + "href", + "/", + ); + expect(screen.queryByRole("button")).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/portfolio-demo-notice.tsx b/src/components/portfolio-demo-notice.tsx new file mode 100644 index 0000000..d012baf --- /dev/null +++ b/src/components/portfolio-demo-notice.tsx @@ -0,0 +1,32 @@ +import { Archive } from "lucide-react"; +import Link from "next/link"; + +export function PortfolioDemoNotice({ + feature, +}: { + feature: "analyzer" | "refresh"; +}) { + const analyzer = feature === "analyzer"; + + return ( +
+
+ ); +} diff --git a/src/lib/portfolio-mode.test.ts b/src/lib/portfolio-mode.test.ts new file mode 100644 index 0000000..796d5e9 --- /dev/null +++ b/src/lib/portfolio-mode.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; + +import { isPortfolioDemoMode } from "./portfolio-mode"; + +describe("isPortfolioDemoMode", () => { + it("turns production into a read-only showcase when live credentials are retired", () => { + expect(isPortfolioDemoMode({ NODE_ENV: "production" })).toBe(true); + expect( + isPortfolioDemoMode({ + NODE_ENV: "production", + GEMINI_API_KEY: "configured", + FINNHUB_KEY: "configured", + }), + ).toBe(false); + }); + + it("keeps local fixture development interactive", () => { + expect(isPortfolioDemoMode({ NODE_ENV: "development" })).toBe(false); + }); +}); diff --git a/src/lib/portfolio-mode.ts b/src/lib/portfolio-mode.ts new file mode 100644 index 0000000..299a957 --- /dev/null +++ b/src/lib/portfolio-mode.ts @@ -0,0 +1,11 @@ +type RuntimeEnvironment = { + NODE_ENV?: string; + GEMINI_API_KEY?: string; + FINNHUB_KEY?: string; +}; + +export const isPortfolioDemoMode = ( + environment: RuntimeEnvironment = process.env, +): boolean => + environment.NODE_ENV === "production" && + (!environment.GEMINI_API_KEY || !environment.FINNHUB_KEY);