import { FounderTraining } from "./founder";
import React, { useEffect, useState } from "react";
import { GuidePage, guideRoutes } from "./guide";
import { createRoot } from "react-dom/client";
import {
  BrowserRouter,
  Routes,
  Route,
  Link,
  useLocation,
  useNavigationType,
  useNavigate,
  useParams,
} from "react-router";
import { LiveReaderSite } from "./reader-site";
import { slugProblem } from "../shared/liveReader";
import type { Project, Command, Publication, Version } from "../shared/model";
import "./style.css";
// After style.css so the collaborator screen's styles win over the legacy globals.
import { Reader, Write } from "./write";
import { ReadersRoom, SiteHome, SiteSamples } from "./site";
import { LiveReaderPage } from "./live-reader-page";
import { OfferPage } from "./offer-page";
import { SitePro } from "./pro";
import { Privacy as SitePrivacy, Refunds, Support, Terms } from "./legal";
// Last, so the shared design system wins over the screen-level styles.
import "./brand.css";
import { studioGuide } from "../shared/studio-guide";
import { api, ApiError } from "./api";
const errorMessage = (e: unknown) =>
  e instanceof Error ? e.message : "Something went wrong";
// A Live Reader book lives at liveauthor.ai/<book>: any first path segment that isn't one of the app's own
// pages or a reserved name is a book's address (src/shared/liveReader.ts).
const SHELL_PAGES = new Set(["", "founder", "studio", "books", "account", "review"]);
function BookOrApp({ children }: React.PropsWithChildren) {
  const first = useLocation().pathname.split("/")[1] ?? "";
  return !SHELL_PAGES.has(first) && !slugProblem(first) ? <LiveReaderSite /> : <>{children}</>;
}
// A new page opens at its top (Sherman, 2026-10-26: Learn more opened /live-reader halfway down). A link to an
// anchor (/#pricing) and the browser's Back and Forward keep their own place.
function ScrollToTop() {
  const { pathname, hash } = useLocation();
  const type = useNavigationType();
  useEffect(() => {
    if (!hash && type !== "POP") window.scrollTo(0, 0);
  }, [pathname, hash, type]);
  return null;
}
function Shell({ children }: React.PropsWithChildren) {
  const [runtime, setRuntime] = useState<{ ai: { provider: string; keyConfigured: boolean; approvalConfigured: boolean } }>();
  const [runtimeError, setRuntimeError] = useState("");
  useEffect(() => {
    api<typeof runtime>("/status").then(setRuntime).catch((e) => setRuntimeError(errorMessage(e)));
  }, []);
  return (
    <>
      <div className="test-banner">
        {runtimeError ? "STUDIO UNAVAILABLE" : !runtime ? "Checking studio connection…" : runtime.ai.provider === "anthropic"
          ? `AI PILOT · Claude selected · ${!runtime.ai.keyConfigured ? "API key missing" : !runtime.ai.approvalConfigured ? "Pilot configuration incomplete" : "Scoped approval and budget required"} · Identity and payments simulated`
          : "LOCAL TEST STUDIO · Simulated AI, identity and payments · No live charges"}
      </div>
      {runtimeError && <p className="error" role="alert">{runtimeError}</p>}
      <header>
        <Link className="brand" to="/">
          Live Author<span className="brand-dot">●</span>
        </Link>
        <nav aria-label="Main">
          <Link to="/studio">Your studio</Link>
          <Link to="/books">Discover books</Link>
          <Link to="/account/library">My library</Link>
        </nav>
      </header>
      {children}
      <footer>
        Live Author · Your words. Your decisions. <span>liveauthor.ai</span>
      </footer>
    </>
  );
}
function Landing() {
  return (
    <main className="landing">
      <div className="eyebrow">A PLACE FOR YOUR NEXT BOOK</div>
      <h1>
        You have a story.
        <br />
        <em>Make room for it.</em>
      </h1>
      <p className="lead">
        From the first conversation to the final page. Develop your book with an
        AI writing team, keep your own voice, and make every final decision.
      </p>
      <Link className="button" to="/studio">
        Start your free session <span>↗</span>
      </Link>
      <p className="small">
        One session. Your idea, an opening, one revision, and a sample to keep.
        <br />
        Target: 10–15 minutes after signup and card setup. No automatic charge.
      </p>
      <div className="feature-grid">
        <article>
          <span>01 / CREATE</span>
          <h2>
            A conversation,
            <br />
            not a blank page.
          </h2>
          <p>
            One useful question at a time. Your work stays with you when you
            return.
          </p>
        </article>
        <article>
          <span>02 / SHAPE</span>
          <h2>
            Your voice.
            <br />
            The final say.
          </h2>
          <p>
            Research, independent diagnosis and reviewable suggestions. You
            control the prose.
          </p>
        </article>
        <article>
          <span>03 / SHARE</span>
          <h2>
            A book to keep.
            <br />A world to open.
          </h2>
          <p>
            Download your files, or choose a separate web service for an
            interactive book.
          </p>
        </article>
      </div>
    </main>
  );
}
type Me = {
  account: { id: string; name: string; email: string; card_setup: number };
  projects: { id: string; title: string }[];
};
function StudioHome() {
  const [me, setMe] = useState<Me>();
  const [loading, setLoading] = useState(true);
  const [unavailable, setUnavailable] = useState(false);
  const [error, setError] = useState("");
  const navigate = useNavigate();
  useEffect(() => {
    api<Me>("/me")
      .then(setMe)
      .catch((e) => {
        // Only an explicit unauthenticated response means this is a new visitor.
        if (e instanceof ApiError && e.status === 401) return;
        setError(errorMessage(e));
        setUnavailable(true);
      })
      .finally(() => setLoading(false));
  }, []);
  async function submit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setError("");
    const f = new FormData(e.currentTarget);
    try {
      if (!me) {
        await api("/test/signup", {
          email: f.get("email"),
          name: f.get("name"),
        });
        setMe(await api<Me>("/me"));
      } else if (!me.account.card_setup) {
        await api("/test/card-setup", {});
        setMe(await api<Me>("/me"));
      } else {
        const p = await api<Project>("/projects", {
          title: f.get("title"),
          mode: f.get("mode"),
          characterId: String(f.get("characterId") || ""),
          form: f.get("form"),
          language: f.get("language"),
        });
        navigate("/studio/" + p.id);
      }
    } catch (e) {
      setError(errorMessage(e));
    }
  }
  return (
    <main className="narrow">
      <div className="eyebrow">YOUR PRIVATE WRITING ROOM</div>
      <h1>{me ? `Welcome, ${me.account.name}.` : "Let’s begin with you."}</h1>
      {error && (
        <p role="alert" className="error">
          {error}
        </p>
      )}
      {loading ? <p role="status">Opening your studio…</p> : unavailable ? (
        <button onClick={() => window.location.reload()}>Retry connection</button>
      ) : me?.projects.length ? (
        <section>
          <h2>Pick up where you left off</h2>
          {me.projects.map((p) => (
            <Link className="project-link" key={p.id} to={"/studio/" + p.id}>
              {p.title} <span>Continue →</span>
            </Link>
          ))}
        </section>
      ) : (
        <form onSubmit={submit}>
          {!me ? (
            <>
              <p>
                This local fixture simulates signup and verification. Use a
                synthetic address.
              </p>
              <label>
                Your name
                <input
                  name="name"
                  required
                  defaultValue="Alex"
                  autoComplete="name"
                />
              </label>
              <label>
                Email
                <input
                  type="email"
                  name="email"
                  required
                  defaultValue="alex@example.test"
                  autoComplete="email"
                />
              </label>
              <button>Enter test studio</button>
            </>
          ) : !me.account.card_setup ? (
            <>
              <h2>Secure card setup</h2>
              <p>
                In the live product, the payment provider handles your card.
                This test records a simulated setup reference; it collects no
                card details and charges nothing.
              </p>
              <button>Simulate completed card setup</button>
            </>
          ) : (
            <>
              <label>
                Working title
                <input
                  name="title"
                  required
                  defaultValue="The light we leave"
                />
              </label>
              <div className="row">
                <label>
                  Starting point
                  <select name="mode">
                    <option value="origination">A new idea</option>
                    <option value="adaptation">Adapt a script</option>
                  </select>
                </label>
                <label>
                  Form
                  <select name="form">
                    <option value="fiction">Fiction</option>
                    <option value="nonfiction">Nonfiction</option>
                  </select>
                </label>
                <label>
                  Manuscript language
                  <select name="language">
                    <option value="en">English</option>
                    <option value="es">Español</option>
                    <option value="fr">Français</option>
                    <option value="de">Deutsch</option>
                  </select>
                </label>
              </div>
              <p className="small">
                The sample and conversation stay in the same project when you
                continue. Choosing a language does not translate existing prose.
              </p>
              <button>Create my private project</button>
            </>
          )}
        </form>
      )}
      <button
        className="quiet"
        onClick={async () => {
          try {
            await api("/logout", {});
            setMe(undefined);
          } catch (e) {
            setError(errorMessage(e));
          }
        }}
      >
        Sign out
      </button>
    </main>
  );
}
const tabs = [
  "Conversation",
  "Manuscript",
  "Book plan",
  "Voice",
  "Research",
  "Editorial",
  "Media & promotion",
  "Private review",
  "Publish",
  "My files",
  "Activity",
];
function Studio() {
  const { id } = useParams();
  const [p, setP] = useState<Project>();
  const [entitlements, setEnt] = useState({ studio: false, web: false });
  const [tab, setTab] = useState("Conversation");
  const [error, setError] = useState("");
  const [notice, setNotice] = useState("");
  const [busy, setBusy] = useState(false);
  const [draft, setDraft] = useState("");
  const [aiUsage, setAIUsage] = useState<{
    calls: { state: string; actual: number | null; ceiling: number; reconciled: number | null }[];
    limits: { calls: number; perCallMicros: number; totalMicros: number };
  }>();
  const [jobs, setJobs] = useState<
    { id: string; kind: string; state: string }[]
  >([]);
  async function load() {
    const r = await api<{
      project: Project;
      entitlements: typeof entitlements;
    }>("/projects/" + id);
    setP(r.project);
    setEnt(r.entitlements);
    return r.project;
  }
  useEffect(() => {
    load().catch((e) => setError(errorMessage(e)));
  }, [id]);
  async function run(fn: () => Promise<unknown>) {
    setBusy(true);
    setError("");
    setNotice("");
    try {
      await fn();
      await load();
    } catch (e) {
      setError(errorMessage(e));
    } finally {
      setBusy(false);
    }
  }
  async function command(c: Command) {
    if (!p) return;
    await api("/projects/" + id + "/command", {
      revision: p.revision,
      requestId: crypto.randomUUID(),
      command: c,
    });
  }
  if (!p)
    return (
      <main>
        <p role="alert">{error || "Opening your studio…"}</p>
        <Link to="/studio">Back to your projects</Link>
      </main>
    );
  const heads = Object.values(p.heads).map(
    (i) => p.versions.find((v) => v.id === i)!,
  );
  const units = heads.filter((v) => v.kind === "unit");
  const next = studioGuide(p, entitlements.studio);
  const approve = (
    v: Version,
    action: "plan" | "voice" | "research" | "final" | "promote",
  ) =>
    run(() =>
      command({ type: "approve", target: v.id, action, expectedHash: v.hash }),
    );
  async function payment(product: "studio" | "web") {
    await run(async () => {
      const r = await api<{ id: string }>("/projects/" + id + "/checkout", {
        product,
        onboardingComplete: true,
        confirmed: true,
        offer: "synthetic-zero-v1",
      });
      await api("/projects/" + id + "/test/payment", {
        paymentId: r.id,
        eventId: crypto.randomUUID(),
        kind: "succeeded",
      });
      setNotice(`Simulated ${product} access activated. $0 charged.`);
    });
  }
  const startJob = (kind: "draft" | "research" | "review") =>
    run(async () => {
      await api("/projects/" + id + "/jobs", { kind, revision: p.revision });
      setNotice(
        "Job queued. It can continue with the browser closed. Refresh Activity to see its outcome.",
      );
    });
  return (
    <div className="workspace">
      <aside>
        <Link className="back" to="/studio">
          ← Your projects
        </Link>
        <h2>{p.title}</h2>
        <p className="small">
          Private · {p.language.toUpperCase()} · Version {p.revision}
        </p>
        <nav aria-label="Studio views">
          {tabs.map((t) => (
            <button
              className={tab === t ? "selected" : ""}
              onClick={() => setTab(t)}
              key={t}
            >
              {t}
            </button>
          ))}
        </nav>
        <div className="status-dot">Saved to your local studio</div>
      </aside>
      <main className="studio-main">
        <div className="view-heading">
          <div>
            <div className="eyebrow">
              {p.form} / {p.mode}
            </div>
            <h1>{tab}</h1>
          </div>
          <button className="quiet" onClick={() => run(load)}>
            Refresh saved work
          </button>
        </div>
        {error && (
          <p className="error" role="alert">
            {error}
          </p>
        )}
        {notice && (
          <p className="notice" role="status">
            {notice}
          </p>
        )}
        <section className="studio-guide" aria-label="Your next step">
          <div className="eyebrow">Your next step</div>
          <h2>{next.title}</h2>
          <p>{next.detail}</p>
          <button className="secondary" onClick={() => setTab(next.tab)}>
            Go to {next.tab}
          </button>
        </section>
        <fieldset disabled={busy} className="surface">
          {tab === "Conversation" && (
            <>
              <div className="conversation-intro">
                <h2>Let’s work on your book together</h2>
                <p>
                  Bring an idea, a passage, or what isn’t working yet. Your
                  story and judgment lead; I can bring perspective and writing
                  methods to help you move it forward.
                </p>
              </div>
              <div className="messages" aria-live="polite">
                {p.messages.map((m) => (
                  <div className={"message " + m.role} key={m.id}>
                    <b>
                      {m.role === "author"
                        ? "You"
                        : "AI collaborator · simulated"}
                    </b>
                    <p>{m.text}</p>
                    {m.attachment && (
                      <p className="small">
                        Saved privately: {m.attachment.title} ·{" "}
                        {m.attachment.kind === "prose"
                          ? "your prose"
                          : m.attachment.kind}
                      </p>
                    )}
                  </div>
                ))}
              </div>
              <form
                onSubmit={(e) => {
                  e.preventDefault();
                  const text = draft;
                  const form = e.currentTarget,
                    data = new FormData(form);
                  const transcript = data.get("transcript") === "on";
                  const materialText = String(data.get("materialText") ?? "");
                  const material = materialText.trim()
                    ? {
                        text: materialText,
                        title: String(
                          data.get("materialTitle") || "Shared material",
                        ),
                        kind: String(data.get("materialKind")) as
                          | "prose"
                          | "reference"
                          | "note",
                      }
                    : undefined;
                  run(async () => {
                    await command({
                      type: "message",
                      text,
                      transcript,
                      material,
                    });
                    setDraft("");
                    form.reset();
                  });
                }}
              >
                <label>
                  Your idea
                  <textarea
                    value={draft}
                    onChange={(e) => setDraft(e.target.value)}
                    required
                    placeholder="Tell me about your book…"
                  />
                </label>
                <label className="check">
                  <input type="checkbox" name="transcript" />
                  This is a reviewed speech transcript
                </label>
                <details>
                  <summary>Add prose or material to this conversation</summary>
                  <label>
                    What are you sharing?
                    <select name="materialKind">
                      <option value="note">
                        Project notes / possibilities
                      </option>
                      <option value="prose">My own prose</option>
                      <option value="reference">Reference material</option>
                    </select>
                  </label>
                  <label>
                    Material title
                    <input
                      name="materialTitle"
                      defaultValue="A passage to discuss"
                      maxLength={100}
                    />
                  </label>
                  <label>
                    Paste your material
                    <textarea
                      name="materialText"
                      placeholder="Paste the passage or notes here. They stay in this private project."
                    />
                  </label>
                </details>
                <button>Keep the conversation going</button>
              </form>
              {p.conversationTurns?.slice(-1).map((turn) => (
                <section key={turn.id} aria-label="Conversation next step">
                  {turn.suggestion && (
                    <>
                      <h2>{turn.suggestion.title}</h2>
                      <p>{turn.suggestion.reason}</p>
                      {turn.suggestion.status === "pending" && (
                        <div className="actions">
                          <button
                            disabled={turn.suggestion.revision !== p.revision}
                            onClick={() =>
                              run(() =>
                                command({
                                  type: "conversation_decision",
                                  turnId: turn.id,
                                  decision: "accept",
                                }),
                              )
                            }
                          >
                            Yes, try this next step
                          </button>
                          <button
                            className="quiet"
                            disabled={turn.suggestion.revision !== p.revision}
                            onClick={() =>
                              run(() =>
                                command({
                                  type: "conversation_decision",
                                  turnId: turn.id,
                                  decision: "dismiss",
                                }),
                              )
                            }
                          >
                            Not now
                          </button>
                        </div>
                      )}
                      {turn.suggestion.status !== "pending" && (
                        <p className="small">
                          {turn.suggestion.status === "accepted"
                            ? "Proposal created for your review. Nothing was approved automatically."
                            : "Set aside. Keep the conversation going."}
                        </p>
                      )}
                      {turn.suggestion.status === "pending" &&
                        turn.suggestion.revision !== p.revision && (
                          <p className="small">
                            Your project changed. Discuss the next step again so
                            it uses your current work.
                          </p>
                        )}
                    </>
                  )}
                  <details>
                    <summary>What informed this reply?</summary>
                    <p className="small">
                      A consistent mentoring approach and relevant writing
                      methods, using only this project’s saved conversation and
                      current material. {turn.contextIds.length} context
                      records; {turn.knowledgeVersion}; {turn.mentorVersion}.
                      Suggestions are not approvals.
                    </p>
                  </details>
                </section>
              ))}
              {heads.filter((v) =>
                ["contract", "plan", "voice"].includes(v.kind),
              ).length > 0 && (
                <details>
                  <summary>
                    Review our current plan and voice proposals here
                  </summary>
                  {heads
                    .filter((v) =>
                      ["contract", "plan", "voice"].includes(v.kind),
                    )
                    .map((v) => (
                      <section key={v.id}>
                        <h2>
                          {v.kind === "contract"
                            ? "Book promise"
                            : v.kind === "plan"
                              ? "Book shape"
                              : "Voice profile"}
                        </h2>
                        <ArtifactForm
                          kind={v.kind as SaveKind}
                          p={p}
                          onSave={(c) => run(() => command(c))}
                        />
                        <button
                          className="secondary"
                          onClick={() =>
                            approve(v, v.kind === "voice" ? "voice" : "plan")
                          }
                        >
                          Approve this exact {v.kind}
                        </button>
                      </section>
                    ))}
                </details>
              )}
              <div className="actions">
                <button
                  className="secondary"
                  disabled={
                    p.trial.stage !== "active" ||
                    !p.messages.some((m) => m.role === "author")
                  }
                  onClick={() => run(() => command({ type: "trial_sample" }))}
                >
                  Create my free sample
                </button>
                <button
                  className="secondary"
                  onClick={() => setTab("Manuscript")}
                >
                  Open my draft →
                </button>
              </div>
              <p className="small">
                AI responses are deterministic fixtures in this build. Literary
                quality and the 10–15-minute target have not been evaluated.
              </p>
              {entitlements.studio && (
                <button
                  className="secondary"
                  onClick={() =>
                    run(async () => {
                      await command({ type: "propose_plan" });
                      setTab("Book plan");
                    })
                  }
                >
                  Suggest a plan from our conversation
                </button>
              )}
              {!entitlements.studio && (
                <section className="offer">
                  <h2>Keep building the same book</h2>
                  <p>
                    Your idea, sample and conversation continue here. Live
                    offers are disabled until prices, limits and terms are
                    approved.
                  </p>
                  <button onClick={() => payment("studio")}>
                    Confirm completed onboarding · activate test studio ($0)
                  </button>
                  <p className="small">
                    This explicit test confirmation simulates verified payment.
                    It does not enroll you in web service.
                  </p>
                </section>
              )}
            </>
          )}
          {tab === "Manuscript" && (
            <>
              {units.map((v) => (
                <section key={v.id}>
                  <div className="row">
                    <h2>{v.key}</h2>
                    <span className="badge">
                      {v.provenance} · {v.id.slice(0, 8)}
                    </span>
                  </div>
                  <form
                    onSubmit={(e) => {
                      e.preventDefault();
                      const f = new FormData(e.currentTarget);
                      run(() =>
                        command({
                          type: "save",
                          kind: "unit",
                          key: v.key,
                          text: String(f.get("text")),
                          baseline: v.id,
                          dependencies: v.dependencies,
                        }),
                      );
                    }}
                  >
                    <label>
                      Your prose
                      <textarea
                        className="manuscript"
                        name="text"
                        defaultValue={v.text}
                      />
                    </label>
                    <button>Save my edit as a new version</button>
                  </form>
                  <div className="actions">
                    <button
                      className="secondary"
                      onClick={() => approve(v, "final")}
                    >
                      Approve my final prose
                    </button>
                  </div>
                  <details>
                    <summary>Version history</summary>
                    {p.versions
                      .filter((x) => x.key === v.key)
                      .map((x) => (
                        <article key={x.id}>
                          <b>
                            {x.provenance} · {x.id.slice(0, 8)}
                          </b>
                          <pre>{x.text}</pre>
                        </article>
                      ))}
                  </details>
                </section>
              ))}
              {p.trial.stage === "sample_ready" &&
                units.some(
                  (v) => v.key === "opening" && v.provenance === "generated",
                ) && (
                  <form
                    onSubmit={(e) => {
                      e.preventDefault();
                      const f = new FormData(e.currentTarget);
                      run(() =>
                        command({
                          type: "trial_revision",
                          instruction: String(f.get("instruction")),
                        }),
                      );
                    }}
                  >
                    <label>
                      One free revision
                      <textarea
                        name="instruction"
                        required
                        placeholder="What would you like to change?"
                      />
                    </label>
                    <button>Revise the generated sample</button>
                  </form>
                )}
              <ArtifactForm
                kind="unit"
                onSave={(c) => run(() => command(c))}
                p={p}
              />
              <button className="secondary" onClick={() => startJob("draft")}>
                Draft next unit from approved context
              </button>
            </>
          )}
          {["Book plan", "Voice", "Research", "Media & promotion"].includes(
            tab,
          ) && (
            <>
              {tab === "Book plan" && (
                <>
                  <button
                    className="secondary"
                    onClick={() => run(() => command({ type: "propose_plan" }))}
                  >
                    Suggest a plan from our conversation
                  </button>
                  <p>
                    Approve the contract and structure. Adaptations also need
                    imported source text and a verified digest. Suggestions
                    remain unapproved; edit them before deciding.
                  </p>
                </>
              )}
              {tab === "Voice" && (
                <>
                  <p>
                    Keep author craft, narrator and character registers
                    distinct. Select genuine author-developed prose as evidence
                    for origination.
                  </p>
                  <form
                    onSubmit={(e) => {
                      e.preventDefault();
                      const f = new FormData(e.currentTarget);
                      run(() =>
                        command({
                          type: "propose_voice",
                          evidence: f.getAll("evidence").map(String),
                        }),
                      );
                    }}
                  >
                    <h2>Let’s find your voice in your own choices</h2>
                    {heads
                      .filter((v) =>
                        p.mode === "origination"
                          ? v.kind === "unit" &&
                            ["author", "mixed"].includes(v.provenance)
                          : ["source", "digest", "unit"].includes(v.kind),
                      )
                      .map((v) => (
                        <label className="check" key={v.id}>
                          <input type="checkbox" name="evidence" value={v.id} />
                          {v.key} · {v.provenance}
                        </label>
                      ))}
                    <button className="secondary">
                      Suggest a voice profile from selected prose
                    </button>
                  </form>
                </>
              )}
              {tab === "Research" && (
                <p>
                  Commission a bounded mission. Sources, access limitations and
                  disputed interpretations must travel with each claim. The mock
                  performs no external research.
                </p>
              )}
              {tab === "Media & promotion" && (
                <p>
                  Prepare a brief, approve it, and review the generated result
                  separately. Image generation is a synthetic fixture only. Real
                  image/video providers and external promotion delivery remain
                  unverified.
                </p>
              )}
              {(tab === "Book plan"
                ? [
                    "contract",
                    "plan",
                    ...(p.mode === "adaptation" ? ["source", "digest"] : []),
                  ]
                : tab === "Voice"
                  ? ["voice", "character"]
                  : tab === "Research"
                    ? ["research_brief", "claim"]
                    : ["media_brief", "promotion"]
              ).map((kind) => (
                <section key={kind}>
                  <h2>{kind.replaceAll("_", " ")}</h2>
                  <ArtifactForm
                    kind={kind as SaveKind}
                    p={p}
                    onSave={(c) => run(() => command(c))}
                  />
                  {heads
                    .filter((v) => v.kind === kind)
                    .map((v) => (
                      <article key={v.id}>
                        <pre>{v.text}</pre>
                        <button
                          className="secondary"
                          onClick={() =>
                            approve(
                              v,
                              kind === "voice"
                                ? "voice"
                                : kind === "research_brief"
                                  ? "research"
                                  : kind === "claim"
                                    ? "promote"
                                    : "plan",
                            )
                          }
                        >
                          Approve this exact version
                        </button>
                      </article>
                    ))}
                </section>
              ))}
              {tab === "Media & promotion" && (
                <MediaPanel p={p} onAction={run} />
              )}
              {tab === "Research" && (
                <button onClick={() => startJob("research")}>
                  Run approved research mission
                </button>
              )}
            </>
          )}
          {tab === "Editorial" && (
            <>
              <p>
                Diagnosis is separate from rewriting. Modeled reading and
                known-ending analysis retain distinct coverage records.
              </p>
              <button onClick={() => startJob("review")}>
                Request a full manuscript review
              </button>
              {p.findings.map((f) => (
                <section key={f.id}>
                  <span className="badge">{f.disposition}</span>
                  <h2>{f.cause}</h2>
                  <p>{f.requirement}</p>
                  <p>{f.strength}</p>
                  {f.possibleSolution && (
                    <p>
                      <b>Possible approach:</b> {f.possibleSolution}
                    </p>
                  )}
                  {f.downstreamCost && (
                    <p>
                      <b>What else could change:</b> {f.downstreamCost}
                    </p>
                  )}
                  {f.evidenceIds && (
                    <p className="small">
                      Evidence:{" "}
                      {f.evidenceIds
                        .map(
                          (id) =>
                            p.versions.find((v) => v.id === id)?.key ?? id,
                        )
                        .join(", ")}
                    </p>
                  )}
                  <form
                    onSubmit={(e) => {
                      e.preventDefault();
                      const d = new FormData(e.currentTarget);
                      run(() =>
                        command({
                          type: "finding",
                          id: f.id,
                          disposition: d.get("disposition") as "author_closed",
                          reason: String(d.get("reason")),
                        }),
                      );
                    }}
                  >
                    <label>
                      Your response
                      <textarea name="reason" required />
                    </label>
                    <label>
                      Decision
                      <select name="disposition">
                        <option value="author_closed">
                          Close this finding
                        </option>
                        <option value="accepted_as_is">
                          Accept the passage as-is
                        </option>
                        <option value="deferred">Defer</option>
                        <option value="withdrawn">Withdraw diagnosis</option>
                        <option value="resolved">Resolved</option>
                      </select>
                    </label>
                    <button>Record my decision</button>
                  </form>
                </section>
              ))}
              {heads
                .filter((v) => v.kind === "review")
                .map((v) => (
                  <details key={v.id}>
                    <summary>Coverage and review artifacts</summary>
                    <pre>{v.text}</pre>
                  </details>
                ))}
            </>
          )}
          {tab === "Private review" && (
            <>
              <p>
                Share only selected manuscript versions with a verified
                recipient. No invitation email is sent by this local build.
                Saved copies cannot be recalled.
              </p>
              <form
                onSubmit={(e) => {
                  e.preventDefault();
                  const d = new FormData(e.currentTarget);
                  run(async () => {
                    const r = await api<{ url: string }>(
                      "/projects/" + id + "/invites",
                      {
                        email: d.get("email"),
                        versionIds: d.getAll("versions"),
                        expires: Date.now() + 86400000 * 7,
                        comments: d.get("comments") === "on",
                        download: d.get("download") === "on",
                      },
                    );
                    setNotice(`Prepared private review: ${r.url}`);
                  });
                }}
              >
                <label>
                  Recipient email
                  <input type="email" name="email" required />
                </label>
                {units.map((v) => (
                  <label className="check" key={v.id}>
                    <input type="checkbox" name="versions" value={v.id} />
                    {v.key} · exact version {v.id.slice(0, 8)}
                  </label>
                ))}
                <label className="check">
                  <input type="checkbox" name="comments" />
                  Allow comments on this snapshot
                </label>
                <label className="check">
                  <input type="checkbox" name="download" />
                  Allow downloading this snapshot
                </label>
                <button>Prepare seven-day private invitation</button>
              </form>
              <form
                onSubmit={(e) => {
                  e.preventDefault();
                  const d = new FormData(e.currentTarget);
                  run(() =>
                    api("/projects/" + id + "/invites/" + d.get("invite"), {}),
                  );
                }}
              >
                <label>
                  Revoke invitation ID
                  <input name="invite" required />
                </label>
                <button className="secondary">Revoke access</button>
              </form>
            </>
          )}
          {tab === "Publish" && (
            <>
              <p>
                Download-only is complete without web service. Optional branded
                book pages render from approved data in this shared application.
              </p>
              {!entitlements.web && (
                <button onClick={() => payment("web")}>
                  Confirm separate test web offer ($0)
                </button>
              )}
              <form
                onSubmit={(e) => {
                  e.preventDefault();
                  const d = new FormData(e.currentTarget);
                  run(() =>
                    command({
                      type: "publication",
                      slug: String(d.get("slug")),
                      author: String(d.get("author")),
                      description: String(d.get("description")),
                      accent: String(d.get("accent")),
                      discoverable: d.get("discoverable") === "on",
                      sampleCount: Number(d.get("sample")),
                      releasedCount: Number(d.get("released")),
                      rights: true,
                      exclusive: false,
                    }),
                  );
                }}
              >
                <div className="row">
                  <label>
                    Book address
                    <input
                      name="slug"
                      required
                      pattern="[a-z0-9]+(-[a-z0-9]+)*"
                      placeholder="the-light-we-leave"
                    />
                  </label>
                  <label>
                    Author name
                    <input name="author" required />
                  </label>
                  <label>
                    Brand color
                    <input type="color" name="accent" defaultValue="#31594f" />
                  </label>
                </div>
                <label>
                  Approved public description
                  <textarea name="description" required />
                </label>
                <div className="row">
                  <label>
                    Released chapters
                    <input
                      name="released"
                      type="number"
                      min="1"
                      max={units.length}
                      defaultValue={units.length || 1}
                    />
                  </label>
                  <label>
                    Public sample chapters
                    <input
                      name="sample"
                      type="number"
                      min="0"
                      max={units.length}
                      defaultValue={units.length}
                    />
                  </label>
                </div>
                <label className="check">
                  <input type="checkbox" required />I confirm rights and no
                  conflicting ebook exclusivity.
                </label>
                <label className="check">
                  <input name="discoverable" type="checkbox" />
                  List approved metadata in the catalog
                </label>
                <button>Prepare exact publication preview</button>
              </form>
              {p.publications.map((pub) => (
                <section
                  key={pub.id}
                  className="publication-preview"
                  style={{ borderColor: pub.accent }}
                >
                  <span className="badge">
                    {pub.template} · {pub.id.slice(0, 8)}
                  </span>
                  <h2>{pub.title}</h2>
                  <p>{pub.description}</p>
                  <p>{pub.aiDisclosure}</p>
                  {pub.media?.map((m) => (
                    <figure key={m.id}>
                      <img
                        src={"/api/projects/" + p.id + "/assets/" + m.id}
                        alt={m.alt}
                        style={{ maxWidth: 240 }}
                      />
                      <figcaption>
                        {m.alt} · spoiler level {m.spoiler}
                      </figcaption>
                    </figure>
                  ))}
                  <p>
                    {pub.releasedCount} released · {pub.sampleCount} sample
                    chapters
                  </p>
                  <details>
                    <summary>Review exact public text</summary>
                    {pub.units.map((u) => (
                      <article key={u.id}>
                        <h3>{u.title}</h3>
                        <pre>{u.text}</pre>
                      </article>
                    ))}
                  </details>
                  <div className="actions">
                    <button
                      onClick={() =>
                        run(async () => {
                          const bytes = new TextEncoder().encode(
                            JSON.stringify(pub),
                          );
                          const h = Array.from(
                            new Uint8Array(
                              await crypto.subtle.digest("SHA-256", bytes),
                            ),
                          )
                            .map((x) => x.toString(16).padStart(2, "0"))
                            .join("");
                          await command({
                            type: "approve",
                            target: pub.id,
                            action: "publish",
                            expectedHash: h,
                          });
                        })
                      }
                    >
                      Approve this exact publication
                    </button>
                    <button
                      className="secondary"
                      onClick={() =>
                        run(async () => {
                          const r = await api<{ url: string }>(
                            "/projects/" + id + "/publish",
                            { snapshotId: pub.id, revision: p.revision },
                          );
                          setNotice("Published locally: " + r.url);
                        })
                      }
                    >
                      Activate approved book page
                    </button>
                    <Link to={"/books/" + pub.slug}>Open book →</Link>
                  </div>
                  <form
                    onSubmit={(e) => {
                      e.preventDefault();
                      const f = new FormData(e.currentTarget);
                      run(async () => {
                        await api("/projects/" + id + "/schedule", {
                          snapshotId: pub.id,
                          revision: p.revision,
                          due: new Date(String(f.get("time"))).getTime(),
                          timezone: String(f.get("timezone")),
                        });
                        setNotice(
                          "Approved edition scheduled. The server checks its authority again before release.",
                        );
                      });
                    }}
                  >
                    <label>
                      Schedule this exact edition
                      <input type="datetime-local" name="time" required />
                    </label>
                    <label>
                      Timezone
                      <input
                        name="timezone"
                        defaultValue={
                          Intl.DateTimeFormat().resolvedOptions().timeZone
                        }
                        required
                      />
                    </label>
                    <button className="secondary">
                      Schedule approved release
                    </button>
                  </form>
                </section>
              ))}
              <button
                className="quiet"
                onClick={() =>
                  run(() => api("/projects/" + id + "/withdraw", {}))
                }
              >
                Withdraw public book
              </button>
            </>
          )}
          {tab === "My files" && (
            <>
              <h2>Your work belongs to you.</h2>
              <p>
                These downloads remain available without a paid subscription.
                Format validation is still being completed; no retailer
                acceptance is claimed.
              </p>
              <div className="actions">
                {["epub", "docx", "archive"].map((f) => (
                  <a
                    className="button secondary"
                    href={"/api/projects/" + id + "/export/" + f}
                    key={f}
                  >
                    Download {f.toUpperCase()}
                  </a>
                ))}
              </div>
              <section>
                <h2>Permanent private deletion</h2>
                <p>
                  Separate from publication withdrawal. The archive includes all
                  current project records and version text. Minimal test billing
                  records and a tombstone remain; backup expiration is not yet
                  implemented.
                </p>
                <form
                  onSubmit={(e) => {
                    e.preventDefault();
                    const d = new FormData(e.currentTarget);
                    run(async () => {
                      await api("/projects/" + id + "/purge", {
                        confirmation: d.get("confirmation"),
                        retainPublic: d.get("retain") === "on",
                        revision: p.revision,
                      });
                      window.location.href = "/studio";
                    });
                  }}
                >
                  <label className="check">
                    <input name="retain" type="checkbox" />
                    Keep the approved public edition and required reader data
                  </label>
                  <label>
                    Type PERMANENTLY DELETE PRIVATE PROJECT
                    <input
                      name="confirmation"
                      required
                      pattern="PERMANENTLY DELETE PRIVATE PROJECT"
                    />
                  </label>
                  <button className="danger">
                    Permanently delete private project
                  </button>
                </form>
              </section>
            </>
          )}
          {tab === "Activity" && (
            <>
              <p>
                Studio: {entitlements.studio ? "test active" : "inactive"} ·
                Web: {entitlements.web ? "test active" : "inactive"}
              </p>
              <p>
                Measured mock-provider API cost: $0. Real usage benchmarks are
                pending.
              </p>
              <button
                onClick={() =>
                  run(async () =>
                    setJobs(await api("/projects/" + id + "/jobs")),
                  )
                }
              >
                Refresh durable jobs
              </button>
              {jobs.map((j) => (
                <article key={j.id}>
                  <b>
                    {j.kind} · {j.state}
                  </b>
                  <button
                    className="quiet"
                    onClick={() =>
                      run(() => api("/projects/" + id + "/jobs/" + j.id, {}))
                    }
                  >
                    Cancel
                  </button>
                </article>
              ))}
              <button
                className="secondary"
                onClick={() =>
                  run(async () =>
                    setAIUsage(await api("/projects/" + id + "/ai-usage")),
                  )
                }
              >
                Check AI pilot usage
              </button>
              {aiUsage && (
                <p role="status">
                  {aiUsage.calls.length} of {aiUsage.limits.calls} calls used · $
                  {(
                    aiUsage.calls.reduce(
                      (n, c) => n + (c.reconciled ?? c.actual ?? c.ceiling),
                      0,
                    ) / 1000000
                  ).toFixed(4)}{" "}
                  recorded or reserved ·{" "}
                  {aiUsage.calls.filter((c) => c.state !== "settled" && c.reconciled == null).length}{" "}
                  calls awaiting reconciliation. Mock usage costs $0.
                </p>
              )}
              <pre>{JSON.stringify(p.usage, null, 2)}</pre>
            </>
          )}
        </fieldset>
      </main>
    </div>
  );
}
type SaveKind = Extract<Command, { type: "save" }>["kind"];
function ArtifactForm({
  kind,
  p,
  onSave,
}: {
  kind: SaveKind;
  p: Project;
  onSave: (c: Command) => unknown;
}) {
  const existing = ["contract", "plan", "voice", "digest"].includes(kind)
    ? p.versions.find((v) => v.id === p.heads[kind])
    : undefined;
  return (
    <form
      key={existing?.id ?? kind}
      onSubmit={(e) => {
        e.preventDefault();
        const f = new FormData(e.currentTarget);
        const key = String(f.get("key") || kind);
        onSave({
          type: "save",
          kind,
          key,
          text:
            kind === "character"
              ? JSON.stringify({
                  name: f.get("name"),
                  register: f.get("register"),
                  revealedAt: Number(f.get("revealedAt")),
                  knowledgeThrough: Number(f.get("knowledgeThrough")),
                })
              : kind === "claim"
                ? JSON.stringify({
                    statement: f.get("statement"),
                    verification: f.get("verification"),
                    disagreement: f.get("disagreement"),
                    sourceAccess: f.get("sourceAccess"),
                    source: f.get("source"),
                    locator: f.get("locator"),
                    qualifications: String(f.get("qualifications"))
                      .split("\n")
                      .filter(Boolean),
                    interpretation: f.get("interpretation"),
                    intendedUse: f.get("intendedUse"),
                    publicationReady: f.get("ready") === "on",
                  })
                : String(f.get("text")),
          baseline: p.heads[key],
          dependencies: f.getAll("dependencies").map(String),
        });
      }}
    >
      <label>
        {kind === "unit" ? "New chapter key" : "Record key"}
        <input
          name="key"
          pattern="[a-z0-9_-]+"
          defaultValue={kind === "unit" ? "chapter-1" : kind}
          required
        />
      </label>
      {kind === "character" ? (
        <>
          <label>
            Character name
            <input name="name" required />
          </label>
          <label>
            Speech register
            <textarea name="register" required />
          </label>
          <div className="row">
            <label>
              First visible in chapter
              <input
                name="revealedAt"
                type="number"
                min="1"
                defaultValue="1"
                required
              />
            </label>
            <label>
              Knows events through chapter
              <input
                name="knowledgeThrough"
                type="number"
                min="1"
                defaultValue="1"
                required
              />
            </label>
          </div>
        </>
      ) : kind === "claim" ? (
        <>
          <label>
            Precise claim
            <textarea name="statement" required />
          </label>
          <div className="row">
            <label>
              Verification
              <select name="verification">
                <option value="unverified">Unverified</option>
                <option value="verified_as_source_statement">
                  Verified as a source’s statement
                </option>
                <option value="verified">Verified</option>
                <option value="not_found">Not found</option>
              </select>
            </label>
            <label>
              Disagreement
              <select name="disagreement">
                <option value="not_assessed">Not assessed</option>
                <option value="contested">Contested</option>
                <option value="none_reported">None reported</option>
              </select>
            </label>
          </div>
          <label>
            Actual access
            <select name="sourceAccess">
              <option value="none">None</option>
              <option value="secondary_report">Secondary report</option>
              <option value="excerpt">Excerpt</option>
              <option value="full_text">Full text</option>
            </select>
          </label>
          <label>
            Source
            <input name="source" />
          </label>
          <label>
            Edition / page or section locator
            <input name="locator" />
          </label>
          <label>
            Qualifications and limits (one per line)
            <textarea name="qualifications" />
          </label>
          <label>
            Claim type
            <select name="interpretation">
              <option value="source_statement">Source statement</option>
              <option value="historical_fact">Historical fact</option>
              <option value="reconstruction">Attributed reconstruction</option>
              <option value="researcher_synthesis">Researcher synthesis</option>
            </select>
          </label>
          <label>
            Intended book use
            <input name="intendedUse" required />
          </label>
          <label className="check">
            <input name="ready" type="checkbox" />
            Ready for publication with these qualifications
          </label>
        </>
      ) : (
        <label>
          Text
          <textarea name="text" required defaultValue={existing?.text ?? ""} />
        </label>
      )}
      <details>
        <summary>Select exact dependencies / evidence</summary>
        {Object.values(p.heads)
          .map((id) => p.versions.find((v) => v.id === id)!)
          .map((v) => (
            <label className="check" key={v.id}>
              <input
                type="checkbox"
                name="dependencies"
                value={v.id}
                disabled={v.id === existing?.id}
                defaultChecked={existing?.dependencies.includes(v.id)}
              />
              {v.key} · {v.provenance}
            </label>
          ))}
      </details>
      <button className="secondary">Save {kind.replaceAll("_", " ")}</button>
    </form>
  );
}
function Catalog() {
  const [books, setBooks] = useState<Publication[]>([]);
  useEffect(() => {
    api<Publication[]>("/catalog").then(setBooks);
  }, []);
  return (
    <main>
      <div className="eyebrow">THE LIVE AUTHOR LIBRARY</div>
      <h1>Stories with a door left open.</h1>
      <div className="feature-grid">
        {books.map((b) => (
          <Link
            className="book-card"
            key={b.slug}
            to={"/books/" + b.slug}
            style={{ borderTopColor: b.accent }}
          >
            <h2>{b.title}</h2>
            <b>{b.author}</b>
            <p>{b.description}</p>
          </Link>
        ))}
      </div>
      {!books.length && <p>No books have been approved for discovery yet.</p>}
    </main>
  );
}
function Book() {
  const { slug } = useParams();
  const [book, setBook] = useState<Publication>();
  const [error, setError] = useState("");
  const [answer, setAnswer] = useState("");
  const [remaining, setRemaining] = useState<number>();
  useEffect(() => {
    api<{ remaining: number }>("/visitor").then((v) =>
      setRemaining(v.remaining),
    );
    api<Publication>("/books/" + slug)
      .then(setBook)
      .catch((e) => setError(errorMessage(e)));
  }, [slug]);
  if (!book)
    return (
      <main>
        <p>{error || "Opening book…"}</p>
      </main>
    );
  return (
    <main className="reading" style={{ borderTopColor: book.accent }}>
      <div className="eyebrow">A LIVE AUTHOR BOOK</div>
      <h1>{book.title}</h1>
      <p className="byline">by {book.author}</p>
      <button
        className="secondary"
        onClick={async () => {
          try {
            await api("/test/reader-purchase", {
              slug,
              confirmed: true,
              offer: "synthetic-reader-zero-v1",
            });
            setBook(await api<Publication>("/books/" + slug));
            setError("Simulated reader purchase confirmed; no charge.");
          } catch (e) {
            setError(errorMessage(e));
          }
        }}
      >
        Confirm test reading access ($0)
      </button>
      <p className="lead">{book.description}</p>
      {book.media?.map((m) => (
        <figure key={m.id}>
          <img
            src={"/api/books/" + book.slug + "/assets/" + m.id}
            alt={m.alt}
            style={{ maxWidth: "100%" }}
          />
          <figcaption>{m.alt}</figcaption>
        </figure>
      ))}
      <p className="small">
        {book.aiDisclosure} · Released sample: {book.units.length} chapters
      </p>
      {book.units.map((u) => (
        <section key={u.id}>
          <h2>{u.title}</h2>
          <pre>{u.text}</pre>
        </section>
      ))}
      <section>
        <h2>Stay a little longer.</h2>
        <p>
          Discuss released chapters with a simulated AI reader guide. Five
          questions per visitor in this local test; no automatic refill.
        </p>
        <p>
          {remaining === undefined
            ? "Up to five free questions"
            : `${remaining} questions remaining`}
        </p>
        <form
          onSubmit={async (e) => {
            e.preventDefault();
            const f = new FormData(e.currentTarget);
            try {
              const r = await api<{ answer: string; remaining: number }>(
                "/books/" + slug + "/chat",
                {
                  question: f.get("question"),
                  ceiling: Number(f.get("ceiling")),
                  mode: f.get("mode"),
                  characterId: String(f.get("characterId") || ""),
                  requestId: crypto.randomUUID(),
                },
              );
              setAnswer(r.answer);
              setRemaining(r.remaining);
              setError("");
            } catch (e) {
              setError(errorMessage(e));
            }
          }}
        >
          <div className="row">
            <label>
              Spoiler ceiling
              <select name="ceiling">
                {book.units.map((u, i) => (
                  <option key={u.id} value={i + 1}>
                    Through {u.title}
                  </option>
                ))}
              </select>
            </label>
            <label>
              Conversation
              <select name="mode">
                <option value="chapter">Chapter discussion</option>
                <option value="character" disabled={!book.characters?.length}>
                  Character interpretation
                </option>
              </select>
            </label>
          </div>
          {!!book.characters?.length && (
            <label>
              Character
              <select name="characterId">
                {book.characters.map((c) => (
                  <option value={c.id} key={c.id}>
                    {c.name}
                  </option>
                ))}
              </select>
            </label>
          )}
          <label>
            Your question
            <textarea name="question" required />
          </label>
          <button>Ask within my reading boundary</button>
        </form>
        {answer && <p role="status">{answer}</p>}
        {error && <p role="alert">{error}</p>}
      </section>
    </main>
  );
}
type Asset = {
  id: string;
  name: string;
  alt: string;
  provenance: string;
  approved: number;
};
function MediaPanel({
  p,
  onAction,
}: {
  p: Project;
  onAction: (fn: () => Promise<unknown>) => Promise<void>;
}) {
  const [assets, setAssets] = useState<Asset[]>([]);
  const refresh = async () =>
    setAssets(await api<Asset[]>("/projects/" + p.id + "/assets"));
  useEffect(() => {
    refresh().catch(() => {});
  }, [p.id]);
  const brief = p.versions.find(
    (v) => v.kind === "media_brief" && p.heads[v.key] === v.id,
  );
  return (
    <section>
      <h2>Images and generated results</h2>
      <p>
        Imported PNG/JPEG files are private and limited to 90 KB in this test
        UI. Signature checking is implemented; production malware scanning is a
        launch gate.
      </p>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          const f = new FormData(e.currentTarget);
          onAction(async () => {
            const file = f.get("file") as File;
            const bytes = new Uint8Array(await file.arrayBuffer());
            if (bytes.length > 90000)
              throw Error("Choose an image under 90 KB for this local test");
            await api("/projects/" + p.id + "/assets", {
              name: file.name,
              mime: file.type,
              base64: btoa(String.fromCharCode(...bytes)),
              alt: String(f.get("alt")),
              provenance: String(f.get("provenance")),
              rights: true,
            });
            await refresh();
          });
        }}
      >
        <label>
          Image file
          <input
            type="file"
            name="file"
            accept="image/png,image/jpeg"
            required
          />
        </label>
        <label>
          Description / alt text
          <input name="alt" required />
        </label>
        <label>
          Provenance
          <select name="provenance">
            <option>human</option>
            <option>AI-generated</option>
            <option>AI-assisted</option>
          </select>
        </label>
        <label className="check">
          <input type="checkbox" required />I confirm the rights to import this
          image
        </label>
        <button>Import private image</button>
      </form>
      {brief && (
        <button
          className="secondary"
          onClick={() =>
            onAction(async () => {
              await api("/projects/" + p.id + "/media/generate", {
                briefId: brief.id,
                kind: "image",
                requestKey: crypto.randomUUID(),
                confirmed: true,
              });
              await refresh();
            })
          }
        >
          Generate test image from approved brief ($0)
        </button>
      )}
      {assets.map((asset) => (
        <article key={asset.id}>
          <img
            src={"/api/projects/" + p.id + "/assets/" + asset.id}
            alt={asset.alt}
            style={{ maxWidth: 200, maxHeight: 200 }}
          />
          <p>
            {asset.name} · {asset.provenance} ·{" "}
            {asset.approved ? "Approved" : "Needs your choice"}
          </p>
          <button
            className="secondary"
            onClick={() =>
              onAction(async () => {
                await api("/projects/" + p.id + "/assets/" + asset.id, {
                  approved: true,
                  rights: true,
                  alt: asset.alt,
                  spoiler: 0,
                });
                await refresh();
              })
            }
          >
            Approve result and rights
          </button>
        </article>
      ))}
    </section>
  );
}
function ReaderLibrary() {
  const [items, setItems] = useState<{ book_slug: string; status: string }[]>(
    [],
  );
  const [error, setError] = useState("");
  useEffect(() => {
    api<typeof items>("/account/library")
      .then(setItems)
      .catch((e) => setError(errorMessage(e)));
  }, []);
  return (
    <main>
      <h1>Your reading library</h1>
      {error && <p role="alert">{error}</p>}
      {items.map((item) => (
        <p key={item.book_slug}>
          <Link to={"/books/" + item.book_slug}>{item.book_slug}</Link> ·{" "}
          {item.status}
        </p>
      ))}
    </main>
  );
}
function Review() {
  const { id } = useParams();
  const [data, setData] = useState<{
    snapshot: Version[];
    comments: boolean;
    download: boolean;
  }>();
  const [error, setError] = useState("");
  useEffect(() => {
    api<{ snapshot: Version[]; comments: boolean; download: boolean }>(
      "/review/" + id,
    )
      .then(setData)
      .catch((e) => setError(errorMessage(e)));
  }, [id]);
  return (
    <main className="reading">
      <h1>Private review</h1>
      {error && <p role="alert">{error}</p>}
      {data?.snapshot.map((v) => (
        <section key={v.id}>
          <h2>{v.key}</h2>
          <pre>{v.text}</pre>
        </section>
      ))}
      {data?.download && (
        <a
          className="button secondary"
          href={"/api/review/" + id + "?download=1"}
        >
          Download authorized snapshot
        </a>
      )}
      {data?.comments && (
        <form
          onSubmit={async (e) => {
            e.preventDefault();
            const f = new FormData(e.currentTarget);
            try {
              await api("/review/" + id, { text: f.get("text") });
              setError("Comment saved to this snapshot.");
            } catch (e) {
              setError(errorMessage(e));
            }
          }}
        >
          <label>
            Your comment
            <textarea name="text" required />
          </label>
          <button>Save comment</button>
        </form>
      )}
      <p className="small">
        Exact approved snapshot. New drafts do not appear automatically. Saved
        copies cannot be recalled.
      </p>
    </main>
  );
}
createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <BrowserRouter>
      <ScrollToTop />
      <Routes>
        <Route path="/write/*" element={<Write />} />
        <Route path="/read/:token" element={<Reader />} />
        <Route path="/" element={<SiteHome />} />
        <Route path="/samples" element={<SiteSamples />} />
        <Route path="/readers-room" element={<ReadersRoom />} />
        <Route path="/pro" element={<SitePro />} />
        <Route path="/live-reader" element={<LiveReaderPage />} />
        {/* Promotional offers (src/shared/offers.ts): one line each, slug reserved in RESERVED_SLUGS. */}
        <Route path="/caddofree" element={<OfferPage slug="caddofree" />} />
        {guideRoutes.map(({ path, g }) => (
          <Route key={path} path={path} element={<GuidePage g={g} />} />
        ))}
        <Route path="/terms" element={<Terms />} />
        <Route path="/privacy" element={<SitePrivacy />} />
        <Route path="/refunds" element={<Refunds />} />
        <Route path="/support" element={<Support />} />
        <Route path="*" element={<BookOrApp>
        <Shell>
        <Routes>
          <Route path="/founder" element={<FounderTraining />} />
          <Route path="/" element={<Landing />} />
          <Route path="/studio" element={<StudioHome />} />
          <Route path="/studio/:id" element={<Studio />} />
          <Route path="/books" element={<Catalog />} />
          <Route path="/account/library" element={<ReaderLibrary />} />
          <Route path="/books/:slug/*" element={<Book />} />
          <Route path="/review/:id" element={<Review />} />
          <Route
            path="*"
            element={
              <main>
                <h1>Page unavailable</h1>
                <Link to="/">Return home</Link>
              </main>
            }
          />
        </Routes>
      </Shell>
      </BookOrApp>} />
      </Routes>
    </BrowserRouter>
  </React.StrictMode>,
);
