import { useId } from "react"; import { z } from "zod"; import { GENERATED_NAME_ATTR } from "../constants"; import type { Action } from "../ir"; import { BUTTON_STYLES } from "../ir"; import type { GenerativeUIDispatch, GenerativeUILibrary, GenerativeUIStatus, } from "../types"; import { actionAttr, fire } from "./dispatch"; import { toTextContent } from "./toTextContent"; const optionSchema = z.object({ label: z.string(), value: z.string(), }); type Option = { label: string; value: string }; const isOption = (option: unknown): option is Option => option !== null && typeof option === "object" && "label" in option && typeof option.label === "string" && "value" in option && typeof option.value === "string"; type RadioGroupRenderProps = { options: Option[]; name?: string; label?: string; defaultValue?: string; $status: GenerativeUIStatus; $action?: Action; $dispatch?: GenerativeUIDispatch; }; function RadioGroupRender({ options, name, label, defaultValue, $action, $dispatch, }: RadioGroupRenderProps) { const generatedName = useId(); const fieldName = name ?? generatedName; const safeOptions = Array.isArray(options) ? options : []; return (
); } export const interactiveVocabulary = { Button: { description: "A clickable button. Carries `$action` describing the side effect or resume value. Set `submit` to submit an ancestor Form/Card instead of firing `$action` on click.", properties: z.object({ label: z.string().describe("Button label."), buttonStyle: z.enum(BUTTON_STYLES).optional().describe("Visual style."), block: z .boolean() .optional() .describe("Whether the button spans the full width."), submit: z .boolean() .optional() .describe( "Render as a submit button for an ancestor Form/Card instead of a click button.", ), }), render: ({ label, buttonStyle, block, submit, $action, $dispatch, children, }) => ( ), }, Select: { description: "A dropdown selector. Carries `$action` describing the on-select behavior.", properties: z.object({ options: z.array(optionSchema).describe("Selectable options."), placeholder: z .string() .optional() .describe("Placeholder shown when nothing is selected."), label: z .string() .optional() .describe("Accessible label for the control."), name: z.string().optional().describe("Field name used inside a Form."), }), render: ({ options, placeholder, label, name, $action, $dispatch, children, }) => { const placeholderText = toTextContent(placeholder); return ( ); }, }, Input: { description: "A text input. Carries `$action` describing the on-submit behavior.", properties: z.object({ placeholder: z.string().optional().describe("Placeholder text."), multiline: z .boolean() .optional() .describe("Render a textarea instead of a single-line input."), label: z .string() .optional() .describe("Accessible label for the control."), name: z.string().optional().describe("Field name used inside a Form."), }), render: ({ placeholder, multiline, label, name, $action, $dispatch }) => { const submit = (v: string) => fire($action, $dispatch, v); return multiline ? (