From 501ffac09743ecb91a07bf2de80d30a6a5240c21 Mon Sep 17 00:00:00 2001 From: hshum Date: Wed, 13 May 2026 23:54:07 -0700 Subject: [PATCH] Redesign reports command center --- src/features/redesign/RedesignShell.tsx | 6 + src/features/redesign/primitives.css | 496 +++++++++++++++++- .../surfaces/HomeV2PrototypeSurface.tsx | 79 ++- .../redesign/surfaces/ReportsSurface.tsx | 459 +++++++++++++--- tests/e2e/home-v2-parity.spec.ts | 32 +- 5 files changed, 972 insertions(+), 100 deletions(-) diff --git a/src/features/redesign/RedesignShell.tsx b/src/features/redesign/RedesignShell.tsx index cde77733..cc4939e3 100644 --- a/src/features/redesign/RedesignShell.tsx +++ b/src/features/redesign/RedesignShell.tsx @@ -161,6 +161,11 @@ export default function RedesignShell() { }; }, []); + useEffect(() => { + const centerPane = document.querySelector("[data-redesign] .rd-shell__main > .rd-pane:first-child"); + centerPane?.scrollTo({ top: 0, left: 0 }); + }, [location.pathname, location.search]); + // Phase 7d (2026-05-08): editorial is the default at /redesign on // both mobile + desktop. Legacy is opt-out via `?classic=1` // (canonical) or `?edition=0` (back-compat). Per @@ -323,6 +328,7 @@ export default function RedesignShell() { if (tab === "brief") navigate(`/redesign/reports/${id}`); else navigate(`/redesign/workspace?report=${id}&tab=${tab}`); }} + onRunBatch={sendPromptToChat} inspectedReportId={selectedReport?.id ?? null} onSelectReport={selectLiveReport} /> diff --git a/src/features/redesign/primitives.css b/src/features/redesign/primitives.css index 0683f6c5..22636c56 100644 --- a/src/features/redesign/primitives.css +++ b/src/features/redesign/primitives.css @@ -2419,6 +2419,10 @@ max-width: 820px; } +[data-redesign] .rd-v2-proto-center.rd-v2-reports-command { + max-width: min(1180px, 100%); +} + [data-redesign] .rd-v2-chat-center { width: 100%; max-width: 820px; @@ -2469,12 +2473,146 @@ min-width: 8px; } +[data-redesign] .rd-v2-command-hero { + display: grid; + gap: 18px; + margin-bottom: 14px; +} + +[data-redesign] .rd-v2-command-title-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 18px; + align-items: end; +} + +[data-redesign] .rd-v2-command-title-row h1 { + margin: 0; + color: var(--rd-ink-strong); + font-size: clamp(38px, 5vw, 68px); + line-height: 0.94; + letter-spacing: -0.02em; +} + +[data-redesign] .rd-v2-command-title-row p { + max-width: 620px; + margin: 12px 0 0; + color: var(--rd-ink-soft); + font-size: 16px; + line-height: 1.45; +} + +[data-redesign] .rd-v2-command-actions, +[data-redesign] .rd-v2-view-row, +[data-redesign] .rd-v2-view-switcher, +[data-redesign] .rd-v2-density-switcher { + display: flex; + align-items: center; + gap: 8px; +} + +[data-redesign] .rd-v2-command-actions button, +[data-redesign] .rd-v2-view-switcher button, +[data-redesign] .rd-v2-density-switcher button { + padding: 7px 11px; + border: 1px solid var(--rd-line); + border-radius: var(--rd-r-pill); + background: var(--rd-panel); + color: var(--rd-ink); + cursor: pointer; + font: inherit; + font-size: 11px; + font-weight: 700; +} + +[data-redesign] .rd-v2-command-actions .rd-v2-btn-primary { + border-color: var(--rd-ink-strong); + background: var(--rd-ink-strong); + color: var(--rd-paper); +} + +[data-redesign] .rd-v2-command-metrics { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + border: 1px solid var(--rd-line-soft); + border-radius: var(--rd-r-md); + background: var(--rd-panel); + overflow: hidden; +} + +[data-redesign] .rd-v2-command-metrics span { + display: grid; + gap: 2px; + padding: 12px 14px; + border-right: 1px solid var(--rd-line-soft); +} + +[data-redesign] .rd-v2-command-metrics span:last-child { + border-right: 0; +} + +[data-redesign] .rd-v2-command-metrics strong { + color: var(--rd-accent-strong); + font-size: 21px; + line-height: 1; +} + +[data-redesign] .rd-v2-command-metrics small, +[data-redesign] .rd-v2-mini-eyebrow { + color: var(--rd-ink-faint); + font-size: 10px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +[data-redesign] .rd-v2-view-row { + justify-content: space-between; + margin: 16px 0 8px; +} + +[data-redesign] .rd-v2-view-switcher button.is-active, +[data-redesign] .rd-v2-density-switcher button.is-active { + border-color: var(--rd-accent); + background: var(--rd-accent-soft); + color: var(--rd-accent-strong); +} + +[data-redesign] .rd-v2-command-toolbar { + margin-bottom: 10px; +} + +[data-redesign] .rd-v2-search-row { + justify-content: stretch; +} + +[data-redesign] .rd-v2-search-row input { + width: 100%; + border: 1px solid var(--rd-line); + border-radius: var(--rd-r-pill); + padding: 10px 14px; + background: var(--rd-panel); + color: var(--rd-ink); +} + [data-redesign] .rd-v2-report-grid { display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 14px; } +[data-redesign] .rd-v2-report-grid[data-density="compact"] { + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); +} + +[data-redesign] .rd-v2-report-grid[data-density="grid"] { + grid-template-columns: repeat(auto-fit, minmax(265px, 1fr)); +} + +[data-redesign] .rd-v2-report-grid[data-density="list"] { + grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); +} + [data-redesign] .rd-v2-report-card { min-height: 214px; padding: 16px; @@ -2609,6 +2747,362 @@ font-size: 10px; } +[data-redesign] .rd-v2-report-card-v2 { + display: flex; + flex-direction: column; + gap: 10px; + min-height: 360px; +} + +[data-redesign] .rd-v2-report-card-v2 .rd-v2-report-thesis { + display: -webkit-box; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; +} + +[data-redesign] .rd-v2-signal-list { + display: grid; + gap: 6px; + max-height: 104px; + overflow: hidden; + padding-top: 2px; +} + +[data-redesign] .rd-v2-signal-list span { + display: -webkit-box; + overflow: hidden; + color: var(--rd-ink); + font-size: 11.5px; + line-height: 1.4; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +[data-redesign] .rd-v2-signal-list span::before { + content: ""; + display: inline-block; + width: 4px; + height: 4px; + margin-right: 7px; + border-radius: 999px; + background: var(--rd-accent); + vertical-align: 2px; +} + +[data-redesign] .rd-v2-score-strip { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 6px; + margin-top: auto; +} + +[data-redesign] .rd-v2-score-strip span { + display: grid; + gap: 2px; + padding: 8px; + border: 1px solid var(--rd-line-soft); + border-radius: var(--rd-r-sm); + background: var(--rd-paper); +} + +[data-redesign] .rd-v2-score-strip b { + color: var(--rd-ink-strong); + font-size: 16px; + line-height: 1; +} + +[data-redesign] .rd-v2-score-strip small { + color: var(--rd-ink-faint); + font-size: 9px; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +[data-redesign] .rd-v2-next-actions { + display: flex; + flex-wrap: wrap; + gap: 7px; + margin-top: 8px; +} + +[data-redesign] .rd-v2-next-actions button, +[data-redesign] .rd-v2-report-table button { + padding: 6px 10px; + border: 1px solid var(--rd-line); + border-radius: var(--rd-r-pill); + background: var(--rd-panel); + color: var(--rd-ink); + cursor: pointer; + font: inherit; + font-size: 10.5px; + font-weight: 700; +} + +[data-redesign] .rd-v2-next-actions .rd-v2-card-primary, +[data-redesign] .rd-v2-report-table button { + border-color: var(--rd-ink-strong); + background: var(--rd-ink-strong); + color: var(--rd-paper); +} + +[data-redesign] .rd-v2-report-board { + display: grid; + grid-template-columns: repeat(6, minmax(160px, 1fr)); + gap: 10px; + overflow-x: auto; + padding-bottom: 8px; +} + +[data-redesign] .rd-v2-board-column { + display: grid; + align-content: start; + gap: 8px; + min-height: 360px; + padding: 10px; + border: 1px solid var(--rd-line-soft); + border-radius: var(--rd-r-md); + background: var(--rd-muted); +} + +[data-redesign] .rd-v2-board-column header { + display: flex; + justify-content: space-between; + color: var(--rd-ink-strong); + font-size: 12px; +} + +[data-redesign] .rd-v2-board-column header span { + color: var(--rd-ink-faint); +} + +[data-redesign] .rd-v2-board-card { + display: grid; + gap: 4px; + width: 100%; + padding: 10px; + border: 1px solid var(--rd-line-soft); + border-radius: var(--rd-r-sm); + background: var(--rd-panel); + color: var(--rd-ink); + cursor: grab; + text-align: left; +} + +[data-redesign] .rd-v2-board-card strong, +[data-redesign] .rd-v2-board-card span, +[data-redesign] .rd-v2-board-card small { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +[data-redesign] .rd-v2-board-card strong { + color: var(--rd-ink-strong); + font-size: 12px; +} + +[data-redesign] .rd-v2-board-card span, +[data-redesign] .rd-v2-board-card small { + color: var(--rd-ink-soft); + font-size: 10.5px; +} + +[data-redesign] .rd-v2-report-table-wrap { + overflow-x: auto; + border: 1px solid var(--rd-line-soft); + border-radius: var(--rd-r-md); + background: var(--rd-panel); +} + +[data-redesign] .rd-v2-report-table { + width: 100%; + min-width: 900px; + border-collapse: collapse; + font-size: 12px; +} + +[data-redesign] .rd-v2-report-table th, +[data-redesign] .rd-v2-report-table td { + padding: 11px 12px; + border-bottom: 1px solid var(--rd-line-soft); + text-align: left; + vertical-align: top; +} + +[data-redesign] .rd-v2-report-table th { + color: var(--rd-ink-faint); + font-size: 10px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +[data-redesign] .rd-v2-report-table td strong, +[data-redesign] .rd-v2-report-table td span { + display: block; +} + +[data-redesign] .rd-v2-report-table td span { + max-width: 280px; + overflow: hidden; + color: var(--rd-ink-soft); + text-overflow: ellipsis; + white-space: nowrap; +} + +[data-redesign] .rd-v2-notebook-preview { + display: grid; + grid-template-columns: 190px minmax(0, 1fr); + min-height: 520px; + border: 1px solid var(--rd-line-soft); + border-radius: var(--rd-r-md); + background: var(--rd-panel); + overflow: hidden; +} + +[data-redesign] .rd-v2-notebook-preview aside { + display: grid; + align-content: start; + gap: 6px; + padding: 16px; + border-right: 1px solid var(--rd-line-soft); + background: var(--rd-muted); +} + +[data-redesign] .rd-v2-notebook-preview aside button { + padding: 7px 0; + border: 0; + background: transparent; + color: var(--rd-ink); + cursor: pointer; + font: inherit; + font-size: 12px; + text-align: left; +} + +[data-redesign] .rd-v2-notebook-preview article { + padding: 24px; +} + +[data-redesign] .rd-v2-notebook-preview h2 { + margin: 6px 0 8px; + color: var(--rd-ink-strong); + font-size: 34px; + line-height: 1; +} + +[data-redesign] .rd-v2-notebook-preview h3 { + margin: 22px 0 6px; + color: var(--rd-ink-strong); + font-size: 15px; +} + +[data-redesign] .rd-v2-notebook-preview p { + color: var(--rd-ink); + font-size: 14px; + line-height: 1.55; +} + +[data-redesign] .rd-v2-notebook-stats { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin: 12px 0; +} + +[data-redesign] .rd-v2-notebook-stats span { + padding: 4px 8px; + border-radius: var(--rd-r-pill); + background: var(--rd-accent-soft); + color: var(--rd-accent-strong); + font-size: 10px; + font-weight: 700; +} + +[data-redesign] .rd-v2-report-canvas { + position: relative; + display: grid; + min-height: 540px; + border: 1px solid var(--rd-line-soft); + border-radius: var(--rd-r-md); + background: + linear-gradient(var(--rd-line-soft) 1px, transparent 1px), + linear-gradient(90deg, var(--rd-line-soft) 1px, transparent 1px), + var(--rd-panel); + background-size: 36px 36px; + overflow: hidden; +} + +[data-redesign] .rd-v2-canvas-node { + position: absolute; + display: grid; + gap: 3px; + width: 150px; + padding: 11px; + border: 1px solid var(--rd-line); + border-radius: var(--rd-r-sm); + background: var(--rd-paper); + color: var(--rd-ink); + cursor: pointer; + text-align: left; + box-shadow: var(--rd-shadow-xs); +} + +[data-redesign] .rd-v2-canvas-node strong { + overflow: hidden; + color: var(--rd-ink-strong); + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +} + +[data-redesign] .rd-v2-canvas-node span { + color: var(--rd-ink-soft); + font-size: 10px; +} + +[data-redesign] .rd-v2-canvas-node--0 { left: 7%; top: 12%; } +[data-redesign] .rd-v2-canvas-node--1 { left: 35%; top: 20%; } +[data-redesign] .rd-v2-canvas-node--2 { left: 62%; top: 12%; } +[data-redesign] .rd-v2-canvas-node--3 { left: 24%; top: 56%; } + +[data-redesign] .rd-v2-report-canvas p { + align-self: end; + max-width: 420px; + margin: 0 20px 20px; + padding: 12px 14px; + border: 1px solid var(--rd-line-soft); + border-radius: var(--rd-r-sm); + background: var(--rd-paper); + color: var(--rd-ink-soft); + font-size: 12px; + line-height: 1.5; +} + +@media (max-width: 820px) { + [data-redesign] .rd-v2-command-title-row, + [data-redesign] .rd-v2-notebook-preview { + grid-template-columns: 1fr; + } + + [data-redesign] .rd-v2-command-actions, + [data-redesign] .rd-v2-view-row { + align-items: stretch; + flex-direction: column; + } + + [data-redesign] .rd-v2-command-metrics { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + [data-redesign] .rd-v2-notebook-preview aside { + border-right: 0; + border-bottom: 1px solid var(--rd-line-soft); + } +} + [data-redesign] .rd-v2-saved-banner { display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto auto; diff --git a/src/features/redesign/surfaces/HomeV2PrototypeSurface.tsx b/src/features/redesign/surfaces/HomeV2PrototypeSurface.tsx index 19b8bbe8..be8ad636 100644 --- a/src/features/redesign/surfaces/HomeV2PrototypeSurface.tsx +++ b/src/features/redesign/surfaces/HomeV2PrototypeSurface.tsx @@ -1058,10 +1058,38 @@ function ChatThreadGroup({ label, items }: { label: string; items: Array<[string function ReportsPrototypeCenter({ selectedEntity = "Anthropic", onSelectEntity }: PrototypeSelectionProps) { return ( -
-
- Entity intelligence - 12 entities · 83% verified · avg score 87 · 48 sources +
+
+
+ Coverage Command Center + 12 reports · 83% verified · 3 need review · 48 source rows +
+
+
+

