AI entry point
A button that sits next to the object it acts on and pairs the sparkle with a verb. The verb and the object are required props, because the icon alone says that something is AI without saying what it does, and the accessible name has to describe the action rather than the brand.
npx shadcn@latest add button
npm i lucide-react
Tokens this needs: --card, --card-foreground, --muted-foreground, --border, --input, --chart-1
────────────────────────────────────────────────────────────────────────
// AiEntryPoint.tsx
────────────────────────────────────────────────────────────────────────
import { Sparkle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
/**
* An entry point is a button that sits next to the thing it acts on and says
* what it does. Both halves are props the caller cannot leave out. `verb`,
* because the sparkle became a genre marker in about two years and now says
* that something is AI rather than what it does. `object`, because the
* accessible name has to describe the action on the thing, and a screen
* reader hearing the product's name learns nothing.
*
* Where it goes is the caller's job, and the point: at the selection for a
* passage, in the header for the document, beside the table for the data.
* A permanent slot in the global toolbar is the one placement this component
* does not offer a style for.
*/
export type Placement = "document" | "selection";
type Props = {
/** What pressing it does, as an imperative: "summarise", "rewrite this". */
verb: string;
/** What it acts on. Only the accessible name carries it; the button beside
* the object does not need to repeat what the eye can see. */
object: string;
/** "document" waits quietly in the header. "selection" rode in on a signal
* the viewer made, so it is marked as belonging to the passage. */
placement: Placement;
onInvoke: () => void;
/** The keyboard path, for people who never reach for the button. */
shortcut?: string;
};
export function AiEntryPoint({ verb, object, placement, onInvoke, shortcut }: Props) {
const name = `${verb}: ${object}`;
return (
);
}
────────────────────────────────────────────────────────────────────────
// demo.tsx
────────────────────────────────────────────────────────────────────────
import { useState } from "react";
import { AiEntryPoint } from "./AiEntryPoint";
/**
* A document with two entry points, each next to its object. "summarise" waits
* in the header because its object is the whole page. "rewrite this" appeared
* because a sentence was selected, and goes away with the selection.
*/
const BEFORE = "Revenue for the quarter came in at £1.84m, three percent ahead of plan, with services carrying most of the beat. ";
const SELECTED = "Costs were higher than we had budgeted for on account of the fact that hiring ran ahead of the plan in the second half of the quarter.";
const AFTER = " Cash at the end of the period stood at £6.2m.";
const REWRITTEN = "Costs ran over budget because hiring outpaced the plan in the second half.";
export default function Demo() {
const [selected, setSelected] = useState(true);
const [sentence, setSentence] = useState(SELECTED);
const [summary, setSummary] = useState();
const rewrite = () => {
setSentence(REWRITTEN);
setSelected(false);
};
return (
FileEditView
Q3 review
setSummary("Revenue £1.84m, 3% over plan. Costs over budget on hiring. Cash £6.2m.")}
/>