Generative UI ב-React: מדריך מעשי
למדו כיצד לממש רכיבי ממשק שנוצרו על ידי AI באפליקציות React שלכם עם דפוסים מהעולם האמיתי.
רוב פרוטוטיפי GenUI נכשלים בחמישה דפוסים אלה
דמואים של Generative UI נראים קסומים. אפליקציות GenUI בייצור נשברות בחמש דרכים צפויות: בחירת כלים שבירה, race conditions במהלך סטרימינג, אי-התאמות props בזמן-ריצה, ללא fallback כשהמודל למטה, ועלות inference ללא גבולות. מדריך זה עובר על חמשת הדפוסים שאמנם שומרים על פיצ'ר GenUI חי מעבר לשלב הדמו — registry, הפרדה, skeletons, error boundaries ומצב — בתוספת הפשרות שכל דפוס מסתיר, והכוונה קונקרטית לשני הקהלים שבדרך כלל מחליטים אם לשלוח: מנהל ההנדסה שבוחר מחסנית, ומפתח עצמאי שמדפלוי פרויקט-צד בתקציב הדוק.
למה React ל-Generative UI?
מודל הרכיבים של React מתאים באופן טבעי ל-Generative UI. רכיבים הם ניתנים להרכבה, מוקלדים, ויכולים להתרנדר בשרת או בלקוח. כשמודל AI "מייצר ממשק", מה שהוא עושה בפועל הוא בחירה והרכבה של רכיבי React עם props ספציפיים.
מדריך זה מכסה את הדפוסים שעובדים בייצור ואת הטעויות שאני רואה צוותים עושים כשהם מתחילים לבנות ממשקים גנרטיביים בפעם הראשונה. אני מניח שיש לכם סביבת Next.js עובדת ושקראתם את היסודות של Vercel AI SDK — זהו השכבה המעשית מעל אותו בסיס.
דפוס 1: רג'יסטרי הכלים
הבסיס של כל מערכת Generative UI הניתנת לתחזוקה הוא רג'יסטרי מפורש ומרוכז של הרכיבים שה-AI יכול להשתמש בהם. אל תפזרו הגדרות כלים על פני server actions שונים.
// lib/genui-registry.ts
import { z } from 'zod';
import { MetricCard } from '@/components/metric-card';
import { DataTable } from '@/components/data-table';
import { BarChart } from '@/components/bar-chart';
import { AlertBanner } from '@/components/alert-banner';
import { LineChart } from '@/components/line-chart';
export const tools = {
metricCard: {
description: 'Display a single KPI metric with a trend indicator. Use for scalar values like revenue, user count, or conversion rate.',
parameters: z.object({
label: z.string().describe('The metric name, e.g. "Monthly Revenue"'),
value: z.string().describe('The formatted value, e.g. "$12,400"'),
change: z.number().describe('Percentage change vs. previous period'),
period: z.string().describe('The comparison period, e.g. "vs last month"'),
}),
component: MetricCard,
},
dataTable: {
description: 'Display tabular data with sortable columns. Use when showing lists of items with multiple attributes.',
parameters: z.object({
columns: z.array(z.object({
key: z.string(),
label: z.string(),
numeric: z.boolean().optional(),
})),
rows: z.array(z.record(z.string())),
caption: z.string().optional(),
}),
component: DataTable,
},
barChart: {
description: 'Display a bar chart for categorical comparisons. Use when comparing values across discrete categories.',
parameters: z.object({
title: z.string(),
data: z.array(z.object({ label: z.string(), value: z.number() })),
yAxisLabel: z.string().optional(),
}),
component: BarChart,
},
lineChart: {
description: 'Display a line chart for time-series data. Use when showing trends over time.',
parameters: z.object({
title: z.string(),
data: z.array(z.object({ date: z.string(), value: z.number() })),
unit: z.string().optional(),
}),
component: LineChart,
},
alertBanner: {
description: 'Display an important notice, warning, or success message. Use sparingly for genuinely important information.',
parameters: z.object({
type: z.enum(['info', 'warning', 'error', 'success']),
title: z.string(),
message: z.string(),
}),
component: AlertBanner,
},
};
export type ToolName = keyof typeof tools;
תובנה מרכזית: שדה ה-description הוא מה שה-AI קורא כדי להחליט איזה רכיב להשתמש. כתבו תיאורים בשביל ה-AI, לא בשביל בני אדם. היו ספציפיים לגבי מתי כל רכיב מתאים, וביקורתיות — מתי הוא לא.
שימו לב ש-lineChart אומר "time-series" ו-barChart אומר "categorical." ללא הבחנה זו, ה-AI יעשה בחירות אקראיות ביניהם. ככל שהתיאורים מדויקים יותר, כך בחירת הרכיבים טובה יותר.
דפוס 2: הפרדת הרג'יסטרי מהסטרימינג
שמרו את הגדרת הרג'יסטרי נפרדת מקריאת ה-streamUI. זה מאפשר לכם לעשות שימוש חוזר בהגדרות כלים על פני כמה server actions ומאפשר לבדוק את הרג'יסטרי בבידוד.
// lib/stream-with-tools.ts
import { streamUI } from 'ai/rsc';
import { openai } from '@ai-sdk/openai';
import { tools } from './genui-registry';
// Convert registry format to streamUI format
function buildStreamTools(toolNames: ToolName[]) {
return Object.fromEntries(
toolNames.map(name => [
name,
{
description: tools[name].description,
parameters: tools[name].parameters,
generate: async function* (params: any) {
yield <ToolSkeleton name={name} />;
const Component = tools[name].component;
return <Component {...params} />;
},
},
])
);
}
// Server action for a data dashboard
export async function generateDashboard(query: string) {
const result = await streamUI({
model: openai('gpt-4o'),
system: 'You are a data analyst assistant. Display information using the appropriate visualization tool.',
prompt: query,
tools: buildStreamTools(['metricCard', 'dataTable', 'barChart', 'lineChart', 'alertBanner']),
});
return result.value;
}
// Server action for a summary view (fewer tools = better focus)
export async function generateSummary(query: string) {
const result = await streamUI({
model: openai('gpt-4o'),
system: 'You are a concise assistant. Show a summary with key metrics only.',
prompt: query,
tools: buildStreamTools(['metricCard', 'alertBanner']),
});
return result.value;
}
העברת קבוצת משנה של כלים לכל server action חשובה. קבוצת כלים ממוקדת מייצרת החלטות AI טובות יותר. אל תתנו ל-AI 20 כלים כשיספיקו 5.
דפוס 3: סטרימינג עם Skeletons
לעולם אל תציגו מסך ריק בזמן שה-AI מייצר. הציגו מצבי skeleton של טעינה שתואמים לצורת הפלט הצפויה. הרציפות הוויזואלית מפחיתה דרמטית את זמן הלטנסי הנתפס.
// components/tool-skeleton.tsx
import { ToolName } from '@/lib/genui-registry';
const SKELETON_HEIGHTS: Record<ToolName, string> = {
metricCard: 'h-28',
dataTable: 'h-48',
barChart: 'h-64',
lineChart: 'h-64',
alertBanner: 'h-16',
};
export function ToolSkeleton({ name }: { name: ToolName }) {
return (
<div
className={`animate-pulse rounded-lg bg-muted ${SKELETON_HEIGHTS[name] ?? 'h-32'} w-full`}
aria-label="Loading..."
aria-busy="true"
/>
);
}
לקבלת skeleton מדויק יותר, התאימו למבנה הפנימי של הרכיב:
export function MetricCardSkeleton() {
return (
<div className="rounded-lg border bg-card p-6">
<div className="h-4 w-24 animate-pulse rounded bg-muted" />
<div className="mt-3 h-8 w-32 animate-pulse rounded bg-muted" />
<div className="mt-2 h-3 w-16 animate-pulse rounded bg-muted" />
</div>
);
}
התאמה למבנה הפנימי אומרת שהמעבר מ-skeleton לרכיב טעון חלק — ללא layout shift, ללא ריצוד.
דפוס 4: Error Boundaries לממשק שנוצר
רכיבים שנוצרו נכשלים בדרכים שונות מרכיבים שנכתבו ידנית. ה-AI עלול להעביר string מספרי במקום מספר, ערך שלילי במקום שרק חיוביים הגיוניים, או מערך ריק לרכיב שדורש לפחות פריט אחד.
תמיד עטפו פלט שנוצר ב-error boundary:
// components/safe-genui.tsx
'use client';
import { ErrorBoundary } from 'react-error-boundary';
function GenUIFallback({ error, resetErrorBoundary }: {
error: Error;
resetErrorBoundary: () => void;
}) {
return (
<div className="rounded-lg border border-destructive/50 bg-destructive/5 p-4">
<p className="text-sm font-medium text-destructive">
רכיב זה לא הצליח להתרנדר
</p>
<p className="mt-1 text-xs text-muted-foreground">{error.message}</p>
<button
onClick={resetErrorBoundary}
className="mt-2 text-xs underline text-muted-foreground"
>
נסו שוב
</button>
</div>
);
}
export function SafeGenUI({ children }: { children: React.ReactNode }) {
return (
<ErrorBoundary FallbackComponent={GenUIFallback}>
{children}
</ErrorBoundary>
);
}
עטפו כל פלט שנוצר עם <SafeGenUI>. שגיאת רינדור ברכיב אחד לא אמורה לשבור את כל התגובה.
דפוס 5: ניהול מצב לאינטראקציות שנוצרו
רכיבים שה-AI מייצר לעיתים קרובות צריכים להיות אינטראקטיביים — טבלה שניתנת למיון, תרשים עם tooltips, טופס שמגיש נתונים. אינטראקטיביות זו חיה בתוך הרכיב עצמו ולא דורשת טיפול מיוחד.
מה שכן דורש חשיבה הוא כשהממשק שנוצר צריך להשפיע על מצב האפליקציה מחוצה לו:
// Using React context to let generated components interact with the app
export const AppStateContext = createContext<{
onDataSelected: (data: unknown) => void;
onActionTriggered: (action: string, params: unknown) => void;
} | null>(null);
// In your generated component
function DataTable({ columns, rows }: DataTableProps) {
const appState = useContext(AppStateContext);
function handleRowClick(row: Record<string, string>) {
appState?.onDataSelected(row);
}
return (
<table>
{/* ... */}
{rows.map((row, i) => (
<tr key={i} onClick={() => handleRowClick(row)} className="cursor-pointer hover:bg-muted">
{/* ... */}
</tr>
))}
</table>
);
}
עצבו את הרכיבים שנוצרים עם API ברור לאינטראקציות חיצוניות. העבירו callback props מ-context במקום לייבא state גלובלי ישירות — רכיבים שנוצרים צריכים להיות ניתנים לניוד.
טעויות נפוצות
יותר מדי כלים. אם תתנו ל-AI 50 רכיבים לבחור מהם, הוא יעשה בחירות גרועות. ראיתי צוותים שמתחילים עם 20+ כלים ואז מגלים שה-AI בוחר את הלא נכונים באופן עקבי. התחילו עם 5–8 כלים מוגדרים היטב ורחיבו רק בהתבסס על נתונים שמראים אילו שאילתות לא נענות.
תיאורים מעורפלים. "Display data" הוא לא תיאור כלי מועיל. "Display tabular data with sortable columns when showing lists of items with multiple attributes" אומר ל-AI בדיוק מתי להשתמש בו.
ללא fallback. כשמודל ה-AI לא זמין או מחזיר שגיאה, המשתמשים לא רואים כלום. תמיד הכינו ממשק סטטי fallback לנתיבים קריטיים. אם משתמשים ב-Generative UI לדשבורד נתונים, תהיה לכם תצוגה סטטית ברירת-מחדל שנטענת כשה-AI לא זמין.
דילוג על ולידציית Zod. ה-AI לפעמים מעביר props לא צפויים — string במקום מספר, null במקום ערך נדרש. ולידציית Zod קפדנית תופסת אלה לפני שמגיעים לרכיב.
over-generation. לא כל אינטראקציה צריכה Generative UI. אם רכיב סטטי עובד, השתמשו בו. GenUI מוסיף 200–800ms לטנסי ועולה כסף. השתמשו בו לאינטראקציות שבהן הווריאביליות באמת מוסיפה ערך.
אי-תיעוד קריאות לכלים. ללא תיעוד של אילו כלים ה-AI בוחר ואילו פרמטרים הוא מעביר, אין לכם נתונים לשיפור. תעדו הכל מהיום הראשון. הדפוסים שתראו אחרי שבוע שימוש ישנו את האופן שבו אתם כותבים תיאורי כלים.
רשימת בדיקה לפני שחרור
לפני שחרור Generative UI לייצור:
- כל הרכיבים שנוצרים עטופים ב-error boundaries
- מצבי skeleton של טעינה לכל כלי
- fallback סטטי כשה-AI לא זמין או מחזיר שגיאה
- ולידציית Zod קפדנית על כל פרמטרי כלים
- תיעוד קריאות לכלים (שם כלי, פרמטרים, לטנסי)
- מעקב לטנסי (התראה אם >2 שניות לרכיב הראשון)
- מעקב עלויות per AI inference
- בדיקת נגישות של כל הרכבות רכיבים שנוצרות
- בדיקת responsive לפריסות שנוצרות על מובייל
- rate limiting על ה-server action
הערה על בדיקות
בדיקת Generative UI דורשת גישה שונה מבדיקת ממשק מסורתי. בקצרה:
- בדקו את הרכיבים שלכם בבידוד עם unit tests רגילים — הם בסך הכל רכיבי React
- בדקו את סכמות Zod שלכם בנפרד כדי לוודא שהן מקבלות קלטים תקינים ודוחות לא תקינים
- לבדיקות integration מול ה-AI, בדקו מאפיינים מבניים (הכלי הנכון נקרא, פרמטרים תקינים) ולא תוכן מדויק (הטמפרטורה היא 22°)
- Mockו את ה-AI ב-CI והריצו בדיקות integration אמיתיות בלילה
הנושא הזה ראוי למאמר עצמאי. לעת עתה, תבניות האימות וטיפול בשגיאות שהופכות בדיקות לאמינות מכוסות ב-Building Generative UI with Vercel AI SDK.
עובדים על מימוש Generative UI ב-React? קבלו הכוונה מומחה על ארכיטקטורה, ביצועים ומוכנות לייצור.
Alex
מהנדס וייעוץ Generative UI
מהנדס בכיר המתמחה בממשקי AI ומערכות Generative UI. מסייע לצוותי מוצר לשלוח מהר יותר עם ה-stack הנכון.
מאמרים קשורים
Κατασκευάζοντας το Πρώτο σας 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, με πλεονεκτήματα, μειονεκτήματα και πότε να χρησιμοποιείτε το καθένα.
הישארו קדימה ב-Generative UI
מאמרים שבועיים, עדכוני framework ומדריכי יישום מעשיים — ישירות לתיבת הדואר.
זקוקים לעזרה ביישום מה שקראתם?
קבעו ייעוץ חינם