feat: improve presenter scanning and mobile navigation
This commit is contained in:
@@ -197,7 +197,7 @@ describe("App", () => {
|
||||
expect(screen.queryByLabelText("Lifecycle Explorer")).toBeNull();
|
||||
});
|
||||
|
||||
it("routes to read-only presenter notes separately from presentation mode", () => {
|
||||
it("routes to read-only presenter notes separately from presentation mode", async () => {
|
||||
window.location.hash = "#scene/thesis/title";
|
||||
render(
|
||||
<MemoryRouter initialEntries={["/presenter"]}>
|
||||
@@ -205,7 +205,7 @@ describe("App", () => {
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("main", { name: /lda.chat presenter notes/i })).toBeInTheDocument();
|
||||
expect(await screen.findByRole("main", { name: /lda.chat presenter notes/i })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("main", { name: /lda.chat presentation/i })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import { Navigate, Route, Routes } from "react-router-dom";
|
||||
import { ConsoleHome } from "./ConsoleHome.js";
|
||||
import { PresentationRoute } from "../presentation/PresentationRoute.js";
|
||||
import { PresenterRoute } from "../presentation/presenter/PresenterRoute.js";
|
||||
|
||||
const PresenterRoute = lazy(() => import("../presentation/presenter/PresenterRoute.js").then((module) => ({
|
||||
default: module.PresenterRoute,
|
||||
})));
|
||||
|
||||
export const AppRoutes = () => (
|
||||
<Routes>
|
||||
<Route path="/" element={<ConsoleHome />} />
|
||||
<Route path="/console" element={<ConsoleHome />} />
|
||||
<Route path="/present" element={<PresentationRoute />} />
|
||||
<Route path="/presenter" element={<PresenterRoute />} />
|
||||
<Route path="/presenter" element={<Suspense fallback={null}><PresenterRoute /></Suspense>} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { PresenterBeatNote } from "./presenter-notes.js";
|
||||
import { presenterHashForNote } from "./presenter-navigation.js";
|
||||
|
||||
type PresenterNavigationBarProps = {
|
||||
readonly currentIndex: number;
|
||||
readonly total: number;
|
||||
readonly previous: PresenterBeatNote | null;
|
||||
readonly next: PresenterBeatNote | null;
|
||||
};
|
||||
|
||||
const DirectionLink = ({ note, children }: { readonly note: PresenterBeatNote | null; readonly children: string }) =>
|
||||
note
|
||||
? <a href={presenterHashForNote(note)}>{children}</a>
|
||||
: <span aria-disabled="true">{children}</span>;
|
||||
|
||||
export const PresenterNavigationBar = ({ currentIndex, total, previous, next }: PresenterNavigationBarProps) => (
|
||||
<nav className="presenter-navigation" aria-label="Presenter note navigation">
|
||||
<DirectionLink note={previous}>← Previous</DirectionLink>
|
||||
<span>{currentIndex + 1} / {total}</span>
|
||||
<DirectionLink note={next}>Next →</DirectionLink>
|
||||
</nav>
|
||||
);
|
||||
@@ -28,7 +28,7 @@ export const PresenterNote = ({ note, cumulativeSeconds, next, covered, onCovere
|
||||
|
||||
<section className="presenter-note__say" aria-labelledby="presenter-say">
|
||||
<span id="presenter-say">Say</span>
|
||||
<p>{note.mustSay}</p>
|
||||
<div className="presenter-note__markdown"><ReactMarkdown>{note.mustSay}</ReactMarkdown></div>
|
||||
</section>
|
||||
|
||||
{note.warning && <aside className="presenter-note__warning"><strong>Warning</strong><p>{note.warning}</p></aside>}
|
||||
@@ -60,10 +60,11 @@ export const PresenterNote = ({ note, cumulativeSeconds, next, covered, onCovere
|
||||
{next && (
|
||||
<section className="presenter-note__next" aria-label="Next beat preview">
|
||||
<span>Next · {formatPresenterTime(next.targetSeconds)}</span>
|
||||
<p>{next.mustSay}</p>
|
||||
<div className="presenter-note__next-copy"><ReactMarkdown>{next.mustSay}</ReactMarkdown></div>
|
||||
<a href={presenterHashForNote(next)}>Go to next beat</a>
|
||||
</section>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
};
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
@@ -14,6 +14,9 @@ describe("PresenterRoute", () => {
|
||||
render(<PresenterRoute />);
|
||||
expect(screen.getByRole("main", { name: /lda.chat presenter notes/i })).toBeInTheDocument();
|
||||
expect(screen.getByText(/This project began with the goal/i)).toBeInTheDocument();
|
||||
expect(screen.getByText("the system underneath the chat").tagName).toBe("STRONG");
|
||||
expect(screen.getByRole("navigation", { name: /presenter note navigation/i })).toHaveTextContent("1 / 42");
|
||||
expect(screen.getByRole("link", { name: "Next →" })).toHaveAttribute("href", "#scene/thesis/substrate");
|
||||
expect(screen.getByRole("link", { name: /open audience slide/i })).toHaveAttribute("href", "/present#scene/thesis/title");
|
||||
expect(screen.queryByText(/live target/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /run prepared workflow/i })).not.toBeInTheDocument();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { findDiscussionBranch } from "../storyboard.js";
|
||||
import { PresenterNote } from "./PresenterNote.js";
|
||||
import { PresenterNavigationBar } from "./PresenterNavigationBar.js";
|
||||
import { PresenterShell } from "./PresenterShell.js";
|
||||
import { presenterNotes } from "./presenter-notes.js";
|
||||
import { presenterHashForNote, presenterNavigationFromHash } from "./presenter-navigation.js";
|
||||
import "./presenter.css";
|
||||
|
||||
@@ -34,7 +36,14 @@ export const PresenterRoute = () => {
|
||||
|
||||
return (
|
||||
<PresenterShell current={navigation.note} covered={covered}>
|
||||
<div className="presenter-route__help" aria-label="Presenter keyboard help">← previous · → next · disclosures stay local</div>
|
||||
{navigation.note && (
|
||||
<PresenterNavigationBar
|
||||
currentIndex={navigation.index}
|
||||
total={presenterNotes.length}
|
||||
previous={navigation.previous}
|
||||
next={navigation.next}
|
||||
/>
|
||||
)}
|
||||
{navigation.note && (
|
||||
<PresenterNote
|
||||
note={navigation.note}
|
||||
|
||||
@@ -60,7 +60,10 @@ describe("presenter note catalog", () => {
|
||||
);
|
||||
|
||||
for (const note of presenterNotes) {
|
||||
expect(speech, `${note.sceneId}/${note.beatId}`).toContain(note.mustSay);
|
||||
// Markdown emphasis is presenter-only typography; the runbook stores the
|
||||
// same spoken words without requiring identical inline formatting.
|
||||
const spokenText = note.mustSay.replaceAll("**", "");
|
||||
expect(speech, `${note.sceneId}/${note.beatId}`).toContain(spokenText);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ export const presenterNotes = [
|
||||
"thesis",
|
||||
"title",
|
||||
22,
|
||||
"This project began with the goal in the title: an AI agent for creating and automating workspace workflows. The difficult engineering problem became the system underneath the chat.",
|
||||
"This project began with the goal in the title: **an AI agent for creating and automating workspace workflows**. The difficult engineering problem became **the system underneath the chat**.",
|
||||
["Thesis Abstract and Introduction"],
|
||||
{
|
||||
warning: "Do not present the submitted system as a bundled autonomous planner.",
|
||||
@@ -64,7 +64,7 @@ export const presenterNotes = [
|
||||
"thesis",
|
||||
"substrate",
|
||||
23,
|
||||
"The submitted contribution is a typed workflow substrate: an external planner can propose work while the platform owns definitions, validation, bindings, execution records, traces, and explicit resume boundaries.",
|
||||
"The submitted contribution is a **typed workflow substrate**: an external planner can propose work while **the platform owns definitions, validation, bindings, execution records, traces, and explicit resume boundaries**.",
|
||||
["Thesis Abstract and Introduction", "Thesis Contributions"],
|
||||
{ qnaBranchIds: ["where-is-ai-agent", "not-just-cli"] },
|
||||
),
|
||||
@@ -72,7 +72,7 @@ export const presenterNotes = [
|
||||
"problem",
|
||||
"direct-actions",
|
||||
22,
|
||||
"A model can call tools and complete one task, but a tool transcript is not reusable automation.",
|
||||
"A model can call tools and complete one task, but **a tool transcript is not reusable automation**.",
|
||||
["Thesis Problem Statement and Requirements"],
|
||||
{ qnaBranchIds: ["direct-orchestration", "not-just-scripts"] },
|
||||
),
|
||||
@@ -80,7 +80,7 @@ export const presenterNotes = [
|
||||
"problem",
|
||||
"missing-contracts",
|
||||
23,
|
||||
"Reuse needs schemas, source bindings, persistence, traces, and declared recovery boundaries, with planning kept separate from execution.",
|
||||
"Reuse needs **schemas, source bindings, persistence, traces, and declared recovery boundaries**, with **planning kept separate from execution**.",
|
||||
["Thesis Problem Statement and Requirements"],
|
||||
{ qnaBranchIds: ["why-schemas", "run-persistence"] },
|
||||
),
|
||||
@@ -104,7 +104,7 @@ export const presenterNotes = [
|
||||
"planner-runtime",
|
||||
"planner",
|
||||
18,
|
||||
"An external model or human proposes and revises workflow structure; this keeps planning outside the runtime.",
|
||||
"An **external model or human proposes and revises workflow structure**; this keeps planning outside the runtime.",
|
||||
["Thesis Architecture Overview"],
|
||||
{ qnaBranchIds: ["where-is-ai-agent", "not-just-cli"] },
|
||||
),
|
||||
@@ -112,7 +112,7 @@ export const presenterNotes = [
|
||||
"planner-runtime",
|
||||
"runtime",
|
||||
18,
|
||||
"For fixed definitions and handler results, the runtime validates the graph, resolves sources, executes steps, records state and traces, and resumes only at declared boundaries.",
|
||||
"For **fixed definitions and handler results**, the runtime validates the graph, resolves sources, executes steps, records state and traces, and **resumes only at declared boundaries**.",
|
||||
["Thesis Workflow Core", "Thesis Architecture Overview"],
|
||||
{ warning: "Qualify determinism; provider code, resource reads, and external side effects can vary.", qnaBranchIds: ["run-persistence", "typed-interrupts"] },
|
||||
),
|
||||
@@ -120,7 +120,7 @@ export const presenterNotes = [
|
||||
"planner-runtime",
|
||||
"boundary",
|
||||
19,
|
||||
"Typed CLI and JSON-RPC operations reach the same Workflow API, making schemas, diagnostics, and lifecycle state machine-readable without importing runtime internals.",
|
||||
"**Typed CLI and JSON-RPC operations reach the same Workflow API**, making schemas, diagnostics, and lifecycle state machine-readable without importing runtime internals.",
|
||||
["Thesis Architecture Overview", "docs/source_architecture.md"],
|
||||
{ qnaBranchIds: ["not-just-cli"] },
|
||||
),
|
||||
@@ -128,7 +128,7 @@ export const presenterNotes = [
|
||||
"lifecycle",
|
||||
"draft",
|
||||
11,
|
||||
"Draft is mutable authoring state.",
|
||||
"**Draft** is mutable authoring state.",
|
||||
["Thesis Workflow Lifecycle"],
|
||||
{ optionalDetail: "Raw plans can also create artifacts without passing through a Draft." },
|
||||
),
|
||||
@@ -136,21 +136,21 @@ export const presenterNotes = [
|
||||
"lifecycle",
|
||||
"artifact",
|
||||
11,
|
||||
"Artifact is an immutable workflow definition.",
|
||||
"**Artifact** is an immutable workflow definition.",
|
||||
["Thesis Workflow Lifecycle"],
|
||||
),
|
||||
beatNote(
|
||||
"lifecycle",
|
||||
"deployment",
|
||||
11,
|
||||
"Deployment binds an artifact version to concrete sources and runtime context.",
|
||||
"**Deployment** binds an artifact version to concrete sources and runtime context.",
|
||||
["Thesis Workflow Lifecycle"],
|
||||
),
|
||||
beatNote(
|
||||
"lifecycle",
|
||||
"run",
|
||||
12,
|
||||
"Run records one execution, including status, diagnostics, output, trace, and an explicit stopped or interrupted state.",
|
||||
"**Run** records one execution, including status, diagnostics, output, trace, and an explicit stopped or interrupted state.",
|
||||
["Thesis Workflow Lifecycle"],
|
||||
{ qnaBranchIds: ["lifecycle-states", "run-persistence"] },
|
||||
),
|
||||
@@ -220,7 +220,7 @@ export const presenterNotes = [
|
||||
"agent-handoff",
|
||||
"request",
|
||||
20,
|
||||
"I will now show a prepared demonstration built on this platform. The chat is a presentation interface, not the autonomous planner evaluated by the thesis. The chat translates a report request into the same public lifecycle operations an external agent could call. This prepared path demonstrates product behavior and recorded evidence, not a fresh model-performance result.",
|
||||
"I will now show a **prepared demonstration built on this platform**. The chat is a presentation interface, **not the autonomous planner evaluated by the thesis**. The chat translates a report request into the same public lifecycle operations an external agent could call. This prepared path demonstrates product behavior and recorded evidence, not a fresh model-performance result.",
|
||||
["Constrained demo agent and prepared replay recipe"],
|
||||
{
|
||||
fallback: "This is the reviewed recording, not a live model planning this workflow.",
|
||||
@@ -276,7 +276,7 @@ export const presenterNotes = [
|
||||
"run-from-deployment",
|
||||
"operation",
|
||||
12,
|
||||
"The public workflow.runs.start operation validates the deployment and input, creates a persisted Run, and begins the reusable graph.",
|
||||
"The public **workflow.runs.start** operation validates the deployment and input, creates a **persisted Run**, and begins the reusable graph.",
|
||||
["workflow.runs.start replay evidence"],
|
||||
{ qnaBranchIds: ["run-persistence"] },
|
||||
),
|
||||
@@ -292,7 +292,7 @@ export const presenterNotes = [
|
||||
"typed-human-boundary",
|
||||
"interrupt",
|
||||
15,
|
||||
"Execution pauses at a typed issue_review interrupt exposing request data, allowed outcomes, request schema, and resume schema.",
|
||||
"Execution pauses at a **typed issue_review interrupt** exposing request data, allowed outcomes, request schema, and resume schema.",
|
||||
["Typed interrupt payload and resume contract"],
|
||||
{ qnaBranchIds: ["typed-interrupts", "why-schemas"] },
|
||||
),
|
||||
@@ -308,7 +308,7 @@ export const presenterNotes = [
|
||||
"resume-output-evidence",
|
||||
"resume",
|
||||
16,
|
||||
"On the submitted path, workflow.runs.resume continues the recorded interrupted Run.",
|
||||
"On the submitted path, **workflow.runs.resume continues the recorded interrupted Run**.",
|
||||
["workflow.runs.resume replay evidence", "Revision replay identity"],
|
||||
{ fallback: "The submitted replay demonstrates same-run continuation; the revision branch is recorded separately.", qnaBranchIds: ["replay-provenance", "prepared-replay-boundary"] },
|
||||
),
|
||||
@@ -332,7 +332,7 @@ export const presenterNotes = [
|
||||
"evaluation",
|
||||
"cohort",
|
||||
40,
|
||||
"The evaluation combines conformance tests, deterministic case studies, and a manually audited external-agent campaign: 36 trials across two challenges, two hosted models, three instruction profiles, and three waves, with three attempts per cell.",
|
||||
"The evaluation combines conformance tests, deterministic case studies, and a **manually audited external-agent campaign**: 36 trials across two challenges, two hosted models, three instruction profiles, and three waves, with three attempts per cell.",
|
||||
["Thesis Evaluation and Appendix C"],
|
||||
{ qnaBranchIds: ["evaluation-validity"] },
|
||||
),
|
||||
@@ -348,7 +348,7 @@ export const presenterNotes = [
|
||||
"evaluation",
|
||||
"findings",
|
||||
40,
|
||||
"Because prompts, product snapshots, and hosted conditions changed across waves, these results are longitudinal engineering evidence. They expose authoring and diagnostic gaps, not a benchmark of model success, token reduction, retry reduction, or superiority.",
|
||||
"Because prompts, product snapshots, and hosted conditions changed across waves, these results are **longitudinal engineering evidence**. They expose authoring and diagnostic gaps, **not a benchmark** of model success, token reduction, retry reduction, or superiority.",
|
||||
["Thesis Evaluation and Appendix C", "Thesis Threats to Validity"],
|
||||
{ warning: "Use non-benchmark wording; do not report the counts as general model performance.", qnaBranchIds: ["evaluation-validity"] },
|
||||
),
|
||||
@@ -372,7 +372,7 @@ export const presenterNotes = [
|
||||
"conclusion",
|
||||
"conclusion",
|
||||
20,
|
||||
"The contribution is architectural and implemented: external planners can propose workflows while a typed platform validates, binds, executes, persists, interrupts, resumes, and inspects them through public operations.",
|
||||
"The contribution is **architectural and implemented**: external planners can propose workflows while a typed platform **validates, binds, executes, persists, interrupts, resumes, and inspects** them through public operations.",
|
||||
["Thesis Contributions", "Thesis Conclusion"],
|
||||
{ qnaBranchIds: ["where-is-ai-agent", "not-just-cli"] },
|
||||
),
|
||||
|
||||
@@ -29,7 +29,10 @@
|
||||
.presenter-sidebar__beats a[data-covered="true"]:not([aria-current="page"]) { background: #dce9e2; }
|
||||
|
||||
.presenter-route__reader { min-width: 0; padding: 1.25rem clamp(1.5rem, 5vw, 6rem) 5rem; }
|
||||
.presenter-route__help { max-width: 72ch; margin: 0 auto 2rem; color: #67655f; font: 0.78rem "IBM Plex Mono", monospace; }
|
||||
.presenter-navigation { max-width: 72ch; min-height: 2.5rem; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 1rem; margin: 0 auto 2rem; border-bottom: 1px solid #d7d5d0; color: #67655f; font: 0.78rem "IBM Plex Mono", monospace; }
|
||||
.presenter-navigation a { color: #155b49; font-weight: 650; text-decoration: none; }
|
||||
.presenter-navigation a:last-child, .presenter-navigation > span:last-child { text-align: right; }
|
||||
.presenter-navigation [aria-disabled="true"] { opacity: 0.35; }
|
||||
.presenter-note, .presenter-qna { max-width: 72ch; margin: 0 auto; }
|
||||
.presenter-note__header { display: flex; justify-content: space-between; gap: 2rem; padding-bottom: 1rem; border-bottom: 2px solid #20201e; }
|
||||
.presenter-note__header span, .presenter-note__timing { color: #67655f; font: 0.78rem "IBM Plex Mono", monospace; }
|
||||
@@ -37,7 +40,8 @@
|
||||
.presenter-note__timing { display: grid; align-content: start; gap: 0.25rem; text-align: right; }
|
||||
.presenter-note__say { margin: 2rem 0; }
|
||||
.presenter-note__say > span { color: #1e6b55; font: 700 0.8rem "IBM Plex Mono", monospace; text-transform: uppercase; }
|
||||
.presenter-note__say p { margin: 0.55rem 0 0; font-size: 1.42rem; line-height: 1.58; text-wrap: pretty; }
|
||||
.presenter-note__markdown p { margin: 0.55rem 0 0; font-size: 1.42rem; line-height: 1.58; text-wrap: pretty; }
|
||||
.presenter-note__markdown strong, .presenter-note__next-copy strong { color: #155b49; font-weight: 750; }
|
||||
.presenter-note__warning, .presenter-note__fallback, .presenter-qna aside { margin: 1.2rem 0; padding: 0.8rem 0; border-block: 1px solid #a36919; }
|
||||
.presenter-note__fallback { border-color: #3c6380; }
|
||||
.presenter-note__warning p, .presenter-note__fallback p, .presenter-qna aside p { margin: 0.2rem 0 0; line-height: 1.5; }
|
||||
@@ -48,7 +52,7 @@
|
||||
.presenter-note__actions a, .presenter-note__next a, .presenter-qna a { color: #155b49; font-weight: 650; }
|
||||
.presenter-note__next { margin-top: 2.5rem; padding-top: 1rem; border-top: 1px solid #a7a49e; color: #4f4d48; }
|
||||
.presenter-note__next > span { font: 0.75rem "IBM Plex Mono", monospace; text-transform: uppercase; }
|
||||
.presenter-note__next p { font-size: 1.05rem; line-height: 1.5; }
|
||||
.presenter-note__next-copy p { font-size: 1.05rem; line-height: 1.5; }
|
||||
.presenter-qna > span { display: block; margin-top: 2rem; color: #67655f; font: 0.75rem "IBM Plex Mono", monospace; text-transform: uppercase; }
|
||||
.presenter-qna__short { font-size: 1.35rem; line-height: 1.55; }
|
||||
.presenter-qna dl { margin-top: 2rem; border-top: 1px solid #d7d5d0; padding-top: 1rem; }
|
||||
@@ -59,11 +63,12 @@
|
||||
.presenter-route { grid-template-columns: 1fr; }
|
||||
.presenter-sidebar { position: static; height: auto; max-height: 15rem; border-right: 0; border-bottom: 1px solid #d7d5d0; }
|
||||
.presenter-sidebar ol { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.presenter-navigation { position: sticky; top: 0; z-index: 3; margin-inline: -0.5rem; padding-inline: 0.5rem; background: #f7f7f5; }
|
||||
}
|
||||
|
||||
@media print {
|
||||
.presenter-route { display: block; background: white; }
|
||||
.presenter-sidebar, .presenter-route__help, .presenter-note__actions { display: none; }
|
||||
.presenter-sidebar, .presenter-navigation, .presenter-note__actions { display: none; }
|
||||
.presenter-route__reader { padding: 0; }
|
||||
.presenter-note { max-width: none; }
|
||||
.presenter-note details { display: block; }
|
||||
|
||||
Reference in New Issue
Block a user