"use client"; import { cn } from "@/lib/utils"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { type Unstable_InteractableToolRenderProps as InteractableToolRenderProps } from "@assistant-ui/react"; import { CheckIcon, CopyIcon, RotateCcwIcon, SquarePenIcon, } from "lucide-react"; import { useEffect, useRef, useState, type FC } from "react"; import { z } from "zod"; // Mirrors the `notepad` tool's parameters schema in docs-toolkit.tsx (which // can't import from this client module on the server). const notepadStateSchema = z.object({ title: z .string() .describe("A short title, shown as the heading of the notepad card."), content: z .string() .describe( "The full plain text, shown to the user in the body of the notepad card.", ), }); export type NotepadArgs = z.infer; const NotepadCard: FC<{ title: React.ReactNode; actions?: React.ReactNode; muted?: boolean; children: React.ReactNode; }> = ({ title, actions, muted, children }) => (
{title}
{actions}
{children}
); const titleClass = "text-foreground focus:bg-accent min-w-0 max-w-full truncate rounded-md bg-transparent px-1 py-0.5 -mx-1 text-sm font-semibold outline-none"; const CopyButton: FC<{ content: string }> = ({ content }) => { const [copied, setCopied] = useState(false); const copyTimeout = useRef>(undefined); const copy = () => { navigator.clipboard?.writeText(content).catch(() => {}); setCopied(true); clearTimeout(copyTimeout.current); copyTimeout.current = setTimeout(() => setCopied(false), 1400); }; return ( {copied ? ( ) : ( )} ); }; type NotepadVersion = NonNullable< InteractableToolRenderProps["version"] >; type UpdateNote = (updater: (prev: NotepadArgs) => NotepadArgs) => void; const sameNote = (a: NotepadArgs, b: NotepadArgs) => a.title === b.title && a.content === b.content; const StreamingNotepad: FC<{ state: NotepadArgs }> = ({ state }) => ( {state.title || "Drafting note..."} } >
{state.content}
); const NotepadEditor: FC<{ note: NotepadArgs; setNote: UpdateNote; onRestore?: () => void; }> = ({ note, setNote, onRestore }) => { const bodyRef = useRef(null); useEffect(() => { const node = bodyRef.current; if (!node) return; if (document.activeElement === node) return; if (node.innerText !== note.content) node.innerText = note.content; }, [note.content]); return ( setNote((prev) => ({ ...prev, title: e.target.value })) } /> } actions={ <> {onRestore && ( <>
)} } >
setNote((prev) => ({ ...prev, content: bodyRef.current?.innerText ?? prev.content, })) } /> ); }; const HistoricalNotepad: FC<{ version: NotepadVersion }> = ({ version }) => { const [note, setNote] = useState(version.state); const canRestore = !sameNote(note, version.state); return ( setNote(updater)} {...(canRestore ? { onRestore: () => setNote(version.state) } : {})} /> ); }; const LiveNotepad: FC<{ state: NotepadArgs; setState: InteractableToolRenderProps["setState"]; version: InteractableToolRenderProps["version"]; }> = ({ state, setState, version }) => { const canRestore = version && !sameNote(state, version.state); return ( setState(updater)} {...(canRestore ? { onRestore: version.restore } : {})} /> ); }; export const Notepad: FC> = ({ state, setState, version, streaming, }) => { if (streaming) return ; if (version && !version.isLatest) return ; return ; };