opncrafter
Module 7 of 10: Generative UI

Dynamic AI Form Generation

Dynamic AI Form Generation

Jan 3, 2026 • 18 min read

Sometimes the best UI is a form. "Book me a flight." → The LLM shouldn't just say "Done". It should present a Confirmation Form ("Please review these details") before execution.

1. The "Human-in-the-Loop" Pattern

Why? Users trust AI to draft content, but they want to verify actions (e.g. sending money, deleting files).
  1. User: "Create a Jira ticket for this bug."
  2. LLM: "I will generate a ticket. Please confirm the details."
  3. UI Renders: A form with "Title", "Description", "Priority" pre-filled.
  4. User: Edits "Priority" from Medium to High. Hits "Submit".
  5. Server Action: `createJiraTicket(formData)` is called.

2. Implementation with React Hook Form

Don't reinvent the wheel. Use react-hook-form and zod. The layout is static, but the values are generative.

'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

// 1. Define the Schema (Shared between Client & AI)
const TicketSchema = z.object({
  title: z.string().min(5, "Title too short"),
  description: z.string(),
  priority: z.enum(['Low', 'Medium', 'High']),
  email: z.string().email()
});

export function TicketForm({ defaultValues, action }) {
  const form = useForm({
    resolver: zodResolver(TicketSchema),
    defaultValues // <--- Populated by LLM
  });

  return (
    <form onSubmit={form.handleSubmit(action)} className="space-y-4">
      <div>
        <label>Title</label>
        <input {...form.register('title')} className="dark-input" />
        {form.formState.errors.title && <p className="text-red-500">{form.formState.errors.title.message}</p>}
      </div>
      
      <div>
        <label>Priority</label>
        <select {...form.register('priority')} className="dark-select">
          <option>Low</option>
          <option>Medium</option>
          <option>High</option>
        </select>
      </div>

      <button type="submit" className="btn-primary" disabled={form.formState.isSubmitting}>
        {form.formState.isSubmitting ? 'Creating...' : 'Create Ticket'}
      </button>
    </form>
  );
}

3. The Server Side (Generating the Form)

How does the AI know to render this?

// actions.tsx (Tool Definition)
generate: async function* ({ ...args }) {
    // args = { title: "Fix bug", priority: "High" } inferred from chat
    
    yield <BotCard>Generating form...</BotCard>;
    
    return (
        <BotCard>
            <TicketForm 
                defaultValues={args} 
                action={async (data) => {
                    'use server';
                    // 1. RE-VALIDATE ON SERVER (Security)
                    const validData = TicketSchema.parse(data);
                    
                    // 2. EXECUTE
                    await jira.issues.create(validData);
                    
                    return <div className="text-green-400">Ticket Created!</div>;
                }} 
            />
        </BotCard>
    );
}

4. Advanced: Streaming Partial Forms

What if the form is HUGE? You don't want to wait for the whole JSON.

New Vercel AI SDK Feature: useObject allows you to stream a structured object to the client.

This allows you to verify the title while the description is still generating.

import { experimental_useObject as useObject } from 'ai/react';

const { object, submit } = useObject({
  api: '/api/generate-ticket',
  schema: TicketSchema,
});

// The input field updates in realtime as 'object.title' streams in!
<input value={object?.title} />

5. Dynamic Dependent Fields

Sometimes the form structure itself needs to change. Use Zod Discriminated Unions.

If type == "Car", show "License Plate".
If type == "Boat", show "Hull ID".

The LLM is excellent at respecting these conditional types if you export the Zod schema to JSON Schema.

Conclusion

Forms are the ultimate interface for structured data entry. Generative AI makes them magical by pre-filling the boring parts while leaving the Human in the Loop for the critical parts.