Costruire la tua prima Generative UI con Vercel AI SDK
Guida passo dopo passo per creare la tua prima interfaccia AI con componenti in streaming.
Prerequisiti
Prima di iniziare, assicurati di avere:
- Node.js 18+ installato
- Un progetto Next.js 14+ che utilizza App Router
- Una chiave API OpenAI (o Anthropic — l'SDK supporta entrambi)
- Familiarità di base con i React Server Components
Se non hai mai lavorato con gli RSC, dedica 15 minuti alla documentazione di Next.js sui Server Components. La funzione streamUI del Vercel AI SDK dipende dagli RSC e diventa molto più chiara una volta che ne hai capito il modello.
Cosa stiamo costruendo
Realizzeremo un semplice assistente AI che genera interfacce interattive a partire dai prompt dell'utente. Al termine di questo tutorial avrai una funzionalità Generative UI funzionante che:
- Riceve un prompt testuale dall'utente
- Fa lo streaming di componenti React dal server
- Renderizza card e grafici interattivi in base alle decisioni dell'AI
Il dominio di esempio è un assistente finanziario che mostra prezzi azionari e dati meteo — abbastanza semplice da capire rapidamente, abbastanza complesso da illustrare pattern reali.
⚠️ AI SDK RSC e
streamUIsono contrassegnati come sperimentali da Vercel. Per i progetti in produzione Vercel raccomanda AI SDK UI (useChatda@ai-sdk/react). Questo articolo mostra un pattern di streaming RSC funzionante per prototipi, demo e ambienti controllati; per la produzione, valuta i trade-off e vedi «Quando Vercel AI SDK NON è la tua scelta». Guida alla migrazione RSC → UI.
Passo 1: Installare le dipendenze
npm install ai@^4 @ai-sdk/openai@^1 zod
Fissa la v4 — l'ultima serie con l'API RSC nella forma parameters: e l'import ai/rsc. Per v5+, vedi la nota sulle differenze alla fine dell'articolo.
Il pacchetto ai è il core del Vercel AI SDK. @ai-sdk/openai è il provider OpenAI (sostituiscilo con @ai-sdk/anthropic se preferisci Claude). zod gestisce la validazione dei parametri degli strumenti — è il modo in cui definisci quali parametri l'AI può passare a ciascun componente.
Aggiungi la tua chiave API al file .env.local:
OPENAI_API_KEY=sk-...
Passo 2: Creare la libreria di componenti
Definisci i componenti che l'AI può generare. Sono normali componenti React — nulla di specifico per l'AI. Il principio di design chiave: costruisci componenti utili anche da soli, e saranno componibili dall'AI.
// components/weather-card.tsx
interface WeatherCardProps {
city: string;
temperature: number;
conditions: string;
humidity: number;
}
export function WeatherCard({ city, temperature, conditions, humidity }: WeatherCardProps) {
return (
<div className="rounded-lg border bg-card p-6 shadow-sm">
<h3 className="text-lg font-semibold">{city}</h3>
<div className="mt-2 flex items-baseline gap-2">
<span className="text-4xl font-bold">{temperature}°C</span>
<span className="text-muted-foreground">{conditions}</span>
</div>
<p className="mt-2 text-sm text-muted-foreground">
Humidity: {humidity}%
</p>
</div>
);
}
// components/stock-ticker.tsx
interface StockTickerProps {
symbol: string;
price: number;
change: number;
changePercent: number;
}
export function StockTicker({ symbol, price, change, changePercent }: StockTickerProps) {
const isPositive = change >= 0;
const sign = isPositive ? '+' : '';
const color = isPositive ? 'text-green-600' : 'text-red-600';
return (
<div className="rounded-lg border bg-card p-6 shadow-sm">
<div className="flex items-center justify-between">
<h3 className="text-xl font-bold">{symbol}</h3>
<span className={`text-sm font-medium ${color}`}>
{sign}{changePercent.toFixed(2)}%
</span>
</div>
<div className="mt-2 flex items-baseline gap-2">
<span className="text-3xl font-bold">${price.toFixed(2)}</span>
<span className={`text-sm ${color}`}>
{sign}{change.toFixed(2)} today
</span>
</div>
</div>
);
}
// components/loading-skeleton.tsx
export function CardSkeleton({ height = 'h-32' }: { height?: string }) {
return (
<div className={`animate-pulse rounded-lg bg-muted ${height} w-full`} />
);
}
Passo 3: Definire gli strumenti AI (Server Action)
Questo è il cuore della Generative UI. Crea una server action che connette i tuoi componenti all'AI come "strumenti" — funzioni che il modello può decidere di chiamare:
// app/actions.tsx
'use server';
export const runtime = 'edge';
import { streamUI } from 'ai/rsc';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
import { WeatherCard } from '@/components/weather-card';
import { StockTicker } from '@/components/stock-ticker';
import { CardSkeleton } from '@/components/loading-skeleton';
export async function generateUI(prompt: string) {
const result = await streamUI({
model: openai('gpt-4o'),
system: `You are a helpful financial and information assistant.
Use the available tools to display information visually
whenever possible. Prefer showing components over text responses.
When asked about weather or stocks, always use the appropriate tool.`,
prompt,
tools: {
showWeather: {
description: 'Display current weather conditions for a city. Use this when the user asks about weather, temperature, or climate.',
parameters: z.object({
city: z.string().describe('The city name, e.g. "Paris" or "New York"'),
temperature: z.number().describe('Current temperature in Celsius'),
conditions: z.string().describe('Weather description, e.g. "Partly cloudy"'),
humidity: z.number().min(0).max(100).describe('Relative humidity percentage'),
}),
generate: async function* (params) {
// Yield a skeleton immediately while data "loads"
yield <CardSkeleton height="h-36" />;
// In a real app, you would fetch live weather data here
return <WeatherCard {...params} />;
},
},
showStock: {
description: 'Display a stock price and daily change. Use this when the user asks about stock prices, market data, or a company\'s shares.',
parameters: z.object({
symbol: z.string().describe('Stock ticker symbol, e.g. "AAPL" or "TSLA"'),
price: z.number().describe('Current stock price in USD'),
change: z.number().describe('Price change today in USD'),
changePercent: z.number().describe('Percentage price change today'),
}),
generate: async function* (params) {
yield <CardSkeleton height="h-32" />;
return <StockTicker {...params} />;
},
},
},
});
return result.value;
}
Vale la pena capire tre aspetti di questo codice:
La funzione generate è un generatore asincrono. La parola chiave yield invia immediatamente lo skeleton — prima che l'AI finisca di risolvere i parametri. Il return invia il componente finale. Ecco come funziona lo streaming nella Generative UI.
Le descrizioni degli strumenti sono istruzioni per l'AI. I campi description sono ciò che il modello legge per decidere quale strumento chiamare. Scrivili in modo chiaro, specificando quando lo strumento va e non va usato.
Gli schema Zod garantiscono il contratto. L'AI non può passare parametri non validi se definisci schema Zod rigorosi. Gli errori di validazione vengono intercettati prima che il componente venga renderizzato.
Passo 4: Costruire l'interfaccia
// app/page.tsx
'use client';
import { useState } from 'react';
import { generateUI } from './actions';
const EXAMPLE_PROMPTS = [
"What's the weather like in Tokyo?",
"Show me Apple's current stock price",
"Compare the weather in London and New York",
"How is Tesla stock doing?",
];
export default function Home() {
const [prompt, setPrompt] = useState('');
const [messages, setMessages] = useState<Array<{ prompt: string; ui: React.ReactNode }>>([]);
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!prompt.trim() || loading) return;
const currentPrompt = prompt;
setPrompt('');
setLoading(true);
const ui = await generateUI(currentPrompt);
setMessages(prev => [...prev, { prompt: currentPrompt, ui }]);
setLoading(false);
}
return (
<main className="mx-auto max-w-2xl p-8">
<h1 className="text-3xl font-bold">Generative UI Demo</h1>
<p className="mt-2 text-muted-foreground">
Ask about weather or stocks — watch the AI generate the right interface.
</p>
{/* Example prompts */}
<div className="mt-4 flex flex-wrap gap-2">
{EXAMPLE_PROMPTS.map(p => (
<button
key={p}
onClick={() => setPrompt(p)}
className="rounded-full border px-3 py-1 text-sm hover:bg-muted"
>
{p}
</button>
))}
</div>
{/* Prompt input */}
<form onSubmit={handleSubmit} className="mt-6 flex gap-2">
<input
value={prompt}
onChange={e => setPrompt(e.target.value)}
placeholder="Ask anything..."
className="flex-1 rounded-md border bg-background px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
/>
<button
type="submit"
disabled={loading || !prompt.trim()}
className="rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground disabled:opacity-50"
>
{loading ? 'Generating...' : 'Ask'}
</button>
</form>
{/* Generated UI output */}
<div className="mt-8 space-y-6">
{messages.map((msg, i) => (
<div key={i}>
<p className="mb-2 text-sm font-medium text-muted-foreground">
"{msg.prompt}"
</p>
{msg.ui}
</div>
))}
</div>
</main>
);
}
Passo 5: Avviare e testare
npm run dev
Prova questi prompt nell'ordine per osservare comportamenti diversi:
- "What's the weather in Paris?" — una singola WeatherCard
- "Show me Apple stock" — un singolo StockTicker
- "Compare the weather in London and New York" — l'AI chiama
showWeatherdue volte, generando due card affiancate - "How's Tesla doing and what's the weather in San Francisco?" — l'AI chiama entrambi gli strumenti, generando tipi di componenti misti
Il terzo prompt è la dimostrazione fondamentale: senza aggiungere una sola riga di codice, il modello compone più componenti per rispondere a una domanda articolata.
Cosa succede sotto il cofano
Quando invii un prompt:
- Il client chiama la server action
generateUI streamUIinvia il prompt e le definizioni degli strumenti all'API OpenAI- Il modello sceglie quali strumenti chiamare e con quali parametri
- La funzione
generatedi ogni strumento produce immediatamente uno skeleton - L'AI finisce di risolvere i parametri e restituisce il componente finale
- React renderizza il componente al posto dello skeleton
Il protocollo di streaming RSC è ciò che rende tutto questo possibile. Il server serializza gli alberi di componenti React e li fa lo streaming al client in modo incrementale. Questo è diverso da un'API JSON — il client riceve componenti già renderizzati, non dati grezzi.
Gestione degli errori
I componenti generati possono fallire in modi che i componenti scritti a mano non hanno. C'è però una sottigliezza da tenere a mente: i React error boundary intercettano solo gli errori in fase di rendering. Un errore dello stream (caduta di rete, timeout OpenAI, errore dello strumento lato server) non verrà intercettato dall'error boundary — devi gestirlo esplicitamente in handleSubmit.
Difenditi su due livelli — try/catch attorno allo stream, e un error boundary attorno alla UI renderizzata:
// components/genui-error-boundary.tsx
'use client';
import { Component, ReactNode } from 'react';
interface Props { children: ReactNode; fallback?: ReactNode }
interface State { hasError: boolean; error: Error | null }
export class GenUIErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? (
<div className="rounded-lg border border-destructive/50 bg-destructive/5 p-4">
<p className="text-sm text-destructive">
Impossibile renderizzare il componente. L'AI potrebbe aver passato dati inattesi.
</p>
</div>
);
}
return this.props.children;
}
}
E gestisci gli errori dello stream lato client:
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!prompt.trim() || loading) return;
const currentPrompt = prompt;
setPrompt('');
setLoading(true);
try {
const ui = await generateUI(currentPrompt);
setMessages(prev => [...prev, { prompt: currentPrompt, ui }]);
} catch (err) {
// Errori di rete, timeout OpenAI, errori della server action — tutto finisce qui
setMessages(prev => [...prev, {
prompt: currentPrompt,
ui: <div className="text-sm text-destructive">Stream fallito. Riprova.</div>,
}]);
} finally {
setLoading(false);
}
}
Racchiudi la UI generata con GenUIErrorBoundary nella pagina — gestisce gli errori di rendering, mentre il try/catch gestisce tutto il resto.
Consigli per il deployment
Variabili d'ambiente: OPENAI_API_KEY deve essere disponibile nel tuo ambiente di produzione. Su Vercel, aggiungila nelle impostazioni del progetto alla voce Environment Variables.
Edge runtime: La funzione streamUI funziona sull'Edge runtime, riducendo significativamente i tempi di cold start. Aggiungi export const runtime = 'edge' al file della tua server action.
Rate limiting: Senza rate limiting, un singolo utente potrebbe generare migliaia di richieste AI. Aggiungi un rate limiter prima della chiamata a streamUI. Il pacchetto @upstash/ratelimit si integra bene con Next.js.
Selezione del modello: gpt-4o produce le migliori selezioni di componenti ma ha costi più alti. gpt-4o-mini è circa 15 volte più economico su input/output (openai.com/api/pricing, 2026-05) e funziona bene per set di componenti semplici. Testa entrambi con le tue definizioni di strumenti specifiche.
Prossimi passi
Questo tutorial ha coperto i fondamentali. Per una Generative UI in produzione:
- Aggiungi più strumenti — ogni nuovo componente che aggiungi al registro espande ciò a cui l'AI può rispondere
- Implementa il caching dei risultati degli strumenti — metti in cache le query comuni per ridurre latenza e costi
- Aggiungi testo in streaming accanto ai componenti UI in modo che l'AI possa spiegare cosa sta mostrando
- Usa output strutturati per una generazione di parametri più affidabile
- Imposta l'osservabilità — registra ogni chiamata agli strumenti, i relativi parametri e le interazioni degli utenti
La documentazione del Vercel AI SDK copre tutti questi pattern in dettaglio, e il repository degli esempi contiene template di partenza di livello produttivo che vale la pena studiare.
Quando NON usare Vercel AI SDK {#quando-non-usare-vercel-ai-sdk}
L'SDK è solido, ma non è una soluzione universale. Evitalo se:
- L'SDK è contrassegnato come sperimentale — limitazioni documentate: gli stream non possono essere interrotti dalle server action, i componenti si rimontano al
.done()(flickering), molti boundary<Suspense>possono mandare in crash la pagina,createStreamableUIproduce un volume di trasferimento quadratico. Per la produzione Vercel raccomanda AI SDK UI. - Non sei su Next.js.
streamUIè costruito su React Server Components, che richiedono il Next.js App Router (o Waku / un altro framework RSC-aware). Per Vite SPA, Remix senza RSC, Vue, Svelte o Angular — vedi le alternative sotto. - Hai bisogno di una UI fissa con dati dinamici. Se l'interfaccia è nota in anticipo e l'LLM deve solo riempire i dati, usa
generateObjectsemplice + il tuo React statico. La Generative UI conviene quando è l'AI a decidere quali componenti mostrare. - Privacy rigorosa o deployment on-prem. L'SDK assume provider hosted (OpenAI, Anthropic). Per LLM self-hosted, un thin layer sopra
vLLM/Ollamapiù il tuo registro di componenti è più semplice. - Il budget di token è critico. Ogni rendering è una chiamata LLM. Oltre MAU > 10k senza caching aggressivo, i costi di gpt-4o possono superare $1k/mese.
Alternative per progetti non Next.js
| Strumento | Stack | Quando sceglierlo |
|---|---|---|
| Thesys C1 API | Qualsiasi (HTTP API) | SaaS che restituisce blocchi UI pronti al rendering via schema JSON. Ottimo per team senza competenze RSC. |
| CopilotKit | React (Next.js + Vite + Remix) | Copilot in-app con stato e azioni. Supporta Generative UI tramite useCopilotAction. |
| Tambo | React (universale) | Catalogo di componenti come concetto di prima classe. Funziona su Vite, senza RSC. |
| A2UI | Qualsiasi (Google) | Formato UI JSON dichiarativo di Google per gli agenti. Agnostico al renderer, funziona su qualsiasi frontend. |
| assistant-ui | React | Libreria chat-first con supporto agli strumenti UI. Base solida per copilot su qualsiasi app React. |
| Fai da te | Qualsiasi | Se hai bisogno di 2–3 tipi di componenti e il controllo è importante — registry + generateObject + uno switch lato client sono ~150 righe. |
Per Vue / Svelte / Angular a maggio 2026, non esiste un equivalente di livello produzione del Vercel AI SDK. La maggior parte dei team distribuisce un thin client verso un'API che restituisce una descrizione JSON del componente e la renderizza autonomamente.
Dimensione del team e TCO
Prima di portare Vercel AI SDK in produzione, valuta la prontezza del team e dello stack:
Competenze richieste:
- React Server Components — senza questa,
streamUIè una scatola nera al primo bug. - TypeScript — gli schema Zod e i parametri degli strumenti degradano senza i tipi.
- Generatori asincroni (
async function*,yield) — non tutti i React engineer di livello medio li hanno usati. - Prompt engineering — le descrizioni degli strumenti e il system prompt definiscono la qualità della selezione dei componenti.
Dimensione del team e TCO (approssimativo, maggio 2026):
| Dimensione | Tempo di ingegneria | Costo LLM (MAU 1k) | Costo LLM (MAU 10k) | Fattibile? |
|---|---|---|---|---|
| Solo (1) | 2–3 settimane per MVP | ~$50/mese (gpt-4o-mini) | ~$500/mese | Sì, ideale per progetti personali |
| Piccolo (2–4) | 4–6 settimane per v1 | ~$150/mese (mix gpt-4o) | ~$1.500/mese | Sì, caso d'uso principale |
| Medio (5–15) | 2–3 mesi per integrazione completa | ~$300/mese | ~$3k–5k/mese | Sì, se esiste una piattaforma |
| Grande (15+) | 4–6 mesi + team di piattaforma | budget negoziato | $10k+/mese | Vale la pena valutare LLM self-hosted |
Metodologia TCO: cifre calcolate sotto queste ipotesi — prompt medio ~800 token di input + ~300 di output su gpt-4o (o ~$0,001 su gpt-4o-mini), 1 richiesta/sessione, prezzi OpenAI al 2026-05, MAU ≈ DAU × 30%. Calibra al tuo carico di lavoro reale.
Su AI SDK v5/v6
Se stai usando versioni più recenti dell'SDK, le differenze principali rispetto al codice in questo articolo:
parameters:nella definizione dello strumento →inputSchema:import { streamUI } from 'ai/rsc'→import { streamUI } from '@ai-sdk/rsc'- RSC rimane contrassegnato come sperimentale da Vercel — per la produzione, AI SDK UI (
useChat) è raccomandato.
Vuoi implementare la Generative UI nel tuo prodotto? Parliamo del tuo caso d'uso — nella nostra esperienza di consulenza, lo stack GenUI si adatta bene a dashboard e strumenti interni; per superfici regolamentate e pagine pubbliche ad alto traffico i trade-off di solito non tornano.
Dichiarazione: i link a prodotti esterni (Thesys, CopilotKit, Tambo, Vercel, Fly.io, Render, Railway, Cloudflare, Upstash) sono raccomandazioni organiche; nessun programma di affiliazione, nessun posizionamento a pagamento. Prezzi accurati al 2026-05-11.
Alex
Ingegnere e Consulente Generative UI
Ingegnere senior specializzato in interfacce AI e sistemi Generative UI. Aiuta i team di prodotto a rilasciare più velocemente con il giusto stack GenUI.
Articoli correlati
Κατασκευάζοντας το Πρώτο σας Generative UI με το Vercel AI SDK
Βήμα-βήμα οδηγός για τη δημιουργία της πρώτης σας AI-powered διεπαφής με streaming συστατικά.
Προσβασιμότητα σε Generative UI: Δημιουργία Συμπεριληπτικών AI Διεπαφών
Πρακτικός οδηγός για προσβάσιμα γεννητικά interfaces — screen readers, πλοήγηση με πληκτρολόγιο και συνδυαστικά προβλήματα προσβασιμότητας.
CopilotKit vs Vercel AI SDK vs Thesys: Σύγκριση Frameworks
Μια ειλικρινής σύγκριση των τριών κύριων frameworks Generative UI, με πλεονεκτήματα, μειονεκτήματα και πότε να χρησιμοποιείτε το καθένα.
Resta aggiornato su Generative UI
Articoli settimanali, aggiornamenti sui framework e guide pratiche di implementazione — direttamente nella tua casella di posta.
Hai bisogno di aiuto per implementare quello che hai appena letto?
Prenota una consulenza gratuita