Reports

+

Agent-generated notebooks organized by entity, evidence quality, lifecycle state, and the next review action.

+
+
+ + +
+
+
+ 12reports + 4active runs + 3need review + 5export ready +
+
+
+
+ {["Gallery", "Board", "Table", "Notebook", "Canvas"].map((item, index) => ( + + ))} +
+
+ {["Compact", "Standard", "Expanded"].map((item, index) => )} +
{["All", "Watching", "Needs review", "Stale", "Updated"].map((item, index) => ( @@ -1072,13 +1100,13 @@ function ReportsPrototypeCenter({ selectedEntity = "Anthropic", onSelectEntity }
- +
-
+
{reportEntities.map((entity) => (
onSelectEntity?.(entity.name)} aria-selected={selectedEntity === entity.name} > @@ -1094,15 +1122,20 @@ function ReportsPrototypeCenter({ selectedEntity = "Anthropic", onSelectEntity }

Referenced in: {entity.ref}

{entity.meta}

+
+ + + +
))}
-

Add entity

-

Capture a company, person, topic, or source cluster and let the coverage agent hydrate the first report.

- +

Run a batch

+

Import companies, people, topics, or source clusters and let the coverage agent hydrate report notebooks.

+
- +
); } @@ -1262,7 +1295,7 @@ function AgentShell({ const pillClassName = (_pill: string, index: number) => { const classes = ["rd-v2-agent-pill"]; if (index === 0) classes.push("is-active"); - if (title === "Coverage agent") { + if (title === "Coverage agent" || title === "Agent Inspector") { if (index === 1) classes.push("is-entity"); if (index === 2) classes.push("is-source"); } @@ -1302,16 +1335,17 @@ function AgentShell({ function ReportsPrototypeRail({ onAsk, selectedEntity = "Anthropic", selectedReport, onSelectEntity }: PrototypeSelectionProps) { const entity = selectedReport ? liveReportToPrototypeEntity(selectedReport) : getPrototypeEntity(selectedEntity); - const reviewText = entity.score < 75 ? `${entity.name} needs review.` : `${entity.name} is in good shape.`; + const reviewText = entity.score < 75 ? `${entity.name} needs report work.` : `${entity.name} is ready for the current coverage task.`; const summaryText = entity.score < 75 - ? `${entity.name} is below coverage threshold. Refresh sources, verify open claims, and decide whether the report needs a notebook patch.` - : `${entity.name} has current sources and enough verified claims for the present coverage task. Keep it on watch.`; + ? `${entity.name} is below coverage threshold. Refresh sources, verify open claims, and decide whether the notebook needs a patch.` + : `${entity.name} has enough current sources and verified claims. Keep monitoring, but the notebook can be opened as the durable artifact.`; + const warningCount = entity.score < 75 ? 3 : 1; return ( {reviewText}

{summaryText}

-
3 steps completedentity_healthfreshness_scanclaim_verify
+
Agent run tracesources_gatheredclaims_extractednotebook_checked
- + +
+
Pipeline progress{warningCount} warnings
+

Done: gathered sources, extracted claims, matched report target.

+

Next: verify weak claims, patch notebook, then approve export.

+
Active entity{entity.score}
{entity.score} {entity.delta}
diff --git a/src/features/redesign/surfaces/ReportsSurface.tsx b/src/features/redesign/surfaces/ReportsSurface.tsx index 575adeb5..1fdf391f 100644 --- a/src/features/redesign/surfaces/ReportsSurface.tsx +++ b/src/features/redesign/surfaces/ReportsSurface.tsx @@ -5,7 +5,7 @@ * Default density: compact. Sticky filter row. */ -import { useMemo, useState, useEffect, useRef } from "react"; +import { useMemo, useState, useEffect, useRef, type DragEvent } from "react"; import { memoStyles, type ReportCardData, type Density, type Universe } from "../fixtures"; import { Pill } from "../components/Pill"; import { useReportsLive } from "../hooks/useReportsLive"; @@ -50,10 +50,37 @@ function displayReportDescription(description: string): string { interface ReportsSurfaceProps { onOpen: (id: string, tab: "brief" | "cards" | "chat") => void; + onRunBatch?: (prompt: string) => void; onSelectReport?: (report: ReportCardData) => void; inspectedReportId?: string | null; } +type ReportViewMode = "gallery" | "board" | "table" | "notebook" | "canvas"; +type ReportStage = "Generating" | "Needs evidence" | "Needs review" | "Verified" | "Export ready" | "Monitoring"; + +const REPORT_VIEW_MODES: Array<{ id: ReportViewMode; label: string }> = [ + { id: "gallery", label: "Gallery" }, + { id: "board", label: "Board" }, + { id: "table", label: "Table" }, + { id: "notebook", label: "Notebook" }, + { id: "canvas", label: "Canvas" }, +]; + +const REPORT_DENSITY_OPTIONS: Array<{ id: Density; label: string; detail: string }> = [ + { id: "compact", label: "Compact", detail: "4-5 cards" }, + { id: "grid", label: "Standard", detail: "3 cards" }, + { id: "list", label: "Expanded", detail: "2 cards" }, +]; + +const REPORT_STAGE_COLUMNS: ReportStage[] = [ + "Generating", + "Needs evidence", + "Needs review", + "Verified", + "Export ready", + "Monitoring", +]; + const STATUS_FILTERS = [ { id: "all", label: "All" }, { id: "verified", label: "Verified" }, @@ -69,14 +96,16 @@ const KIND_FILTERS = [ { id: "Coverage", label: "Coverage" }, ] as const; -export function ReportsSurface({ onOpen, onSelectReport, inspectedReportId }: ReportsSurfaceProps) { +export function ReportsSurface({ onOpen, onRunBatch, onSelectReport, inspectedReportId }: ReportsSurfaceProps) { const [filter, setFilter] = useState("all"); const [kindFilter, setKindFilter] = useState("all"); - const [density, setDensity] = useState("compact"); + const [density, setDensity] = useState("grid"); + const [viewMode, setViewMode] = useState("gallery"); const [query, setQuery] = useState(""); const [selected, setSelected] = useState>(new Set()); const [sortKey, setSortKey] = useState("updated"); const [sortOpen, setSortOpen] = useState(false); + const [stageOverrides, setStageOverrides] = useState>({}); const sortRef = useRef(null); // Click-outside dismiss for sort dropdown @@ -162,18 +191,114 @@ export function ReportsSurface({ onOpen, onSelectReport, inspectedReportId }: Re setSelected(new Set()); }; - const visibleReports = filtered.slice(0, 12); + const visibleReports = filtered.slice(0, 24); const verifiedShare = reports.length > 0 ? Math.round(((counts.verified ?? 0) / reports.length) * 100) : 0; + const totalSources = filtered.reduce((sum, report) => sum + report.sources, 0); + const reviewCount = filtered.filter((report) => report.status === "review" || getReportStage(report, stageOverrides[report.id]) === "Needs review").length; + const exportReadyCount = filtered.filter((report) => getReportStage(report, stageOverrides[report.id]) === "Export ready").length; + const activeRunCount = filtered.filter((report) => { + const stage = getReportStage(report, stageOverrides[report.id]); + return stage === "Generating" || stage === "Needs evidence" || stage === "Needs review"; + }).length; + const selectedReport = filtered.find((report) => report.id === inspectedReportId) ?? visibleReports[0] ?? reports[0] ?? null; + const selectedStage = selectedReport ? getReportStage(selectedReport, stageOverrides[selectedReport.id]) : null; + + const boardReportsByStage = useMemo(() => { + const grouped = new Map(); + for (const stage of REPORT_STAGE_COLUMNS) grouped.set(stage, []); + for (const report of visibleReports) { + grouped.get(getReportStage(report, stageOverrides[report.id]))?.push(report); + } + return grouped; + }, [stageOverrides, visibleReports]); + + const runBatchPrompt = () => { + const prompt = "Run a research batch for my active entity universe. Generate banking-style report notebooks, verify sources, create review queue items, and show the agent run trace."; + if (onRunBatch) { + onRunBatch(prompt); + return; + } + onOpen("new", "chat"); + }; + + const handleDragStart = (event: DragEvent, reportId: string) => { + event.dataTransfer.setData("text/plain", reportId); + event.dataTransfer.effectAllowed = "move"; + }; + + const handleStageDrop = (event: DragEvent, stage: ReportStage) => { + event.preventDefault(); + const reportId = event.dataTransfer.getData("text/plain"); + if (!reportId) return; + setStageOverrides((prev) => ({ ...prev, [reportId]: stage })); + showToast({ tone: "success", message: `Moved report to ${stage}. This is a local review-board preview until writes are approved.` }); + }; + + const selectAndOpen = (report: ReportCardData, tab: "brief" | "cards" | "chat") => { + onSelectReport?.(report); + onOpen(report.id, tab); + }; return ( -
-
- Entity intelligence - - {isLoading && reports.length === 0 - ? "Checking Convex-backed artifacts" - : `${reports.length} live reports · ${verifiedShare}% verified · ${counts.review ?? 0} need review · ${reports.reduce((sum, report) => sum + report.sources, 0)} sources`} - +
+
+
+ Coverage Command Center + + {isLoading && reports.length === 0 + ? "Checking Convex-backed artifacts" + : `${reports.length} live reports - ${verifiedShare}% verified - ${reviewCount} need review - ${totalSources} source rows`} + +
+
+
+

Reports

+

+ Agent-generated notebooks organized by entity, evidence quality, lifecycle state, and the next review action. +

+
+
+ + +
+
+
+ {filtered.length}live reports + {activeRunCount}active runs + {reviewCount}need review + {exportReadyCount}export ready +
+
+
+
+ {REPORT_VIEW_MODES.map((item) => ( + + ))} +
+ {viewMode === "gallery" && ( +
+ {REPORT_DENSITY_OPTIONS.map((item) => ( + + ))} +
+ )}
{STATUS_FILTERS.map((item) => ( @@ -188,10 +313,10 @@ export function ReportsSurface({ onOpen, onSelectReport, inspectedReportId }: Re ))}
-
+
@@ -221,26 +346,18 @@ export function ReportsSurface({ onOpen, onSelectReport, inspectedReportId }: Re
-
+
setQuery(e.target.value)} - placeholder={isLive ? `Search ${reports.length} live reports...` : "Search live reports..."} - style={{ - width: "100%", - border: "1px solid var(--rd-line)", - borderRadius: 999, - padding: "10px 14px", - background: "var(--rd-panel)", - color: "var(--rd-ink)", - }} + onChange={(event) => setQuery(event.target.value)} + placeholder={isLive ? `Search ${reports.length} live reports by entity, claim, source, or action...` : "Search live reports by entity, claim, source, or action..."} />
{visibleReports.length === 0 ? ( @@ -251,60 +368,134 @@ export function ReportsSurface({ onOpen, onSelectReport, inspectedReportId }: Re ? "Clear the filters to return to the live report set." : "Convex returned zero report artifacts for this session. Run research from Chat to create the first report."}

- - ) : ( -
- {visibleReports.map((report) => { - const active = inspectedReportId === report.id; + ) : viewMode === "board" ? ( +
+ {REPORT_STAGE_COLUMNS.map((stage) => { + const stageReports = boardReportsByStage.get(stage) ?? []; return (
onSelectReport?.(report)} - aria-selected={active} - role="button" - tabIndex={0} - onKeyDown={(event) => { - if (event.key !== "Enter" && event.key !== " ") return; - event.preventDefault(); - onSelectReport?.(report); - }} + key={stage} + className="rd-v2-board-column" + onDrop={(event) => handleStageDrop(event, stage)} + onDragOver={(event) => event.preventDefault()} > -
- {report.entity.slice(0, 1).toUpperCase()} -

{report.entity}

- {report.status === "review" ? "review" : report.status} -
-

{report.kind}

-

{displayReportDescription(report.description)}

-
- {report.sources} sources - {report.claims} claims - {report.followUps} follow-ups -
-

Referenced in: {sourceLabel}

-

Updated {report.updatedAt}

-
- - - -
+
{stage}{stageReports.length}
+ {stageReports.map((report) => ( + + ))}
); })} +
+ ) : viewMode === "table" ? ( +
+ + + + + + + + + + + + + + + {visibleReports.map((report) => { + const scores = getReportScores(report); + return ( + onSelectReport?.(report)}> + + + + + + + + + + ); + })} + +
ReportTypeStatusSourcesClaimsScoresNotebookNext
{report.entity}{displayReportDescription(report.description)}{getReportType(report)}{getReportStage(report, stageOverrides[report.id])}{report.sources}{report.claims}{scores.evidence}/{scores.freshness}/{scores.confidence}{getNotebookState(report)}
+
+ ) : viewMode === "notebook" && selectedReport ? ( +
+ +
+ {getReportType(selectedReport)} - {selectedStage} +

{selectedReport.entity}

+

{displayReportDescription(selectedReport.description)}

+
+ {selectedReport.sources} sources + {selectedReport.claims} claims + {getNotebookState(selectedReport)} +
+

Executive read

+

{buildReportSignals(selectedReport)[0]}

+

Evidence appendix

+

Sources, claims, warnings, and prior chat handles stay attached to the notebook before any export or write.

+ +
+
+ ) : viewMode === "canvas" ? ( +
+ {visibleReports.slice(0, 10).map((report, index) => ( + + ))} +

Canvas mode maps companies, people, sources, claims, and notebooks. Writes stay gated through the agent inspector.

+
+ ) : ( +
+ {visibleReports.map((report) => ( + onSelectReport?.(report)} + onOpen={selectAndOpen} + onExport={() => showToast({ tone: "info", message: `Prepared export preview for ${report.entity}. Connector writes require approval.` })} + /> + ))}
-

Add entity

-

Capture a company, person, topic, or source cluster and let the coverage agent hydrate the first report.

- +

Run a batch

+

Import companies, people, topics, or source clusters and let the agent create report notebooks with review state.

+
)} {filtered.length > visibleReports.length && ( )}
@@ -507,6 +698,140 @@ export function ReportsSurface({ onOpen, onSelectReport, inspectedReportId }: Re ); } +function ReportCardV2({ + report, + active, + stage, + sourceLabel, + onSelect, + onOpen, + onExport, +}: { + report: ReportCardData; + active: boolean; + stage: ReportStage; + sourceLabel: string; + onSelect: () => void; + onOpen: (report: ReportCardData, tab: "brief" | "cards" | "chat") => void; + onExport: () => void; +}) { + const scores = getReportScores(report); + const signals = buildReportSignals(report); + const type = getReportType(report); + + return ( +
{ + if (event.key !== "Enter" && event.key !== " ") return; + event.preventDefault(); + onSelect(); + }} + > +
+ {type.slice(0, 1).toUpperCase()} +

{report.entity}

+ {stage} +
+

{type} - {report.kind}

+

{displayReportDescription(report.description)}

+
+ {signals.map((signal) => {signal})} +
+
+ {scores.evidence}Evidence + {scores.freshness}Freshness + {scores.confidence}Confidence +
+
+ {report.sources} sources + {report.claims} claims + {report.followUps} follow-ups +
+

Referenced in: {sourceLabel}

+

Notebook: {getNotebookState(report)} - Updated {report.updatedAt}

+
+ + + + +
+
+ ); +} + +function getReportType(report: ReportCardData): string { + const text = `${report.kind} ${report.entity} ${report.description}`.toLowerCase(); + if (text.includes("daily brief") || text.includes("brief")) return "Daily Brief"; + if (text.includes("funding") || text.includes("round") || text.includes("raise")) return "Funding Tracker"; + if (text.includes("person") || text.includes("founder") || text.includes("operator")) return "Person Report"; + if (text.includes("market map") || text.includes("landscape")) return "Market Map"; + if (text.includes("batch") || report.id.startsWith("run_")) return "Batch Run"; + if (text.includes("source") || text.includes("dossier")) return "Source Dossier"; + if (text.includes("theme") || text.includes("topic")) return "Topic Report"; + return "Company Report"; +} + +function getReportStage(report: ReportCardData, override?: ReportStage): ReportStage { + if (override) return override; + if (report.status === "review") return "Needs review"; + if (report.sources === 0 || report.claims === 0) return "Needs evidence"; + if (report.followUps > 3) return "Needs review"; + if (report.status === "watching") return "Monitoring"; + if (report.sources >= 8 && report.claims >= 3) return "Export ready"; + return "Verified"; +} + +function getReportScores(report: ReportCardData): { evidence: number; freshness: number; confidence: number } { + const evidence = clampScore(48 + report.sources * 6 + Math.min(report.claims, 8) * 3); + const freshness = report.updatedAt.includes("h") || report.updatedAt.includes("m") ? 88 : report.updatedAt.includes("d") ? 76 : 64; + const confidence = clampScore(54 + report.claims * 5 + report.sources * 2 - report.followUps * 4); + return { evidence, freshness, confidence }; +} + +function clampScore(value: number): number { + return Math.max(42, Math.min(96, Math.round(value))); +} + +function getNotebookState(report: ReportCardData): string { + if (report.status === "review" || report.followUps > 2) return "Patch suggested"; + if (report.sources < 2 || report.claims < 1) return "Needs sources"; + return "Ready"; +} + +function getNextAction(report: ReportCardData): string { + if (report.status === "review" || report.followUps > 2) return "Review evidence"; + if (getNotebookState(report) === "Needs sources") return "Find sources"; + return "Open notebook"; +} + +function buildReportSignals(report: ReportCardData): string[] { + const clean = displayReportDescription(report.description).replace(/\s+/g, " ").trim(); + const split = clean + .split(/(?<=[.!?])\s+|\s+\|\s+|;\s+/) + .map((item) => item.replace(/^\d+\.\s*/, "").trim()) + .filter(Boolean) + .slice(0, 3); + while (split.length < 3) { + if (split.length === 0) split.push(`${report.sources} sources attached for evidence review.`); + else if (split.length === 1) split.push(`${report.claims} claims available for verification.`); + else split.push(`${report.followUps} follow-ups route into the review queue.`); + } + return split; +} + +function getNotebookSections(report: ReportCardData): string[] { + const type = getReportType(report); + if (type === "Person Report") return ["Executive read", "Current role", "Relationship map", "Outreach angle", "Evidence appendix"]; + if (type === "Topic Report" || type === "Market Map") return ["Executive read", "Market context", "Key entities", "Contradictions", "Recommended research", "Evidence appendix"]; + if (type === "Daily Brief") return ["What changed", "Why it matters", "Top entities", "Reports needing review", "Recommended follow-ups", "Evidence appendix"]; + return ["Executive read", "Business overview", "Product and market", "Traction signals", "Risks and unknowns", "Recommended action", "Evidence appendix"]; +} + function ReportsLiveEmptyState() { return (
diff --git a/tests/e2e/home-v2-parity.spec.ts b/tests/e2e/home-v2-parity.spec.ts index dfe946da..3db500fb 100644 --- a/tests/e2e/home-v2-parity.spec.ts +++ b/tests/e2e/home-v2-parity.spec.ts @@ -146,10 +146,10 @@ test.describe("Home v2 /redesign parity", () => { await expect(page.getByRole("complementary", { name: "Reports navigation" })).toBeVisible(); await expect(page.getByLabel("Filter entities")).toBeVisible(); - await expect(page.getByText("Entity intelligence").first()).toBeVisible(); + await expect(page.getByText("Coverage Command Center").first()).toBeVisible(); await expect(page.locator(".rd-v2-report-grid")).toBeVisible({ timeout: 15_000 }); await expectV2CenterCanvas(page, ".rd-v2-proto-center"); - await expect(page.getByRole("complementary", { name: "Coverage agent" })).toBeVisible(); + await expect(page.getByRole("complementary", { name: "Agent Inspector" })).toBeVisible(); const rail = rightRail(page); await expectRailInViewport(rail); @@ -162,10 +162,18 @@ test.describe("Home v2 /redesign parity", () => { const firstTitle = await reportTitle(firstCard); const secondTitle = await reportTitle(secondCard); + await page.getByRole("tab", { name: "Board" }).click(); + await expect(page.getByRole("region", { name: "Draggable report review board" })).toBeVisible(); + await page.getByRole("tab", { name: "Table" }).click(); + await expect(page.getByRole("region", { name: "Analyst report table" })).toBeVisible(); + await page.getByRole("tab", { name: "Notebook" }).click(); + await expect(page.getByRole("region", { name: "Selected report notebook preview" })).toBeVisible(); + await page.getByRole("tab", { name: "Gallery" }).click(); + await selectReport(firstCard); await expect.poll(async () => await normalizedText(rail)).toContain(firstTitle); const firstRailText = await normalizedText(rail); - await expect(rail).toContainText("3 steps completed"); + await expect(rail).toContainText("Agent run trace"); await selectReport(secondCard); await expect.poll(async () => await normalizedText(rail)).toContain(secondTitle); @@ -285,8 +293,8 @@ test.describe("Home v2 prototype kit mode", () => { { path: `/redesign/reports?${qa}`, nav: "Reports", - center: "Entity intelligence", - rail: "Coverage agent", + center: "Coverage Command Center", + rail: "Agent Inspector", }, { path: `/redesign/chat?${qa}`, @@ -325,7 +333,7 @@ test.describe("Home v2 prototype kit mode", () => { await page.getByRole("button", { name: "Reports" }).click(); await expect(page).toHaveURL(/\/redesign\/reports\?qa=home-v2-implementation$/); - await expect(page.getByText("Entity intelligence").first()).toBeVisible(); + await expect(page.getByText("Coverage Command Center").first()).toBeVisible(); await page.getByRole("button", { name: "Chat" }).click(); await expect(page).toHaveURL(/\/redesign\/chat\?qa=home-v2-implementation$/); @@ -337,13 +345,13 @@ test.describe("Home v2 prototype kit mode", () => { await page.goto(`/redesign/reports?${qa}`, { waitUntil: "domcontentloaded" }); await page.getByText("Sequoia Capital").first().click(); - await expect(page.getByRole("complementary", { name: "Coverage agent" })).toContainText("Sequoia Capital"); - await expect(page.getByRole("complementary", { name: "Coverage agent" })).toContainText("Score 74"); - await expect(page.getByRole("complementary", { name: "Coverage agent" })).toContainText("Full-ratchet anti-dilution clause added"); + await expect(page.getByRole("complementary", { name: "Agent Inspector" })).toContainText("Sequoia Capital"); + await expect(page.getByRole("complementary", { name: "Agent Inspector" })).toContainText("Score 74"); + await expect(page.getByRole("complementary", { name: "Agent Inspector" })).toContainText("Full-ratchet anti-dilution clause added"); await page.getByRole("button", { name: /OpenAI 62/i }).click(); - await expect(page.getByRole("complementary", { name: "Coverage agent" })).toContainText("OpenAI"); - await expect(page.getByRole("complementary", { name: "Coverage agent" })).toContainText("Score 62"); + await expect(page.getByRole("complementary", { name: "Agent Inspector" })).toContainText("OpenAI"); + await expect(page.getByRole("complementary", { name: "Agent Inspector" })).toContainText("Score 62"); }); test("prototype Chat includes answer packet details and interactive utility tabs", async ({ page }) => { @@ -378,7 +386,7 @@ test.describe("Home v2 prototype kit mode", () => { await page.setViewportSize({ width: 390, height: 844 }); await page.goto(`/redesign/reports?${qa}`, { waitUntil: "domcontentloaded" }); - await expect(page.getByText("Entity intelligence").first()).toBeVisible(); + await expect(page.getByText("Coverage Command Center").first()).toBeVisible(); await expect(page.getByText("Enterprise tier repriced").first()).toBeVisible(); await expect(page.getByText("No reports yet")).toHaveCount(0); });