Pricing page draft
{intro}
{PLANS.map((p) =>{p}
)}Assistant sidebar
A panel down one edge, worth its width only if it can see the work beside it. It names what it currently sees, routes an answer back into the document, and every pixel it takes comes off the thing being written.
npx shadcn@latest add resizable button input
npm i lucide-react
Tokens this needs: --background, --foreground, --card, --card-foreground, --muted, --muted-foreground, --border, --input, --ring, --chart-1
────────────────────────────────────────────────────────────────────────
// AssistantSidebar.tsx
────────────────────────────────────────────────────────────────────────
import { useEffect, useState, type ReactNode } from "react";
import { CornerDownLeft, PanelRightOpen } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
/** One turn in the panel. `insert` is the text an answer offers to put into
* the document; an answer without one is a remark, and gets no button. */
export type Turn = { role: "user" | "assistant"; text: string; insert?: string };
type Props = {
/** What the assistant can currently see, in the viewer's words. Required:
* a panel that will not say gets the question answered by experiment. */
scope: string;
onChangeScope: () => void;
turns: Turn[];
onSend: (text: string) => void;
/** The path from an answer back into the document. A button, never a
* copy and paste. */
onInsert: (text: string) => void;
};
export function AssistantSidebar({ scope, onChangeScope, turns, onSend, onInsert }: Props) {
const [draft, setDraft] = useState("");
const send = () => {
if (!draft.trim()) return;
onSend(draft.trim());
setDraft("");
};
return (
);
}
/** The work on the left, the panel down the right edge, one keystroke to open
* and the same one to close. The split is the viewer's and is remembered
* under `id`. Closed, the panel collapses to a rail rather than vanishing. */
export function AssistantLayout({ id, document, sidebar, open, onToggle, defaultWidth = 38 }: {
id: string;
document: ReactNode;
sidebar: ReactNode;
open: boolean;
onToggle: () => void;
/** Percent of the group. Prose wants roughly 360 to 420px to read. */
defaultWidth?: number;
}) {
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === "j") { e.preventDefault(); onToggle(); }
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [onToggle]);
return (
{intro}
{PLANS.map((p) =>{p}
)}