+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/reading-room.tsx b/frontend/src/components/reading-room.tsx
index a1d02c9..2eae7d7 100644
--- a/frontend/src/components/reading-room.tsx
+++ b/frontend/src/components/reading-room.tsx
@@ -73,6 +73,10 @@ export function ReadingRoom({ id }: { id: string }) {
← Back to run
+ {" · "}
+
+ Library (v2 preview)
+
{report.title}
diff --git a/frontend/src/components/run-view.tsx b/frontend/src/components/run-view.tsx
index 91a07c3..a805224 100644
--- a/frontend/src/components/run-view.tsx
+++ b/frontend/src/components/run-view.tsx
@@ -216,6 +216,11 @@ export function RunView({ id }: { id: string }) {
Read the report
+
+
+ Library (v2 preview)
+
+
);
}
diff --git a/frontend/src/components/three-click-path.test.tsx b/frontend/src/components/three-click-path.test.tsx
new file mode 100644
index 0000000..a6de76e
--- /dev/null
+++ b/frontend/src/components/three-click-path.test.tsx
@@ -0,0 +1,92 @@
+/**
+ * 3-click happy-path validation (T-3.5, REQ-61).
+ *
+ * With defaults only, a new user reaches a streaming research run in
+ * at most 3 discrete clicks:
+ * 1. "Research this" (Ask)
+ * 2. "Start research" (Choose — Deep Report, the recommended default)
+ * 3. "Approve plan" (Plan & Run)
+ * The test drives the REAL components against the REAL mock binding
+ * and counts the clicks — the criterion is pinned, not assumed.
+ */
+
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { AskForm } from "@/components/ask-form";
+import { MethodologyCard } from "@/components/methodology-card";
+import { RunView } from "@/components/run-view";
+import { api } from "@/lib/api/client";
+import { clearAskState, getAskState } from "@/lib/ask-store";
+
+const pushMock = vi.hoisted(() => vi.fn());
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({ push: pushMock }),
+}));
+
+const QUESTION = "What are the latest advances in solid-state batteries?";
+
+describe("3-click happy path (REQ-61)", () => {
+ beforeEach(() => {
+ vi.useFakeTimers({ shouldAdvanceTime: true });
+ pushMock.mockClear();
+ clearAskState();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ cleanup();
+ });
+
+ it("reaches a streaming run in ≤3 clicks on defaults", async () => {
+ const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
+ let clicks = 0;
+
+ /* Screen 1 — Ask ***********************************************/
+ const { unmount: unmountAsk } = render();
+ const questionBox = screen.getByLabelText(/what do you want to know\?/i);
+ await user.type(questionBox, QUESTION);
+ // Click 1: Research this — defaults mean empty context is fine.
+ await user.click(screen.getByRole("button", { name: "Research this" }));
+ clicks += 1;
+ // State carried: the question survives navigation to Choose.
+ const carried = getAskState();
+ if (carried === undefined) throw new Error("ask state not carried to /choose");
+ expect(carried.question).toBe(QUESTION);
+ unmountAsk();
+
+ /* Screen 2 — Choose ********************************************/
+ const methodologies = await api.getMethodologies();
+ const recommended = methodologies.find((m) => m.recommended);
+ if (recommended === undefined) throw new Error("no recommended default");
+ const { unmount: unmountChoose } = render(
+ ,
+ );
+ // Click 2: Start research — on the recommended default card,
+ // no browsing or drawer interaction needed.
+ await user.click(screen.getByRole("button", { name: "Start research" }));
+ clicks += 1;
+ const runRoute = pushMock.mock.calls
+ .flat()
+ .find((c): c is string => typeof c === "string" && c.startsWith("/run/"));
+ if (runRoute === undefined) throw new Error("no run route pushed");
+ expect(runRoute).toMatch(/^\/run\/run-[a-z0-9]+$/);
+ unmountChoose();
+
+ /* Screen 3 — Plan & Run ******************************************/
+ const runId = runRoute.replace("/run/", "");
+ render();
+ // Click 3: Approve plan & start research — trace starts streaming.
+ const approve = await screen.findByRole("button", {
+ name: /Approve plan & start research/i,
+ });
+ await user.click(approve);
+ clicks += 1;
+
+ // Criterion: a streaming run reached with at most 3 clicks.
+ expect(clicks).toBeLessThanOrEqual(3);
+ // And the stream is live: the first trace event has arrived.
+ expect(await screen.findByText(/Plan drafted/i)).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /⏸ Pause/i })).toBeInTheDocument();
+ });
+});
\ No newline at end of file
diff --git a/frontend/src/lib/api/client.ts b/frontend/src/lib/api/client.ts
index 816dfaf..cf36826 100644
--- a/frontend/src/lib/api/client.ts
+++ b/frontend/src/lib/api/client.ts
@@ -10,7 +10,9 @@
import type {
CitationLedger,
DialValues,
+ InboxItem,
Methodology,
+ ProjectGraph,
Report,
ResearchRun,
TraceEvent,
@@ -22,7 +24,11 @@ export type {
Citation,
CitationLedger,
DialValues,
+ GraphEdge,
+ GraphNode,
+ InboxItem,
Methodology,
+ ProjectGraph,
ResearchRun,
RunPlan,
RunStatus,
@@ -93,6 +99,12 @@ export interface ApiClient {
id: string,
format: "markdown" | "json",
): Promise<{ filename: string; content: string }>;
+
+ /** The cross-project graph mockup (maps to GET /api/graph in v0.2). */
+ getProjectGraph(): Promise;
+
+ /** The task inbox mockup (maps to GET /api/tasks in v0.2). */
+ getTaskInbox(): Promise;
}
/**
diff --git a/frontend/src/lib/api/mock/library-fixtures.ts b/frontend/src/lib/api/mock/library-fixtures.ts
new file mode 100644
index 0000000..4e1d39d
--- /dev/null
+++ b/frontend/src/lib/api/mock/library-fixtures.ts
@@ -0,0 +1,105 @@
+/**
+ * Library fixtures (T-3.1, REQ-60) — the v2 surface mockup data.
+ *
+ * Project graph: cross-linked research topics sharing sources/findings.
+ * Task inbox: open questions from past runs as checkable follow-ups.
+ * Realistic fixture domains, clearly example content.
+ */
+
+import type { InboxItem, ProjectGraph } from "@/lib/types";
+
+export const projectGraphFixture: ProjectGraph = {
+ nodes: [
+ {
+ id: "proj-batteries",
+ topic: "Solid-state batteries",
+ linkedSources: [
+ "Sulfide Electrolytes for All-Solid-State Lithium Batteries: A Review",
+ "Critical Materials Outlook: Lithium and Beyond",
+ ],
+ findings: [
+ "Interface degradation — not bulk conductivity — is the commercial bottleneck.",
+ "Lithium-metal anodes re-concentrate supply risk on refining capacity.",
+ ],
+ },
+ {
+ id: "proj-grid-storage",
+ topic: "Grid-scale storage economics",
+ linkedSources: [
+ "Critical Materials Outlook: Lithium and Beyond",
+ "Thermal Safety of Next-Generation Battery Chemistries",
+ ],
+ findings: [
+ "Safety advantages shift the siting calculus toward dense urban deployments.",
+ ],
+ },
+ {
+ id: "proj-materials",
+ topic: "Critical materials supply chains",
+ linkedSources: [
+ "Critical Materials Outlook: Lithium and Beyond",
+ "Sulfide Electrolytes for All-Solid-State Lithium Batteries: A Review",
+ ],
+ findings: [
+ "Refining capacity — not reserves — is the choke point for solid-state scale.",
+ ],
+ },
+ {
+ id: "proj-transport",
+ topic: "Automotive electrification timelines",
+ linkedSources: [
+ "Automakers Begin Pilot Production of Solid-State Cells",
+ "Venture Funding Flows Into Solid-State Startups",
+ ],
+ findings: [
+ "Pilot-line announcements cluster around 2027 vehicle deployment targets.",
+ ],
+ },
+ {
+ id: "proj-safety",
+ topic: "Battery safety testing standards",
+ linkedSources: [
+ "Thermal Safety of Next-Generation Battery Chemistries",
+ "Automakers Begin Pilot Production of Solid-State Cells",
+ ],
+ findings: [
+ "Abuse testing shows reduced heat release; dendrite persistence at high rates.",
+ ],
+ },
+ ],
+ edges: [
+ { fromId: "proj-batteries", toId: "proj-materials", sharedSourceCount: 2 },
+ { fromId: "proj-batteries", toId: "proj-grid-storage", sharedSourceCount: 1 },
+ { fromId: "proj-batteries", toId: "proj-transport", sharedSourceCount: 0 },
+ { fromId: "proj-grid-storage", toId: "proj-safety", sharedSourceCount: 1 },
+ { fromId: "proj-materials", toId: "proj-transport", sharedSourceCount: 0 },
+ { fromId: "proj-safety", toId: "proj-transport", sharedSourceCount: 1 },
+ ],
+};
+
+export const taskInboxFixture: InboxItem[] = [
+ {
+ id: "task-001",
+ question: "Do sulfide electrolytes outperform oxide ceramics below room temperature?",
+ sourceRunId: "run-solid-state",
+ status: "open",
+ },
+ {
+ id: "task-002",
+ question: "Which solid-state startups have shipped pilot cells to automakers?",
+ sourceRunId: "run-solid-state",
+ status: "in_progress",
+ },
+ {
+ id: "task-003",
+ question: "How do 2027 deployment targets compare across Japanese and EU consortia?",
+ sourceRunId: "run-transport",
+ status: "done",
+ },
+ {
+ id: "task-004",
+ question: "What is the lithium refining capacity expansion pipeline through 2030?",
+ sourceRunId: "run-materials",
+ status: "open",
+ },
+];
\ No newline at end of file
diff --git a/frontend/src/lib/api/mock/mock-client.ts b/frontend/src/lib/api/mock/mock-client.ts
index 139aca2..ba3cca4 100644
--- a/frontend/src/lib/api/mock/mock-client.ts
+++ b/frontend/src/lib/api/mock/mock-client.ts
@@ -8,6 +8,10 @@
import type { ApiClient, CreateRunInput } from "@/lib/api/client";
import { methodologyFixtures, findMethodologyBySlug } from "@/lib/api/mock/fixtures";
+import {
+ projectGraphFixture,
+ taskInboxFixture,
+} from "@/lib/api/mock/library-fixtures";
import {
buildScriptedLedger,
buildScriptedReport,
@@ -16,7 +20,9 @@ import {
} from "@/lib/api/mock/scripted-run";
import type {
CitationLedger,
+ InboxItem,
Methodology,
+ ProjectGraph,
Report,
ResearchRun,
TraceEvent,
@@ -196,6 +202,16 @@ export class MockApiClient implements ApiClient {
};
}
+ /** The cross-project graph mockup (v2 preview, REQ-60). */
+ async getProjectGraph(): Promise {
+ return structuredClone(projectGraphFixture);
+ }
+
+ /** The task inbox mockup (v2 preview, REQ-60). */
+ async getTaskInbox(): Promise {
+ return structuredClone(taskInboxFixture);
+ }
+
/** The scripted run for the prototype's demo question. */
#defaultRun(): ResearchRun {
return buildScriptedRun({
diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts
index 5372e5b..4c9195c 100644
--- a/frontend/src/lib/types.ts
+++ b/frontend/src/lib/types.ts
@@ -257,6 +257,41 @@ export interface Report {
citations: Citation[];
}
+/* ------------------------------------------------------------------ */
+/* v2 surfaces (library mockups — typed early, rendered in P3) */
+/* ------------------------------------------------------------------ */
+
+/** A node in the cross-project graph (v2 vision vocabulary). */
+export interface GraphNode {
+ id: string;
+ topic: string;
+ /** Source titles/passages shared with other projects. */
+ linkedSources: string[];
+ /** Findings this project contributed. */
+ findings: string[];
+}
+
+/** A cross-link between two projects — evidence of shared grounding. */
+export interface GraphEdge {
+ fromId: string;
+ toId: string;
+ sharedSourceCount: number;
+}
+
+/** The project graph mockup payload (maps to GET /api/graph in v0.2). */
+export interface ProjectGraph {
+ nodes: GraphNode[];
+ edges: GraphEdge[];
+}
+
+/** An inbox follow-up extracted from a past run (v2 task vocabulary). */
+export interface InboxItem {
+ id: string;
+ question: string;
+ sourceRunId: string;
+ status: TaskStatus;
+}
+
/* ------------------------------------------------------------------ */
/* Tasks (v2 task inbox vocabulary — typed early, rendered in P3) */
/* ------------------------------------------------------------------ */