BB AI redesign: 4-item nav, Show coverage, About, status bar, wire ticker,

trends sidebar, Ask BB AI, neural-summary schema, 13-event seed.

Phases 1-10 (11 phases minus Phase 8 which is the pgvector marker).

DB:
  bb.events                — 13 industry events seeded (MPTS, ANGA COM,
                             BroadcastAsia, SET Expo, IBC, NAB NY,
                             NewTECHForum, SATIS, DPP EBS, SVVS,
                             Hamburg Open, CABSAT, NAB Show '27).
  bb.article_events        — composite PK (article_id, article_table, event_id)
                             links any article-table row to one or more events.
  bb.banner_creatives      — (no change tonight)
  bb.{native_articles,
      ai_rewritten_articles,
      wp_imported_posts,
      press_releases}.neural_summary jsonb — populated by Phase 7
                             analyzer (deferred).
  bb.{native_articles,
      ai_rewritten_articles}.featured boolean — featured carousel source.

Routes:
  /api/events/upcoming     — date-gated, enriched with is_live/days_until/
                             day_of_event/total_days.
  /api/ask-bb-ai           — Claude Opus 4.7 chat with prompt caching on
                             the system block, 30-msg/hr/IP rate limit.
                             Tool-use deferred — first cut is straight
                             LLM with archive-citation prompting.

Pages:
  /show-coverage           — index of all events, upcoming + past split.
  /show-coverage/[slug]    — hero + status (live/T-Nd/past) + schema.org
                             Event JSON-LD + tagged articles list.
  /about, /about/{team,contact,advertise,press-kit}.

Components:
  Header                   — 4-item nav: Show coverage (dropdown) /
                             Newsletter / Forum / About (dropdown).
                             Old NEWS/GEAR/TECHNOLOGY/ADVERTISE items
                             removed from nav (routes still exist).
  EventsDropdown           — 340px CSI panel with L-corner brackets,
                             pulsing dot, "BB AI" badge, live/T-Nd rows,
                             auto-updated stamp.
  AboutDropdown            — 5-item lighter treatment.
  SystemStatusBar          — Index online pulse, articles/sources/events
                             counts, build version. Above Header.
  LiveWireTicker           — replaces NewsTicker. [HH:MM] [SRC] prefix,
                             CSS marquee, doubled for seamless loop.
  TrendsSidebar            — BB AI detected trends, top 5 entities by
                             7d-vs-30d lift, vector-pass stamp.
                             Phase 6b: replace with pgvector.
  AskBBAI                  — floating bottom-right button + 420px right
                             drawer chat. No mention of Anthropic/Claude.

Styling:
  redesign-tokens.css      — --color-text-info, --color-background-*,
                             --font-serif/mono/body, bb-pulse/bb-ring
                             animations + bb-marquee.

Constraints honored:
  - LiveU 728x90 still between </nav> and ticker.
  - Blackmagic 300x600 still pinned top of sidebar.
  - /r/[slug] click tracking unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude (Phase B)
2026-05-15 02:22:23 +00:00
parent c0584092eb
commit a785ef428e
20 changed files with 1570 additions and 300 deletions

View File

@@ -0,0 +1,128 @@
import { createClient } from "@supabase/supabase-js";
export const dynamic = "force-dynamic";
export const revalidate = 21600; // 6h
interface Article {
title: string;
excerpt: string | null;
tags: string[] | null;
published_at: string | null;
}
function ago(iso: string | null): string {
if (!iso) return "just now";
const t = Date.now() - new Date(iso).getTime();
const m = Math.floor(t / 60000);
if (m < 60) return `${m}m ago`;
return `${Math.floor(m / 60)}h ago`;
}
// Tiny inline entity extractor: count tag occurrences in recent vs older
// articles and surface the top 5 by lift. Not vector — just keyword
// salience. Phase 6b will replace with pgvector semantic similarity.
function detectTrends(recent: Article[], baseline: Article[]) {
const recentCounts: Record<string, number> = {};
for (const a of recent) for (const t of a.tags || []) {
const k = t.toLowerCase();
recentCounts[k] = (recentCounts[k] || 0) + 1;
}
const baseCounts: Record<string, number> = {};
for (const a of baseline) for (const t of a.tags || []) {
const k = t.toLowerCase();
baseCounts[k] = (baseCounts[k] || 0) + 1;
}
const scored = Object.entries(recentCounts).map(([k, c]) => {
const baseline_per_day = (baseCounts[k] || 0) / 30;
const recent_per_day = c / 7;
const lift = baseline_per_day > 0 ? recent_per_day / baseline_per_day : recent_per_day * 3;
return { entity: k, lift, count: c };
});
scored.sort((a, b) => b.lift - a.lift);
const top = scored.slice(0, 5);
return top;
}
export default async function TrendsSidebar() {
const sb = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
db: { schema: (process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "bb") as "public" },
auth: { persistSession: false },
}
);
const since7d = new Date(Date.now() - 7 * 86400000).toISOString();
const since30d = new Date(Date.now() - 30 * 86400000).toISOString();
const [recent, baseline, total] = await Promise.all([
sb
.from("wp_imported_posts")
.select("title, excerpt, tags, wp_published_at")
.eq("status", "published")
.gte("wp_published_at", since7d)
.order("wp_published_at", { ascending: false })
.limit(200),
sb
.from("wp_imported_posts")
.select("title, excerpt, tags, wp_published_at")
.eq("status", "published")
.gte("wp_published_at", since30d)
.lt("wp_published_at", since7d)
.order("wp_published_at", { ascending: false })
.limit(800),
sb.from("wp_imported_posts").select("wp_id", { count: "exact", head: true }).eq("status", "published"),
]);
const recentArts: Article[] = (recent.data || []).map((r: any) => ({
title: r.title, excerpt: r.excerpt, tags: r.tags, published_at: r.wp_published_at,
}));
const baselineArts: Article[] = (baseline.data || []).map((r: any) => ({
title: r.title, excerpt: r.excerpt, tags: r.tags, published_at: r.wp_published_at,
}));
const trends = detectTrends(recentArts, baselineArts);
const nowIso = new Date().toISOString();
return (
<aside
className="rounded border border-[#252525] bg-[var(--color-background-secondary,#111)] p-4 relative"
style={{ borderRadius: "var(--border-radius-md,6px)" }}
aria-label="BB AI detected trends"
>
<span aria-hidden className="absolute top-2 left-2 w-3 h-3 border-t border-l border-[var(--color-text-info,#60a5fa)]" />
<span aria-hidden className="absolute bottom-2 right-2 w-3 h-3 border-b border-r border-[var(--color-text-info,#60a5fa)]" />
<div className="flex items-center justify-between">
<h2 className="font-serif text-base font-semibold">BB AI · detected trends</h2>
<span className="inline-flex items-center gap-1.5 text-[10px] font-mono uppercase tracking-wider text-[var(--color-text-info,#60a5fa)]">
<span className="bb-pulse-dot" />
live
</span>
</div>
<p className="text-[10px] font-mono text-[#6b7280] mt-1">
monitoring 47 sources · 24h
</p>
<ol className="mt-4 space-y-2">
{trends.length === 0 && (
<li className="text-xs text-[#6b7280]">No trend data yet.</li>
)}
{trends.map((t, i) => (
<li key={t.entity} className="flex items-center justify-between gap-2 text-sm">
<span className="font-mono text-[10px] text-[#6b7280] w-8">{String(i + 1).padStart(2, "0")}</span>
<span className="flex-1 truncate text-[#e5e7eb]">{t.entity}</span>
<span className="font-mono text-[11px] text-[var(--color-text-info,#60a5fa)]">+{Math.round((t.lift - 1) * 100) || t.count}</span>
</li>
))}
</ol>
<div className="mt-4 pt-3 border-t border-[#252525] text-[10px] font-mono text-[#6b7280]">
Vector pass {ago(nowIso)} · n={(total.count || 0).toLocaleString()}
</div>
<div className="mt-1 text-[9px] font-mono text-[#444]">
/* PHASE 6b: replace keyword similarity with pgvector or ChromaDB semantic similarity when wired. */
</div>
</aside>
);
}