[{"data":1,"prerenderedAt":15457},["ShallowReactive",2],{"learn-articles":3},[4,337,1177,1862,4887,7092,8411,11298,13442,15307],{"id":5,"title":6,"author":7,"body":8,"category":321,"date":322,"description":323,"extension":324,"meta":325,"navigation":327,"path":328,"readTime":329,"seo":330,"stem":331,"tags":332,"__hash__":336},"content\u002Flearn\u002Fgetting-started-with-generative-ui.md","Getting Started with Generative UI","Alex",{"type":9,"value":10,"toc":315},"minimark",[11,16,20,24,32,55,59,62,74,281,285,311],[12,13,15],"h2",{"id":14},"what-is-generative-ui","What is Generative UI?",[17,18,19],"p",{},"Generative UI is a paradigm where AI systems produce interactive user interface components — not just text — as their output. Instead of returning a markdown string that says \"here's a chart of your data,\" a Generative UI system returns an actual interactive chart component that users can filter, sort, and explore.",[12,21,23],{"id":22},"why-it-matters","Why It Matters",[17,25,26,27,31],{},"Traditional chatbots return text. Generative UI returns ",[28,29,30],"em",{},"interfaces",". This distinction matters because:",[33,34,35,43,49],"ul",{},[36,37,38,42],"li",{},[39,40,41],"strong",{},"Higher information density"," — a well-designed component conveys more than paragraphs of text",[36,44,45,48],{},[39,46,47],{},"Direct manipulation"," — users interact with the output, not just read it",[36,50,51,54],{},[39,52,53],{},"Contextual actions"," — generated components can include buttons, forms, and workflows",[12,56,58],{"id":57},"getting-started","Getting Started",[17,60,61],{},"To build your first Generative UI application, you need three things:",[63,64,65,68,71],"ol",{},[36,66,67],{},"A framework that supports streaming UI components (like the Vercel AI SDK)",[36,69,70],{},"A set of pre-built components the AI can compose",[36,72,73],{},"An LLM that understands your component schema",[75,76,81],"pre",{"className":77,"code":78,"language":79,"meta":80,"style":80},"language-typescript shiki shiki-themes github-light github-dark","\u002F\u002F Example: defining a tool that returns a UI component\nconst tools = {\n  showWeather: {\n    description: 'Display weather information',\n    parameters: z.object({\n      city: z.string(),\n      unit: z.enum(['celsius', 'fahrenheit']),\n    }),\n    generate: async ({ city, unit }) => {\n      const data = await fetchWeather(city, unit)\n      return \u003CWeatherCard data={data} \u002F>\n    },\n  },\n}\n","typescript","",[82,83,84,93,111,117,130,143,155,179,185,217,237,263,269,275],"code",{"__ignoreMap":80},[85,86,89],"span",{"class":87,"line":88},"line",1,[85,90,92],{"class":91},"sJ8bj","\u002F\u002F Example: defining a tool that returns a UI component\n",[85,94,96,100,104,107],{"class":87,"line":95},2,[85,97,99],{"class":98},"szBVR","const",[85,101,103],{"class":102},"sj4cs"," tools",[85,105,106],{"class":98}," =",[85,108,110],{"class":109},"sVt8B"," {\n",[85,112,114],{"class":87,"line":113},3,[85,115,116],{"class":109},"  showWeather: {\n",[85,118,120,123,127],{"class":87,"line":119},4,[85,121,122],{"class":109},"    description: ",[85,124,126],{"class":125},"sZZnC","'Display weather information'",[85,128,129],{"class":109},",\n",[85,131,133,136,140],{"class":87,"line":132},5,[85,134,135],{"class":109},"    parameters: z.",[85,137,139],{"class":138},"sScJk","object",[85,141,142],{"class":109},"({\n",[85,144,146,149,152],{"class":87,"line":145},6,[85,147,148],{"class":109},"      city: z.",[85,150,151],{"class":138},"string",[85,153,154],{"class":109},"(),\n",[85,156,158,161,164,167,170,173,176],{"class":87,"line":157},7,[85,159,160],{"class":109},"      unit: z.",[85,162,163],{"class":138},"enum",[85,165,166],{"class":109},"([",[85,168,169],{"class":125},"'celsius'",[85,171,172],{"class":109},", ",[85,174,175],{"class":125},"'fahrenheit'",[85,177,178],{"class":109},"]),\n",[85,180,182],{"class":87,"line":181},8,[85,183,184],{"class":109},"    }),\n",[85,186,188,191,194,197,200,204,206,209,212,215],{"class":87,"line":187},9,[85,189,190],{"class":138},"    generate",[85,192,193],{"class":109},": ",[85,195,196],{"class":98},"async",[85,198,199],{"class":109}," ({ ",[85,201,203],{"class":202},"s4XuR","city",[85,205,172],{"class":109},[85,207,208],{"class":202},"unit",[85,210,211],{"class":109}," }) ",[85,213,214],{"class":98},"=>",[85,216,110],{"class":109},[85,218,220,223,226,228,231,234],{"class":87,"line":219},10,[85,221,222],{"class":98},"      const",[85,224,225],{"class":102}," data",[85,227,106],{"class":98},[85,229,230],{"class":98}," await",[85,232,233],{"class":138}," fetchWeather",[85,235,236],{"class":109},"(city, unit)\n",[85,238,240,243,246,249,251,254,257,260],{"class":87,"line":239},11,[85,241,242],{"class":98},"      return",[85,244,245],{"class":109}," \u003C",[85,247,248],{"class":138},"WeatherCard",[85,250,225],{"class":138},[85,252,253],{"class":98},"=",[85,255,256],{"class":109},"{",[85,258,259],{"class":202},"data",[85,261,262],{"class":109},"} \u002F>\n",[85,264,266],{"class":87,"line":265},12,[85,267,268],{"class":109},"    },\n",[85,270,272],{"class":87,"line":271},13,[85,273,274],{"class":109},"  },\n",[85,276,278],{"class":87,"line":277},14,[85,279,280],{"class":109},"}\n",[12,282,284],{"id":283},"next-steps","Next Steps",[33,286,287,295,303],{},[36,288,289,290],{},"Read our ",[291,292,294],"a",{"href":293},"\u002Fgenerative-ui","complete guide to Generative UI",[36,296,297,298,302],{},"Try the ",[291,299,301],{"href":300},"\u002Ftools\u002Fswot","SWOT Analysis tool"," to see Generative UI in action",[36,304,305,306,310],{},"Explore ",[291,307,309],{"href":308},"\u002Fservices","our services"," if you need help implementing GenUI in your product",[312,313,314],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":80,"searchDepth":95,"depth":95,"links":316},[317,318,319,320],{"id":14,"depth":95,"text":15},{"id":22,"depth":95,"text":23},{"id":57,"depth":95,"text":58},{"id":283,"depth":95,"text":284},"tutorial","2026-03-30","An introduction to Generative UI — what it is, why it matters, and how to build your first generative interface.","md",{"featured":326},false,true,"\u002Flearn\u002Fgetting-started-with-generative-ui","8 min read",{"title":6,"description":323},"learn\u002Fgetting-started-with-generative-ui",[333,57,334,335],"generative-ui","beginner","vercel-ai-sdk","A_tuRg_oTf7VCmGV5ZJ2br-sewB-AukM-Y6-dllRHZU",{"id":338,"title":339,"author":7,"body":340,"category":1164,"date":1165,"description":1166,"extension":324,"meta":1167,"navigation":327,"path":1168,"readTime":1169,"seo":1170,"stem":1171,"tags":1172,"__hash__":1176},"content\u002Flearn\u002Fwhat-is-generative-ui.md","What is Generative UI? The Complete Guide",{"type":9,"value":341,"toc":1148},[342,344,347,350,353,356,360,363,389,392,395,756,768,772,776,783,786,792,796,807,810,815,819,822,825,830,834,837,843,849,855,861,867,871,874,974,977,981,987,993,999,1005,1009,1015,1021,1027,1033,1039,1041,1044,1054,1060,1066,1072,1078,1082,1085,1091,1097,1103,1107,1113,1119,1125,1131,1137,1140,1145],[12,343,15],{"id":14},[17,345,346],{},"Generative UI is a paradigm shift in how we build user interfaces. Instead of developers manually creating every screen, component, and interaction, AI models generate interactive UI components in real time — based on context, user intent, and data.",[17,348,349],{},"Think of it this way: traditional chatbots respond with text. Generative UI systems respond with fully interactive interface elements — charts, forms, cards, tables, and entire page sections that users can click, fill out, and interact with.",[17,351,352],{},"When you ask a traditional chatbot \"Show me my sales data,\" you get a text description. When you ask a Generative UI system the same question, you get an interactive chart with filters, date pickers, and drill-down capabilities — all generated on the fly.",[17,354,355],{},"This is not science fiction. The Vercel AI SDK, CopilotKit, and Thesys are all production-grade tools that ship Generative UI to millions of users today.",[12,357,359],{"id":358},"how-generative-ui-works","How Generative UI Works",[17,361,362],{},"At a technical level, Generative UI systems operate through a four-step pipeline:",[63,364,365,371,377,383],{},[36,366,367,370],{},[39,368,369],{},"User intent recognition"," — The AI model interprets what the user wants to accomplish",[36,372,373,376],{},[39,374,375],{},"Component selection"," — The model chooses from a library of available UI components",[36,378,379,382],{},[39,380,381],{},"Data binding"," — The selected components are populated with relevant data",[36,384,385,388],{},[39,386,387],{},"Streaming render"," — Components are sent to the client and rendered progressively",[17,390,391],{},"The key innovation is in steps 2 and 3: the AI model does not just pick a template — it composes components dynamically based on the specific context. The same query might produce different UI layouts depending on the data shape, screen size, and user history.",[17,393,394],{},"Here is a simplified example using the Vercel AI SDK:",[75,396,398],{"className":77,"code":397,"language":79,"meta":80,"style":80},"import { streamUI } from 'ai\u002Frsc';\nimport { openai } from '@ai-sdk\u002Fopenai';\nimport { z } from 'zod';\n\nconst result = await streamUI({\n  model: openai('gpt-4o'),\n  prompt: 'Show me a weather dashboard for Paris',\n  tools: {\n    weatherCard: {\n      description: 'Display current weather information for a city',\n      parameters: z.object({\n        city: z.string(),\n        temperature: z.number(),\n        conditions: z.string(),\n      }),\n      generate: async ({ city, temperature, conditions }) => {\n        return \u003CWeatherCard city={city} temp={temperature} conditions={conditions} \u002F>;\n      },\n    },\n    forecastChart: {\n      description: 'Display a 7-day forecast as a chart',\n      parameters: z.object({\n        data: z.array(z.object({ day: z.string(), high: z.number(), low: z.number() })),\n      }),\n      generate: async ({ data }) => {\n        return \u003CForecastChart data={data} \u002F>;\n      },\n    },\n  },\n});\n",[82,399,400,417,431,445,450,466,483,493,498,503,513,522,531,541,550,556,586,625,631,636,642,652,661,693,698,717,735,740,745,750],{"__ignoreMap":80},[85,401,402,405,408,411,414],{"class":87,"line":88},[85,403,404],{"class":98},"import",[85,406,407],{"class":109}," { streamUI } ",[85,409,410],{"class":98},"from",[85,412,413],{"class":125}," 'ai\u002Frsc'",[85,415,416],{"class":109},";\n",[85,418,419,421,424,426,429],{"class":87,"line":95},[85,420,404],{"class":98},[85,422,423],{"class":109}," { openai } ",[85,425,410],{"class":98},[85,427,428],{"class":125}," '@ai-sdk\u002Fopenai'",[85,430,416],{"class":109},[85,432,433,435,438,440,443],{"class":87,"line":113},[85,434,404],{"class":98},[85,436,437],{"class":109}," { z } ",[85,439,410],{"class":98},[85,441,442],{"class":125}," 'zod'",[85,444,416],{"class":109},[85,446,447],{"class":87,"line":119},[85,448,449],{"emptyLinePlaceholder":327},"\n",[85,451,452,454,457,459,461,464],{"class":87,"line":132},[85,453,99],{"class":98},[85,455,456],{"class":102}," result",[85,458,106],{"class":98},[85,460,230],{"class":98},[85,462,463],{"class":138}," streamUI",[85,465,142],{"class":109},[85,467,468,471,474,477,480],{"class":87,"line":145},[85,469,470],{"class":109},"  model: ",[85,472,473],{"class":138},"openai",[85,475,476],{"class":109},"(",[85,478,479],{"class":125},"'gpt-4o'",[85,481,482],{"class":109},"),\n",[85,484,485,488,491],{"class":87,"line":157},[85,486,487],{"class":109},"  prompt: ",[85,489,490],{"class":125},"'Show me a weather dashboard for Paris'",[85,492,129],{"class":109},[85,494,495],{"class":87,"line":181},[85,496,497],{"class":109},"  tools: {\n",[85,499,500],{"class":87,"line":187},[85,501,502],{"class":109},"    weatherCard: {\n",[85,504,505,508,511],{"class":87,"line":219},[85,506,507],{"class":109},"      description: ",[85,509,510],{"class":125},"'Display current weather information for a city'",[85,512,129],{"class":109},[85,514,515,518,520],{"class":87,"line":239},[85,516,517],{"class":109},"      parameters: z.",[85,519,139],{"class":138},[85,521,142],{"class":109},[85,523,524,527,529],{"class":87,"line":265},[85,525,526],{"class":109},"        city: z.",[85,528,151],{"class":138},[85,530,154],{"class":109},[85,532,533,536,539],{"class":87,"line":271},[85,534,535],{"class":109},"        temperature: z.",[85,537,538],{"class":138},"number",[85,540,154],{"class":109},[85,542,543,546,548],{"class":87,"line":277},[85,544,545],{"class":109},"        conditions: z.",[85,547,151],{"class":138},[85,549,154],{"class":109},[85,551,553],{"class":87,"line":552},15,[85,554,555],{"class":109},"      }),\n",[85,557,559,562,564,566,568,570,572,575,577,580,582,584],{"class":87,"line":558},16,[85,560,561],{"class":138},"      generate",[85,563,193],{"class":109},[85,565,196],{"class":98},[85,567,199],{"class":109},[85,569,203],{"class":202},[85,571,172],{"class":109},[85,573,574],{"class":202},"temperature",[85,576,172],{"class":109},[85,578,579],{"class":202},"conditions",[85,581,211],{"class":109},[85,583,214],{"class":98},[85,585,110],{"class":109},[85,587,589,592,594,596,599,602,604,607,610,612,614,616,618,620,622],{"class":87,"line":588},17,[85,590,591],{"class":98},"        return",[85,593,245],{"class":109},[85,595,248],{"class":138},[85,597,598],{"class":138}," city",[85,600,601],{"class":109},"={",[85,603,203],{"class":202},[85,605,606],{"class":109},"} ",[85,608,609],{"class":138},"temp",[85,611,601],{"class":109},[85,613,574],{"class":202},[85,615,606],{"class":109},[85,617,579],{"class":138},[85,619,601],{"class":109},[85,621,579],{"class":202},[85,623,624],{"class":109},"} \u002F>;\n",[85,626,628],{"class":87,"line":627},18,[85,629,630],{"class":109},"      },\n",[85,632,634],{"class":87,"line":633},19,[85,635,268],{"class":109},[85,637,639],{"class":87,"line":638},20,[85,640,641],{"class":109},"    forecastChart: {\n",[85,643,645,647,650],{"class":87,"line":644},21,[85,646,507],{"class":109},[85,648,649],{"class":125},"'Display a 7-day forecast as a chart'",[85,651,129],{"class":109},[85,653,655,657,659],{"class":87,"line":654},22,[85,656,517],{"class":109},[85,658,139],{"class":138},[85,660,142],{"class":109},[85,662,664,667,670,673,675,678,680,683,685,688,690],{"class":87,"line":663},23,[85,665,666],{"class":109},"        data: z.",[85,668,669],{"class":138},"array",[85,671,672],{"class":109},"(z.",[85,674,139],{"class":138},[85,676,677],{"class":109},"({ day: z.",[85,679,151],{"class":138},[85,681,682],{"class":109},"(), high: z.",[85,684,538],{"class":138},[85,686,687],{"class":109},"(), low: z.",[85,689,538],{"class":138},[85,691,692],{"class":109},"() })),\n",[85,694,696],{"class":87,"line":695},24,[85,697,555],{"class":109},[85,699,701,703,705,707,709,711,713,715],{"class":87,"line":700},25,[85,702,561],{"class":138},[85,704,193],{"class":109},[85,706,196],{"class":98},[85,708,199],{"class":109},[85,710,259],{"class":202},[85,712,211],{"class":109},[85,714,214],{"class":98},[85,716,110],{"class":109},[85,718,720,722,724,727,729,731,733],{"class":87,"line":719},26,[85,721,591],{"class":98},[85,723,245],{"class":109},[85,725,726],{"class":138},"ForecastChart",[85,728,225],{"class":138},[85,730,601],{"class":109},[85,732,259],{"class":202},[85,734,624],{"class":109},[85,736,738],{"class":87,"line":737},27,[85,739,630],{"class":109},[85,741,743],{"class":87,"line":742},28,[85,744,268],{"class":109},[85,746,748],{"class":87,"line":747},29,[85,749,274],{"class":109},[85,751,753],{"class":87,"line":752},30,[85,754,755],{"class":109},"});\n",[17,757,758,759,761,762,764,765,767],{},"The model decides which tools (components) to call based on the prompt. It might render a ",[82,760,248],{}," for a simple query or combine ",[82,763,248],{}," + ",[82,766,726],{}," for a dashboard request. The developer does not write that logic — the AI infers it.",[12,769,771],{"id":770},"key-frameworks","Key Frameworks",[773,774,775],"h3",{"id":335},"Vercel AI SDK",[17,777,778,779,782],{},"The most widely adopted framework for Generative UI in the React ecosystem. With 20M+ monthly npm downloads, it provides the ",[82,780,781],{},"streamUI"," function that enables React Server Components to be streamed directly from AI model responses.",[17,784,785],{},"The RSC-based streaming approach means generated components run on the server first, then hydrate on the client — giving you the best of both performance and interactivity.",[17,787,788,791],{},[39,789,790],{},"Best for:"," Next.js projects, React Server Components, production applications where performance matters.",[773,793,795],{"id":794},"copilotkit","CopilotKit",[17,797,798,799,802,803,806],{},"An open-source framework (22K+ GitHub stars) focused on building \"copilot\" experiences within existing applications. It provides drop-in components like ",[82,800,801],{},"\u003CCopilotChat>"," and ",[82,804,805],{},"\u003CCopilotTextarea>"," that add AI-powered UI to any React app without requiring Next.js or RSC.",[17,808,809],{},"The copilot pattern — a sidebar AI that can read and modify the main application state — is a natural fit for tools, dashboards, and productivity apps.",[17,811,812,814],{},[39,813,790],{}," Adding AI copilot features to existing React applications, fast time-to-integration.",[773,816,818],{"id":817},"thesys-json-render","Thesys (json-render)",[17,820,821],{},"A newer entrant (13K GitHub stars in its first three months since January 2026) that takes a fundamentally different approach: AI models output JSON schemas describing UI components, and a client-side renderer turns them into interactive interfaces.",[17,823,824],{},"This schema-based approach is framework-agnostic — the same JSON output can be rendered by React, Vue, or even a native mobile client.",[17,826,827,829],{},[39,828,790],{}," Multi-framework projects, teams that want to inspect and cache generated UIs as plain data.",[12,831,833],{"id":832},"use-cases","Use Cases",[17,835,836],{},"Generative UI is not a research concept — it is being used in production across several categories.",[17,838,839,842],{},[39,840,841],{},"Customer support dashboards:"," AI analyzes a support ticket and generates a custom interface showing relevant customer data, order history, and suggested actions — all in one adaptive view tailored to that specific customer's situation.",[17,844,845,848],{},[39,846,847],{},"Data exploration:"," Instead of building dozens of static dashboard views, a Generative UI system creates the right visualization for any data query. Ask for revenue by region and you get a map. Ask for trends over time and you get a line chart. The interface fits the question.",[17,850,851,854],{},[39,852,853],{},"Adaptive forms:"," Insurance applications, medical intake forms, and financial questionnaires that adapt based on previous answers — not just showing and hiding fields, but generating entirely new form sections based on what the user tells the system.",[17,856,857,860],{},[39,858,859],{},"Developer tools:"," Code review interfaces that generate diff views, test result dashboards, and deployment status panels based on the specific PR context. Each review session gets a purpose-built interface.",[17,862,863,866],{},[39,864,865],{},"Internal business tools:"," The long tail of internal tooling — reports, data lookups, workflow helpers — where building dedicated screens for every need is impractical.",[12,868,870],{"id":869},"generative-ui-vs-traditional-ui","Generative UI vs. Traditional UI",[17,872,873],{},"The comparison is not either\u002For. Both approaches belong in a mature application.",[875,876,877,893],"table",{},[878,879,880],"thead",{},[881,882,883,887,890],"tr",{},[884,885,886],"th",{},"Aspect",[884,888,889],{},"Traditional UI",[884,891,892],{},"Generative UI",[894,895,896,908,919,930,941,952,963],"tbody",{},[881,897,898,902,905],{},[899,900,901],"td",{},"Creation",[899,903,904],{},"Manually coded per screen",[899,906,907],{},"AI-generated per interaction",[881,909,910,913,916],{},[899,911,912],{},"Adaptability",[899,914,915],{},"Fixed layouts with conditional logic",[899,917,918],{},"Dynamic layouts based on context",[881,920,921,924,927],{},[899,922,923],{},"Dev time (new view)",[899,925,926],{},"High — every screen designed",[899,928,929],{},"Near-zero for the AI to handle",[881,931,932,935,938],{},[899,933,934],{},"Consistency",[899,936,937],{},"Guaranteed by design system",[899,939,940],{},"Requires component constraints",[881,942,943,946,949],{},[899,944,945],{},"Performance",[899,947,948],{},"Optimized at build time",[899,950,951],{},"Requires streaming optimization",[881,953,954,957,960],{},[899,955,956],{},"Testing",[899,958,959],{},"Standard E2E\u002Funit tests",[899,961,962],{},"Requires output validation",[881,964,965,968,971],{},[899,966,967],{},"Cost per view",[899,969,970],{},"Fixed hosting cost",[899,972,973],{},"$0.001–$0.01 per AI inference",[17,975,976],{},"The important insight: Generative UI does not replace traditional UI. Your design system, component library, and core application screens — navigation, authentication, settings, checkout — should still be hand-crafted. Generative UI excels at the long tail: the hundreds of possible views that would be impractical to build manually.",[12,978,980],{"id":979},"benefits","Benefits",[17,982,983,986],{},[39,984,985],{},"Compresses the view explosion problem."," A data-rich application might need hundreds of different views to surface all its information usefully. Building each one manually is not feasible. Generative UI handles this combinatorial problem naturally.",[17,988,989,992],{},[39,990,991],{},"Reduces design-to-production time for new features."," Adding a new data type or metric to a Generative UI system often means adding one new component to the registry, not designing, building, and QA-ing a new screen.",[17,994,995,998],{},[39,996,997],{},"Enables richer personalization."," Instead of A\u002FB testing 4 layouts, a Generative UI system creates interfaces that adapt to each user's role, data, and history — without explicit if\u002Felse branching for each case.",[17,1000,1001,1004],{},[39,1002,1003],{},"Future-proof architecture."," As AI models improve, the same tool definitions produce better interfaces automatically. The component library appreciates over time.",[12,1006,1008],{"id":1007},"challenges","Challenges",[17,1010,1011,1014],{},[39,1012,1013],{},"Non-determinism."," The same prompt may produce different component selections on different runs. This complicates testing and makes regression detection harder. The solution is property-based testing: assert structural properties rather than exact output.",[17,1016,1017,1020],{},[39,1018,1019],{},"Latency."," AI inference adds 200–800ms before the first component renders. This is manageable with streaming and skeleton loading states, but it is a real constraint compared to a cached server render.",[17,1022,1023,1026],{},[39,1024,1025],{},"Accessibility."," AI models do not automatically produce accessible interfaces. The component library must enforce ARIA labels, focus management, keyboard navigation, and screen reader support. The AI cannot be trusted to add these.",[17,1028,1029,1032],{},[39,1030,1031],{},"Cost at scale."," At $0.001–$0.01 per interaction, AI inference is cheap for low-traffic features. At millions of daily interactions, the cost is meaningful. Rate limiting, caching, and model selection are real engineering concerns.",[17,1034,1035,1038],{},[39,1036,1037],{},"Design system maturity required."," Generative UI is only as good as the components the AI can use. If your design system is immature or inconsistent, generated interfaces will reflect that. Invest in your component library first.",[12,1040,58],{"id":57},[17,1042,1043],{},"If you want to start building with Generative UI today, here is the most practical path:",[17,1045,1046,1049,1050,1053],{},[39,1047,1048],{},"1. Start with your component library."," Generative UI is only as good as the components the AI can select. Ensure your components are well-typed, well-documented, and composable. Each component's ",[82,1051,1052],{},"description"," field in your tool definition is what the AI reads to decide whether to use it — write those descriptions carefully.",[17,1055,1056,1059],{},[39,1057,1058],{},"2. Pick a framework."," For React\u002FNext.js projects, the Vercel AI SDK is the most production-ready choice. For adding AI to an existing app quickly, CopilotKit offers faster time-to-value. For multi-framework needs, evaluate Thesys.",[17,1061,1062,1065],{},[39,1063,1064],{},"3. Define your tool set conservatively."," Map out 5–8 components the AI should be able to generate. Too many tools leads to poor selection decisions. Start small and expand based on real usage data.",[17,1067,1068,1071],{},[39,1069,1070],{},"4. Pick a constrained use case first."," Do not try to make everything generative. Choose one feature — a data explorer, a form builder, a contextual help panel — and make that excellent before expanding.",[17,1073,1074,1077],{},[39,1075,1076],{},"5. Instrument from day one."," Track which components the AI selects, how users interact with generated UIs, and where the model makes poor choices. This data drives every subsequent improvement.",[12,1079,1081],{"id":1080},"performance-considerations","Performance Considerations",[17,1083,1084],{},"Generative UI adds latency, and managing that latency well is the difference between a delightful feature and a frustrating one.",[17,1086,1087,1090],{},[39,1088,1089],{},"Time to first component"," is the key metric. Target under 500ms. The Vercel AI SDK's streaming approach yields skeleton loading states immediately, with real components appearing as the AI resolves parameters.",[17,1092,1093,1096],{},[39,1094,1095],{},"Streaming smoothness"," matters more than raw speed. Components that appear progressively as the AI works feel faster than a long blank wait followed by everything appearing at once.",[17,1098,1099,1102],{},[39,1100,1101],{},"Fallback behavior"," is non-negotiable. When the AI is slow or unavailable, users must see something. A static default UI is better than a loading spinner that never resolves.",[12,1104,1106],{"id":1105},"faq","FAQ",[17,1108,1109,1112],{},[39,1110,1111],{},"Is Generative UI production-ready?","\nYes. Companies are running Vercel AI SDK and CopilotKit in production across applications serving millions of users. The technology is past the experimental phase.",[17,1114,1115,1118],{},[39,1116,1117],{},"Does Generative UI replace frontend developers?","\nNo — it changes what they build. Instead of creating every possible screen, developers build component libraries and define the rules for how AI composes them. The design system becomes more important, not less.",[17,1120,1121,1124],{},[39,1122,1123],{},"What about accessibility?","\nThis is an active challenge. Generated UIs must maintain proper heading hierarchy, ARIA labels, keyboard navigation, and screen reader support. The component library must enforce accessibility — the AI will not add it automatically.",[17,1126,1127,1130],{},[39,1128,1129],{},"How much does it cost to run?","\nLLM API costs for UI generation are typically $0.001–$0.01 per interaction, depending on model and complexity. At modest scale, this is comparable to traditional server rendering costs. At high scale, cost optimization (model selection, caching, batching) becomes necessary.",[17,1132,1133,1136],{},[39,1134,1135],{},"Do I need to use React?","\nThe Vercel AI SDK and CopilotKit are React-specific. Thesys supports any framework through its JSON schema approach. For non-React stacks, the JSON-based pattern is the most practical path.",[1138,1139],"hr",{},[17,1141,1142],{},[28,1143,1144],{},"This article is regularly updated as the Generative UI landscape evolves. Last updated: March 2026.",[312,1146,1147],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":80,"searchDepth":95,"depth":95,"links":1149},[1150,1151,1152,1157,1158,1159,1160,1161,1162,1163],{"id":14,"depth":95,"text":15},{"id":358,"depth":95,"text":359},{"id":770,"depth":95,"text":771,"children":1153},[1154,1155,1156],{"id":335,"depth":113,"text":775},{"id":794,"depth":113,"text":795},{"id":817,"depth":113,"text":818},{"id":832,"depth":95,"text":833},{"id":869,"depth":95,"text":870},{"id":979,"depth":95,"text":980},{"id":1007,"depth":95,"text":1008},{"id":57,"depth":95,"text":58},{"id":1080,"depth":95,"text":1081},{"id":1105,"depth":95,"text":1106},"deep-dive","2026-03-15","Everything you need to know about AI systems that generate interactive UI components, not just text.",{"featured":327},"\u002Flearn\u002Fwhat-is-generative-ui","12 min read",{"title":339,"description":1166},"learn\u002Fwhat-is-generative-ui",[333,1173,1174,1175],"ai","guide","frameworks","1Q_RtoCxqtQ55HhZ40_5JRuz687bam8dtwRaJXQMjiY",{"id":1178,"title":1179,"author":7,"body":1180,"category":1164,"date":1852,"description":1853,"extension":324,"meta":1854,"navigation":327,"path":1855,"readTime":329,"seo":1856,"stem":1857,"tags":1858,"__hash__":1861},"content\u002Flearn\u002Fgenerative-ui-vs-traditional-ui.md","Generative UI vs Traditional UI: Key Differences",{"type":9,"value":1181,"toc":1834},[1182,1186,1189,1192,1195,1199,1203,1206,1220,1223,1226,1230,1233,1244,1247,1251,1254,1260,1266,1280,1283,1291,1295,1299,1305,1311,1317,1323,1329,1333,1339,1345,1351,1357,1363,1367,1370,1376,1383,1387,1390,1396,1406,1601,1605,1735,1739,1742,1745,1759,1762,1766,1772,1778,1784,1790,1794,1797,1817,1820,1822,1831],[12,1183,1185],{"id":1184},"the-core-distinction","The Core Distinction",[17,1187,1188],{},"Traditional UI development follows a straightforward pattern: a designer creates mockups, a developer implements them as static templates, and conditional logic handles variations. Every screen a user might see was explicitly designed and coded by a human.",[17,1190,1191],{},"Generative UI flips this model. Instead of pre-building every possible view, you build a component library and let an AI model compose the right interface for each interaction. The UI is generated at runtime, not at build time.",[17,1193,1194],{},"This sounds abstract, so here is a concrete comparison.",[12,1196,1198],{"id":1197},"a-real-world-example-customer-dashboard","A Real-World Example: Customer Dashboard",[773,1200,1202],{"id":1201},"traditional-approach","Traditional Approach",[17,1204,1205],{},"You design and build:",[33,1207,1208,1211,1214,1217],{},[36,1209,1210],{},"A dashboard template with 6 fixed widget slots",[36,1212,1213],{},"15 different widget types (revenue chart, user table, funnel, etc.)",[36,1215,1216],{},"A settings panel where users configure which widgets appear where",[36,1218,1219],{},"Responsive layouts for every combination",[17,1221,1222],{},"Total development time: 3–4 weeks for the initial build, plus ongoing maintenance every time a new widget type is added.",[17,1224,1225],{},"The critical constraint: you can only show users what you had time to build. Any novel data question that does not map to one of your 15 widgets gets answered with \"that's not available in the dashboard.\"",[773,1227,1229],{"id":1228},"generative-ui-approach","Generative UI Approach",[17,1231,1232],{},"You build:",[33,1234,1235,1238,1241],{},[36,1236,1237],{},"The same 15 widget components",[36,1239,1240],{},"A natural language prompt interface: \"Show me revenue trends and top customers this quarter\"",[36,1242,1243],{},"An AI pipeline that selects and arranges widgets based on the query",[17,1245,1246],{},"Total development time: 1 week for the AI pipeline (assuming components exist). From that point forward, every new data question gets a custom dashboard without additional development — the AI composes the answer from existing components.",[12,1248,1250],{"id":1249},"the-rendering-model","The Rendering Model",[17,1252,1253],{},"This is where the architectures diverge most sharply at the technical level.",[17,1255,1256,1259],{},[39,1257,1258],{},"Traditional UI rendering:"," At build time (or request time for SSR), the server renders a predetermined template. The component tree is fixed before the user sees anything. React, Vue, and other frameworks all follow this model by default.",[17,1261,1262,1265],{},[39,1263,1264],{},"Generative UI rendering:"," At request time, the system:",[63,1267,1268,1271,1274,1277],{},[36,1269,1270],{},"Sends the user's intent to an LLM",[36,1272,1273],{},"The LLM selects tools (components) and their parameters",[36,1275,1276],{},"The server renders those components",[36,1278,1279],{},"The rendered output streams to the client",[17,1281,1282],{},"The component tree is unknown until the LLM decides. This fundamental difference is what creates both the power (infinite view variability) and the challenges (latency, non-determinism, cost).",[75,1284,1289],{"className":1285,"code":1287,"language":1288},[1286],"language-text","Traditional:\nUser request → Server → Predetermined template → Client\n\nGenerative:\nUser request → Server → LLM inference → Component selection → Streaming render → Client\n                                       (200–800ms added here)\n","text",[82,1290,1287],{"__ignoreMap":80},[12,1292,1294],{"id":1293},"when-to-use-each-approach","When to Use Each Approach",[773,1296,1298],{"id":1297},"use-traditional-ui-when","Use Traditional UI When",[17,1300,1301,1304],{},[39,1302,1303],{},"The interface is well-defined and stable."," Login screens, navigation, settings pages, and checkout flows should be hand-crafted. Users expect consistency in these core flows, and the requirements do not change per interaction.",[17,1306,1307,1310],{},[39,1308,1309],{},"Pixel-perfect design matters."," Marketing pages, brand experiences, and critical conversion funnels need precise design control. Generative UI introduces variability that you do not want in these contexts.",[17,1312,1313,1316],{},[39,1314,1315],{},"Performance is critical with no tolerance for latency."," Generative UI adds 200–800ms of AI processing time. For interfaces that need to be instant — search typeahead, real-time collaboration, game UIs — traditional rendering is the only option.",[17,1318,1319,1322],{},[39,1320,1321],{},"Regulatory compliance requires deterministic output."," In healthcare, finance, or legal contexts where every interface element must be auditable and reproducible, the non-deterministic nature of AI generation can be a compliance issue. You need to be able to show exactly what was shown to a user at a given time.",[17,1324,1325,1328],{},[39,1326,1327],{},"You have a small, well-understood set of views."," If your feature needs 3 screens, build 3 screens. The overhead of a Generative UI pipeline is not justified for small, stable view sets.",[773,1330,1332],{"id":1331},"use-generative-ui-when","Use Generative UI When",[17,1334,1335,1338],{},[39,1336,1337],{},"The number of possible views is large."," Data dashboards, analytics tools, and admin interfaces often have hundreds of possible configurations. Building each one manually is impractical. Generative UI handles this combinatorial problem naturally.",[17,1340,1341,1344],{},[39,1342,1343],{},"User queries are unpredictable."," Support tools, data exploration interfaces, and internal business tools receive requests that were not anticipated during design. Generative UI adapts to novel queries instead of returning \"not supported.\"",[17,1346,1347,1350],{},[39,1348,1349],{},"Personalization depth matters."," Instead of A\u002FB testing 4 layouts, a Generative UI system creates interfaces that adapt to each user's role, data, and interaction history — without explicit branching for each case.",[17,1352,1353,1356],{},[39,1354,1355],{},"Development speed outweighs design precision."," For internal tools, prototypes, and MVP features, Generative UI can produce functional interfaces faster than the full traditional design-and-build cycle.",[17,1358,1359,1362],{},[39,1360,1361],{},"You are building a question-answering or analytics feature."," If users ask questions and expect visual answers, Generative UI is purpose-built for this pattern.",[12,1364,1366],{"id":1365},"the-hybrid-reality","The Hybrid Reality",[17,1368,1369],{},"In practice, no production application is 100% generative or 100% traditional. The most effective architecture uses both:",[75,1371,1374],{"className":1372,"code":1373,"language":1288},[1286],"Traditional UI (hand-crafted):\n  - Navigation shell and chrome\n  - Authentication and onboarding flows\n  - Settings and preferences\n  - Core CRUD operations\n  - Marketing and landing pages\n  - Payment and checkout flows\n\nGenerative UI (AI-composed):\n  - Data exploration and dashboards\n  - Search result interfaces\n  - Support and help experiences\n  - Report generation\n  - Contextual tool panels\n  - Analytics and insights\n",[82,1375,1373],{"__ignoreMap":80},[17,1377,1378,1379,1382],{},"The boundary between the two often falls along a simple question: ",[39,1380,1381],{},"Is this interface the same for every user, or does it vary based on context?"," If it varies significantly, Generative UI is worth considering.",[12,1384,1386],{"id":1385},"data-flow-comparison","Data Flow Comparison",[17,1388,1389],{},"The way data moves through the system is different in important ways.",[17,1391,1392,1395],{},[39,1393,1394],{},"Traditional:"," Data is fetched based on the route or query params, then bound to predetermined component props. The data shape is known at build time. Type safety is straightforward.",[17,1397,1398,1401,1402,1405],{},[39,1399,1400],{},"Generative:"," The AI model determines what data to request based on user intent. Data fetching happens inside tool ",[82,1403,1404],{},"generate"," functions, triggered by the model's decisions. You do not know which data will be fetched until the model runs.",[75,1407,1409],{"className":77,"code":1408,"language":79,"meta":80,"style":80},"\u002F\u002F Traditional: data flow is predetermined\nexport async function getServerSideProps({ params }) {\n  const data = await fetchDashboardData(params.userId);\n  return { props: { data } };\n}\n\n\u002F\u002F Generative: data flow is determined by the AI\ntools: {\n  revenueChart: {\n    description: 'Show revenue data as a chart',\n    parameters: z.object({ period: z.string(), metric: z.string() }),\n    generate: async ({ period, metric }) => {\n      \u002F\u002F This fetch only happens if the AI calls this tool\n      const data = await fetchRevenueData(period, metric);\n      return \u003CRevenueChart data={data} \u002F>;\n    },\n  },\n}\n",[82,1410,1411,1416,1439,1456,1464,1468,1472,1477,1485,1492,1504,1527,1551,1556,1572,1589,1593,1597],{"__ignoreMap":80},[85,1412,1413],{"class":87,"line":88},[85,1414,1415],{"class":91},"\u002F\u002F Traditional: data flow is predetermined\n",[85,1417,1418,1421,1424,1427,1430,1433,1436],{"class":87,"line":95},[85,1419,1420],{"class":98},"export",[85,1422,1423],{"class":98}," async",[85,1425,1426],{"class":98}," function",[85,1428,1429],{"class":138}," getServerSideProps",[85,1431,1432],{"class":109},"({ ",[85,1434,1435],{"class":202},"params",[85,1437,1438],{"class":109}," }) {\n",[85,1440,1441,1444,1446,1448,1450,1453],{"class":87,"line":113},[85,1442,1443],{"class":98},"  const",[85,1445,225],{"class":102},[85,1447,106],{"class":98},[85,1449,230],{"class":98},[85,1451,1452],{"class":138}," fetchDashboardData",[85,1454,1455],{"class":109},"(params.userId);\n",[85,1457,1458,1461],{"class":87,"line":119},[85,1459,1460],{"class":98},"  return",[85,1462,1463],{"class":109}," { props: { data } };\n",[85,1465,1466],{"class":87,"line":132},[85,1467,280],{"class":109},[85,1469,1470],{"class":87,"line":145},[85,1471,449],{"emptyLinePlaceholder":327},[85,1473,1474],{"class":87,"line":157},[85,1475,1476],{"class":91},"\u002F\u002F Generative: data flow is determined by the AI\n",[85,1478,1479,1482],{"class":87,"line":181},[85,1480,1481],{"class":138},"tools",[85,1483,1484],{"class":109},": {\n",[85,1486,1487,1490],{"class":87,"line":187},[85,1488,1489],{"class":138},"  revenueChart",[85,1491,1484],{"class":109},[85,1493,1494,1497,1499,1502],{"class":87,"line":219},[85,1495,1496],{"class":138},"    description",[85,1498,193],{"class":109},[85,1500,1501],{"class":125},"'Show revenue data as a chart'",[85,1503,129],{"class":109},[85,1505,1506,1509,1512,1514,1517,1519,1522,1524],{"class":87,"line":239},[85,1507,1508],{"class":138},"    parameters",[85,1510,1511],{"class":109},": z.",[85,1513,139],{"class":138},[85,1515,1516],{"class":109},"({ period: z.",[85,1518,151],{"class":138},[85,1520,1521],{"class":109},"(), metric: z.",[85,1523,151],{"class":138},[85,1525,1526],{"class":109},"() }),\n",[85,1528,1529,1531,1533,1535,1537,1540,1542,1545,1547,1549],{"class":87,"line":265},[85,1530,190],{"class":138},[85,1532,193],{"class":109},[85,1534,196],{"class":98},[85,1536,199],{"class":109},[85,1538,1539],{"class":202},"period",[85,1541,172],{"class":109},[85,1543,1544],{"class":202},"metric",[85,1546,211],{"class":109},[85,1548,214],{"class":98},[85,1550,110],{"class":109},[85,1552,1553],{"class":87,"line":271},[85,1554,1555],{"class":91},"      \u002F\u002F This fetch only happens if the AI calls this tool\n",[85,1557,1558,1560,1562,1564,1566,1569],{"class":87,"line":277},[85,1559,222],{"class":98},[85,1561,225],{"class":102},[85,1563,106],{"class":98},[85,1565,230],{"class":98},[85,1567,1568],{"class":138}," fetchRevenueData",[85,1570,1571],{"class":109},"(period, metric);\n",[85,1573,1574,1576,1578,1581,1583,1585,1587],{"class":87,"line":552},[85,1575,242],{"class":98},[85,1577,245],{"class":109},[85,1579,1580],{"class":138},"RevenueChart",[85,1582,225],{"class":138},[85,1584,601],{"class":109},[85,1586,259],{"class":202},[85,1588,624],{"class":109},[85,1590,1591],{"class":87,"line":558},[85,1592,268],{"class":109},[85,1594,1595],{"class":87,"line":588},[85,1596,274],{"class":109},[85,1598,1599],{"class":87,"line":627},[85,1600,280],{"class":109},[12,1602,1604],{"id":1603},"technical-comparison","Technical Comparison",[875,1606,1607,1620],{},[878,1608,1609],{},[881,1610,1611,1614,1617],{},[884,1612,1613],{},"Dimension",[884,1615,1616],{},"Traditional",[884,1618,1619],{},"Generative",[894,1621,1622,1635,1648,1660,1672,1685,1696,1709,1722],{},[881,1623,1624,1629,1632],{},[899,1625,1626],{},[39,1627,1628],{},"Rendering",[899,1630,1631],{},"Build-time or server-render",[899,1633,1634],{},"Runtime AI inference + streaming",[881,1636,1637,1642,1645],{},[899,1638,1639],{},[39,1640,1641],{},"Latency",[899,1643,1644],{},"\u003C100ms (cached\u002FSSR)",[899,1646,1647],{},"200–800ms (model inference)",[881,1649,1650,1654,1657],{},[899,1651,1652],{},[39,1653,934],{},[899,1655,1656],{},"Deterministic",[899,1658,1659],{},"Probabilistic (mitigated by component constraints)",[881,1661,1662,1666,1669],{},[899,1663,1664],{},[39,1665,956],{},[899,1667,1668],{},"Standard unit\u002FE2E",[899,1670,1671],{},"Output validation + component testing",[881,1673,1674,1679,1682],{},[899,1675,1676],{},[39,1677,1678],{},"Maintenance",[899,1680,1681],{},"Update each screen manually",[899,1683,1684],{},"Update component library + prompt engineering",[881,1686,1687,1691,1693],{},[899,1688,1689],{},[39,1690,967],{},[899,1692,970],{},[899,1694,1695],{},"Variable ($0.001–$0.01 per inference)",[881,1697,1698,1703,1706],{},[899,1699,1700],{},[39,1701,1702],{},"Scalability of views",[899,1704,1705],{},"Linear (each new view = dev time)",[899,1707,1708],{},"Near-zero marginal cost per new view",[881,1710,1711,1716,1719],{},[899,1712,1713],{},[39,1714,1715],{},"Design control",[899,1717,1718],{},"Complete control",[899,1720,1721],{},"Constrained by component library",[881,1723,1724,1729,1732],{},[899,1725,1726],{},[39,1727,1728],{},"Accessibility",[899,1730,1731],{},"Implemented per component",[899,1733,1734],{},"Must be enforced by component library",[12,1736,1738],{"id":1737},"developer-experience","Developer Experience",[17,1740,1741],{},"Traditional UI development has decades of tooling: hot reload, browser devtools, React DevTools, Storybook. Debugging is straightforward — you can set a breakpoint and inspect the component tree.",[17,1743,1744],{},"Generative UI adds a layer of indirection. When something looks wrong, it could be:",[33,1746,1747,1750,1753,1756],{},[36,1748,1749],{},"The AI selecting the wrong component",[36,1751,1752],{},"The AI passing unexpected parameters",[36,1754,1755],{},"A component rendering incorrectly with those parameters",[36,1757,1758],{},"A data fetching error in the tool's generate function",[17,1760,1761],{},"Debugging requires inspecting LLM tool call logs in addition to the normal React component debugging workflow. This overhead is real and should factor into team readiness assessments.",[12,1763,1765],{"id":1764},"common-misconceptions","Common Misconceptions",[17,1767,1768,1771],{},[39,1769,1770],{},"\"Generative UI means the AI designs the interface.\""," The AI selects and composes from pre-built, human-designed components. The design system is more important than ever — it defines the quality ceiling.",[17,1773,1774,1777],{},[39,1775,1776],{},"\"Generative UI is just chatbots with fancy output.\""," Some implementations start with chat, but the full vision is broader. Any interface where the layout, content, or component composition is determined by an AI model qualifies — not just chat-based interactions.",[17,1779,1780,1783],{},[39,1781,1782],{},"\"Traditional UI is dead.\""," Not remotely. Generative UI is additive, not a replacement. It handles the long tail of interface variations that would be impractical to build manually.",[17,1785,1786,1789],{},[39,1787,1788],{},"\"Generative UI is slower.\""," It is slower to the first component than a cached static render. But for complex queries that would require users to navigate through multiple static screens, Generative UI can deliver a more complete answer faster.",[12,1791,1793],{"id":1792},"making-the-decision","Making the Decision",[17,1795,1796],{},"Ask yourself three questions:",[63,1798,1799,1805,1811],{},[36,1800,1801,1804],{},[39,1802,1803],{},"How many possible views does this feature need?"," If fewer than 10, build them traditionally. If more than 50, Generative UI saves significant time.",[36,1806,1807,1810],{},[39,1808,1809],{},"Can you accept 500ms of latency?"," If not, traditional. If yes, Generative UI is viable. Streaming and skeleton loading states make this latency feel acceptable in most cases.",[36,1812,1813,1816],{},[39,1814,1815],{},"Do you have a solid component library?"," Generative UI is only as good as the components the AI can use. If your design system is immature, invest there first.",[17,1818,1819],{},"The teams getting the most value from Generative UI are those with strong design systems, clear component APIs, and specific use cases where the variability of possible views exceeds what manual development can handle.",[1138,1821],{},[17,1823,1824],{},[28,1825,1826,1827,1830],{},"Need help deciding if Generative UI is right for your product? ",[291,1828,1829],{"href":308},"Book a free consultation"," to discuss your specific use case.",[312,1832,1833],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":80,"searchDepth":95,"depth":95,"links":1835},[1836,1837,1841,1842,1846,1847,1848,1849,1850,1851],{"id":1184,"depth":95,"text":1185},{"id":1197,"depth":95,"text":1198,"children":1838},[1839,1840],{"id":1201,"depth":113,"text":1202},{"id":1228,"depth":113,"text":1229},{"id":1249,"depth":95,"text":1250},{"id":1293,"depth":95,"text":1294,"children":1843},[1844,1845],{"id":1297,"depth":113,"text":1298},{"id":1331,"depth":113,"text":1332},{"id":1365,"depth":95,"text":1366},{"id":1385,"depth":95,"text":1386},{"id":1603,"depth":95,"text":1604},{"id":1737,"depth":95,"text":1738},{"id":1764,"depth":95,"text":1765},{"id":1792,"depth":95,"text":1793},"2026-03-08","How generative interfaces differ from conventional UIs and when each approach makes sense.",{"featured":326},"\u002Flearn\u002Fgenerative-ui-vs-traditional-ui",{"title":1179,"description":1853},"learn\u002Fgenerative-ui-vs-traditional-ui",[333,1859,1860],"comparison","architecture","T0DJ70BzYtXcg3D9ebPMMDg9Lx3b79CRknmPWN-RGGk",{"id":1863,"title":1864,"author":7,"body":1865,"category":321,"date":4876,"description":4877,"extension":324,"meta":4878,"navigation":327,"path":4879,"readTime":4880,"seo":4881,"stem":4882,"tags":4883,"__hash__":4886},"content\u002Flearn\u002Fbuilding-generative-ui-vercel-ai-sdk.md","Building Your First Generative UI with Vercel AI SDK",{"type":9,"value":1866,"toc":4863},[1867,1871,1874,1888,1894,1898,1901,1912,1915,1919,1943,1961,1968,1983,1987,1990,2261,2664,2745,2749,2752,3346,3349,3365,3373,3379,3383,4331,4335,4350,4353,4383,4386,4390,4393,4422,4425,4429,4432,4755,4758,4762,4771,4783,4796,4809,4811,4814,4846,4849,4851,4860],[12,1868,1870],{"id":1869},"prerequisites","Prerequisites",[17,1872,1873],{},"Before we start, make sure you have:",[33,1875,1876,1879,1882,1885],{},[36,1877,1878],{},"Node.js 18+ installed",[36,1880,1881],{},"A Next.js 14+ project using the App Router",[36,1883,1884],{},"An OpenAI API key (or Anthropic — the SDK supports both)",[36,1886,1887],{},"Basic familiarity with React Server Components",[17,1889,1890,1891,1893],{},"If you are new to RSC, spend 15 minutes with the Next.js docs on Server Components first. The Vercel AI SDK's ",[82,1892,781],{}," function depends on RSC and becomes much clearer once you understand the model.",[12,1895,1897],{"id":1896},"what-were-building","What We're Building",[17,1899,1900],{},"We will build a simple AI-powered assistant that generates interactive UI based on user prompts. By the end of this tutorial, you will have a working Generative UI feature that:",[63,1902,1903,1906,1909],{},[36,1904,1905],{},"Takes a text prompt from the user",[36,1907,1908],{},"Streams React components back from the server",[36,1910,1911],{},"Renders interactive cards and charts based on the AI's decisions",[17,1913,1914],{},"The example domain is a financial assistant that can show stock prices and weather data — simple enough to understand quickly, complex enough to demonstrate real patterns.",[12,1916,1918],{"id":1917},"step-1-install-dependencies","Step 1: Install Dependencies",[75,1920,1924],{"className":1921,"code":1922,"language":1923,"meta":80,"style":80},"language-bash shiki shiki-themes github-light github-dark","npm install ai @ai-sdk\u002Fopenai zod\n","bash",[82,1925,1926],{"__ignoreMap":80},[85,1927,1928,1931,1934,1937,1940],{"class":87,"line":88},[85,1929,1930],{"class":138},"npm",[85,1932,1933],{"class":125}," install",[85,1935,1936],{"class":125}," ai",[85,1938,1939],{"class":125}," @ai-sdk\u002Fopenai",[85,1941,1942],{"class":125}," zod\n",[17,1944,1945,1946,1948,1949,1952,1953,1956,1957,1960],{},"The ",[82,1947,1173],{}," package is the Vercel AI SDK core. ",[82,1950,1951],{},"@ai-sdk\u002Fopenai"," is the OpenAI provider (swap in ",[82,1954,1955],{},"@ai-sdk\u002Fanthropic"," if you prefer Claude). ",[82,1958,1959],{},"zod"," handles tool parameter validation — it is how you define what parameters the AI can pass to each component.",[17,1962,1963,1964,1967],{},"Add your API key to ",[82,1965,1966],{},".env.local",":",[75,1969,1971],{"className":1921,"code":1970,"language":1923,"meta":80,"style":80},"OPENAI_API_KEY=sk-...\n",[82,1972,1973],{"__ignoreMap":80},[85,1974,1975,1978,1980],{"class":87,"line":88},[85,1976,1977],{"class":109},"OPENAI_API_KEY",[85,1979,253],{"class":98},[85,1981,1982],{"class":125},"sk-...\n",[12,1984,1986],{"id":1985},"step-2-create-your-component-library","Step 2: Create Your Component Library",[17,1988,1989],{},"Define the components the AI can generate. These are regular React components — nothing AI-specific about them. The key design principle: build components that are useful standalone, and they will be composable by the AI.",[75,1991,1995],{"className":1992,"code":1993,"language":1994,"meta":80,"style":80},"language-tsx shiki shiki-themes github-light github-dark","\u002F\u002F components\u002Fweather-card.tsx\ninterface WeatherCardProps {\n  city: string;\n  temperature: number;\n  conditions: string;\n  humidity: number;\n}\n\nexport function WeatherCard({ city, temperature, conditions, humidity }: WeatherCardProps) {\n  return (\n    \u003Cdiv className=\"rounded-lg border bg-card p-6 shadow-sm\">\n      \u003Ch3 className=\"text-lg font-semibold\">{city}\u003C\u002Fh3>\n      \u003Cdiv className=\"mt-2 flex items-baseline gap-2\">\n        \u003Cspan className=\"text-4xl font-bold\">{temperature}°C\u003C\u002Fspan>\n        \u003Cspan className=\"text-muted-foreground\">{conditions}\u003C\u002Fspan>\n      \u003C\u002Fdiv>\n      \u003Cp className=\"mt-2 text-sm text-muted-foreground\">\n        Humidity: {humidity}%\n      \u003C\u002Fp>\n    \u003C\u002Fdiv>\n  );\n}\n","tsx",[82,1996,1997,2002,2012,2024,2036,2047,2058,2062,2066,2102,2109,2129,2150,2165,2186,2206,2215,2230,2235,2243,2252,2257],{"__ignoreMap":80},[85,1998,1999],{"class":87,"line":88},[85,2000,2001],{"class":91},"\u002F\u002F components\u002Fweather-card.tsx\n",[85,2003,2004,2007,2010],{"class":87,"line":95},[85,2005,2006],{"class":98},"interface",[85,2008,2009],{"class":138}," WeatherCardProps",[85,2011,110],{"class":109},[85,2013,2014,2017,2019,2022],{"class":87,"line":113},[85,2015,2016],{"class":202},"  city",[85,2018,1967],{"class":98},[85,2020,2021],{"class":102}," string",[85,2023,416],{"class":109},[85,2025,2026,2029,2031,2034],{"class":87,"line":119},[85,2027,2028],{"class":202},"  temperature",[85,2030,1967],{"class":98},[85,2032,2033],{"class":102}," number",[85,2035,416],{"class":109},[85,2037,2038,2041,2043,2045],{"class":87,"line":132},[85,2039,2040],{"class":202},"  conditions",[85,2042,1967],{"class":98},[85,2044,2021],{"class":102},[85,2046,416],{"class":109},[85,2048,2049,2052,2054,2056],{"class":87,"line":145},[85,2050,2051],{"class":202},"  humidity",[85,2053,1967],{"class":98},[85,2055,2033],{"class":102},[85,2057,416],{"class":109},[85,2059,2060],{"class":87,"line":157},[85,2061,280],{"class":109},[85,2063,2064],{"class":87,"line":181},[85,2065,449],{"emptyLinePlaceholder":327},[85,2067,2068,2070,2072,2075,2077,2079,2081,2083,2085,2087,2089,2092,2095,2097,2099],{"class":87,"line":187},[85,2069,1420],{"class":98},[85,2071,1426],{"class":98},[85,2073,2074],{"class":138}," WeatherCard",[85,2076,1432],{"class":109},[85,2078,203],{"class":202},[85,2080,172],{"class":109},[85,2082,574],{"class":202},[85,2084,172],{"class":109},[85,2086,579],{"class":202},[85,2088,172],{"class":109},[85,2090,2091],{"class":202},"humidity",[85,2093,2094],{"class":109}," }",[85,2096,1967],{"class":98},[85,2098,2009],{"class":138},[85,2100,2101],{"class":109},") {\n",[85,2103,2104,2106],{"class":87,"line":219},[85,2105,1460],{"class":98},[85,2107,2108],{"class":109}," (\n",[85,2110,2111,2114,2118,2121,2123,2126],{"class":87,"line":239},[85,2112,2113],{"class":109},"    \u003C",[85,2115,2117],{"class":2116},"s9eBZ","div",[85,2119,2120],{"class":138}," className",[85,2122,253],{"class":98},[85,2124,2125],{"class":125},"\"rounded-lg border bg-card p-6 shadow-sm\"",[85,2127,2128],{"class":109},">\n",[85,2130,2131,2134,2136,2138,2140,2143,2146,2148],{"class":87,"line":265},[85,2132,2133],{"class":109},"      \u003C",[85,2135,773],{"class":2116},[85,2137,2120],{"class":138},[85,2139,253],{"class":98},[85,2141,2142],{"class":125},"\"text-lg font-semibold\"",[85,2144,2145],{"class":109},">{city}\u003C\u002F",[85,2147,773],{"class":2116},[85,2149,2128],{"class":109},[85,2151,2152,2154,2156,2158,2160,2163],{"class":87,"line":271},[85,2153,2133],{"class":109},[85,2155,2117],{"class":2116},[85,2157,2120],{"class":138},[85,2159,253],{"class":98},[85,2161,2162],{"class":125},"\"mt-2 flex items-baseline gap-2\"",[85,2164,2128],{"class":109},[85,2166,2167,2170,2172,2174,2176,2179,2182,2184],{"class":87,"line":277},[85,2168,2169],{"class":109},"        \u003C",[85,2171,85],{"class":2116},[85,2173,2120],{"class":138},[85,2175,253],{"class":98},[85,2177,2178],{"class":125},"\"text-4xl font-bold\"",[85,2180,2181],{"class":109},">{temperature}°C\u003C\u002F",[85,2183,85],{"class":2116},[85,2185,2128],{"class":109},[85,2187,2188,2190,2192,2194,2196,2199,2202,2204],{"class":87,"line":552},[85,2189,2169],{"class":109},[85,2191,85],{"class":2116},[85,2193,2120],{"class":138},[85,2195,253],{"class":98},[85,2197,2198],{"class":125},"\"text-muted-foreground\"",[85,2200,2201],{"class":109},">{conditions}\u003C\u002F",[85,2203,85],{"class":2116},[85,2205,2128],{"class":109},[85,2207,2208,2211,2213],{"class":87,"line":558},[85,2209,2210],{"class":109},"      \u003C\u002F",[85,2212,2117],{"class":2116},[85,2214,2128],{"class":109},[85,2216,2217,2219,2221,2223,2225,2228],{"class":87,"line":588},[85,2218,2133],{"class":109},[85,2220,17],{"class":2116},[85,2222,2120],{"class":138},[85,2224,253],{"class":98},[85,2226,2227],{"class":125},"\"mt-2 text-sm text-muted-foreground\"",[85,2229,2128],{"class":109},[85,2231,2232],{"class":87,"line":627},[85,2233,2234],{"class":109},"        Humidity: {humidity}%\n",[85,2236,2237,2239,2241],{"class":87,"line":633},[85,2238,2210],{"class":109},[85,2240,17],{"class":2116},[85,2242,2128],{"class":109},[85,2244,2245,2248,2250],{"class":87,"line":638},[85,2246,2247],{"class":109},"    \u003C\u002F",[85,2249,2117],{"class":2116},[85,2251,2128],{"class":109},[85,2253,2254],{"class":87,"line":644},[85,2255,2256],{"class":109},"  );\n",[85,2258,2259],{"class":87,"line":654},[85,2260,280],{"class":109},[75,2262,2264],{"className":1992,"code":2263,"language":1994,"meta":80,"style":80},"\u002F\u002F components\u002Fstock-ticker.tsx\ninterface StockTickerProps {\n  symbol: string;\n  price: number;\n  change: number;\n  changePercent: number;\n}\n\nexport function StockTicker({ symbol, price, change, changePercent }: StockTickerProps) {\n  const isPositive = change >= 0;\n  const sign = isPositive ? '+' : '';\n  const color = isPositive ? 'text-green-600' : 'text-red-600';\n\n  return (\n    \u003Cdiv className=\"rounded-lg border bg-card p-6 shadow-sm\">\n      \u003Cdiv className=\"flex items-center justify-between\">\n        \u003Ch3 className=\"text-xl font-bold\">{symbol}\u003C\u002Fh3>\n        \u003Cspan className={`text-sm font-medium ${color}`}>\n          {sign}{changePercent.toFixed(2)}%\n        \u003C\u002Fspan>\n      \u003C\u002Fdiv>\n      \u003Cdiv className=\"mt-2 flex items-baseline gap-2\">\n        \u003Cspan className=\"text-3xl font-bold\">${price.toFixed(2)}\u003C\u002Fspan>\n        \u003Cspan className={`text-sm ${color}`}>\n          {sign}{change.toFixed(2)} today\n        \u003C\u002Fspan>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  );\n}\n",[82,2265,2266,2271,2280,2291,2302,2313,2324,2328,2332,2369,2389,2415,2438,2442,2448,2462,2477,2497,2521,2537,2546,2554,2568,2597,2618,2632,2640,2648,2656,2660],{"__ignoreMap":80},[85,2267,2268],{"class":87,"line":88},[85,2269,2270],{"class":91},"\u002F\u002F components\u002Fstock-ticker.tsx\n",[85,2272,2273,2275,2278],{"class":87,"line":95},[85,2274,2006],{"class":98},[85,2276,2277],{"class":138}," StockTickerProps",[85,2279,110],{"class":109},[85,2281,2282,2285,2287,2289],{"class":87,"line":113},[85,2283,2284],{"class":202},"  symbol",[85,2286,1967],{"class":98},[85,2288,2021],{"class":102},[85,2290,416],{"class":109},[85,2292,2293,2296,2298,2300],{"class":87,"line":119},[85,2294,2295],{"class":202},"  price",[85,2297,1967],{"class":98},[85,2299,2033],{"class":102},[85,2301,416],{"class":109},[85,2303,2304,2307,2309,2311],{"class":87,"line":132},[85,2305,2306],{"class":202},"  change",[85,2308,1967],{"class":98},[85,2310,2033],{"class":102},[85,2312,416],{"class":109},[85,2314,2315,2318,2320,2322],{"class":87,"line":145},[85,2316,2317],{"class":202},"  changePercent",[85,2319,1967],{"class":98},[85,2321,2033],{"class":102},[85,2323,416],{"class":109},[85,2325,2326],{"class":87,"line":157},[85,2327,280],{"class":109},[85,2329,2330],{"class":87,"line":181},[85,2331,449],{"emptyLinePlaceholder":327},[85,2333,2334,2336,2338,2341,2343,2346,2348,2351,2353,2356,2358,2361,2363,2365,2367],{"class":87,"line":187},[85,2335,1420],{"class":98},[85,2337,1426],{"class":98},[85,2339,2340],{"class":138}," StockTicker",[85,2342,1432],{"class":109},[85,2344,2345],{"class":202},"symbol",[85,2347,172],{"class":109},[85,2349,2350],{"class":202},"price",[85,2352,172],{"class":109},[85,2354,2355],{"class":202},"change",[85,2357,172],{"class":109},[85,2359,2360],{"class":202},"changePercent",[85,2362,2094],{"class":109},[85,2364,1967],{"class":98},[85,2366,2277],{"class":138},[85,2368,2101],{"class":109},[85,2370,2371,2373,2376,2378,2381,2384,2387],{"class":87,"line":219},[85,2372,1443],{"class":98},[85,2374,2375],{"class":102}," isPositive",[85,2377,106],{"class":98},[85,2379,2380],{"class":109}," change ",[85,2382,2383],{"class":98},">=",[85,2385,2386],{"class":102}," 0",[85,2388,416],{"class":109},[85,2390,2391,2393,2396,2398,2401,2404,2407,2410,2413],{"class":87,"line":239},[85,2392,1443],{"class":98},[85,2394,2395],{"class":102}," sign",[85,2397,106],{"class":98},[85,2399,2400],{"class":109}," isPositive ",[85,2402,2403],{"class":98},"?",[85,2405,2406],{"class":125}," '+'",[85,2408,2409],{"class":98}," :",[85,2411,2412],{"class":125}," ''",[85,2414,416],{"class":109},[85,2416,2417,2419,2422,2424,2426,2428,2431,2433,2436],{"class":87,"line":265},[85,2418,1443],{"class":98},[85,2420,2421],{"class":102}," color",[85,2423,106],{"class":98},[85,2425,2400],{"class":109},[85,2427,2403],{"class":98},[85,2429,2430],{"class":125}," 'text-green-600'",[85,2432,2409],{"class":98},[85,2434,2435],{"class":125}," 'text-red-600'",[85,2437,416],{"class":109},[85,2439,2440],{"class":87,"line":271},[85,2441,449],{"emptyLinePlaceholder":327},[85,2443,2444,2446],{"class":87,"line":277},[85,2445,1460],{"class":98},[85,2447,2108],{"class":109},[85,2449,2450,2452,2454,2456,2458,2460],{"class":87,"line":552},[85,2451,2113],{"class":109},[85,2453,2117],{"class":2116},[85,2455,2120],{"class":138},[85,2457,253],{"class":98},[85,2459,2125],{"class":125},[85,2461,2128],{"class":109},[85,2463,2464,2466,2468,2470,2472,2475],{"class":87,"line":558},[85,2465,2133],{"class":109},[85,2467,2117],{"class":2116},[85,2469,2120],{"class":138},[85,2471,253],{"class":98},[85,2473,2474],{"class":125},"\"flex items-center justify-between\"",[85,2476,2128],{"class":109},[85,2478,2479,2481,2483,2485,2487,2490,2493,2495],{"class":87,"line":588},[85,2480,2169],{"class":109},[85,2482,773],{"class":2116},[85,2484,2120],{"class":138},[85,2486,253],{"class":98},[85,2488,2489],{"class":125},"\"text-xl font-bold\"",[85,2491,2492],{"class":109},">{symbol}\u003C\u002F",[85,2494,773],{"class":2116},[85,2496,2128],{"class":109},[85,2498,2499,2501,2503,2505,2507,2509,2512,2515,2518],{"class":87,"line":627},[85,2500,2169],{"class":109},[85,2502,85],{"class":2116},[85,2504,2120],{"class":138},[85,2506,253],{"class":98},[85,2508,256],{"class":109},[85,2510,2511],{"class":125},"`text-sm font-medium ${",[85,2513,2514],{"class":109},"color",[85,2516,2517],{"class":125},"}`",[85,2519,2520],{"class":109},"}>\n",[85,2522,2523,2526,2529,2531,2534],{"class":87,"line":633},[85,2524,2525],{"class":109},"          {sign}{changePercent.",[85,2527,2528],{"class":138},"toFixed",[85,2530,476],{"class":109},[85,2532,2533],{"class":102},"2",[85,2535,2536],{"class":109},")}%\n",[85,2538,2539,2542,2544],{"class":87,"line":638},[85,2540,2541],{"class":109},"        \u003C\u002F",[85,2543,85],{"class":2116},[85,2545,2128],{"class":109},[85,2547,2548,2550,2552],{"class":87,"line":644},[85,2549,2210],{"class":109},[85,2551,2117],{"class":2116},[85,2553,2128],{"class":109},[85,2555,2556,2558,2560,2562,2564,2566],{"class":87,"line":654},[85,2557,2133],{"class":109},[85,2559,2117],{"class":2116},[85,2561,2120],{"class":138},[85,2563,253],{"class":98},[85,2565,2162],{"class":125},[85,2567,2128],{"class":109},[85,2569,2570,2572,2574,2576,2578,2581,2584,2586,2588,2590,2593,2595],{"class":87,"line":663},[85,2571,2169],{"class":109},[85,2573,85],{"class":2116},[85,2575,2120],{"class":138},[85,2577,253],{"class":98},[85,2579,2580],{"class":125},"\"text-3xl font-bold\"",[85,2582,2583],{"class":109},">${price.",[85,2585,2528],{"class":138},[85,2587,476],{"class":109},[85,2589,2533],{"class":102},[85,2591,2592],{"class":109},")}\u003C\u002F",[85,2594,85],{"class":2116},[85,2596,2128],{"class":109},[85,2598,2599,2601,2603,2605,2607,2609,2612,2614,2616],{"class":87,"line":695},[85,2600,2169],{"class":109},[85,2602,85],{"class":2116},[85,2604,2120],{"class":138},[85,2606,253],{"class":98},[85,2608,256],{"class":109},[85,2610,2611],{"class":125},"`text-sm ${",[85,2613,2514],{"class":109},[85,2615,2517],{"class":125},[85,2617,2520],{"class":109},[85,2619,2620,2623,2625,2627,2629],{"class":87,"line":700},[85,2621,2622],{"class":109},"          {sign}{change.",[85,2624,2528],{"class":138},[85,2626,476],{"class":109},[85,2628,2533],{"class":102},[85,2630,2631],{"class":109},")} today\n",[85,2633,2634,2636,2638],{"class":87,"line":719},[85,2635,2541],{"class":109},[85,2637,85],{"class":2116},[85,2639,2128],{"class":109},[85,2641,2642,2644,2646],{"class":87,"line":737},[85,2643,2210],{"class":109},[85,2645,2117],{"class":2116},[85,2647,2128],{"class":109},[85,2649,2650,2652,2654],{"class":87,"line":742},[85,2651,2247],{"class":109},[85,2653,2117],{"class":2116},[85,2655,2128],{"class":109},[85,2657,2658],{"class":87,"line":747},[85,2659,2256],{"class":109},[85,2661,2662],{"class":87,"line":752},[85,2663,280],{"class":109},[75,2665,2667],{"className":1992,"code":2666,"language":1994,"meta":80,"style":80},"\u002F\u002F components\u002Floading-skeleton.tsx\nexport function CardSkeleton({ height = 'h-32' }: { height?: string }) {\n  return (\n    \u003Cdiv className={`animate-pulse rounded-lg bg-muted ${height} w-full`} \u002F>\n  );\n}\n",[82,2668,2669,2674,2709,2715,2737,2741],{"__ignoreMap":80},[85,2670,2671],{"class":87,"line":88},[85,2672,2673],{"class":91},"\u002F\u002F components\u002Floading-skeleton.tsx\n",[85,2675,2676,2678,2680,2683,2685,2688,2690,2693,2695,2697,2700,2702,2705,2707],{"class":87,"line":95},[85,2677,1420],{"class":98},[85,2679,1426],{"class":98},[85,2681,2682],{"class":138}," CardSkeleton",[85,2684,1432],{"class":109},[85,2686,2687],{"class":202},"height",[85,2689,106],{"class":98},[85,2691,2692],{"class":125}," 'h-32'",[85,2694,2094],{"class":109},[85,2696,1967],{"class":98},[85,2698,2699],{"class":109}," { ",[85,2701,2687],{"class":202},[85,2703,2704],{"class":98},"?:",[85,2706,2021],{"class":102},[85,2708,1438],{"class":109},[85,2710,2711,2713],{"class":87,"line":113},[85,2712,1460],{"class":98},[85,2714,2108],{"class":109},[85,2716,2717,2719,2721,2723,2725,2727,2730,2732,2735],{"class":87,"line":119},[85,2718,2113],{"class":109},[85,2720,2117],{"class":2116},[85,2722,2120],{"class":138},[85,2724,253],{"class":98},[85,2726,256],{"class":109},[85,2728,2729],{"class":125},"`animate-pulse rounded-lg bg-muted ${",[85,2731,2687],{"class":109},[85,2733,2734],{"class":125},"} w-full`",[85,2736,262],{"class":109},[85,2738,2739],{"class":87,"line":132},[85,2740,2256],{"class":109},[85,2742,2743],{"class":87,"line":145},[85,2744,280],{"class":109},[12,2746,2748],{"id":2747},"step-3-define-ai-tools-server-action","Step 3: Define AI Tools (Server Action)",[17,2750,2751],{},"This is the core of Generative UI. Create a server action that connects your components to the AI as \"tools\" — functions the model can decide to call:",[75,2753,2755],{"className":1992,"code":2754,"language":1994,"meta":80,"style":80},"\u002F\u002F app\u002Factions.tsx\n'use server';\n\nimport { streamUI } from 'ai\u002Frsc';\nimport { openai } from '@ai-sdk\u002Fopenai';\nimport { z } from 'zod';\nimport { WeatherCard } from '@\u002Fcomponents\u002Fweather-card';\nimport { StockTicker } from '@\u002Fcomponents\u002Fstock-ticker';\nimport { CardSkeleton } from '@\u002Fcomponents\u002Floading-skeleton';\n\nexport async function generateUI(prompt: string) {\n  const result = await streamUI({\n    model: openai('gpt-4o'),\n    system: `You are a helpful financial and information assistant.\n             Use the available tools to display information visually\n             whenever possible. Prefer showing components over text responses.\n             When asked about weather or stocks, always use the appropriate tool.`,\n    prompt,\n    tools: {\n      showWeather: {\n        description: 'Display current weather conditions for a city. Use this when the user asks about weather, temperature, or climate.',\n        parameters: z.object({\n          city: z.string().describe('The city name, e.g. \"Paris\" or \"New York\"'),\n          temperature: z.number().describe('Current temperature in Celsius'),\n          conditions: z.string().describe('Weather description, e.g. \"Partly cloudy\"'),\n          humidity: z.number().min(0).max(100).describe('Relative humidity percentage'),\n        }),\n        generate: async function* (params) {\n          \u002F\u002F Yield a skeleton immediately while data \"loads\"\n          yield \u003CCardSkeleton height=\"h-36\" \u002F>;\n          \u002F\u002F In a real app, you would fetch live weather data here\n          return \u003CWeatherCard {...params} \u002F>;\n        },\n      },\n      showStock: {\n        description: 'Display a stock price and daily change. Use this when the user asks about stock prices, market data, or a company\\'s shares.',\n        parameters: z.object({\n          symbol: z.string().describe('Stock ticker symbol, e.g. \"AAPL\" or \"TSLA\"'),\n          price: z.number().describe('Current stock price in USD'),\n          change: z.number().describe('Price change today in USD'),\n          changePercent: z.number().describe('Percentage price change today'),\n        }),\n        generate: async function* (params) {\n          yield \u003CCardSkeleton height=\"h-32\" \u002F>;\n          return \u003CStockTicker {...params} \u002F>;\n        },\n      },\n    },\n  });\n\n  return result.value;\n}\n",[82,2756,2757,2762,2769,2773,2785,2797,2809,2823,2837,2851,2855,2877,2891,2904,2912,2917,2922,2929,2934,2939,2944,2954,2963,2983,3001,3019,3058,3063,3082,3087,3108,3114,3133,3139,3144,3150,3166,3175,3194,3213,3232,3251,3256,3273,3291,3307,3312,3317,3322,3328,3333,3341],{"__ignoreMap":80},[85,2758,2759],{"class":87,"line":88},[85,2760,2761],{"class":91},"\u002F\u002F app\u002Factions.tsx\n",[85,2763,2764,2767],{"class":87,"line":95},[85,2765,2766],{"class":125},"'use server'",[85,2768,416],{"class":109},[85,2770,2771],{"class":87,"line":113},[85,2772,449],{"emptyLinePlaceholder":327},[85,2774,2775,2777,2779,2781,2783],{"class":87,"line":119},[85,2776,404],{"class":98},[85,2778,407],{"class":109},[85,2780,410],{"class":98},[85,2782,413],{"class":125},[85,2784,416],{"class":109},[85,2786,2787,2789,2791,2793,2795],{"class":87,"line":132},[85,2788,404],{"class":98},[85,2790,423],{"class":109},[85,2792,410],{"class":98},[85,2794,428],{"class":125},[85,2796,416],{"class":109},[85,2798,2799,2801,2803,2805,2807],{"class":87,"line":145},[85,2800,404],{"class":98},[85,2802,437],{"class":109},[85,2804,410],{"class":98},[85,2806,442],{"class":125},[85,2808,416],{"class":109},[85,2810,2811,2813,2816,2818,2821],{"class":87,"line":157},[85,2812,404],{"class":98},[85,2814,2815],{"class":109}," { WeatherCard } ",[85,2817,410],{"class":98},[85,2819,2820],{"class":125}," '@\u002Fcomponents\u002Fweather-card'",[85,2822,416],{"class":109},[85,2824,2825,2827,2830,2832,2835],{"class":87,"line":181},[85,2826,404],{"class":98},[85,2828,2829],{"class":109}," { StockTicker } ",[85,2831,410],{"class":98},[85,2833,2834],{"class":125}," '@\u002Fcomponents\u002Fstock-ticker'",[85,2836,416],{"class":109},[85,2838,2839,2841,2844,2846,2849],{"class":87,"line":187},[85,2840,404],{"class":98},[85,2842,2843],{"class":109}," { CardSkeleton } ",[85,2845,410],{"class":98},[85,2847,2848],{"class":125}," '@\u002Fcomponents\u002Floading-skeleton'",[85,2850,416],{"class":109},[85,2852,2853],{"class":87,"line":219},[85,2854,449],{"emptyLinePlaceholder":327},[85,2856,2857,2859,2861,2863,2866,2868,2871,2873,2875],{"class":87,"line":239},[85,2858,1420],{"class":98},[85,2860,1423],{"class":98},[85,2862,1426],{"class":98},[85,2864,2865],{"class":138}," generateUI",[85,2867,476],{"class":109},[85,2869,2870],{"class":202},"prompt",[85,2872,1967],{"class":98},[85,2874,2021],{"class":102},[85,2876,2101],{"class":109},[85,2878,2879,2881,2883,2885,2887,2889],{"class":87,"line":265},[85,2880,1443],{"class":98},[85,2882,456],{"class":102},[85,2884,106],{"class":98},[85,2886,230],{"class":98},[85,2888,463],{"class":138},[85,2890,142],{"class":109},[85,2892,2893,2896,2898,2900,2902],{"class":87,"line":271},[85,2894,2895],{"class":109},"    model: ",[85,2897,473],{"class":138},[85,2899,476],{"class":109},[85,2901,479],{"class":125},[85,2903,482],{"class":109},[85,2905,2906,2909],{"class":87,"line":277},[85,2907,2908],{"class":109},"    system: ",[85,2910,2911],{"class":125},"`You are a helpful financial and information assistant.\n",[85,2913,2914],{"class":87,"line":552},[85,2915,2916],{"class":125},"             Use the available tools to display information visually\n",[85,2918,2919],{"class":87,"line":558},[85,2920,2921],{"class":125},"             whenever possible. Prefer showing components over text responses.\n",[85,2923,2924,2927],{"class":87,"line":588},[85,2925,2926],{"class":125},"             When asked about weather or stocks, always use the appropriate tool.`",[85,2928,129],{"class":109},[85,2930,2931],{"class":87,"line":627},[85,2932,2933],{"class":109},"    prompt,\n",[85,2935,2936],{"class":87,"line":633},[85,2937,2938],{"class":109},"    tools: {\n",[85,2940,2941],{"class":87,"line":638},[85,2942,2943],{"class":109},"      showWeather: {\n",[85,2945,2946,2949,2952],{"class":87,"line":644},[85,2947,2948],{"class":109},"        description: ",[85,2950,2951],{"class":125},"'Display current weather conditions for a city. Use this when the user asks about weather, temperature, or climate.'",[85,2953,129],{"class":109},[85,2955,2956,2959,2961],{"class":87,"line":654},[85,2957,2958],{"class":109},"        parameters: z.",[85,2960,139],{"class":138},[85,2962,142],{"class":109},[85,2964,2965,2968,2970,2973,2976,2978,2981],{"class":87,"line":663},[85,2966,2967],{"class":109},"          city: z.",[85,2969,151],{"class":138},[85,2971,2972],{"class":109},"().",[85,2974,2975],{"class":138},"describe",[85,2977,476],{"class":109},[85,2979,2980],{"class":125},"'The city name, e.g. \"Paris\" or \"New York\"'",[85,2982,482],{"class":109},[85,2984,2985,2988,2990,2992,2994,2996,2999],{"class":87,"line":695},[85,2986,2987],{"class":109},"          temperature: z.",[85,2989,538],{"class":138},[85,2991,2972],{"class":109},[85,2993,2975],{"class":138},[85,2995,476],{"class":109},[85,2997,2998],{"class":125},"'Current temperature in Celsius'",[85,3000,482],{"class":109},[85,3002,3003,3006,3008,3010,3012,3014,3017],{"class":87,"line":700},[85,3004,3005],{"class":109},"          conditions: z.",[85,3007,151],{"class":138},[85,3009,2972],{"class":109},[85,3011,2975],{"class":138},[85,3013,476],{"class":109},[85,3015,3016],{"class":125},"'Weather description, e.g. \"Partly cloudy\"'",[85,3018,482],{"class":109},[85,3020,3021,3024,3026,3028,3031,3033,3036,3039,3042,3044,3047,3049,3051,3053,3056],{"class":87,"line":719},[85,3022,3023],{"class":109},"          humidity: z.",[85,3025,538],{"class":138},[85,3027,2972],{"class":109},[85,3029,3030],{"class":138},"min",[85,3032,476],{"class":109},[85,3034,3035],{"class":102},"0",[85,3037,3038],{"class":109},").",[85,3040,3041],{"class":138},"max",[85,3043,476],{"class":109},[85,3045,3046],{"class":102},"100",[85,3048,3038],{"class":109},[85,3050,2975],{"class":138},[85,3052,476],{"class":109},[85,3054,3055],{"class":125},"'Relative humidity percentage'",[85,3057,482],{"class":109},[85,3059,3060],{"class":87,"line":737},[85,3061,3062],{"class":109},"        }),\n",[85,3064,3065,3068,3070,3072,3075,3078,3080],{"class":87,"line":742},[85,3066,3067],{"class":138},"        generate",[85,3069,193],{"class":109},[85,3071,196],{"class":98},[85,3073,3074],{"class":98}," function*",[85,3076,3077],{"class":109}," (",[85,3079,1435],{"class":202},[85,3081,2101],{"class":109},[85,3083,3084],{"class":87,"line":747},[85,3085,3086],{"class":91},"          \u002F\u002F Yield a skeleton immediately while data \"loads\"\n",[85,3088,3089,3092,3094,3097,3100,3102,3105],{"class":87,"line":752},[85,3090,3091],{"class":98},"          yield",[85,3093,245],{"class":109},[85,3095,3096],{"class":102},"CardSkeleton",[85,3098,3099],{"class":138}," height",[85,3101,253],{"class":98},[85,3103,3104],{"class":125},"\"h-36\"",[85,3106,3107],{"class":109}," \u002F>;\n",[85,3109,3111],{"class":87,"line":3110},31,[85,3112,3113],{"class":91},"          \u002F\u002F In a real app, you would fetch live weather data here\n",[85,3115,3117,3120,3122,3124,3127,3130],{"class":87,"line":3116},32,[85,3118,3119],{"class":98},"          return",[85,3121,245],{"class":109},[85,3123,248],{"class":102},[85,3125,3126],{"class":109}," {",[85,3128,3129],{"class":98},"...",[85,3131,3132],{"class":109},"params} \u002F>;\n",[85,3134,3136],{"class":87,"line":3135},33,[85,3137,3138],{"class":109},"        },\n",[85,3140,3142],{"class":87,"line":3141},34,[85,3143,630],{"class":109},[85,3145,3147],{"class":87,"line":3146},35,[85,3148,3149],{"class":109},"      showStock: {\n",[85,3151,3153,3155,3158,3161,3164],{"class":87,"line":3152},36,[85,3154,2948],{"class":109},[85,3156,3157],{"class":125},"'Display a stock price and daily change. Use this when the user asks about stock prices, market data, or a company",[85,3159,3160],{"class":102},"\\'",[85,3162,3163],{"class":125},"s shares.'",[85,3165,129],{"class":109},[85,3167,3169,3171,3173],{"class":87,"line":3168},37,[85,3170,2958],{"class":109},[85,3172,139],{"class":138},[85,3174,142],{"class":109},[85,3176,3178,3181,3183,3185,3187,3189,3192],{"class":87,"line":3177},38,[85,3179,3180],{"class":109},"          symbol: z.",[85,3182,151],{"class":138},[85,3184,2972],{"class":109},[85,3186,2975],{"class":138},[85,3188,476],{"class":109},[85,3190,3191],{"class":125},"'Stock ticker symbol, e.g. \"AAPL\" or \"TSLA\"'",[85,3193,482],{"class":109},[85,3195,3197,3200,3202,3204,3206,3208,3211],{"class":87,"line":3196},39,[85,3198,3199],{"class":109},"          price: z.",[85,3201,538],{"class":138},[85,3203,2972],{"class":109},[85,3205,2975],{"class":138},[85,3207,476],{"class":109},[85,3209,3210],{"class":125},"'Current stock price in USD'",[85,3212,482],{"class":109},[85,3214,3216,3219,3221,3223,3225,3227,3230],{"class":87,"line":3215},40,[85,3217,3218],{"class":109},"          change: z.",[85,3220,538],{"class":138},[85,3222,2972],{"class":109},[85,3224,2975],{"class":138},[85,3226,476],{"class":109},[85,3228,3229],{"class":125},"'Price change today in USD'",[85,3231,482],{"class":109},[85,3233,3235,3238,3240,3242,3244,3246,3249],{"class":87,"line":3234},41,[85,3236,3237],{"class":109},"          changePercent: z.",[85,3239,538],{"class":138},[85,3241,2972],{"class":109},[85,3243,2975],{"class":138},[85,3245,476],{"class":109},[85,3247,3248],{"class":125},"'Percentage price change today'",[85,3250,482],{"class":109},[85,3252,3254],{"class":87,"line":3253},42,[85,3255,3062],{"class":109},[85,3257,3259,3261,3263,3265,3267,3269,3271],{"class":87,"line":3258},43,[85,3260,3067],{"class":138},[85,3262,193],{"class":109},[85,3264,196],{"class":98},[85,3266,3074],{"class":98},[85,3268,3077],{"class":109},[85,3270,1435],{"class":202},[85,3272,2101],{"class":109},[85,3274,3276,3278,3280,3282,3284,3286,3289],{"class":87,"line":3275},44,[85,3277,3091],{"class":98},[85,3279,245],{"class":109},[85,3281,3096],{"class":102},[85,3283,3099],{"class":138},[85,3285,253],{"class":98},[85,3287,3288],{"class":125},"\"h-32\"",[85,3290,3107],{"class":109},[85,3292,3294,3296,3298,3301,3303,3305],{"class":87,"line":3293},45,[85,3295,3119],{"class":98},[85,3297,245],{"class":109},[85,3299,3300],{"class":102},"StockTicker",[85,3302,3126],{"class":109},[85,3304,3129],{"class":98},[85,3306,3132],{"class":109},[85,3308,3310],{"class":87,"line":3309},46,[85,3311,3138],{"class":109},[85,3313,3315],{"class":87,"line":3314},47,[85,3316,630],{"class":109},[85,3318,3320],{"class":87,"line":3319},48,[85,3321,268],{"class":109},[85,3323,3325],{"class":87,"line":3324},49,[85,3326,3327],{"class":109},"  });\n",[85,3329,3331],{"class":87,"line":3330},50,[85,3332,449],{"emptyLinePlaceholder":327},[85,3334,3336,3338],{"class":87,"line":3335},51,[85,3337,1460],{"class":98},[85,3339,3340],{"class":109}," result.value;\n",[85,3342,3344],{"class":87,"line":3343},52,[85,3345,280],{"class":109},[17,3347,3348],{},"Three things worth understanding about this code:",[17,3350,3351,3356,3357,3360,3361,3364],{},[39,3352,1945,3353,3355],{},[82,3354,1404],{}," function is an async generator."," The ",[82,3358,3359],{},"yield"," keyword sends the skeleton immediately — before the AI finishes resolving parameters. The ",[82,3362,3363],{},"return"," sends the final component. This is how streaming Generative UI works.",[17,3366,3367,3356,3370,3372],{},[39,3368,3369],{},"Tool descriptions are instructions to the AI.",[82,3371,1052],{}," fields are what the model reads to decide which tool to call. Write them clearly, including when the tool should and should not be used.",[17,3374,3375,3378],{},[39,3376,3377],{},"Zod schemas enforce the contract."," The AI cannot pass invalid parameters if you define strict Zod schemas. Validation failures are caught before the component renders.",[12,3380,3382],{"id":3381},"step-4-build-the-ui","Step 4: Build the UI",[75,3384,3386],{"className":1992,"code":3385,"language":1994,"meta":80,"style":80},"\u002F\u002F app\u002Fpage.tsx\n'use client';\n\nimport { useState } from 'react';\nimport { generateUI } from '.\u002Factions';\n\nconst EXAMPLE_PROMPTS = [\n  \"What's the weather like in Tokyo?\",\n  \"Show me Apple's current stock price\",\n  \"Compare the weather in London and New York\",\n  \"How is Tesla stock doing?\",\n];\n\nexport default function Home() {\n  const [prompt, setPrompt] = useState('');\n  const [messages, setMessages] = useState\u003CArray\u003C{ prompt: string; ui: React.ReactNode }>>([]);\n  const [loading, setLoading] = useState(false);\n\n  async function handleSubmit(e: React.FormEvent) {\n    e.preventDefault();\n    if (!prompt.trim() || loading) return;\n\n    const currentPrompt = prompt;\n    setPrompt('');\n    setLoading(true);\n\n    const ui = await generateUI(currentPrompt);\n    setMessages(prev => [...prev, { prompt: currentPrompt, ui }]);\n    setLoading(false);\n  }\n\n  return (\n    \u003Cmain className=\"mx-auto max-w-2xl p-8\">\n      \u003Ch1 className=\"text-3xl font-bold\">Generative UI Demo\u003C\u002Fh1>\n      \u003Cp className=\"mt-2 text-muted-foreground\">\n        Ask about weather or stocks — watch the AI generate the right interface.\n      \u003C\u002Fp>\n\n      {\u002F* Example prompts *\u002F}\n      \u003Cdiv className=\"mt-4 flex flex-wrap gap-2\">\n        {EXAMPLE_PROMPTS.map(p => (\n          \u003Cbutton\n            key={p}\n            onClick={() => setPrompt(p)}\n            className=\"rounded-full border px-3 py-1 text-sm hover:bg-muted\"\n          >\n            {p}\n          \u003C\u002Fbutton>\n        ))}\n      \u003C\u002Fdiv>\n\n      {\u002F* Prompt input *\u002F}\n      \u003Cform onSubmit={handleSubmit} className=\"mt-6 flex gap-2\">\n        \u003Cinput\n          value={prompt}\n          onChange={e => setPrompt(e.target.value)}\n          placeholder=\"Ask anything...\"\n          className=\"flex-1 rounded-md border bg-background px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring\"\n        \u002F>\n        \u003Cbutton\n          type=\"submit\"\n          disabled={loading || !prompt.trim()}\n          className=\"rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground disabled:opacity-50\"\n        >\n          {loading ? 'Generating...' : 'Ask'}\n        \u003C\u002Fbutton>\n      \u003C\u002Fform>\n\n      {\u002F* Generated UI output *\u002F}\n      \u003Cdiv className=\"mt-8 space-y-6\">\n        {messages.map((msg, i) => (\n          \u003Cdiv key={i}>\n            \u003Cp className=\"mb-2 text-sm font-medium text-muted-foreground\">\n              \"{msg.prompt}\"\n            \u003C\u002Fp>\n            {msg.ui}\n          \u003C\u002Fdiv>\n        ))}\n      \u003C\u002Fdiv>\n    \u003C\u002Fmain>\n  );\n}\n",[82,3387,3388,3393,3400,3404,3418,3432,3436,3448,3455,3462,3469,3476,3481,3485,3500,3530,3585,3612,3616,3642,3653,3682,3686,3699,3710,3722,3726,3742,3762,3772,3777,3781,3787,3803,3823,3838,3843,3851,3855,3865,3880,3901,3909,3919,3937,3947,3952,3957,3967,3972,3980,3984,3993,4019,4027,4038,4057,4068,4079,4085,4092,4103,4126,4136,4142,4160,4169,4178,4183,4193,4209,4235,4250,4267,4273,4283,4289,4298,4303,4312,4321,4326],{"__ignoreMap":80},[85,3389,3390],{"class":87,"line":88},[85,3391,3392],{"class":91},"\u002F\u002F app\u002Fpage.tsx\n",[85,3394,3395,3398],{"class":87,"line":95},[85,3396,3397],{"class":125},"'use client'",[85,3399,416],{"class":109},[85,3401,3402],{"class":87,"line":113},[85,3403,449],{"emptyLinePlaceholder":327},[85,3405,3406,3408,3411,3413,3416],{"class":87,"line":119},[85,3407,404],{"class":98},[85,3409,3410],{"class":109}," { useState } ",[85,3412,410],{"class":98},[85,3414,3415],{"class":125}," 'react'",[85,3417,416],{"class":109},[85,3419,3420,3422,3425,3427,3430],{"class":87,"line":132},[85,3421,404],{"class":98},[85,3423,3424],{"class":109}," { generateUI } ",[85,3426,410],{"class":98},[85,3428,3429],{"class":125}," '.\u002Factions'",[85,3431,416],{"class":109},[85,3433,3434],{"class":87,"line":145},[85,3435,449],{"emptyLinePlaceholder":327},[85,3437,3438,3440,3443,3445],{"class":87,"line":157},[85,3439,99],{"class":98},[85,3441,3442],{"class":102}," EXAMPLE_PROMPTS",[85,3444,106],{"class":98},[85,3446,3447],{"class":109}," [\n",[85,3449,3450,3453],{"class":87,"line":181},[85,3451,3452],{"class":125},"  \"What's the weather like in Tokyo?\"",[85,3454,129],{"class":109},[85,3456,3457,3460],{"class":87,"line":187},[85,3458,3459],{"class":125},"  \"Show me Apple's current stock price\"",[85,3461,129],{"class":109},[85,3463,3464,3467],{"class":87,"line":219},[85,3465,3466],{"class":125},"  \"Compare the weather in London and New York\"",[85,3468,129],{"class":109},[85,3470,3471,3474],{"class":87,"line":239},[85,3472,3473],{"class":125},"  \"How is Tesla stock doing?\"",[85,3475,129],{"class":109},[85,3477,3478],{"class":87,"line":265},[85,3479,3480],{"class":109},"];\n",[85,3482,3483],{"class":87,"line":271},[85,3484,449],{"emptyLinePlaceholder":327},[85,3486,3487,3489,3492,3494,3497],{"class":87,"line":277},[85,3488,1420],{"class":98},[85,3490,3491],{"class":98}," default",[85,3493,1426],{"class":98},[85,3495,3496],{"class":138}," Home",[85,3498,3499],{"class":109},"() {\n",[85,3501,3502,3504,3507,3509,3511,3514,3517,3519,3522,3524,3527],{"class":87,"line":552},[85,3503,1443],{"class":98},[85,3505,3506],{"class":109}," [",[85,3508,2870],{"class":102},[85,3510,172],{"class":109},[85,3512,3513],{"class":102},"setPrompt",[85,3515,3516],{"class":109},"] ",[85,3518,253],{"class":98},[85,3520,3521],{"class":138}," useState",[85,3523,476],{"class":109},[85,3525,3526],{"class":125},"''",[85,3528,3529],{"class":109},");\n",[85,3531,3532,3534,3536,3539,3541,3544,3546,3548,3550,3553,3556,3559,3561,3563,3565,3568,3571,3573,3576,3579,3582],{"class":87,"line":558},[85,3533,1443],{"class":98},[85,3535,3506],{"class":109},[85,3537,3538],{"class":102},"messages",[85,3540,172],{"class":109},[85,3542,3543],{"class":102},"setMessages",[85,3545,3516],{"class":109},[85,3547,253],{"class":98},[85,3549,3521],{"class":138},[85,3551,3552],{"class":109},"\u003C",[85,3554,3555],{"class":138},"Array",[85,3557,3558],{"class":109},"\u003C{ ",[85,3560,2870],{"class":202},[85,3562,1967],{"class":98},[85,3564,2021],{"class":102},[85,3566,3567],{"class":109},"; ",[85,3569,3570],{"class":202},"ui",[85,3572,1967],{"class":98},[85,3574,3575],{"class":138}," React",[85,3577,3578],{"class":109},".",[85,3580,3581],{"class":138},"ReactNode",[85,3583,3584],{"class":109}," }>>([]);\n",[85,3586,3587,3589,3591,3594,3596,3599,3601,3603,3605,3607,3610],{"class":87,"line":588},[85,3588,1443],{"class":98},[85,3590,3506],{"class":109},[85,3592,3593],{"class":102},"loading",[85,3595,172],{"class":109},[85,3597,3598],{"class":102},"setLoading",[85,3600,3516],{"class":109},[85,3602,253],{"class":98},[85,3604,3521],{"class":138},[85,3606,476],{"class":109},[85,3608,3609],{"class":102},"false",[85,3611,3529],{"class":109},[85,3613,3614],{"class":87,"line":627},[85,3615,449],{"emptyLinePlaceholder":327},[85,3617,3618,3621,3623,3626,3628,3631,3633,3635,3637,3640],{"class":87,"line":633},[85,3619,3620],{"class":98},"  async",[85,3622,1426],{"class":98},[85,3624,3625],{"class":138}," handleSubmit",[85,3627,476],{"class":109},[85,3629,3630],{"class":202},"e",[85,3632,1967],{"class":98},[85,3634,3575],{"class":138},[85,3636,3578],{"class":109},[85,3638,3639],{"class":138},"FormEvent",[85,3641,2101],{"class":109},[85,3643,3644,3647,3650],{"class":87,"line":638},[85,3645,3646],{"class":109},"    e.",[85,3648,3649],{"class":138},"preventDefault",[85,3651,3652],{"class":109},"();\n",[85,3654,3655,3658,3660,3663,3666,3669,3672,3675,3678,3680],{"class":87,"line":644},[85,3656,3657],{"class":98},"    if",[85,3659,3077],{"class":109},[85,3661,3662],{"class":98},"!",[85,3664,3665],{"class":109},"prompt.",[85,3667,3668],{"class":138},"trim",[85,3670,3671],{"class":109},"() ",[85,3673,3674],{"class":98},"||",[85,3676,3677],{"class":109}," loading) ",[85,3679,3363],{"class":98},[85,3681,416],{"class":109},[85,3683,3684],{"class":87,"line":654},[85,3685,449],{"emptyLinePlaceholder":327},[85,3687,3688,3691,3694,3696],{"class":87,"line":663},[85,3689,3690],{"class":98},"    const",[85,3692,3693],{"class":102}," currentPrompt",[85,3695,106],{"class":98},[85,3697,3698],{"class":109}," prompt;\n",[85,3700,3701,3704,3706,3708],{"class":87,"line":695},[85,3702,3703],{"class":138},"    setPrompt",[85,3705,476],{"class":109},[85,3707,3526],{"class":125},[85,3709,3529],{"class":109},[85,3711,3712,3715,3717,3720],{"class":87,"line":700},[85,3713,3714],{"class":138},"    setLoading",[85,3716,476],{"class":109},[85,3718,3719],{"class":102},"true",[85,3721,3529],{"class":109},[85,3723,3724],{"class":87,"line":719},[85,3725,449],{"emptyLinePlaceholder":327},[85,3727,3728,3730,3733,3735,3737,3739],{"class":87,"line":737},[85,3729,3690],{"class":98},[85,3731,3732],{"class":102}," ui",[85,3734,106],{"class":98},[85,3736,230],{"class":98},[85,3738,2865],{"class":138},[85,3740,3741],{"class":109},"(currentPrompt);\n",[85,3743,3744,3747,3749,3752,3755,3757,3759],{"class":87,"line":742},[85,3745,3746],{"class":138},"    setMessages",[85,3748,476],{"class":109},[85,3750,3751],{"class":202},"prev",[85,3753,3754],{"class":98}," =>",[85,3756,3506],{"class":109},[85,3758,3129],{"class":98},[85,3760,3761],{"class":109},"prev, { prompt: currentPrompt, ui }]);\n",[85,3763,3764,3766,3768,3770],{"class":87,"line":747},[85,3765,3714],{"class":138},[85,3767,476],{"class":109},[85,3769,3609],{"class":102},[85,3771,3529],{"class":109},[85,3773,3774],{"class":87,"line":752},[85,3775,3776],{"class":109},"  }\n",[85,3778,3779],{"class":87,"line":3110},[85,3780,449],{"emptyLinePlaceholder":327},[85,3782,3783,3785],{"class":87,"line":3116},[85,3784,1460],{"class":98},[85,3786,2108],{"class":109},[85,3788,3789,3791,3794,3796,3798,3801],{"class":87,"line":3135},[85,3790,2113],{"class":109},[85,3792,3793],{"class":2116},"main",[85,3795,2120],{"class":138},[85,3797,253],{"class":98},[85,3799,3800],{"class":125},"\"mx-auto max-w-2xl p-8\"",[85,3802,2128],{"class":109},[85,3804,3805,3807,3810,3812,3814,3816,3819,3821],{"class":87,"line":3141},[85,3806,2133],{"class":109},[85,3808,3809],{"class":2116},"h1",[85,3811,2120],{"class":138},[85,3813,253],{"class":98},[85,3815,2580],{"class":125},[85,3817,3818],{"class":109},">Generative UI Demo\u003C\u002F",[85,3820,3809],{"class":2116},[85,3822,2128],{"class":109},[85,3824,3825,3827,3829,3831,3833,3836],{"class":87,"line":3146},[85,3826,2133],{"class":109},[85,3828,17],{"class":2116},[85,3830,2120],{"class":138},[85,3832,253],{"class":98},[85,3834,3835],{"class":125},"\"mt-2 text-muted-foreground\"",[85,3837,2128],{"class":109},[85,3839,3840],{"class":87,"line":3152},[85,3841,3842],{"class":109},"        Ask about weather or stocks — watch the AI generate the right interface.\n",[85,3844,3845,3847,3849],{"class":87,"line":3168},[85,3846,2210],{"class":109},[85,3848,17],{"class":2116},[85,3850,2128],{"class":109},[85,3852,3853],{"class":87,"line":3177},[85,3854,449],{"emptyLinePlaceholder":327},[85,3856,3857,3860,3863],{"class":87,"line":3196},[85,3858,3859],{"class":109},"      {",[85,3861,3862],{"class":91},"\u002F* Example prompts *\u002F",[85,3864,280],{"class":109},[85,3866,3867,3869,3871,3873,3875,3878],{"class":87,"line":3215},[85,3868,2133],{"class":109},[85,3870,2117],{"class":2116},[85,3872,2120],{"class":138},[85,3874,253],{"class":98},[85,3876,3877],{"class":125},"\"mt-4 flex flex-wrap gap-2\"",[85,3879,2128],{"class":109},[85,3881,3882,3885,3888,3890,3893,3895,3897,3899],{"class":87,"line":3234},[85,3883,3884],{"class":109},"        {",[85,3886,3887],{"class":102},"EXAMPLE_PROMPTS",[85,3889,3578],{"class":109},[85,3891,3892],{"class":138},"map",[85,3894,476],{"class":109},[85,3896,17],{"class":202},[85,3898,3754],{"class":98},[85,3900,2108],{"class":109},[85,3902,3903,3906],{"class":87,"line":3253},[85,3904,3905],{"class":109},"          \u003C",[85,3907,3908],{"class":2116},"button\n",[85,3910,3911,3914,3916],{"class":87,"line":3258},[85,3912,3913],{"class":138},"            key",[85,3915,253],{"class":98},[85,3917,3918],{"class":109},"{p}\n",[85,3920,3921,3924,3926,3929,3931,3934],{"class":87,"line":3275},[85,3922,3923],{"class":138},"            onClick",[85,3925,253],{"class":98},[85,3927,3928],{"class":109},"{() ",[85,3930,214],{"class":98},[85,3932,3933],{"class":138}," setPrompt",[85,3935,3936],{"class":109},"(p)}\n",[85,3938,3939,3942,3944],{"class":87,"line":3293},[85,3940,3941],{"class":138},"            className",[85,3943,253],{"class":98},[85,3945,3946],{"class":125},"\"rounded-full border px-3 py-1 text-sm hover:bg-muted\"\n",[85,3948,3949],{"class":87,"line":3309},[85,3950,3951],{"class":109},"          >\n",[85,3953,3954],{"class":87,"line":3314},[85,3955,3956],{"class":109},"            {p}\n",[85,3958,3959,3962,3965],{"class":87,"line":3319},[85,3960,3961],{"class":109},"          \u003C\u002F",[85,3963,3964],{"class":2116},"button",[85,3966,2128],{"class":109},[85,3968,3969],{"class":87,"line":3324},[85,3970,3971],{"class":109},"        ))}\n",[85,3973,3974,3976,3978],{"class":87,"line":3330},[85,3975,2210],{"class":109},[85,3977,2117],{"class":2116},[85,3979,2128],{"class":109},[85,3981,3982],{"class":87,"line":3335},[85,3983,449],{"emptyLinePlaceholder":327},[85,3985,3986,3988,3991],{"class":87,"line":3343},[85,3987,3859],{"class":109},[85,3989,3990],{"class":91},"\u002F* Prompt input *\u002F",[85,3992,280],{"class":109},[85,3994,3996,3998,4001,4004,4006,4009,4012,4014,4017],{"class":87,"line":3995},53,[85,3997,2133],{"class":109},[85,3999,4000],{"class":2116},"form",[85,4002,4003],{"class":138}," onSubmit",[85,4005,253],{"class":98},[85,4007,4008],{"class":109},"{handleSubmit} ",[85,4010,4011],{"class":138},"className",[85,4013,253],{"class":98},[85,4015,4016],{"class":125},"\"mt-6 flex gap-2\"",[85,4018,2128],{"class":109},[85,4020,4022,4024],{"class":87,"line":4021},54,[85,4023,2169],{"class":109},[85,4025,4026],{"class":2116},"input\n",[85,4028,4030,4033,4035],{"class":87,"line":4029},55,[85,4031,4032],{"class":138},"          value",[85,4034,253],{"class":98},[85,4036,4037],{"class":109},"{prompt}\n",[85,4039,4041,4044,4046,4048,4050,4052,4054],{"class":87,"line":4040},56,[85,4042,4043],{"class":138},"          onChange",[85,4045,253],{"class":98},[85,4047,256],{"class":109},[85,4049,3630],{"class":202},[85,4051,3754],{"class":98},[85,4053,3933],{"class":138},[85,4055,4056],{"class":109},"(e.target.value)}\n",[85,4058,4060,4063,4065],{"class":87,"line":4059},57,[85,4061,4062],{"class":138},"          placeholder",[85,4064,253],{"class":98},[85,4066,4067],{"class":125},"\"Ask anything...\"\n",[85,4069,4071,4074,4076],{"class":87,"line":4070},58,[85,4072,4073],{"class":138},"          className",[85,4075,253],{"class":98},[85,4077,4078],{"class":125},"\"flex-1 rounded-md border bg-background px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring\"\n",[85,4080,4082],{"class":87,"line":4081},59,[85,4083,4084],{"class":109},"        \u002F>\n",[85,4086,4088,4090],{"class":87,"line":4087},60,[85,4089,2169],{"class":109},[85,4091,3908],{"class":2116},[85,4093,4095,4098,4100],{"class":87,"line":4094},61,[85,4096,4097],{"class":138},"          type",[85,4099,253],{"class":98},[85,4101,4102],{"class":125},"\"submit\"\n",[85,4104,4106,4109,4111,4114,4116,4119,4121,4123],{"class":87,"line":4105},62,[85,4107,4108],{"class":138},"          disabled",[85,4110,253],{"class":98},[85,4112,4113],{"class":109},"{loading ",[85,4115,3674],{"class":98},[85,4117,4118],{"class":98}," !",[85,4120,3665],{"class":109},[85,4122,3668],{"class":138},[85,4124,4125],{"class":109},"()}\n",[85,4127,4129,4131,4133],{"class":87,"line":4128},63,[85,4130,4073],{"class":138},[85,4132,253],{"class":98},[85,4134,4135],{"class":125},"\"rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground disabled:opacity-50\"\n",[85,4137,4139],{"class":87,"line":4138},64,[85,4140,4141],{"class":109},"        >\n",[85,4143,4145,4148,4150,4153,4155,4158],{"class":87,"line":4144},65,[85,4146,4147],{"class":109},"          {loading ",[85,4149,2403],{"class":98},[85,4151,4152],{"class":125}," 'Generating...'",[85,4154,2409],{"class":98},[85,4156,4157],{"class":125}," 'Ask'",[85,4159,280],{"class":109},[85,4161,4163,4165,4167],{"class":87,"line":4162},66,[85,4164,2541],{"class":109},[85,4166,3964],{"class":2116},[85,4168,2128],{"class":109},[85,4170,4172,4174,4176],{"class":87,"line":4171},67,[85,4173,2210],{"class":109},[85,4175,4000],{"class":2116},[85,4177,2128],{"class":109},[85,4179,4181],{"class":87,"line":4180},68,[85,4182,449],{"emptyLinePlaceholder":327},[85,4184,4186,4188,4191],{"class":87,"line":4185},69,[85,4187,3859],{"class":109},[85,4189,4190],{"class":91},"\u002F* Generated UI output *\u002F",[85,4192,280],{"class":109},[85,4194,4196,4198,4200,4202,4204,4207],{"class":87,"line":4195},70,[85,4197,2133],{"class":109},[85,4199,2117],{"class":2116},[85,4201,2120],{"class":138},[85,4203,253],{"class":98},[85,4205,4206],{"class":125},"\"mt-8 space-y-6\"",[85,4208,2128],{"class":109},[85,4210,4212,4215,4217,4220,4223,4225,4228,4231,4233],{"class":87,"line":4211},71,[85,4213,4214],{"class":109},"        {messages.",[85,4216,3892],{"class":138},[85,4218,4219],{"class":109},"((",[85,4221,4222],{"class":202},"msg",[85,4224,172],{"class":109},[85,4226,4227],{"class":202},"i",[85,4229,4230],{"class":109},") ",[85,4232,214],{"class":98},[85,4234,2108],{"class":109},[85,4236,4238,4240,4242,4245,4247],{"class":87,"line":4237},72,[85,4239,3905],{"class":109},[85,4241,2117],{"class":2116},[85,4243,4244],{"class":138}," key",[85,4246,253],{"class":98},[85,4248,4249],{"class":109},"{i}>\n",[85,4251,4253,4256,4258,4260,4262,4265],{"class":87,"line":4252},73,[85,4254,4255],{"class":109},"            \u003C",[85,4257,17],{"class":2116},[85,4259,2120],{"class":138},[85,4261,253],{"class":98},[85,4263,4264],{"class":125},"\"mb-2 text-sm font-medium text-muted-foreground\"",[85,4266,2128],{"class":109},[85,4268,4270],{"class":87,"line":4269},74,[85,4271,4272],{"class":109},"              \"{msg.prompt}\"\n",[85,4274,4276,4279,4281],{"class":87,"line":4275},75,[85,4277,4278],{"class":109},"            \u003C\u002F",[85,4280,17],{"class":2116},[85,4282,2128],{"class":109},[85,4284,4286],{"class":87,"line":4285},76,[85,4287,4288],{"class":109},"            {msg.ui}\n",[85,4290,4292,4294,4296],{"class":87,"line":4291},77,[85,4293,3961],{"class":109},[85,4295,2117],{"class":2116},[85,4297,2128],{"class":109},[85,4299,4301],{"class":87,"line":4300},78,[85,4302,3971],{"class":109},[85,4304,4306,4308,4310],{"class":87,"line":4305},79,[85,4307,2210],{"class":109},[85,4309,2117],{"class":2116},[85,4311,2128],{"class":109},[85,4313,4315,4317,4319],{"class":87,"line":4314},80,[85,4316,2247],{"class":109},[85,4318,3793],{"class":2116},[85,4320,2128],{"class":109},[85,4322,4324],{"class":87,"line":4323},81,[85,4325,2256],{"class":109},[85,4327,4329],{"class":87,"line":4328},82,[85,4330,280],{"class":109},[12,4332,4334],{"id":4333},"step-5-run-and-test","Step 5: Run and Test",[75,4336,4338],{"className":1921,"code":4337,"language":1923,"meta":80,"style":80},"npm run dev\n",[82,4339,4340],{"__ignoreMap":80},[85,4341,4342,4344,4347],{"class":87,"line":88},[85,4343,1930],{"class":138},[85,4345,4346],{"class":125}," run",[85,4348,4349],{"class":125}," dev\n",[17,4351,4352],{},"Try these prompts in order to see different behaviors:",[33,4354,4355,4361,4367,4377],{},[36,4356,4357,4360],{},[39,4358,4359],{},"\"What's the weather in Paris?\""," — single WeatherCard",[36,4362,4363,4366],{},[39,4364,4365],{},"\"Show me Apple stock\""," — single StockTicker",[36,4368,4369,4372,4373,4376],{},[39,4370,4371],{},"\"Compare the weather in London and New York\""," — the AI calls ",[82,4374,4375],{},"showWeather"," twice, generating two cards side by side",[36,4378,4379,4382],{},[39,4380,4381],{},"\"How's Tesla doing and what's the weather in San Francisco?\""," — the AI calls both tools, generating mixed component types",[17,4384,4385],{},"That third prompt is the key demonstration: without any additional code, the model composes multiple components to answer a multi-part question.",[12,4387,4389],{"id":4388},"whats-happening-under-the-hood","What's Happening Under the Hood",[17,4391,4392],{},"When you submit a prompt:",[63,4394,4395,4402,4407,4410,4416,4419],{},[36,4396,4397,4398,4401],{},"The client calls the ",[82,4399,4400],{},"generateUI"," server action",[36,4403,4404,4406],{},[82,4405,781],{}," sends the prompt + tool definitions to the OpenAI API",[36,4408,4409],{},"The model chooses which tools to call and with what parameters",[36,4411,4412,4413,4415],{},"Each tool's ",[82,4414,1404],{}," function immediately yields a skeleton",[36,4417,4418],{},"The AI finishes resolving parameters, and the final component is returned",[36,4420,4421],{},"React renders the component in place of the skeleton",[17,4423,4424],{},"The RSC streaming protocol is what makes this work. The server serializes React component trees and streams them to the client incrementally. This is different from a JSON API — the client receives rendered components, not raw data.",[12,4426,4428],{"id":4427},"error-handling","Error Handling",[17,4430,4431],{},"Generated components can fail in ways hand-coded components do not. Wrap generated output in an error boundary:",[75,4433,4435],{"className":1992,"code":4434,"language":1994,"meta":80,"style":80},"\u002F\u002F components\u002Fgenui-error-boundary.tsx\n'use client';\n\nimport { Component, ReactNode } from 'react';\n\ninterface Props { children: ReactNode }\ninterface State { hasError: boolean; error: Error | null }\n\nexport class GenUIErrorBoundary extends Component\u003CProps, State> {\n  constructor(props: Props) {\n    super(props);\n    this.state = { hasError: false, error: null };\n  }\n\n  static getDerivedStateFromError(error: Error) {\n    return { hasError: true, error };\n  }\n\n  render() {\n    if (this.state.hasError) {\n      return (\n        \u003Cdiv className=\"rounded-lg border border-destructive\u002F50 bg-destructive\u002F5 p-4\">\n          \u003Cp className=\"text-sm text-destructive\">\n            This component could not render. The AI may have passed unexpected data.\n          \u003C\u002Fp>\n        \u003C\u002Fdiv>\n      );\n    }\n    return this.props.children;\n  }\n}\n",[82,4436,4437,4442,4448,4452,4465,4469,4489,4524,4528,4557,4573,4581,4605,4609,4613,4631,4643,4647,4651,4658,4670,4676,4691,4706,4711,4719,4727,4732,4737,4747,4751],{"__ignoreMap":80},[85,4438,4439],{"class":87,"line":88},[85,4440,4441],{"class":91},"\u002F\u002F components\u002Fgenui-error-boundary.tsx\n",[85,4443,4444,4446],{"class":87,"line":95},[85,4445,3397],{"class":125},[85,4447,416],{"class":109},[85,4449,4450],{"class":87,"line":113},[85,4451,449],{"emptyLinePlaceholder":327},[85,4453,4454,4456,4459,4461,4463],{"class":87,"line":119},[85,4455,404],{"class":98},[85,4457,4458],{"class":109}," { Component, ReactNode } ",[85,4460,410],{"class":98},[85,4462,3415],{"class":125},[85,4464,416],{"class":109},[85,4466,4467],{"class":87,"line":132},[85,4468,449],{"emptyLinePlaceholder":327},[85,4470,4471,4473,4476,4478,4481,4483,4486],{"class":87,"line":145},[85,4472,2006],{"class":98},[85,4474,4475],{"class":138}," Props",[85,4477,2699],{"class":109},[85,4479,4480],{"class":202},"children",[85,4482,1967],{"class":98},[85,4484,4485],{"class":138}," ReactNode",[85,4487,4488],{"class":109}," }\n",[85,4490,4491,4493,4496,4498,4501,4503,4506,4508,4511,4513,4516,4519,4522],{"class":87,"line":157},[85,4492,2006],{"class":98},[85,4494,4495],{"class":138}," State",[85,4497,2699],{"class":109},[85,4499,4500],{"class":202},"hasError",[85,4502,1967],{"class":98},[85,4504,4505],{"class":102}," boolean",[85,4507,3567],{"class":109},[85,4509,4510],{"class":202},"error",[85,4512,1967],{"class":98},[85,4514,4515],{"class":138}," Error",[85,4517,4518],{"class":98}," |",[85,4520,4521],{"class":102}," null",[85,4523,4488],{"class":109},[85,4525,4526],{"class":87,"line":181},[85,4527,449],{"emptyLinePlaceholder":327},[85,4529,4530,4532,4535,4538,4541,4544,4546,4549,4551,4554],{"class":87,"line":187},[85,4531,1420],{"class":98},[85,4533,4534],{"class":98}," class",[85,4536,4537],{"class":138}," GenUIErrorBoundary",[85,4539,4540],{"class":98}," extends",[85,4542,4543],{"class":138}," Component",[85,4545,3552],{"class":109},[85,4547,4548],{"class":138},"Props",[85,4550,172],{"class":109},[85,4552,4553],{"class":138},"State",[85,4555,4556],{"class":109},"> {\n",[85,4558,4559,4562,4564,4567,4569,4571],{"class":87,"line":219},[85,4560,4561],{"class":98},"  constructor",[85,4563,476],{"class":109},[85,4565,4566],{"class":202},"props",[85,4568,1967],{"class":98},[85,4570,4475],{"class":138},[85,4572,2101],{"class":109},[85,4574,4575,4578],{"class":87,"line":239},[85,4576,4577],{"class":102},"    super",[85,4579,4580],{"class":109},"(props);\n",[85,4582,4583,4586,4589,4591,4594,4596,4599,4602],{"class":87,"line":265},[85,4584,4585],{"class":102},"    this",[85,4587,4588],{"class":109},".state ",[85,4590,253],{"class":98},[85,4592,4593],{"class":109}," { hasError: ",[85,4595,3609],{"class":102},[85,4597,4598],{"class":109},", error: ",[85,4600,4601],{"class":102},"null",[85,4603,4604],{"class":109}," };\n",[85,4606,4607],{"class":87,"line":271},[85,4608,3776],{"class":109},[85,4610,4611],{"class":87,"line":277},[85,4612,449],{"emptyLinePlaceholder":327},[85,4614,4615,4618,4621,4623,4625,4627,4629],{"class":87,"line":552},[85,4616,4617],{"class":98},"  static",[85,4619,4620],{"class":138}," getDerivedStateFromError",[85,4622,476],{"class":109},[85,4624,4510],{"class":202},[85,4626,1967],{"class":98},[85,4628,4515],{"class":138},[85,4630,2101],{"class":109},[85,4632,4633,4636,4638,4640],{"class":87,"line":558},[85,4634,4635],{"class":98},"    return",[85,4637,4593],{"class":109},[85,4639,3719],{"class":102},[85,4641,4642],{"class":109},", error };\n",[85,4644,4645],{"class":87,"line":588},[85,4646,3776],{"class":109},[85,4648,4649],{"class":87,"line":627},[85,4650,449],{"emptyLinePlaceholder":327},[85,4652,4653,4656],{"class":87,"line":633},[85,4654,4655],{"class":138},"  render",[85,4657,3499],{"class":109},[85,4659,4660,4662,4664,4667],{"class":87,"line":638},[85,4661,3657],{"class":98},[85,4663,3077],{"class":109},[85,4665,4666],{"class":102},"this",[85,4668,4669],{"class":109},".state.hasError) {\n",[85,4671,4672,4674],{"class":87,"line":644},[85,4673,242],{"class":98},[85,4675,2108],{"class":109},[85,4677,4678,4680,4682,4684,4686,4689],{"class":87,"line":654},[85,4679,2169],{"class":109},[85,4681,2117],{"class":2116},[85,4683,2120],{"class":138},[85,4685,253],{"class":98},[85,4687,4688],{"class":125},"\"rounded-lg border border-destructive\u002F50 bg-destructive\u002F5 p-4\"",[85,4690,2128],{"class":109},[85,4692,4693,4695,4697,4699,4701,4704],{"class":87,"line":663},[85,4694,3905],{"class":109},[85,4696,17],{"class":2116},[85,4698,2120],{"class":138},[85,4700,253],{"class":98},[85,4702,4703],{"class":125},"\"text-sm text-destructive\"",[85,4705,2128],{"class":109},[85,4707,4708],{"class":87,"line":695},[85,4709,4710],{"class":109},"            This component could not render. The AI may have passed unexpected data.\n",[85,4712,4713,4715,4717],{"class":87,"line":700},[85,4714,3961],{"class":109},[85,4716,17],{"class":2116},[85,4718,2128],{"class":109},[85,4720,4721,4723,4725],{"class":87,"line":719},[85,4722,2541],{"class":109},[85,4724,2117],{"class":2116},[85,4726,2128],{"class":109},[85,4728,4729],{"class":87,"line":737},[85,4730,4731],{"class":109},"      );\n",[85,4733,4734],{"class":87,"line":742},[85,4735,4736],{"class":109},"    }\n",[85,4738,4739,4741,4744],{"class":87,"line":747},[85,4740,4635],{"class":98},[85,4742,4743],{"class":102}," this",[85,4745,4746],{"class":109},".props.children;\n",[85,4748,4749],{"class":87,"line":752},[85,4750,3776],{"class":109},[85,4752,4753],{"class":87,"line":3110},[85,4754,280],{"class":109},[17,4756,4757],{},"Wrap it in your page around the generated UI output.",[12,4759,4761],{"id":4760},"deployment-tips","Deployment Tips",[17,4763,4764,4767,4768,4770],{},[39,4765,4766],{},"Environment variables:"," ",[82,4769,1977],{}," must be available in your production environment. On Vercel, add it in project settings under Environment Variables.",[17,4772,4773,3356,4776,4778,4779,4782],{},[39,4774,4775],{},"Edge runtime:",[82,4777,781],{}," function works on Edge runtime, which reduces cold start times significantly. Add ",[82,4780,4781],{},"export const runtime = 'edge'"," to your server action file.",[17,4784,4785,4788,4789,4791,4792,4795],{},[39,4786,4787],{},"Rate limiting:"," Without rate limiting, a single user could generate thousands of AI requests. Add a rate limiter before the ",[82,4790,781],{}," call. The ",[82,4793,4794],{},"@upstash\u002Fratelimit"," package integrates well with Next.js.",[17,4797,4798,4767,4801,4804,4805,4808],{},[39,4799,4800],{},"Model selection:",[82,4802,4803],{},"gpt-4o"," produces the best component selections but costs more. ",[82,4806,4807],{},"gpt-4o-mini"," is 10x cheaper and works well for simple component sets. Test both with your specific tool definitions.",[12,4810,284],{"id":283},[17,4812,4813],{},"This tutorial covered the fundamentals. For production Generative UI:",[33,4815,4816,4822,4828,4834,4840],{},[36,4817,4818,4821],{},[39,4819,4820],{},"Add more tools"," — each new component you add to the registry expands what the AI can answer",[36,4823,4824,4827],{},[39,4825,4826],{},"Implement tool result caching"," — cache common queries to reduce latency and cost",[36,4829,4830,4833],{},[39,4831,4832],{},"Add streaming text"," alongside UI components so the AI can explain what it is showing",[36,4835,4836,4839],{},[39,4837,4838],{},"Use structured outputs"," for more reliable parameter generation",[36,4841,4842,4845],{},[39,4843,4844],{},"Set up observability"," — log every tool call, its parameters, and user interactions",[17,4847,4848],{},"The Vercel AI SDK documentation covers all of these patterns in depth, and the examples repository has production-grade starter templates worth studying.",[1138,4850],{},[17,4852,4853],{},[28,4854,4855,4856,4859],{},"Want to implement Generative UI in your product? ",[291,4857,4858],{"href":308},"Let's discuss your use case"," — I've built production GenUI systems across multiple industries.",[312,4861,4862],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":80,"searchDepth":95,"depth":95,"links":4864},[4865,4866,4867,4868,4869,4870,4871,4872,4873,4874,4875],{"id":1869,"depth":95,"text":1870},{"id":1896,"depth":95,"text":1897},{"id":1917,"depth":95,"text":1918},{"id":1985,"depth":95,"text":1986},{"id":2747,"depth":95,"text":2748},{"id":3381,"depth":95,"text":3382},{"id":4333,"depth":95,"text":4334},{"id":4388,"depth":95,"text":4389},{"id":4427,"depth":95,"text":4428},{"id":4760,"depth":95,"text":4761},{"id":283,"depth":95,"text":284},"2026-02-28","Step-by-step guide to creating your first AI-powered interface with streaming components.",{"featured":326},"\u002Flearn\u002Fbuilding-generative-ui-vercel-ai-sdk","15 min read",{"title":1864,"description":4877},"learn\u002Fbuilding-generative-ui-vercel-ai-sdk",[335,4884,321,4885],"react","streaming","3urVpgATLEvKMPtp3hB901W82wfTj8KS43mRklKYGs4",{"id":4888,"title":4889,"author":7,"body":4890,"category":321,"date":7081,"description":7082,"extension":324,"meta":7083,"navigation":327,"path":7084,"readTime":7085,"seo":7086,"stem":7087,"tags":7088,"__hash__":7091},"content\u002Flearn\u002Fgenerative-ui-react-practical-guide.md","Generative UI in React: A Practical Guide",{"type":9,"value":4891,"toc":7070},[4892,4896,4899,4902,4906,4909,5510,5518,5529,5533,5539,5973,5976,5980,5983,6198,6201,6300,6303,6307,6310,6313,6611,6618,6622,6625,6628,6946,6949,6953,6959,6965,6971,6977,6983,6989,6993,6996,7028,7032,7035,7049,7056,7058,7067],[12,4893,4895],{"id":4894},"why-react-for-generative-ui","Why React for Generative UI?",[17,4897,4898],{},"React's component model is naturally suited for Generative UI. Components are composable, typed, and can be rendered on the server or client. When an AI model \"generates UI,\" what it actually does is select and compose React components with specific props.",[17,4900,4901],{},"This guide covers the patterns that work in production and the mistakes I see teams make when they first start building generative interfaces. I am assuming you have a working Next.js setup and have read the Vercel AI SDK basics — this is the practical layer on top of that foundation.",[12,4903,4905],{"id":4904},"pattern-1-the-tool-registry","Pattern 1: The Tool Registry",[17,4907,4908],{},"The foundation of any maintainable Generative UI system is an explicit, centralized registry of components the AI can use. Do not scatter tool definitions across server actions.",[75,4910,4912],{"className":77,"code":4911,"language":79,"meta":80,"style":80},"\u002F\u002F lib\u002Fgenui-registry.ts\nimport { z } from 'zod';\nimport { MetricCard } from '@\u002Fcomponents\u002Fmetric-card';\nimport { DataTable } from '@\u002Fcomponents\u002Fdata-table';\nimport { BarChart } from '@\u002Fcomponents\u002Fbar-chart';\nimport { AlertBanner } from '@\u002Fcomponents\u002Falert-banner';\nimport { LineChart } from '@\u002Fcomponents\u002Fline-chart';\n\nexport const tools = {\n  metricCard: {\n    description: 'Display a single KPI metric with a trend indicator. Use for scalar values like revenue, user count, or conversion rate.',\n    parameters: z.object({\n      label: z.string().describe('The metric name, e.g. \"Monthly Revenue\"'),\n      value: z.string().describe('The formatted value, e.g. \"$12,400\"'),\n      change: z.number().describe('Percentage change vs. previous period'),\n      period: z.string().describe('The comparison period, e.g. \"vs last month\"'),\n    }),\n    component: MetricCard,\n  },\n  dataTable: {\n    description: 'Display tabular data with sortable columns. Use when showing lists of items with multiple attributes.',\n    parameters: z.object({\n      columns: z.array(z.object({\n        key: z.string(),\n        label: z.string(),\n        numeric: z.boolean().optional(),\n      })),\n      rows: z.array(z.record(z.string())),\n      caption: z.string().optional(),\n    }),\n    component: DataTable,\n  },\n  barChart: {\n    description: 'Display a bar chart for categorical comparisons. Use when comparing values across discrete categories.',\n    parameters: z.object({\n      title: z.string(),\n      data: z.array(z.object({ label: z.string(), value: z.number() })),\n      yAxisLabel: z.string().optional(),\n    }),\n    component: BarChart,\n  },\n  lineChart: {\n    description: 'Display a line chart for time-series data. Use when showing trends over time.',\n    parameters: z.object({\n      title: z.string(),\n      data: z.array(z.object({ date: z.string(), value: z.number() })),\n      unit: z.string().optional(),\n    }),\n    component: LineChart,\n  },\n  alertBanner: {\n    description: 'Display an important notice, warning, or success message. Use sparingly for genuinely important information.',\n    parameters: z.object({\n      type: z.enum(['info', 'warning', 'error', 'success']),\n      title: z.string(),\n      message: z.string(),\n    }),\n    component: AlertBanner,\n  },\n};\n\nexport type ToolName = keyof typeof tools;\n",[82,4913,4914,4919,4931,4945,4959,4973,4987,5001,5005,5018,5023,5032,5040,5058,5076,5094,5112,5116,5121,5125,5130,5139,5147,5160,5169,5178,5193,5198,5217,5230,5234,5239,5243,5248,5257,5265,5274,5297,5310,5314,5319,5323,5328,5337,5345,5353,5374,5386,5390,5395,5399,5404,5413,5421,5450,5458,5467,5471,5476,5480,5485,5489],{"__ignoreMap":80},[85,4915,4916],{"class":87,"line":88},[85,4917,4918],{"class":91},"\u002F\u002F lib\u002Fgenui-registry.ts\n",[85,4920,4921,4923,4925,4927,4929],{"class":87,"line":95},[85,4922,404],{"class":98},[85,4924,437],{"class":109},[85,4926,410],{"class":98},[85,4928,442],{"class":125},[85,4930,416],{"class":109},[85,4932,4933,4935,4938,4940,4943],{"class":87,"line":113},[85,4934,404],{"class":98},[85,4936,4937],{"class":109}," { MetricCard } ",[85,4939,410],{"class":98},[85,4941,4942],{"class":125}," '@\u002Fcomponents\u002Fmetric-card'",[85,4944,416],{"class":109},[85,4946,4947,4949,4952,4954,4957],{"class":87,"line":119},[85,4948,404],{"class":98},[85,4950,4951],{"class":109}," { DataTable } ",[85,4953,410],{"class":98},[85,4955,4956],{"class":125}," '@\u002Fcomponents\u002Fdata-table'",[85,4958,416],{"class":109},[85,4960,4961,4963,4966,4968,4971],{"class":87,"line":132},[85,4962,404],{"class":98},[85,4964,4965],{"class":109}," { BarChart } ",[85,4967,410],{"class":98},[85,4969,4970],{"class":125}," '@\u002Fcomponents\u002Fbar-chart'",[85,4972,416],{"class":109},[85,4974,4975,4977,4980,4982,4985],{"class":87,"line":145},[85,4976,404],{"class":98},[85,4978,4979],{"class":109}," { AlertBanner } ",[85,4981,410],{"class":98},[85,4983,4984],{"class":125}," '@\u002Fcomponents\u002Falert-banner'",[85,4986,416],{"class":109},[85,4988,4989,4991,4994,4996,4999],{"class":87,"line":157},[85,4990,404],{"class":98},[85,4992,4993],{"class":109}," { LineChart } ",[85,4995,410],{"class":98},[85,4997,4998],{"class":125}," '@\u002Fcomponents\u002Fline-chart'",[85,5000,416],{"class":109},[85,5002,5003],{"class":87,"line":181},[85,5004,449],{"emptyLinePlaceholder":327},[85,5006,5007,5009,5012,5014,5016],{"class":87,"line":187},[85,5008,1420],{"class":98},[85,5010,5011],{"class":98}," const",[85,5013,103],{"class":102},[85,5015,106],{"class":98},[85,5017,110],{"class":109},[85,5019,5020],{"class":87,"line":219},[85,5021,5022],{"class":109},"  metricCard: {\n",[85,5024,5025,5027,5030],{"class":87,"line":239},[85,5026,122],{"class":109},[85,5028,5029],{"class":125},"'Display a single KPI metric with a trend indicator. Use for scalar values like revenue, user count, or conversion rate.'",[85,5031,129],{"class":109},[85,5033,5034,5036,5038],{"class":87,"line":265},[85,5035,135],{"class":109},[85,5037,139],{"class":138},[85,5039,142],{"class":109},[85,5041,5042,5045,5047,5049,5051,5053,5056],{"class":87,"line":271},[85,5043,5044],{"class":109},"      label: z.",[85,5046,151],{"class":138},[85,5048,2972],{"class":109},[85,5050,2975],{"class":138},[85,5052,476],{"class":109},[85,5054,5055],{"class":125},"'The metric name, e.g. \"Monthly Revenue\"'",[85,5057,482],{"class":109},[85,5059,5060,5063,5065,5067,5069,5071,5074],{"class":87,"line":277},[85,5061,5062],{"class":109},"      value: z.",[85,5064,151],{"class":138},[85,5066,2972],{"class":109},[85,5068,2975],{"class":138},[85,5070,476],{"class":109},[85,5072,5073],{"class":125},"'The formatted value, e.g. \"$12,400\"'",[85,5075,482],{"class":109},[85,5077,5078,5081,5083,5085,5087,5089,5092],{"class":87,"line":552},[85,5079,5080],{"class":109},"      change: z.",[85,5082,538],{"class":138},[85,5084,2972],{"class":109},[85,5086,2975],{"class":138},[85,5088,476],{"class":109},[85,5090,5091],{"class":125},"'Percentage change vs. previous period'",[85,5093,482],{"class":109},[85,5095,5096,5099,5101,5103,5105,5107,5110],{"class":87,"line":558},[85,5097,5098],{"class":109},"      period: z.",[85,5100,151],{"class":138},[85,5102,2972],{"class":109},[85,5104,2975],{"class":138},[85,5106,476],{"class":109},[85,5108,5109],{"class":125},"'The comparison period, e.g. \"vs last month\"'",[85,5111,482],{"class":109},[85,5113,5114],{"class":87,"line":588},[85,5115,184],{"class":109},[85,5117,5118],{"class":87,"line":627},[85,5119,5120],{"class":109},"    component: MetricCard,\n",[85,5122,5123],{"class":87,"line":633},[85,5124,274],{"class":109},[85,5126,5127],{"class":87,"line":638},[85,5128,5129],{"class":109},"  dataTable: {\n",[85,5131,5132,5134,5137],{"class":87,"line":644},[85,5133,122],{"class":109},[85,5135,5136],{"class":125},"'Display tabular data with sortable columns. Use when showing lists of items with multiple attributes.'",[85,5138,129],{"class":109},[85,5140,5141,5143,5145],{"class":87,"line":654},[85,5142,135],{"class":109},[85,5144,139],{"class":138},[85,5146,142],{"class":109},[85,5148,5149,5152,5154,5156,5158],{"class":87,"line":663},[85,5150,5151],{"class":109},"      columns: z.",[85,5153,669],{"class":138},[85,5155,672],{"class":109},[85,5157,139],{"class":138},[85,5159,142],{"class":109},[85,5161,5162,5165,5167],{"class":87,"line":695},[85,5163,5164],{"class":109},"        key: z.",[85,5166,151],{"class":138},[85,5168,154],{"class":109},[85,5170,5171,5174,5176],{"class":87,"line":700},[85,5172,5173],{"class":109},"        label: z.",[85,5175,151],{"class":138},[85,5177,154],{"class":109},[85,5179,5180,5183,5186,5188,5191],{"class":87,"line":719},[85,5181,5182],{"class":109},"        numeric: z.",[85,5184,5185],{"class":138},"boolean",[85,5187,2972],{"class":109},[85,5189,5190],{"class":138},"optional",[85,5192,154],{"class":109},[85,5194,5195],{"class":87,"line":737},[85,5196,5197],{"class":109},"      })),\n",[85,5199,5200,5203,5205,5207,5210,5212,5214],{"class":87,"line":742},[85,5201,5202],{"class":109},"      rows: z.",[85,5204,669],{"class":138},[85,5206,672],{"class":109},[85,5208,5209],{"class":138},"record",[85,5211,672],{"class":109},[85,5213,151],{"class":138},[85,5215,5216],{"class":109},"())),\n",[85,5218,5219,5222,5224,5226,5228],{"class":87,"line":747},[85,5220,5221],{"class":109},"      caption: z.",[85,5223,151],{"class":138},[85,5225,2972],{"class":109},[85,5227,5190],{"class":138},[85,5229,154],{"class":109},[85,5231,5232],{"class":87,"line":752},[85,5233,184],{"class":109},[85,5235,5236],{"class":87,"line":3110},[85,5237,5238],{"class":109},"    component: DataTable,\n",[85,5240,5241],{"class":87,"line":3116},[85,5242,274],{"class":109},[85,5244,5245],{"class":87,"line":3135},[85,5246,5247],{"class":109},"  barChart: {\n",[85,5249,5250,5252,5255],{"class":87,"line":3141},[85,5251,122],{"class":109},[85,5253,5254],{"class":125},"'Display a bar chart for categorical comparisons. Use when comparing values across discrete categories.'",[85,5256,129],{"class":109},[85,5258,5259,5261,5263],{"class":87,"line":3146},[85,5260,135],{"class":109},[85,5262,139],{"class":138},[85,5264,142],{"class":109},[85,5266,5267,5270,5272],{"class":87,"line":3152},[85,5268,5269],{"class":109},"      title: z.",[85,5271,151],{"class":138},[85,5273,154],{"class":109},[85,5275,5276,5279,5281,5283,5285,5288,5290,5293,5295],{"class":87,"line":3168},[85,5277,5278],{"class":109},"      data: z.",[85,5280,669],{"class":138},[85,5282,672],{"class":109},[85,5284,139],{"class":138},[85,5286,5287],{"class":109},"({ label: z.",[85,5289,151],{"class":138},[85,5291,5292],{"class":109},"(), value: z.",[85,5294,538],{"class":138},[85,5296,692],{"class":109},[85,5298,5299,5302,5304,5306,5308],{"class":87,"line":3177},[85,5300,5301],{"class":109},"      yAxisLabel: z.",[85,5303,151],{"class":138},[85,5305,2972],{"class":109},[85,5307,5190],{"class":138},[85,5309,154],{"class":109},[85,5311,5312],{"class":87,"line":3196},[85,5313,184],{"class":109},[85,5315,5316],{"class":87,"line":3215},[85,5317,5318],{"class":109},"    component: BarChart,\n",[85,5320,5321],{"class":87,"line":3234},[85,5322,274],{"class":109},[85,5324,5325],{"class":87,"line":3253},[85,5326,5327],{"class":109},"  lineChart: {\n",[85,5329,5330,5332,5335],{"class":87,"line":3258},[85,5331,122],{"class":109},[85,5333,5334],{"class":125},"'Display a line chart for time-series data. Use when showing trends over time.'",[85,5336,129],{"class":109},[85,5338,5339,5341,5343],{"class":87,"line":3275},[85,5340,135],{"class":109},[85,5342,139],{"class":138},[85,5344,142],{"class":109},[85,5346,5347,5349,5351],{"class":87,"line":3293},[85,5348,5269],{"class":109},[85,5350,151],{"class":138},[85,5352,154],{"class":109},[85,5354,5355,5357,5359,5361,5363,5366,5368,5370,5372],{"class":87,"line":3309},[85,5356,5278],{"class":109},[85,5358,669],{"class":138},[85,5360,672],{"class":109},[85,5362,139],{"class":138},[85,5364,5365],{"class":109},"({ date: z.",[85,5367,151],{"class":138},[85,5369,5292],{"class":109},[85,5371,538],{"class":138},[85,5373,692],{"class":109},[85,5375,5376,5378,5380,5382,5384],{"class":87,"line":3314},[85,5377,160],{"class":109},[85,5379,151],{"class":138},[85,5381,2972],{"class":109},[85,5383,5190],{"class":138},[85,5385,154],{"class":109},[85,5387,5388],{"class":87,"line":3319},[85,5389,184],{"class":109},[85,5391,5392],{"class":87,"line":3324},[85,5393,5394],{"class":109},"    component: LineChart,\n",[85,5396,5397],{"class":87,"line":3330},[85,5398,274],{"class":109},[85,5400,5401],{"class":87,"line":3335},[85,5402,5403],{"class":109},"  alertBanner: {\n",[85,5405,5406,5408,5411],{"class":87,"line":3343},[85,5407,122],{"class":109},[85,5409,5410],{"class":125},"'Display an important notice, warning, or success message. Use sparingly for genuinely important information.'",[85,5412,129],{"class":109},[85,5414,5415,5417,5419],{"class":87,"line":3995},[85,5416,135],{"class":109},[85,5418,139],{"class":138},[85,5420,142],{"class":109},[85,5422,5423,5426,5428,5430,5433,5435,5438,5440,5443,5445,5448],{"class":87,"line":4021},[85,5424,5425],{"class":109},"      type: z.",[85,5427,163],{"class":138},[85,5429,166],{"class":109},[85,5431,5432],{"class":125},"'info'",[85,5434,172],{"class":109},[85,5436,5437],{"class":125},"'warning'",[85,5439,172],{"class":109},[85,5441,5442],{"class":125},"'error'",[85,5444,172],{"class":109},[85,5446,5447],{"class":125},"'success'",[85,5449,178],{"class":109},[85,5451,5452,5454,5456],{"class":87,"line":4029},[85,5453,5269],{"class":109},[85,5455,151],{"class":138},[85,5457,154],{"class":109},[85,5459,5460,5463,5465],{"class":87,"line":4040},[85,5461,5462],{"class":109},"      message: z.",[85,5464,151],{"class":138},[85,5466,154],{"class":109},[85,5468,5469],{"class":87,"line":4059},[85,5470,184],{"class":109},[85,5472,5473],{"class":87,"line":4070},[85,5474,5475],{"class":109},"    component: AlertBanner,\n",[85,5477,5478],{"class":87,"line":4081},[85,5479,274],{"class":109},[85,5481,5482],{"class":87,"line":4087},[85,5483,5484],{"class":109},"};\n",[85,5486,5487],{"class":87,"line":4094},[85,5488,449],{"emptyLinePlaceholder":327},[85,5490,5491,5493,5496,5499,5501,5504,5507],{"class":87,"line":4105},[85,5492,1420],{"class":98},[85,5494,5495],{"class":98}," type",[85,5497,5498],{"class":138}," ToolName",[85,5500,106],{"class":98},[85,5502,5503],{"class":98}," keyof",[85,5505,5506],{"class":98}," typeof",[85,5508,5509],{"class":109}," tools;\n",[17,5511,5512,3356,5515,5517],{},[39,5513,5514],{},"Key insight:",[82,5516,1052],{}," field is what the AI reads to decide which component to use. Write descriptions for the AI, not for humans. Be specific about when each component is appropriate and, critically, when it is not.",[17,5519,5520,5521,5524,5525,5528],{},"Notice ",[82,5522,5523],{},"lineChart"," says \"time-series\" and ",[82,5526,5527],{},"barChart"," says \"categorical.\" Without that distinction, the AI will make random choices between them. The more precise your descriptions, the better the component selection.",[12,5530,5532],{"id":5531},"pattern-2-separate-registry-from-streaming","Pattern 2: Separate Registry from Streaming",[17,5534,5535,5536,5538],{},"Keep the registry definition separate from the ",[82,5537,781],{}," call. This lets you reuse tool definitions across multiple server actions and makes the registry testable in isolation.",[75,5540,5542],{"className":77,"code":5541,"language":79,"meta":80,"style":80},"\u002F\u002F lib\u002Fstream-with-tools.ts\nimport { streamUI } from 'ai\u002Frsc';\nimport { openai } from '@ai-sdk\u002Fopenai';\nimport { tools } from '.\u002Fgenui-registry';\n\n\u002F\u002F Convert registry format to streamUI format\nfunction buildStreamTools(toolNames: ToolName[]) {\n  return Object.fromEntries(\n    toolNames.map(name => [\n      name,\n      {\n        description: tools[name].description,\n        parameters: tools[name].parameters,\n        generate: async function* (params: any) {\n          yield \u003CToolSkeleton name={name} \u002F>;\n          const Component = tools[name].component;\n          return \u003CComponent {...params} \u002F>;\n        },\n      },\n    ])\n  );\n}\n\n\u002F\u002F Server action for a data dashboard\nexport async function generateDashboard(query: string) {\n  const result = await streamUI({\n    model: openai('gpt-4o'),\n    system: 'You are a data analyst assistant. Display information using the appropriate visualization tool.',\n    prompt: query,\n    tools: buildStreamTools(['metricCard', 'dataTable', 'barChart', 'lineChart', 'alertBanner']),\n  });\n  return result.value;\n}\n\n\u002F\u002F Server action for a summary view (fewer tools = better focus)\nexport async function generateSummary(query: string) {\n  const result = await streamUI({\n    model: openai('gpt-4o'),\n    system: 'You are a concise assistant. Show a summary with key metrics only.',\n    prompt: query,\n    tools: buildStreamTools(['metricCard', 'alertBanner']),\n  });\n  return result.value;\n}\n",[82,5543,5544,5549,5561,5573,5587,5591,5596,5616,5629,5645,5650,5655,5660,5665,5686,5704,5716,5733,5737,5741,5746,5750,5754,5758,5763,5785,5799,5811,5820,5825,5860,5864,5870,5874,5878,5883,5904,5918,5930,5939,5943,5959,5963,5969],{"__ignoreMap":80},[85,5545,5546],{"class":87,"line":88},[85,5547,5548],{"class":91},"\u002F\u002F lib\u002Fstream-with-tools.ts\n",[85,5550,5551,5553,5555,5557,5559],{"class":87,"line":95},[85,5552,404],{"class":98},[85,5554,407],{"class":109},[85,5556,410],{"class":98},[85,5558,413],{"class":125},[85,5560,416],{"class":109},[85,5562,5563,5565,5567,5569,5571],{"class":87,"line":113},[85,5564,404],{"class":98},[85,5566,423],{"class":109},[85,5568,410],{"class":98},[85,5570,428],{"class":125},[85,5572,416],{"class":109},[85,5574,5575,5577,5580,5582,5585],{"class":87,"line":119},[85,5576,404],{"class":98},[85,5578,5579],{"class":109}," { tools } ",[85,5581,410],{"class":98},[85,5583,5584],{"class":125}," '.\u002Fgenui-registry'",[85,5586,416],{"class":109},[85,5588,5589],{"class":87,"line":132},[85,5590,449],{"emptyLinePlaceholder":327},[85,5592,5593],{"class":87,"line":145},[85,5594,5595],{"class":91},"\u002F\u002F Convert registry format to streamUI format\n",[85,5597,5598,5601,5604,5606,5609,5611,5613],{"class":87,"line":157},[85,5599,5600],{"class":98},"function",[85,5602,5603],{"class":138}," buildStreamTools",[85,5605,476],{"class":109},[85,5607,5608],{"class":202},"toolNames",[85,5610,1967],{"class":98},[85,5612,5498],{"class":138},[85,5614,5615],{"class":109},"[]) {\n",[85,5617,5618,5620,5623,5626],{"class":87,"line":181},[85,5619,1460],{"class":98},[85,5621,5622],{"class":109}," Object.",[85,5624,5625],{"class":138},"fromEntries",[85,5627,5628],{"class":109},"(\n",[85,5630,5631,5634,5636,5638,5641,5643],{"class":87,"line":187},[85,5632,5633],{"class":109},"    toolNames.",[85,5635,3892],{"class":138},[85,5637,476],{"class":109},[85,5639,5640],{"class":202},"name",[85,5642,3754],{"class":98},[85,5644,3447],{"class":109},[85,5646,5647],{"class":87,"line":219},[85,5648,5649],{"class":109},"      name,\n",[85,5651,5652],{"class":87,"line":239},[85,5653,5654],{"class":109},"      {\n",[85,5656,5657],{"class":87,"line":265},[85,5658,5659],{"class":109},"        description: tools[name].description,\n",[85,5661,5662],{"class":87,"line":271},[85,5663,5664],{"class":109},"        parameters: tools[name].parameters,\n",[85,5666,5667,5669,5671,5673,5675,5677,5679,5681,5684],{"class":87,"line":277},[85,5668,3067],{"class":138},[85,5670,193],{"class":109},[85,5672,196],{"class":98},[85,5674,3074],{"class":98},[85,5676,3077],{"class":109},[85,5678,1435],{"class":202},[85,5680,1967],{"class":98},[85,5682,5683],{"class":102}," any",[85,5685,2101],{"class":109},[85,5687,5688,5690,5692,5695,5698,5700,5702],{"class":87,"line":552},[85,5689,3091],{"class":98},[85,5691,245],{"class":109},[85,5693,5694],{"class":138},"ToolSkeleton",[85,5696,5697],{"class":138}," name",[85,5699,601],{"class":109},[85,5701,5640],{"class":202},[85,5703,624],{"class":109},[85,5705,5706,5709,5711,5713],{"class":87,"line":558},[85,5707,5708],{"class":98},"          const",[85,5710,4543],{"class":102},[85,5712,106],{"class":98},[85,5714,5715],{"class":109}," tools[name].component;\n",[85,5717,5718,5720,5722,5725,5727,5729,5731],{"class":87,"line":588},[85,5719,3119],{"class":98},[85,5721,245],{"class":109},[85,5723,5724],{"class":138},"Component",[85,5726,3126],{"class":109},[85,5728,3129],{"class":98},[85,5730,1435],{"class":138},[85,5732,624],{"class":109},[85,5734,5735],{"class":87,"line":627},[85,5736,3138],{"class":109},[85,5738,5739],{"class":87,"line":633},[85,5740,630],{"class":109},[85,5742,5743],{"class":87,"line":638},[85,5744,5745],{"class":109},"    ])\n",[85,5747,5748],{"class":87,"line":644},[85,5749,2256],{"class":109},[85,5751,5752],{"class":87,"line":654},[85,5753,280],{"class":109},[85,5755,5756],{"class":87,"line":663},[85,5757,449],{"emptyLinePlaceholder":327},[85,5759,5760],{"class":87,"line":695},[85,5761,5762],{"class":91},"\u002F\u002F Server action for a data dashboard\n",[85,5764,5765,5767,5769,5771,5774,5776,5779,5781,5783],{"class":87,"line":700},[85,5766,1420],{"class":98},[85,5768,1423],{"class":98},[85,5770,1426],{"class":98},[85,5772,5773],{"class":138}," generateDashboard",[85,5775,476],{"class":109},[85,5777,5778],{"class":202},"query",[85,5780,1967],{"class":98},[85,5782,2021],{"class":102},[85,5784,2101],{"class":109},[85,5786,5787,5789,5791,5793,5795,5797],{"class":87,"line":719},[85,5788,1443],{"class":98},[85,5790,456],{"class":102},[85,5792,106],{"class":98},[85,5794,230],{"class":98},[85,5796,463],{"class":138},[85,5798,142],{"class":109},[85,5800,5801,5803,5805,5807,5809],{"class":87,"line":737},[85,5802,2895],{"class":109},[85,5804,473],{"class":138},[85,5806,476],{"class":109},[85,5808,479],{"class":125},[85,5810,482],{"class":109},[85,5812,5813,5815,5818],{"class":87,"line":742},[85,5814,2908],{"class":109},[85,5816,5817],{"class":125},"'You are a data analyst assistant. Display information using the appropriate visualization tool.'",[85,5819,129],{"class":109},[85,5821,5822],{"class":87,"line":747},[85,5823,5824],{"class":109},"    prompt: query,\n",[85,5826,5827,5830,5833,5835,5838,5840,5843,5845,5848,5850,5853,5855,5858],{"class":87,"line":752},[85,5828,5829],{"class":109},"    tools: ",[85,5831,5832],{"class":138},"buildStreamTools",[85,5834,166],{"class":109},[85,5836,5837],{"class":125},"'metricCard'",[85,5839,172],{"class":109},[85,5841,5842],{"class":125},"'dataTable'",[85,5844,172],{"class":109},[85,5846,5847],{"class":125},"'barChart'",[85,5849,172],{"class":109},[85,5851,5852],{"class":125},"'lineChart'",[85,5854,172],{"class":109},[85,5856,5857],{"class":125},"'alertBanner'",[85,5859,178],{"class":109},[85,5861,5862],{"class":87,"line":3110},[85,5863,3327],{"class":109},[85,5865,5866,5868],{"class":87,"line":3116},[85,5867,1460],{"class":98},[85,5869,3340],{"class":109},[85,5871,5872],{"class":87,"line":3135},[85,5873,280],{"class":109},[85,5875,5876],{"class":87,"line":3141},[85,5877,449],{"emptyLinePlaceholder":327},[85,5879,5880],{"class":87,"line":3146},[85,5881,5882],{"class":91},"\u002F\u002F Server action for a summary view (fewer tools = better focus)\n",[85,5884,5885,5887,5889,5891,5894,5896,5898,5900,5902],{"class":87,"line":3152},[85,5886,1420],{"class":98},[85,5888,1423],{"class":98},[85,5890,1426],{"class":98},[85,5892,5893],{"class":138}," generateSummary",[85,5895,476],{"class":109},[85,5897,5778],{"class":202},[85,5899,1967],{"class":98},[85,5901,2021],{"class":102},[85,5903,2101],{"class":109},[85,5905,5906,5908,5910,5912,5914,5916],{"class":87,"line":3168},[85,5907,1443],{"class":98},[85,5909,456],{"class":102},[85,5911,106],{"class":98},[85,5913,230],{"class":98},[85,5915,463],{"class":138},[85,5917,142],{"class":109},[85,5919,5920,5922,5924,5926,5928],{"class":87,"line":3177},[85,5921,2895],{"class":109},[85,5923,473],{"class":138},[85,5925,476],{"class":109},[85,5927,479],{"class":125},[85,5929,482],{"class":109},[85,5931,5932,5934,5937],{"class":87,"line":3196},[85,5933,2908],{"class":109},[85,5935,5936],{"class":125},"'You are a concise assistant. Show a summary with key metrics only.'",[85,5938,129],{"class":109},[85,5940,5941],{"class":87,"line":3215},[85,5942,5824],{"class":109},[85,5944,5945,5947,5949,5951,5953,5955,5957],{"class":87,"line":3234},[85,5946,5829],{"class":109},[85,5948,5832],{"class":138},[85,5950,166],{"class":109},[85,5952,5837],{"class":125},[85,5954,172],{"class":109},[85,5956,5857],{"class":125},[85,5958,178],{"class":109},[85,5960,5961],{"class":87,"line":3253},[85,5962,3327],{"class":109},[85,5964,5965,5967],{"class":87,"line":3258},[85,5966,1460],{"class":98},[85,5968,3340],{"class":109},[85,5970,5971],{"class":87,"line":3275},[85,5972,280],{"class":109},[17,5974,5975],{},"Passing a subset of tools to each server action is important. A focused tool set produces better AI decisions. Do not give the AI 20 tools when 5 will do.",[12,5977,5979],{"id":5978},"pattern-3-streaming-with-skeletons","Pattern 3: Streaming with Skeletons",[17,5981,5982],{},"Never show a blank screen while the AI generates. Show skeleton loading states that match the expected output shape. The visual continuity reduces perceived latency dramatically.",[75,5984,5986],{"className":1992,"code":5985,"language":1994,"meta":80,"style":80},"\u002F\u002F components\u002Ftool-skeleton.tsx\nimport { ToolName } from '@\u002Flib\u002Fgenui-registry';\n\nconst SKELETON_HEIGHTS: Record\u003CToolName, string> = {\n  metricCard: 'h-28',\n  dataTable: 'h-48',\n  barChart: 'h-64',\n  lineChart: 'h-64',\n  alertBanner: 'h-16',\n};\n\nexport function ToolSkeleton({ name }: { name: ToolName }) {\n  return (\n    \u003Cdiv\n      className={`animate-pulse rounded-lg bg-muted ${SKELETON_HEIGHTS[name] ?? 'h-32'} w-full`}\n      aria-label=\"Loading...\"\n      aria-busy=\"true\"\n    \u002F>\n  );\n}\n",[82,5987,5988,5993,6007,6011,6039,6049,6059,6069,6078,6088,6092,6096,6123,6129,6136,6165,6175,6185,6190,6194],{"__ignoreMap":80},[85,5989,5990],{"class":87,"line":88},[85,5991,5992],{"class":91},"\u002F\u002F components\u002Ftool-skeleton.tsx\n",[85,5994,5995,5997,6000,6002,6005],{"class":87,"line":95},[85,5996,404],{"class":98},[85,5998,5999],{"class":109}," { ToolName } ",[85,6001,410],{"class":98},[85,6003,6004],{"class":125}," '@\u002Flib\u002Fgenui-registry'",[85,6006,416],{"class":109},[85,6008,6009],{"class":87,"line":113},[85,6010,449],{"emptyLinePlaceholder":327},[85,6012,6013,6015,6018,6020,6023,6025,6028,6030,6032,6035,6037],{"class":87,"line":119},[85,6014,99],{"class":98},[85,6016,6017],{"class":102}," SKELETON_HEIGHTS",[85,6019,1967],{"class":98},[85,6021,6022],{"class":138}," Record",[85,6024,3552],{"class":109},[85,6026,6027],{"class":138},"ToolName",[85,6029,172],{"class":109},[85,6031,151],{"class":102},[85,6033,6034],{"class":109},"> ",[85,6036,253],{"class":98},[85,6038,110],{"class":109},[85,6040,6041,6044,6047],{"class":87,"line":132},[85,6042,6043],{"class":109},"  metricCard: ",[85,6045,6046],{"class":125},"'h-28'",[85,6048,129],{"class":109},[85,6050,6051,6054,6057],{"class":87,"line":145},[85,6052,6053],{"class":109},"  dataTable: ",[85,6055,6056],{"class":125},"'h-48'",[85,6058,129],{"class":109},[85,6060,6061,6064,6067],{"class":87,"line":157},[85,6062,6063],{"class":109},"  barChart: ",[85,6065,6066],{"class":125},"'h-64'",[85,6068,129],{"class":109},[85,6070,6071,6074,6076],{"class":87,"line":181},[85,6072,6073],{"class":109},"  lineChart: ",[85,6075,6066],{"class":125},[85,6077,129],{"class":109},[85,6079,6080,6083,6086],{"class":87,"line":187},[85,6081,6082],{"class":109},"  alertBanner: ",[85,6084,6085],{"class":125},"'h-16'",[85,6087,129],{"class":109},[85,6089,6090],{"class":87,"line":219},[85,6091,5484],{"class":109},[85,6093,6094],{"class":87,"line":239},[85,6095,449],{"emptyLinePlaceholder":327},[85,6097,6098,6100,6102,6105,6107,6109,6111,6113,6115,6117,6119,6121],{"class":87,"line":265},[85,6099,1420],{"class":98},[85,6101,1426],{"class":98},[85,6103,6104],{"class":138}," ToolSkeleton",[85,6106,1432],{"class":109},[85,6108,5640],{"class":202},[85,6110,2094],{"class":109},[85,6112,1967],{"class":98},[85,6114,2699],{"class":109},[85,6116,5640],{"class":202},[85,6118,1967],{"class":98},[85,6120,5498],{"class":138},[85,6122,1438],{"class":109},[85,6124,6125,6127],{"class":87,"line":271},[85,6126,1460],{"class":98},[85,6128,2108],{"class":109},[85,6130,6131,6133],{"class":87,"line":277},[85,6132,2113],{"class":109},[85,6134,6135],{"class":2116},"div\n",[85,6137,6138,6141,6143,6145,6147,6150,6153,6155,6157,6160,6163],{"class":87,"line":552},[85,6139,6140],{"class":138},"      className",[85,6142,253],{"class":98},[85,6144,256],{"class":109},[85,6146,2729],{"class":125},[85,6148,6149],{"class":102},"SKELETON_HEIGHTS",[85,6151,6152],{"class":125},"[",[85,6154,5640],{"class":109},[85,6156,3516],{"class":125},[85,6158,6159],{"class":98},"??",[85,6161,6162],{"class":125}," 'h-32'} w-full`",[85,6164,280],{"class":109},[85,6166,6167,6170,6172],{"class":87,"line":558},[85,6168,6169],{"class":138},"      aria-label",[85,6171,253],{"class":98},[85,6173,6174],{"class":125},"\"Loading...\"\n",[85,6176,6177,6180,6182],{"class":87,"line":588},[85,6178,6179],{"class":138},"      aria-busy",[85,6181,253],{"class":98},[85,6183,6184],{"class":125},"\"true\"\n",[85,6186,6187],{"class":87,"line":627},[85,6188,6189],{"class":109},"    \u002F>\n",[85,6191,6192],{"class":87,"line":633},[85,6193,2256],{"class":109},[85,6195,6196],{"class":87,"line":638},[85,6197,280],{"class":109},[17,6199,6200],{},"For more accurate skeletons, match the component's internal structure:",[75,6202,6204],{"className":1992,"code":6203,"language":1994,"meta":80,"style":80},"export function MetricCardSkeleton() {\n  return (\n    \u003Cdiv className=\"rounded-lg border bg-card p-6\">\n      \u003Cdiv className=\"h-4 w-24 animate-pulse rounded bg-muted\" \u002F>\n      \u003Cdiv className=\"mt-3 h-8 w-32 animate-pulse rounded bg-muted\" \u002F>\n      \u003Cdiv className=\"mt-2 h-3 w-16 animate-pulse rounded bg-muted\" \u002F>\n    \u003C\u002Fdiv>\n  );\n}\n",[82,6205,6206,6217,6223,6238,6254,6269,6284,6292,6296],{"__ignoreMap":80},[85,6207,6208,6210,6212,6215],{"class":87,"line":88},[85,6209,1420],{"class":98},[85,6211,1426],{"class":98},[85,6213,6214],{"class":138}," MetricCardSkeleton",[85,6216,3499],{"class":109},[85,6218,6219,6221],{"class":87,"line":95},[85,6220,1460],{"class":98},[85,6222,2108],{"class":109},[85,6224,6225,6227,6229,6231,6233,6236],{"class":87,"line":113},[85,6226,2113],{"class":109},[85,6228,2117],{"class":2116},[85,6230,2120],{"class":138},[85,6232,253],{"class":98},[85,6234,6235],{"class":125},"\"rounded-lg border bg-card p-6\"",[85,6237,2128],{"class":109},[85,6239,6240,6242,6244,6246,6248,6251],{"class":87,"line":119},[85,6241,2133],{"class":109},[85,6243,2117],{"class":2116},[85,6245,2120],{"class":138},[85,6247,253],{"class":98},[85,6249,6250],{"class":125},"\"h-4 w-24 animate-pulse rounded bg-muted\"",[85,6252,6253],{"class":109}," \u002F>\n",[85,6255,6256,6258,6260,6262,6264,6267],{"class":87,"line":132},[85,6257,2133],{"class":109},[85,6259,2117],{"class":2116},[85,6261,2120],{"class":138},[85,6263,253],{"class":98},[85,6265,6266],{"class":125},"\"mt-3 h-8 w-32 animate-pulse rounded bg-muted\"",[85,6268,6253],{"class":109},[85,6270,6271,6273,6275,6277,6279,6282],{"class":87,"line":145},[85,6272,2133],{"class":109},[85,6274,2117],{"class":2116},[85,6276,2120],{"class":138},[85,6278,253],{"class":98},[85,6280,6281],{"class":125},"\"mt-2 h-3 w-16 animate-pulse rounded bg-muted\"",[85,6283,6253],{"class":109},[85,6285,6286,6288,6290],{"class":87,"line":157},[85,6287,2247],{"class":109},[85,6289,2117],{"class":2116},[85,6291,2128],{"class":109},[85,6293,6294],{"class":87,"line":181},[85,6295,2256],{"class":109},[85,6297,6298],{"class":87,"line":187},[85,6299,280],{"class":109},[17,6301,6302],{},"Matching the internal shape means the transition from skeleton to loaded component is smooth — no layout shift, no flicker.",[12,6304,6306],{"id":6305},"pattern-4-error-boundaries-for-generated-ui","Pattern 4: Error Boundaries for Generated UI",[17,6308,6309],{},"Generated components fail in different ways than hand-coded ones. The AI might pass a numeric string where a number is expected, a negative value where only positives make sense, or an empty array to a component that requires at least one item.",[17,6311,6312],{},"Always wrap generated output in an error boundary:",[75,6314,6316],{"className":1992,"code":6315,"language":1994,"meta":80,"style":80},"\u002F\u002F components\u002Fsafe-genui.tsx\n'use client';\n\nimport { ErrorBoundary } from 'react-error-boundary';\n\nfunction GenUIFallback({ error, resetErrorBoundary }: {\n  error: Error;\n  resetErrorBoundary: () => void;\n}) {\n  return (\n    \u003Cdiv className=\"rounded-lg border border-destructive\u002F50 bg-destructive\u002F5 p-4\">\n      \u003Cp className=\"text-sm font-medium text-destructive\">\n        This component could not render\n      \u003C\u002Fp>\n      \u003Cp className=\"mt-1 text-xs text-muted-foreground\">{error.message}\u003C\u002Fp>\n      \u003Cbutton\n        onClick={resetErrorBoundary}\n        className=\"mt-2 text-xs underline text-muted-foreground\"\n      >\n        Try again\n      \u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n  );\n}\n\nexport function SafeGenUI({ children }: { children: React.ReactNode }) {\n  return (\n    \u003CErrorBoundary FallbackComponent={GenUIFallback}>\n      {children}\n    \u003C\u002FErrorBoundary>\n  );\n}\n",[82,6317,6318,6323,6329,6333,6347,6351,6373,6384,6401,6406,6412,6426,6441,6446,6454,6474,6480,6490,6500,6505,6510,6518,6526,6530,6534,6538,6569,6575,6590,6595,6603,6607],{"__ignoreMap":80},[85,6319,6320],{"class":87,"line":88},[85,6321,6322],{"class":91},"\u002F\u002F components\u002Fsafe-genui.tsx\n",[85,6324,6325,6327],{"class":87,"line":95},[85,6326,3397],{"class":125},[85,6328,416],{"class":109},[85,6330,6331],{"class":87,"line":113},[85,6332,449],{"emptyLinePlaceholder":327},[85,6334,6335,6337,6340,6342,6345],{"class":87,"line":119},[85,6336,404],{"class":98},[85,6338,6339],{"class":109}," { ErrorBoundary } ",[85,6341,410],{"class":98},[85,6343,6344],{"class":125}," 'react-error-boundary'",[85,6346,416],{"class":109},[85,6348,6349],{"class":87,"line":132},[85,6350,449],{"emptyLinePlaceholder":327},[85,6352,6353,6355,6358,6360,6362,6364,6367,6369,6371],{"class":87,"line":145},[85,6354,5600],{"class":98},[85,6356,6357],{"class":138}," GenUIFallback",[85,6359,1432],{"class":109},[85,6361,4510],{"class":202},[85,6363,172],{"class":109},[85,6365,6366],{"class":202},"resetErrorBoundary",[85,6368,2094],{"class":109},[85,6370,1967],{"class":98},[85,6372,110],{"class":109},[85,6374,6375,6378,6380,6382],{"class":87,"line":157},[85,6376,6377],{"class":202},"  error",[85,6379,1967],{"class":98},[85,6381,4515],{"class":138},[85,6383,416],{"class":109},[85,6385,6386,6389,6391,6394,6396,6399],{"class":87,"line":181},[85,6387,6388],{"class":138},"  resetErrorBoundary",[85,6390,1967],{"class":98},[85,6392,6393],{"class":109}," () ",[85,6395,214],{"class":98},[85,6397,6398],{"class":102}," void",[85,6400,416],{"class":109},[85,6402,6403],{"class":87,"line":187},[85,6404,6405],{"class":109},"}) {\n",[85,6407,6408,6410],{"class":87,"line":219},[85,6409,1460],{"class":98},[85,6411,2108],{"class":109},[85,6413,6414,6416,6418,6420,6422,6424],{"class":87,"line":239},[85,6415,2113],{"class":109},[85,6417,2117],{"class":2116},[85,6419,2120],{"class":138},[85,6421,253],{"class":98},[85,6423,4688],{"class":125},[85,6425,2128],{"class":109},[85,6427,6428,6430,6432,6434,6436,6439],{"class":87,"line":265},[85,6429,2133],{"class":109},[85,6431,17],{"class":2116},[85,6433,2120],{"class":138},[85,6435,253],{"class":98},[85,6437,6438],{"class":125},"\"text-sm font-medium text-destructive\"",[85,6440,2128],{"class":109},[85,6442,6443],{"class":87,"line":271},[85,6444,6445],{"class":109},"        This component could not render\n",[85,6447,6448,6450,6452],{"class":87,"line":277},[85,6449,2210],{"class":109},[85,6451,17],{"class":2116},[85,6453,2128],{"class":109},[85,6455,6456,6458,6460,6462,6464,6467,6470,6472],{"class":87,"line":552},[85,6457,2133],{"class":109},[85,6459,17],{"class":2116},[85,6461,2120],{"class":138},[85,6463,253],{"class":98},[85,6465,6466],{"class":125},"\"mt-1 text-xs text-muted-foreground\"",[85,6468,6469],{"class":109},">{error.message}\u003C\u002F",[85,6471,17],{"class":2116},[85,6473,2128],{"class":109},[85,6475,6476,6478],{"class":87,"line":558},[85,6477,2133],{"class":109},[85,6479,3908],{"class":2116},[85,6481,6482,6485,6487],{"class":87,"line":588},[85,6483,6484],{"class":138},"        onClick",[85,6486,253],{"class":98},[85,6488,6489],{"class":109},"{resetErrorBoundary}\n",[85,6491,6492,6495,6497],{"class":87,"line":627},[85,6493,6494],{"class":138},"        className",[85,6496,253],{"class":98},[85,6498,6499],{"class":125},"\"mt-2 text-xs underline text-muted-foreground\"\n",[85,6501,6502],{"class":87,"line":633},[85,6503,6504],{"class":109},"      >\n",[85,6506,6507],{"class":87,"line":638},[85,6508,6509],{"class":109},"        Try again\n",[85,6511,6512,6514,6516],{"class":87,"line":644},[85,6513,2210],{"class":109},[85,6515,3964],{"class":2116},[85,6517,2128],{"class":109},[85,6519,6520,6522,6524],{"class":87,"line":654},[85,6521,2247],{"class":109},[85,6523,2117],{"class":2116},[85,6525,2128],{"class":109},[85,6527,6528],{"class":87,"line":663},[85,6529,2256],{"class":109},[85,6531,6532],{"class":87,"line":695},[85,6533,280],{"class":109},[85,6535,6536],{"class":87,"line":700},[85,6537,449],{"emptyLinePlaceholder":327},[85,6539,6540,6542,6544,6547,6549,6551,6553,6555,6557,6559,6561,6563,6565,6567],{"class":87,"line":719},[85,6541,1420],{"class":98},[85,6543,1426],{"class":98},[85,6545,6546],{"class":138}," SafeGenUI",[85,6548,1432],{"class":109},[85,6550,4480],{"class":202},[85,6552,2094],{"class":109},[85,6554,1967],{"class":98},[85,6556,2699],{"class":109},[85,6558,4480],{"class":202},[85,6560,1967],{"class":98},[85,6562,3575],{"class":138},[85,6564,3578],{"class":109},[85,6566,3581],{"class":138},[85,6568,1438],{"class":109},[85,6570,6571,6573],{"class":87,"line":737},[85,6572,1460],{"class":98},[85,6574,2108],{"class":109},[85,6576,6577,6579,6582,6585,6587],{"class":87,"line":742},[85,6578,2113],{"class":109},[85,6580,6581],{"class":102},"ErrorBoundary",[85,6583,6584],{"class":138}," FallbackComponent",[85,6586,253],{"class":98},[85,6588,6589],{"class":109},"{GenUIFallback}>\n",[85,6591,6592],{"class":87,"line":747},[85,6593,6594],{"class":109},"      {children}\n",[85,6596,6597,6599,6601],{"class":87,"line":752},[85,6598,2247],{"class":109},[85,6600,6581],{"class":102},[85,6602,2128],{"class":109},[85,6604,6605],{"class":87,"line":3110},[85,6606,2256],{"class":109},[85,6608,6609],{"class":87,"line":3116},[85,6610,280],{"class":109},[17,6612,6613,6614,6617],{},"Wrap every piece of generated output with ",[82,6615,6616],{},"\u003CSafeGenUI>",". A rendering error in one component should not break the entire response.",[12,6619,6621],{"id":6620},"pattern-5-state-management-for-generated-interactions","Pattern 5: State Management for Generated Interactions",[17,6623,6624],{},"Components the AI generates often need to be interactive — a table that can be sorted, a chart with tooltips, a form that submits data. This interactivity lives in the component itself and does not require special consideration.",[17,6626,6627],{},"What does require thought is when the generated UI needs to affect the application state outside of itself:",[75,6629,6631],{"className":1992,"code":6630,"language":1994,"meta":80,"style":80},"\u002F\u002F Using React context to let generated components interact with the app\nexport const AppStateContext = createContext\u003C{\n  onDataSelected: (data: unknown) => void;\n  onActionTriggered: (action: string, params: unknown) => void;\n} | null>(null);\n\n\u002F\u002F In your generated component\nfunction DataTable({ columns, rows }: DataTableProps) {\n  const appState = useContext(AppStateContext);\n\n  function handleRowClick(row: Record\u003Cstring, string>) {\n    appState?.onDataSelected(row);\n  }\n\n  return (\n    \u003Ctable>\n      {\u002F* ... *\u002F}\n      {rows.map((row, i) => (\n        \u003Ctr key={i} onClick={() => handleRowClick(row)} className=\"cursor-pointer hover:bg-muted\">\n          {\u002F* ... *\u002F}\n        \u003C\u002Ftr>\n      ))}\n    \u003C\u002Ftable>\n  );\n}\n",[82,6632,6633,6638,6655,6679,6711,6727,6731,6736,6762,6777,6781,6809,6820,6824,6828,6834,6842,6851,6872,6908,6917,6925,6930,6938,6942],{"__ignoreMap":80},[85,6634,6635],{"class":87,"line":88},[85,6636,6637],{"class":91},"\u002F\u002F Using React context to let generated components interact with the app\n",[85,6639,6640,6642,6644,6647,6649,6652],{"class":87,"line":95},[85,6641,1420],{"class":98},[85,6643,5011],{"class":98},[85,6645,6646],{"class":102}," AppStateContext",[85,6648,106],{"class":98},[85,6650,6651],{"class":138}," createContext",[85,6653,6654],{"class":109},"\u003C{\n",[85,6656,6657,6660,6662,6664,6666,6668,6671,6673,6675,6677],{"class":87,"line":113},[85,6658,6659],{"class":138},"  onDataSelected",[85,6661,1967],{"class":98},[85,6663,3077],{"class":109},[85,6665,259],{"class":202},[85,6667,1967],{"class":98},[85,6669,6670],{"class":102}," unknown",[85,6672,4230],{"class":109},[85,6674,214],{"class":98},[85,6676,6398],{"class":102},[85,6678,416],{"class":109},[85,6680,6681,6684,6686,6688,6691,6693,6695,6697,6699,6701,6703,6705,6707,6709],{"class":87,"line":119},[85,6682,6683],{"class":138},"  onActionTriggered",[85,6685,1967],{"class":98},[85,6687,3077],{"class":109},[85,6689,6690],{"class":202},"action",[85,6692,1967],{"class":98},[85,6694,2021],{"class":102},[85,6696,172],{"class":109},[85,6698,1435],{"class":202},[85,6700,1967],{"class":98},[85,6702,6670],{"class":102},[85,6704,4230],{"class":109},[85,6706,214],{"class":98},[85,6708,6398],{"class":102},[85,6710,416],{"class":109},[85,6712,6713,6715,6718,6720,6723,6725],{"class":87,"line":132},[85,6714,606],{"class":109},[85,6716,6717],{"class":98},"|",[85,6719,4521],{"class":102},[85,6721,6722],{"class":109},">(",[85,6724,4601],{"class":102},[85,6726,3529],{"class":109},[85,6728,6729],{"class":87,"line":145},[85,6730,449],{"emptyLinePlaceholder":327},[85,6732,6733],{"class":87,"line":157},[85,6734,6735],{"class":91},"\u002F\u002F In your generated component\n",[85,6737,6738,6740,6743,6745,6748,6750,6753,6755,6757,6760],{"class":87,"line":181},[85,6739,5600],{"class":98},[85,6741,6742],{"class":138}," DataTable",[85,6744,1432],{"class":109},[85,6746,6747],{"class":202},"columns",[85,6749,172],{"class":109},[85,6751,6752],{"class":202},"rows",[85,6754,2094],{"class":109},[85,6756,1967],{"class":98},[85,6758,6759],{"class":138}," DataTableProps",[85,6761,2101],{"class":109},[85,6763,6764,6766,6769,6771,6774],{"class":87,"line":187},[85,6765,1443],{"class":98},[85,6767,6768],{"class":102}," appState",[85,6770,106],{"class":98},[85,6772,6773],{"class":138}," useContext",[85,6775,6776],{"class":109},"(AppStateContext);\n",[85,6778,6779],{"class":87,"line":219},[85,6780,449],{"emptyLinePlaceholder":327},[85,6782,6783,6786,6789,6791,6794,6796,6798,6800,6802,6804,6806],{"class":87,"line":239},[85,6784,6785],{"class":98},"  function",[85,6787,6788],{"class":138}," handleRowClick",[85,6790,476],{"class":109},[85,6792,6793],{"class":202},"row",[85,6795,1967],{"class":98},[85,6797,6022],{"class":138},[85,6799,3552],{"class":109},[85,6801,151],{"class":102},[85,6803,172],{"class":109},[85,6805,151],{"class":102},[85,6807,6808],{"class":109},">) {\n",[85,6810,6811,6814,6817],{"class":87,"line":265},[85,6812,6813],{"class":109},"    appState?.",[85,6815,6816],{"class":138},"onDataSelected",[85,6818,6819],{"class":109},"(row);\n",[85,6821,6822],{"class":87,"line":271},[85,6823,3776],{"class":109},[85,6825,6826],{"class":87,"line":277},[85,6827,449],{"emptyLinePlaceholder":327},[85,6829,6830,6832],{"class":87,"line":552},[85,6831,1460],{"class":98},[85,6833,2108],{"class":109},[85,6835,6836,6838,6840],{"class":87,"line":558},[85,6837,2113],{"class":109},[85,6839,875],{"class":2116},[85,6841,2128],{"class":109},[85,6843,6844,6846,6849],{"class":87,"line":588},[85,6845,3859],{"class":109},[85,6847,6848],{"class":91},"\u002F* ... *\u002F",[85,6850,280],{"class":109},[85,6852,6853,6856,6858,6860,6862,6864,6866,6868,6870],{"class":87,"line":627},[85,6854,6855],{"class":109},"      {rows.",[85,6857,3892],{"class":138},[85,6859,4219],{"class":109},[85,6861,6793],{"class":202},[85,6863,172],{"class":109},[85,6865,4227],{"class":202},[85,6867,4230],{"class":109},[85,6869,214],{"class":98},[85,6871,2108],{"class":109},[85,6873,6874,6876,6878,6880,6882,6885,6888,6890,6892,6894,6896,6899,6901,6903,6906],{"class":87,"line":633},[85,6875,2169],{"class":109},[85,6877,881],{"class":2116},[85,6879,4244],{"class":138},[85,6881,253],{"class":98},[85,6883,6884],{"class":109},"{i} ",[85,6886,6887],{"class":138},"onClick",[85,6889,253],{"class":98},[85,6891,3928],{"class":109},[85,6893,214],{"class":98},[85,6895,6788],{"class":138},[85,6897,6898],{"class":109},"(row)} ",[85,6900,4011],{"class":138},[85,6902,253],{"class":98},[85,6904,6905],{"class":125},"\"cursor-pointer hover:bg-muted\"",[85,6907,2128],{"class":109},[85,6909,6910,6913,6915],{"class":87,"line":638},[85,6911,6912],{"class":109},"          {",[85,6914,6848],{"class":91},[85,6916,280],{"class":109},[85,6918,6919,6921,6923],{"class":87,"line":644},[85,6920,2541],{"class":109},[85,6922,881],{"class":2116},[85,6924,2128],{"class":109},[85,6926,6927],{"class":87,"line":654},[85,6928,6929],{"class":109},"      ))}\n",[85,6931,6932,6934,6936],{"class":87,"line":663},[85,6933,2247],{"class":109},[85,6935,875],{"class":2116},[85,6937,2128],{"class":109},[85,6939,6940],{"class":87,"line":695},[85,6941,2256],{"class":109},[85,6943,6944],{"class":87,"line":700},[85,6945,280],{"class":109},[17,6947,6948],{},"Design your generated components with a clear API for external interactions. Pass callback props from a context rather than importing global state directly — generated components should be portable.",[12,6950,6952],{"id":6951},"common-mistakes","Common Mistakes",[17,6954,6955,6958],{},[39,6956,6957],{},"Too many tools."," If you give the AI 50 components to choose from, it will make poor choices. I have seen teams start with 20+ tools, then find the AI consistently picks the wrong ones. Start with 5–8 well-defined tools and expand only based on data showing which queries are unmatched.",[17,6960,6961,6964],{},[39,6962,6963],{},"Vague descriptions."," \"Display data\" is not a useful tool description. \"Display tabular data with sortable columns when showing lists of items with multiple attributes\" tells the AI exactly when to use it.",[17,6966,6967,6970],{},[39,6968,6969],{},"No fallback."," When the AI model is down or returns an error, users see nothing. Always have a static fallback UI for critical paths. If you are using Generative UI for a data dashboard, have a default static view that loads when the AI is unavailable.",[17,6972,6973,6976],{},[39,6974,6975],{},"Skipping Zod validation."," The AI will occasionally pass unexpected props — a string where a number is expected, a null where a value is required. Strict Zod validation catches these before they reach your component.",[17,6978,6979,6982],{},[39,6980,6981],{},"Over-generating."," Not every interaction needs Generative UI. If a static component works, use it. GenUI adds 200–800ms of latency and costs money. Use it for the interactions where the variability is genuinely valuable.",[17,6984,6985,6988],{},[39,6986,6987],{},"Not logging tool calls."," Without logging which tools the AI selects and what parameters it passes, you have no data for improvement. Log everything from day one. The patterns you see after a week of usage will change how you write your tool descriptions.",[12,6990,6992],{"id":6991},"production-checklist","Production Checklist",[17,6994,6995],{},"Before shipping Generative UI to production:",[33,6997,6998,7001,7004,7007,7010,7013,7016,7019,7022,7025],{},[36,6999,7000],{},"All generated components wrapped in error boundaries",[36,7002,7003],{},"Skeleton loading states for every tool",[36,7005,7006],{},"Static fallback when AI is unavailable or returns an error",[36,7008,7009],{},"Strict Zod validation on all tool parameters",[36,7011,7012],{},"Tool call logging in place (tool name, parameters, latency)",[36,7014,7015],{},"Latency monitoring (alert if >2s to first component)",[36,7017,7018],{},"Cost tracking per AI inference",[36,7020,7021],{},"Accessibility audit of all generated component compositions",[36,7023,7024],{},"Mobile responsive testing of generated layouts",[36,7026,7027],{},"Rate limiting on the server action",[12,7029,7031],{"id":7030},"a-note-on-testing","A Note on Testing",[17,7033,7034],{},"Testing Generative UI requires a different approach than traditional UI testing. The short version:",[33,7036,7037,7040,7043,7046],{},[36,7038,7039],{},"Test your components in isolation with standard unit tests — they are just React components",[36,7041,7042],{},"Test your Zod schemas separately to ensure they accept valid and reject invalid inputs",[36,7044,7045],{},"For integration tests against the AI, test structural properties (correct tool called, valid parameters) not exact content (the temperature is 22°)",[36,7047,7048],{},"Mock the AI in CI and run real AI integration tests nightly",[17,7050,7051,7052,3578],{},"This topic deserves its own article, and we have written one: ",[291,7053,7055],{"href":7054},"\u002Flearn\u002Ftesting-generative-ui-applications","Testing Generative UI Applications",[1138,7057],{},[17,7059,7060],{},[28,7061,7062,7063,7066],{},"Working on a React Generative UI implementation? ",[291,7064,7065],{"href":308},"Get expert guidance"," on architecture, performance, and production readiness.",[312,7068,7069],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":80,"searchDepth":95,"depth":95,"links":7071},[7072,7073,7074,7075,7076,7077,7078,7079,7080],{"id":4894,"depth":95,"text":4895},{"id":4904,"depth":95,"text":4905},{"id":5531,"depth":95,"text":5532},{"id":5978,"depth":95,"text":5979},{"id":6305,"depth":95,"text":6306},{"id":6620,"depth":95,"text":6621},{"id":6951,"depth":95,"text":6952},{"id":6991,"depth":95,"text":6992},{"id":7030,"depth":95,"text":7031},"2026-02-20","Learn how to implement AI-generated UI components in your React applications with real-world patterns.",{"featured":326},"\u002Flearn\u002Fgenerative-ui-react-practical-guide","10 min read",{"title":4889,"description":7082},"learn\u002Fgenerative-ui-react-practical-guide",[4884,333,7089,7090],"patterns","implementation","qv7Ii5puWGfMgjXFVnlFeM-lM3e_vE9UzfAoby634M8",{"id":7093,"title":7094,"author":7,"body":7095,"category":8401,"date":8402,"description":8403,"extension":324,"meta":8404,"navigation":327,"path":8405,"readTime":1169,"seo":8406,"stem":8407,"tags":8408,"__hash__":8410},"content\u002Flearn\u002Fcopilotkit-vs-vercel-ai-sdk-vs-thesys.md","CopilotKit vs Vercel AI SDK vs Thesys: Framework Comparison",{"type":9,"value":7096,"toc":8377},[7097,7101,7104,7107,7111,7251,7255,7261,7265,7268,7432,7440,7444,7478,7482,7510,7514,7517,7519,7523,7529,7532,7535,7890,7893,7896,7936,7939,7965,7969,7972,7974,7978,7981,7984,7987,8208,8211,8214,8246,8249,8275,8279,8282,8284,8288,8291,8296,8307,8312,8323,8326,8330,8336,8342,8348,8354,8360,8363,8365,8374],[12,7098,7100],{"id":7099},"the-landscape-in-early-2026","The Landscape in Early 2026",[17,7102,7103],{},"Three frameworks dominate the Generative UI space right now. Each takes a fundamentally different approach to the same problem: how do you let AI models generate interactive user interfaces?",[17,7105,7106],{},"Here is what I have found after building production features with all three.",[12,7108,7110],{"id":7109},"quick-comparison","Quick Comparison",[875,7112,7113,7126],{},[878,7114,7115],{},[881,7116,7117,7120,7122,7124],{},[884,7118,7119],{},"Feature",[884,7121,775],{},[884,7123,795],{},[884,7125,818],{},[894,7127,7128,7142,7156,7170,7184,7198,7210,7224,7237],{},[881,7129,7130,7133,7136,7139],{},[899,7131,7132],{},"GitHub Stars",[899,7134,7135],{},"~45K",[899,7137,7138],{},"22K",[899,7140,7141],{},"13K (3 months old)",[881,7143,7144,7147,7150,7153],{},[899,7145,7146],{},"npm Downloads",[899,7148,7149],{},"20M+\u002Fmonth",[899,7151,7152],{},"~200K\u002Fmonth",[899,7154,7155],{},"~50K\u002Fmonth",[881,7157,7158,7161,7164,7167],{},[899,7159,7160],{},"Approach",[899,7162,7163],{},"Stream React via RSC",[899,7165,7166],{},"Copilot pattern components",[899,7168,7169],{},"JSON schema rendering",[881,7171,7172,7175,7178,7181],{},[899,7173,7174],{},"Framework lock-in",[899,7176,7177],{},"Next.js (primarily)",[899,7179,7180],{},"React (any bundler)",[899,7182,7183],{},"Framework-agnostic",[881,7185,7186,7189,7192,7195],{},[899,7187,7188],{},"Learning curve",[899,7190,7191],{},"Medium",[899,7193,7194],{},"Low",[899,7196,7197],{},"Low–Medium",[881,7199,7200,7203,7206,7208],{},[899,7201,7202],{},"Production readiness",[899,7204,7205],{},"High",[899,7207,7205],{},[899,7209,7191],{},[881,7211,7212,7215,7218,7221],{},[899,7213,7214],{},"Best for",[899,7216,7217],{},"Full-stack Next.js apps",[899,7219,7220],{},"Adding AI to existing apps",[899,7222,7223],{},"Multi-framework projects",[881,7225,7226,7229,7232,7235],{},[899,7227,7228],{},"License",[899,7230,7231],{},"Apache 2.0",[899,7233,7234],{},"MIT",[899,7236,7234],{},[881,7238,7239,7242,7245,7248],{},[899,7240,7241],{},"Managed hosting option",[899,7243,7244],{},"Vercel",[899,7246,7247],{},"CopilotKit Cloud",[899,7249,7250],{},"Thesys Cloud",[12,7252,7254],{"id":7253},"vercel-ai-sdk-the-full-stack-choice","Vercel AI SDK: The Full-Stack Choice",[17,7256,7257,7258,7260],{},"The Vercel AI SDK's ",[82,7259,781],{}," function is the most powerful approach — and the most opinionated. It streams actual React Server Components from the server, which means AI output is real React code rendered server-side.",[773,7262,7264],{"id":7263},"how-it-works","How It Works",[17,7266,7267],{},"You define tools as async generator functions that yield loading states and return React components. The SDK handles serializing the component tree and streaming it to the client via the RSC protocol.",[75,7269,7271],{"className":77,"code":7270,"language":79,"meta":80,"style":80},"import { streamUI } from 'ai\u002Frsc';\n\nconst result = await streamUI({\n  model: openai('gpt-4o'),\n  prompt: 'Show revenue for Q1',\n  tools: {\n    revenueChart: {\n      description: 'Display a revenue chart',\n      parameters: z.object({ period: z.string(), data: z.array(...) }),\n      generate: async function* (params) {\n        yield \u003CChartSkeleton \u002F>;          \u002F\u002F immediate loading state\n        return \u003CRevenueChart {...params} \u002F>; \u002F\u002F final component\n      },\n    },\n  },\n});\n",[82,7272,7273,7285,7289,7303,7315,7324,7328,7333,7342,7364,7380,7396,7416,7420,7424,7428],{"__ignoreMap":80},[85,7274,7275,7277,7279,7281,7283],{"class":87,"line":88},[85,7276,404],{"class":98},[85,7278,407],{"class":109},[85,7280,410],{"class":98},[85,7282,413],{"class":125},[85,7284,416],{"class":109},[85,7286,7287],{"class":87,"line":95},[85,7288,449],{"emptyLinePlaceholder":327},[85,7290,7291,7293,7295,7297,7299,7301],{"class":87,"line":113},[85,7292,99],{"class":98},[85,7294,456],{"class":102},[85,7296,106],{"class":98},[85,7298,230],{"class":98},[85,7300,463],{"class":138},[85,7302,142],{"class":109},[85,7304,7305,7307,7309,7311,7313],{"class":87,"line":119},[85,7306,470],{"class":109},[85,7308,473],{"class":138},[85,7310,476],{"class":109},[85,7312,479],{"class":125},[85,7314,482],{"class":109},[85,7316,7317,7319,7322],{"class":87,"line":132},[85,7318,487],{"class":109},[85,7320,7321],{"class":125},"'Show revenue for Q1'",[85,7323,129],{"class":109},[85,7325,7326],{"class":87,"line":145},[85,7327,497],{"class":109},[85,7329,7330],{"class":87,"line":157},[85,7331,7332],{"class":109},"    revenueChart: {\n",[85,7334,7335,7337,7340],{"class":87,"line":181},[85,7336,507],{"class":109},[85,7338,7339],{"class":125},"'Display a revenue chart'",[85,7341,129],{"class":109},[85,7343,7344,7346,7348,7350,7352,7355,7357,7359,7361],{"class":87,"line":187},[85,7345,517],{"class":109},[85,7347,139],{"class":138},[85,7349,1516],{"class":109},[85,7351,151],{"class":138},[85,7353,7354],{"class":109},"(), data: z.",[85,7356,669],{"class":138},[85,7358,476],{"class":109},[85,7360,3129],{"class":98},[85,7362,7363],{"class":109},") }),\n",[85,7365,7366,7368,7370,7372,7374,7376,7378],{"class":87,"line":219},[85,7367,561],{"class":138},[85,7369,193],{"class":109},[85,7371,196],{"class":98},[85,7373,3074],{"class":98},[85,7375,3077],{"class":109},[85,7377,1435],{"class":202},[85,7379,2101],{"class":109},[85,7381,7382,7385,7387,7390,7393],{"class":87,"line":239},[85,7383,7384],{"class":98},"        yield",[85,7386,245],{"class":109},[85,7388,7389],{"class":138},"ChartSkeleton",[85,7391,7392],{"class":109}," \u002F>;          ",[85,7394,7395],{"class":91},"\u002F\u002F immediate loading state\n",[85,7397,7398,7400,7402,7404,7406,7408,7410,7413],{"class":87,"line":265},[85,7399,591],{"class":98},[85,7401,245],{"class":109},[85,7403,1580],{"class":138},[85,7405,3126],{"class":109},[85,7407,3129],{"class":98},[85,7409,1435],{"class":138},[85,7411,7412],{"class":109},"} \u002F>; ",[85,7414,7415],{"class":91},"\u002F\u002F final component\n",[85,7417,7418],{"class":87,"line":271},[85,7419,630],{"class":109},[85,7421,7422],{"class":87,"line":277},[85,7423,268],{"class":109},[85,7425,7426],{"class":87,"line":552},[85,7427,274],{"class":109},[85,7429,7430],{"class":87,"line":558},[85,7431,755],{"class":109},[17,7433,1945,7434,7436,7437,7439],{},[82,7435,3359],{}," \u002F ",[82,7438,3363],{}," pattern is the defining characteristic. The skeleton appears instantly while the AI resolves parameters. When parameters are ready, the real component replaces it — all in one streaming response.",[773,7441,7443],{"id":7442},"strengths","Strengths",[33,7445,7446,7454,7460,7466,7472],{},[36,7447,7448,4767,7451,7453],{},[39,7449,7450],{},"Deepest Next.js integration.",[82,7452,781],{}," is designed around App Router and RSC. If you are building a Next.js application, this is the most idiomatic choice.",[36,7455,7456,7459],{},[39,7457,7458],{},"True server-side rendering."," Generated components render on the server, which means they can access databases, file systems, and private APIs directly in their render functions.",[36,7461,7462,7465],{},[39,7463,7464],{},"Largest ecosystem."," 20M+ monthly downloads means abundant examples, Stack Overflow answers, and community support.",[36,7467,7468,7471],{},[39,7469,7470],{},"Best TypeScript support."," The SDK's types are thorough. Tool parameters, model responses, and streaming values are all properly typed.",[36,7473,7474,7477],{},[39,7475,7476],{},"Provider flexibility."," The SDK abstracts over model providers — switch from OpenAI to Anthropic to Google by changing one import.",[773,7479,7481],{"id":7480},"weaknesses","Weaknesses",[33,7483,7484,7492,7498,7504],{},[36,7485,7486,4767,7489,7491],{},[39,7487,7488],{},"Next.js dependency.",[82,7490,781],{}," requires React Server Components. It works in Next.js App Router. Running it outside that environment requires significant workaround effort.",[36,7493,7494,7497],{},[39,7495,7496],{},"RSC debugging complexity."," When something goes wrong in a server component that is being streamed, the debugging experience is worse than a regular server error. Error messages can be cryptic.",[36,7499,7500,7503],{},[39,7501,7502],{},"Server component limitations."," RSC cannot use hooks, browser APIs, or client-side state directly. Interactive behavior requires careful splitting of server and client components.",[36,7505,7506,7509],{},[39,7507,7508],{},"Vercel-adjacent."," While the SDK works on any platform that supports Node.js, some features are optimized for Vercel's infrastructure.",[773,7511,7513],{"id":7512},"when-to-choose-vercel-ai-sdk","When to Choose Vercel AI SDK",[17,7515,7516],{},"You are building a new Next.js application. You want the most production-ready, performant Generative UI implementation. You are comfortable with React Server Components and the App Router. You want the widest selection of community examples.",[1138,7518],{},[12,7520,7522],{"id":7521},"copilotkit-the-integration-choice","CopilotKit: The Integration Choice",[17,7524,7525,7526,7528],{},"CopilotKit takes a different philosophy. Instead of streaming components from the server, it provides client-side React components that create \"copilot\" experiences. Drop ",[82,7527,801],{}," into your existing application and you have a sidebar AI that can read and modify your app's state.",[773,7530,7264],{"id":7531},"how-it-works-1",[17,7533,7534],{},"CopilotKit introduces two main primitives: actions and readable state. You define what the AI can do and what it can see, then CopilotKit handles the rest.",[75,7536,7538],{"className":1992,"code":7537,"language":1994,"meta":80,"style":80},"import { CopilotKit, CopilotChat } from '@copilotkit\u002Freact-core';\nimport { useCopilotAction, useCopilotReadable } from '@copilotkit\u002Freact-core';\n\nfunction Dashboard() {\n  const [filters, setFilters] = useState({ period: 'month', metric: 'revenue' });\n\n  \u002F\u002F Let the AI read the current state\n  useCopilotReadable({\n    description: 'Current dashboard filters',\n    value: filters,\n  });\n\n  \u002F\u002F Let the AI modify the filters\n  useCopilotAction({\n    name: 'updateFilters',\n    description: 'Update the dashboard view',\n    parameters: [\n      { name: 'period', type: 'string' },\n      { name: 'metric', type: 'string' },\n    ],\n    handler: ({ period, metric }) => setFilters({ period, metric }),\n  });\n\n  return (\n    \u003Cdiv className=\"flex\">\n      \u003CDashboardView filters={filters} \u002F>\n      \u003CCopilotChat instructions=\"Help the user explore their dashboard data.\" \u002F>\n    \u003C\u002Fdiv>\n  );\n}\n\n\u002F\u002F Wrap the app with CopilotKit\nfunction App() {\n  return (\n    \u003CCopilotKit runtimeUrl=\"\u002Fapi\u002Fcopilotkit\">\n      \u003CDashboard \u002F>\n    \u003C\u002FCopilotKit>\n  );\n}\n",[82,7539,7540,7554,7567,7571,7580,7615,7619,7624,7631,7640,7645,7649,7653,7658,7665,7675,7684,7689,7706,7719,7724,7748,7752,7756,7762,7777,7792,7809,7817,7821,7825,7829,7834,7843,7849,7865,7874,7882,7886],{"__ignoreMap":80},[85,7541,7542,7544,7547,7549,7552],{"class":87,"line":88},[85,7543,404],{"class":98},[85,7545,7546],{"class":109}," { CopilotKit, CopilotChat } ",[85,7548,410],{"class":98},[85,7550,7551],{"class":125}," '@copilotkit\u002Freact-core'",[85,7553,416],{"class":109},[85,7555,7556,7558,7561,7563,7565],{"class":87,"line":95},[85,7557,404],{"class":98},[85,7559,7560],{"class":109}," { useCopilotAction, useCopilotReadable } ",[85,7562,410],{"class":98},[85,7564,7551],{"class":125},[85,7566,416],{"class":109},[85,7568,7569],{"class":87,"line":113},[85,7570,449],{"emptyLinePlaceholder":327},[85,7572,7573,7575,7578],{"class":87,"line":119},[85,7574,5600],{"class":98},[85,7576,7577],{"class":138}," Dashboard",[85,7579,3499],{"class":109},[85,7581,7582,7584,7586,7589,7591,7594,7596,7598,7600,7603,7606,7609,7612],{"class":87,"line":132},[85,7583,1443],{"class":98},[85,7585,3506],{"class":109},[85,7587,7588],{"class":102},"filters",[85,7590,172],{"class":109},[85,7592,7593],{"class":102},"setFilters",[85,7595,3516],{"class":109},[85,7597,253],{"class":98},[85,7599,3521],{"class":138},[85,7601,7602],{"class":109},"({ period: ",[85,7604,7605],{"class":125},"'month'",[85,7607,7608],{"class":109},", metric: ",[85,7610,7611],{"class":125},"'revenue'",[85,7613,7614],{"class":109}," });\n",[85,7616,7617],{"class":87,"line":145},[85,7618,449],{"emptyLinePlaceholder":327},[85,7620,7621],{"class":87,"line":157},[85,7622,7623],{"class":91},"  \u002F\u002F Let the AI read the current state\n",[85,7625,7626,7629],{"class":87,"line":181},[85,7627,7628],{"class":138},"  useCopilotReadable",[85,7630,142],{"class":109},[85,7632,7633,7635,7638],{"class":87,"line":187},[85,7634,122],{"class":109},[85,7636,7637],{"class":125},"'Current dashboard filters'",[85,7639,129],{"class":109},[85,7641,7642],{"class":87,"line":219},[85,7643,7644],{"class":109},"    value: filters,\n",[85,7646,7647],{"class":87,"line":239},[85,7648,3327],{"class":109},[85,7650,7651],{"class":87,"line":265},[85,7652,449],{"emptyLinePlaceholder":327},[85,7654,7655],{"class":87,"line":271},[85,7656,7657],{"class":91},"  \u002F\u002F Let the AI modify the filters\n",[85,7659,7660,7663],{"class":87,"line":277},[85,7661,7662],{"class":138},"  useCopilotAction",[85,7664,142],{"class":109},[85,7666,7667,7670,7673],{"class":87,"line":552},[85,7668,7669],{"class":109},"    name: ",[85,7671,7672],{"class":125},"'updateFilters'",[85,7674,129],{"class":109},[85,7676,7677,7679,7682],{"class":87,"line":558},[85,7678,122],{"class":109},[85,7680,7681],{"class":125},"'Update the dashboard view'",[85,7683,129],{"class":109},[85,7685,7686],{"class":87,"line":588},[85,7687,7688],{"class":109},"    parameters: [\n",[85,7690,7691,7694,7697,7700,7703],{"class":87,"line":627},[85,7692,7693],{"class":109},"      { name: ",[85,7695,7696],{"class":125},"'period'",[85,7698,7699],{"class":109},", type: ",[85,7701,7702],{"class":125},"'string'",[85,7704,7705],{"class":109}," },\n",[85,7707,7708,7710,7713,7715,7717],{"class":87,"line":633},[85,7709,7693],{"class":109},[85,7711,7712],{"class":125},"'metric'",[85,7714,7699],{"class":109},[85,7716,7702],{"class":125},[85,7718,7705],{"class":109},[85,7720,7721],{"class":87,"line":638},[85,7722,7723],{"class":109},"    ],\n",[85,7725,7726,7729,7732,7734,7736,7738,7740,7742,7745],{"class":87,"line":644},[85,7727,7728],{"class":138},"    handler",[85,7730,7731],{"class":109},": ({ ",[85,7733,1539],{"class":202},[85,7735,172],{"class":109},[85,7737,1544],{"class":202},[85,7739,211],{"class":109},[85,7741,214],{"class":98},[85,7743,7744],{"class":138}," setFilters",[85,7746,7747],{"class":109},"({ period, metric }),\n",[85,7749,7750],{"class":87,"line":654},[85,7751,3327],{"class":109},[85,7753,7754],{"class":87,"line":663},[85,7755,449],{"emptyLinePlaceholder":327},[85,7757,7758,7760],{"class":87,"line":695},[85,7759,1460],{"class":98},[85,7761,2108],{"class":109},[85,7763,7764,7766,7768,7770,7772,7775],{"class":87,"line":700},[85,7765,2113],{"class":109},[85,7767,2117],{"class":2116},[85,7769,2120],{"class":138},[85,7771,253],{"class":98},[85,7773,7774],{"class":125},"\"flex\"",[85,7776,2128],{"class":109},[85,7778,7779,7781,7784,7787,7789],{"class":87,"line":719},[85,7780,2133],{"class":109},[85,7782,7783],{"class":102},"DashboardView",[85,7785,7786],{"class":138}," filters",[85,7788,253],{"class":98},[85,7790,7791],{"class":109},"{filters} \u002F>\n",[85,7793,7794,7796,7799,7802,7804,7807],{"class":87,"line":737},[85,7795,2133],{"class":109},[85,7797,7798],{"class":102},"CopilotChat",[85,7800,7801],{"class":138}," instructions",[85,7803,253],{"class":98},[85,7805,7806],{"class":125},"\"Help the user explore their dashboard data.\"",[85,7808,6253],{"class":109},[85,7810,7811,7813,7815],{"class":87,"line":742},[85,7812,2247],{"class":109},[85,7814,2117],{"class":2116},[85,7816,2128],{"class":109},[85,7818,7819],{"class":87,"line":747},[85,7820,2256],{"class":109},[85,7822,7823],{"class":87,"line":752},[85,7824,280],{"class":109},[85,7826,7827],{"class":87,"line":3110},[85,7828,449],{"emptyLinePlaceholder":327},[85,7830,7831],{"class":87,"line":3116},[85,7832,7833],{"class":91},"\u002F\u002F Wrap the app with CopilotKit\n",[85,7835,7836,7838,7841],{"class":87,"line":3135},[85,7837,5600],{"class":98},[85,7839,7840],{"class":138}," App",[85,7842,3499],{"class":109},[85,7844,7845,7847],{"class":87,"line":3141},[85,7846,1460],{"class":98},[85,7848,2108],{"class":109},[85,7850,7851,7853,7855,7858,7860,7863],{"class":87,"line":3146},[85,7852,2113],{"class":109},[85,7854,795],{"class":102},[85,7856,7857],{"class":138}," runtimeUrl",[85,7859,253],{"class":98},[85,7861,7862],{"class":125},"\"\u002Fapi\u002Fcopilotkit\"",[85,7864,2128],{"class":109},[85,7866,7867,7869,7872],{"class":87,"line":3152},[85,7868,2133],{"class":109},[85,7870,7871],{"class":102},"Dashboard",[85,7873,6253],{"class":109},[85,7875,7876,7878,7880],{"class":87,"line":3168},[85,7877,2247],{"class":109},[85,7879,795],{"class":102},[85,7881,2128],{"class":109},[85,7883,7884],{"class":87,"line":3177},[85,7885,2256],{"class":109},[85,7887,7888],{"class":87,"line":3196},[85,7889,280],{"class":109},[17,7891,7892],{},"The copilot pattern is distinct: the AI is a sidebar assistant that interacts with the existing UI, rather than generating new UI from scratch.",[773,7894,7443],{"id":7895},"strengths-1",[33,7897,7898,7904,7910,7916,7928],{},[36,7899,7900,7903],{},[39,7901,7902],{},"Fastest time-to-integration."," Adding a copilot sidebar to an existing React app takes hours, not days. The components work out of the box.",[36,7905,7906,7909],{},[39,7907,7908],{},"Works with any React setup."," Create React App, Vite, Remix, Next.js — CopilotKit does not require RSC or a specific bundler.",[36,7911,7912,7915],{},[39,7913,7914],{},"Natural copilot pattern."," The sidebar AI that assists with the existing UI is a well-understood pattern that users immediately understand.",[36,7917,7918,4767,7921,802,7924,7927],{},[39,7919,7920],{},"State synchronization built in.",[82,7922,7923],{},"useCopilotReadable",[82,7925,7926],{},"useCopilotAction"," create a clean bidirectional contract between your app and the AI.",[36,7929,7930,3356,7933,7935],{},[39,7931,7932],{},"Strong default UI.",[82,7934,801],{}," component is production-quality and customizable without requiring you to build your own chat interface.",[773,7937,7481],{"id":7938},"weaknesses-1",[33,7940,7941,7947,7953,7959],{},[36,7942,7943,7946],{},[39,7944,7945],{},"Client-side rendering model."," CopilotKit renders AI output on the client. There is no SSR for generated components, which impacts performance and SEO for public-facing content.",[36,7948,7949,7952],{},[39,7950,7951],{},"The copilot pattern is not universal."," If your use case is not \"sidebar AI assistant that helps with the main interface,\" CopilotKit requires more customization to fit.",[36,7954,7955,7958],{},[39,7956,7957],{},"Less control over rendering pipeline."," For complex custom component generation, the Vercel AI SDK gives you more flexibility.",[36,7960,7961,7964],{},[39,7962,7963],{},"Bundle size."," Client-side rendering means the component library ships to the browser. For performance-sensitive applications, this requires attention.",[773,7966,7968],{"id":7967},"when-to-choose-copilotkit","When to Choose CopilotKit",[17,7970,7971],{},"You have an existing React application and want to add AI-powered features quickly. The copilot pattern — an AI sidebar that can read and modify the main interface — fits your product. You do not want to deal with RSC.",[1138,7973],{},[12,7975,7977],{"id":7976},"thesys-json-render-the-universal-choice","Thesys (json-render): The Universal Choice",[17,7979,7980],{},"Thesys, launched in January 2026 and already at 13K GitHub stars, takes the most framework-agnostic approach. AI models output JSON that describes a UI component tree, and a renderer turns that JSON into interactive components.",[773,7982,7264],{"id":7983},"how-it-works-2",[17,7985,7986],{},"The AI outputs JSON instead of triggering React tool calls. That JSON describes a component hierarchy, and the Thesys renderer interprets it:",[75,7988,7990],{"className":77,"code":7989,"language":79,"meta":80,"style":80},"\u002F\u002F The AI outputs something like this:\nconst aiOutput = {\n  type: \"layout\",\n  direction: \"grid\",\n  columns: 2,\n  children: [\n    {\n      type: \"MetricCard\",\n      props: {\n        label: \"Monthly Revenue\",\n        value: \"$84,200\",\n        change: 12.4,\n        period: \"vs last month\"\n      }\n    },\n    {\n      type: \"AlertBanner\",\n      props: {\n        type: \"info\",\n        title: \"New record\",\n        message: \"Best month in company history\"\n      }\n    }\n  ]\n};\n\n\u002F\u002F The renderer turns it into UI\nimport { render } from '@thesys\u002Fjson-render';\nconst ui = render(aiOutput, componentRegistry);\n",[82,7991,7992,7997,8008,8018,8028,8037,8042,8047,8057,8062,8072,8082,8092,8100,8105,8109,8113,8122,8126,8136,8146,8154,8158,8162,8167,8171,8175,8180,8194],{"__ignoreMap":80},[85,7993,7994],{"class":87,"line":88},[85,7995,7996],{"class":91},"\u002F\u002F The AI outputs something like this:\n",[85,7998,7999,8001,8004,8006],{"class":87,"line":95},[85,8000,99],{"class":98},[85,8002,8003],{"class":102}," aiOutput",[85,8005,106],{"class":98},[85,8007,110],{"class":109},[85,8009,8010,8013,8016],{"class":87,"line":113},[85,8011,8012],{"class":109},"  type: ",[85,8014,8015],{"class":125},"\"layout\"",[85,8017,129],{"class":109},[85,8019,8020,8023,8026],{"class":87,"line":119},[85,8021,8022],{"class":109},"  direction: ",[85,8024,8025],{"class":125},"\"grid\"",[85,8027,129],{"class":109},[85,8029,8030,8033,8035],{"class":87,"line":132},[85,8031,8032],{"class":109},"  columns: ",[85,8034,2533],{"class":102},[85,8036,129],{"class":109},[85,8038,8039],{"class":87,"line":145},[85,8040,8041],{"class":109},"  children: [\n",[85,8043,8044],{"class":87,"line":157},[85,8045,8046],{"class":109},"    {\n",[85,8048,8049,8052,8055],{"class":87,"line":181},[85,8050,8051],{"class":109},"      type: ",[85,8053,8054],{"class":125},"\"MetricCard\"",[85,8056,129],{"class":109},[85,8058,8059],{"class":87,"line":187},[85,8060,8061],{"class":109},"      props: {\n",[85,8063,8064,8067,8070],{"class":87,"line":219},[85,8065,8066],{"class":109},"        label: ",[85,8068,8069],{"class":125},"\"Monthly Revenue\"",[85,8071,129],{"class":109},[85,8073,8074,8077,8080],{"class":87,"line":239},[85,8075,8076],{"class":109},"        value: ",[85,8078,8079],{"class":125},"\"$84,200\"",[85,8081,129],{"class":109},[85,8083,8084,8087,8090],{"class":87,"line":265},[85,8085,8086],{"class":109},"        change: ",[85,8088,8089],{"class":102},"12.4",[85,8091,129],{"class":109},[85,8093,8094,8097],{"class":87,"line":271},[85,8095,8096],{"class":109},"        period: ",[85,8098,8099],{"class":125},"\"vs last month\"\n",[85,8101,8102],{"class":87,"line":277},[85,8103,8104],{"class":109},"      }\n",[85,8106,8107],{"class":87,"line":552},[85,8108,268],{"class":109},[85,8110,8111],{"class":87,"line":558},[85,8112,8046],{"class":109},[85,8114,8115,8117,8120],{"class":87,"line":588},[85,8116,8051],{"class":109},[85,8118,8119],{"class":125},"\"AlertBanner\"",[85,8121,129],{"class":109},[85,8123,8124],{"class":87,"line":627},[85,8125,8061],{"class":109},[85,8127,8128,8131,8134],{"class":87,"line":633},[85,8129,8130],{"class":109},"        type: ",[85,8132,8133],{"class":125},"\"info\"",[85,8135,129],{"class":109},[85,8137,8138,8141,8144],{"class":87,"line":638},[85,8139,8140],{"class":109},"        title: ",[85,8142,8143],{"class":125},"\"New record\"",[85,8145,129],{"class":109},[85,8147,8148,8151],{"class":87,"line":644},[85,8149,8150],{"class":109},"        message: ",[85,8152,8153],{"class":125},"\"Best month in company history\"\n",[85,8155,8156],{"class":87,"line":654},[85,8157,8104],{"class":109},[85,8159,8160],{"class":87,"line":663},[85,8161,4736],{"class":109},[85,8163,8164],{"class":87,"line":695},[85,8165,8166],{"class":109},"  ]\n",[85,8168,8169],{"class":87,"line":700},[85,8170,5484],{"class":109},[85,8172,8173],{"class":87,"line":719},[85,8174,449],{"emptyLinePlaceholder":327},[85,8176,8177],{"class":87,"line":737},[85,8178,8179],{"class":91},"\u002F\u002F The renderer turns it into UI\n",[85,8181,8182,8184,8187,8189,8192],{"class":87,"line":742},[85,8183,404],{"class":98},[85,8185,8186],{"class":109}," { render } ",[85,8188,410],{"class":98},[85,8190,8191],{"class":125}," '@thesys\u002Fjson-render'",[85,8193,416],{"class":109},[85,8195,8196,8198,8200,8202,8205],{"class":87,"line":747},[85,8197,99],{"class":98},[85,8199,3732],{"class":102},[85,8201,106],{"class":98},[85,8203,8204],{"class":138}," render",[85,8206,8207],{"class":109},"(aiOutput, componentRegistry);\n",[17,8209,8210],{},"The JSON schema is the artifact. It can be logged, cached, replayed, and rendered on any platform that has a Thesys renderer.",[773,8212,7443],{"id":8213},"strengths-2",[33,8215,8216,8222,8228,8234,8240],{},[36,8217,8218,8221],{},[39,8219,8220],{},"Framework-agnostic."," The same JSON schema renders in React, Vue, Angular, or native mobile. One AI response, many renderers.",[36,8223,8224,8227],{},[39,8225,8226],{},"Debuggable."," The JSON output is a plain data structure you can inspect in any JSON viewer. Debugging \"why did the AI generate this?\" is straightforward.",[36,8229,8230,8233],{},[39,8231,8232],{},"Cacheable."," Cache by prompt hash and the AI response is reused without inference cost. This is harder with RSC streaming.",[36,8235,8236,8239],{},[39,8237,8238],{},"Inspectable history."," Storing generated UI as JSON means you can audit exactly what was shown to users, replaying any interaction.",[36,8241,8242,8245],{},[39,8243,8244],{},"Simpler mental model."," JSON in, UI out. The abstraction is easy to explain to non-React developers.",[773,8247,7481],{"id":8248},"weaknesses-2",[33,8250,8251,8257,8263,8269],{},[36,8252,8253,8256],{},[39,8254,8255],{},"Younger project."," Thesys launched in January 2026. Less battle-tested in production than alternatives. Breaking changes are more likely.",[36,8258,8259,8262],{},[39,8260,8261],{},"JSON abstraction limits interactivity."," Complex interactive patterns — forms with validation logic, real-time data, animated transitions — are harder to express in a JSON schema than in React code.",[36,8264,8265,8268],{},[39,8266,8267],{},"Client-side rendering."," Like CopilotKit, the rendering happens on the client. No SSR.",[36,8270,8271,8274],{},[39,8272,8273],{},"Smaller community."," 13K stars in 3 months is impressive growth, but the community is a fraction of Vercel AI SDK's size.",[773,8276,8278],{"id":8277},"when-to-choose-thesys","When to Choose Thesys",[17,8280,8281],{},"Your project uses multiple frontend frameworks or needs to support mobile clients. You value the ability to inspect, cache, and replay generated UIs. You want a simpler mental model. You are comfortable being an early adopter.",[1138,8283],{},[12,8285,8287],{"id":8286},"migration-considerations","Migration Considerations",[17,8289,8290],{},"Switching between frameworks later is not free, but it is less expensive than it looks.",[17,8292,8293],{},[39,8294,8295],{},"What is portable:",[33,8297,8298,8301,8304],{},[36,8299,8300],{},"Your component library (pure React, no framework dependency)",[36,8302,8303],{},"Your system prompts and tool descriptions",[36,8305,8306],{},"Your Zod schemas for tool parameters",[17,8308,8309],{},[39,8310,8311],{},"What requires rewriting:",[33,8313,8314,8317,8320],{},[36,8315,8316],{},"The server action \u002F API endpoint structure",[36,8318,8319],{},"The streaming integration code",[36,8321,8322],{},"The client-side rendering setup",[17,8324,8325],{},"Estimate 2–5 days to migrate between frameworks for a medium-complexity feature. The component library itself — usually the majority of the investment — moves without changes.",[12,8327,8329],{"id":8328},"recommendation-matrix","Recommendation Matrix",[17,8331,8332,8335],{},[39,8333,8334],{},"Starting a new Next.js app from scratch:"," Vercel AI SDK. No contest for pure Next.js builds.",[17,8337,8338,8341],{},[39,8339,8340],{},"Adding AI to an existing React app:"," CopilotKit. Fastest time-to-value for the integration use case.",[17,8343,8344,8347],{},[39,8345,8346],{},"Multi-framework or non-React stack:"," Thesys. The only practical option for framework-agnostic needs.",[17,8349,8350,8353],{},[39,8351,8352],{},"Undecided and want to start exploring:"," Vercel AI SDK. The largest community means the most examples and the most answers to your questions.",[17,8355,8356,8359],{},[39,8357,8358],{},"Betting on the future of AI interfaces:"," Watch all three. The space is moving fast and the winners of today may not be the winners in 18 months.",[17,8361,8362],{},"One more principle worth stating plainly: do not over-invest in framework choice early. The components you build are the valuable, durable artifact. The framework is plumbing. Build great components, keep them clean of framework-specific code, and you can switch frameworks with a week of effort if you need to.",[1138,8364],{},[17,8366,8367],{},[28,8368,8369,8370,8373],{},"Building with one of these frameworks and need guidance? ",[291,8371,8372],{"href":308},"Book a consultation"," — I have production experience with all three.",[312,8375,8376],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":80,"searchDepth":95,"depth":95,"links":8378},[8379,8380,8381,8387,8393,8399,8400],{"id":7099,"depth":95,"text":7100},{"id":7109,"depth":95,"text":7110},{"id":7253,"depth":95,"text":7254,"children":8382},[8383,8384,8385,8386],{"id":7263,"depth":113,"text":7264},{"id":7442,"depth":113,"text":7443},{"id":7480,"depth":113,"text":7481},{"id":7512,"depth":113,"text":7513},{"id":7521,"depth":95,"text":7522,"children":8388},[8389,8390,8391,8392],{"id":7531,"depth":113,"text":7264},{"id":7895,"depth":113,"text":7443},{"id":7938,"depth":113,"text":7481},{"id":7967,"depth":113,"text":7968},{"id":7976,"depth":95,"text":7977,"children":8394},[8395,8396,8397,8398],{"id":7983,"depth":113,"text":7264},{"id":8213,"depth":113,"text":7443},{"id":8248,"depth":113,"text":7481},{"id":8277,"depth":113,"text":8278},{"id":8286,"depth":95,"text":8287},{"id":8328,"depth":95,"text":8329},"framework-guide","2026-02-14","An honest comparison of the three major Generative UI frameworks, with pros, cons, and when to use each.",{"featured":326},"\u002Flearn\u002Fcopilotkit-vs-vercel-ai-sdk-vs-thesys",{"title":7094,"description":8403},"learn\u002Fcopilotkit-vs-vercel-ai-sdk-vs-thesys",[794,335,8409,1859,1175],"thesys","ZT6qMBbFHkI1FB1twGTMNVfiQdPbgcYNkm9UFvU2hqM",{"id":8412,"title":7055,"author":7,"body":8413,"category":1164,"date":11287,"description":11288,"extension":324,"meta":11289,"navigation":327,"path":7054,"readTime":11290,"seo":11291,"stem":11292,"tags":11293,"__hash__":11297},"content\u002Flearn\u002Ftesting-generative-ui-applications.md",{"type":9,"value":8414,"toc":11276},[8415,8419,8422,8425,8428,8432,8435,8441,8444,8450,8453,8457,8460,8855,8858,8862,8865,9405,9408,9412,9415,9418,9429,9937,9941,9944,10434,10437,10441,10444,10449,10715,10727,10733,10937,10940,10944,10947,11211,11214,11218,11221,11259,11262,11264,11273],[12,8416,8418],{"id":8417},"the-testing-problem","The Testing Problem",[17,8420,8421],{},"Generative UI breaks a fundamental assumption of traditional UI testing: that the same input produces the same output. When an AI model decides which components to render and with what parameters, tests cannot assert exact HTML output.",[17,8423,8424],{},"If you try to snapshot a generated dashboard and diff it against the next run, you will get false failures constantly — the AI may pick the same components but generate slightly different data, reorder them, or make a different composition choice entirely.",[17,8426,8427],{},"This does not mean Generative UI is untestable. It means you need different strategies, applied at different layers of the stack.",[12,8429,8431],{"id":8430},"the-testing-pyramid-for-genui","The Testing Pyramid for GenUI",[17,8433,8434],{},"A conventional UI testing pyramid looks like this (wide base = many tests, narrow top = few):",[75,8436,8439],{"className":8437,"code":8438,"language":1288},[1286],"         \u002F    E2E Tests     \\\n        \u002F   Integration Tests \\\n       \u002F     Unit Tests        \\\n",[82,8440,8438],{"__ignoreMap":80},[17,8442,8443],{},"For Generative UI, the pyramid changes shape:",[75,8445,8448],{"className":8446,"code":8447,"language":1288},[1286],"         \u002F  AI Integration Tests  \\     \u003C- Nightly, few, expensive\n        \u002F  Tool Validation Tests    \\   \u003C- Every PR, moderate\n       \u002F  Component Unit Tests        \\ \u003C- Every PR, most, fast\n",[82,8449,8447],{"__ignoreMap":80},[17,8451,8452],{},"The key difference: AI integration tests (where real inference runs) are expensive and slow, so they run nightly rather than on every commit. The heavy lifting of catching regressions falls on the two deterministic layers below.",[12,8454,8456],{"id":8455},"layer-1-component-testing-fully-deterministic","Layer 1: Component Testing (Fully Deterministic)",[17,8458,8459],{},"Your component library is fully deterministic. Test every component in isolation with standard tools — React Testing Library, Vitest, Jest. There is nothing special about components that happen to be used in a Generative UI system.",[75,8461,8463],{"className":1992,"code":8462,"language":1994,"meta":80,"style":80},"\u002F\u002F __tests__\u002Fweather-card.test.tsx\nimport { render, screen } from '@testing-library\u002Freact';\nimport { WeatherCard } from '@\u002Fcomponents\u002Fweather-card';\n\ndescribe('WeatherCard', () => {\n  test('renders city, temperature, and conditions', () => {\n    render(\n      \u003CWeatherCard city=\"Paris\" temperature={22} conditions=\"Partly cloudy\" humidity={45} \u002F>\n    );\n    expect(screen.getByText('Paris')).toBeInTheDocument();\n    expect(screen.getByText('22°C')).toBeInTheDocument();\n    expect(screen.getByText('Partly cloudy')).toBeInTheDocument();\n  });\n\n  test('handles negative temperatures', () => {\n    render(\n      \u003CWeatherCard city=\"Oslo\" temperature={-8} conditions=\"Snow\" humidity={80} \u002F>\n    );\n    expect(screen.getByText('-8°C')).toBeInTheDocument();\n  });\n\n  test('renders humidity as percentage', () => {\n    render(\n      \u003CWeatherCard city=\"London\" temperature={15} conditions=\"Foggy\" humidity={92} \u002F>\n    );\n    expect(screen.getByText(\u002F92%\u002F)).toBeInTheDocument();\n  });\n});\n",[82,8464,8465,8470,8484,8496,8500,8516,8532,8539,8583,8588,8612,8631,8650,8654,8658,8673,8679,8724,8728,8747,8751,8755,8770,8776,8818,8822,8847,8851],{"__ignoreMap":80},[85,8466,8467],{"class":87,"line":88},[85,8468,8469],{"class":91},"\u002F\u002F __tests__\u002Fweather-card.test.tsx\n",[85,8471,8472,8474,8477,8479,8482],{"class":87,"line":95},[85,8473,404],{"class":98},[85,8475,8476],{"class":109}," { render, screen } ",[85,8478,410],{"class":98},[85,8480,8481],{"class":125}," '@testing-library\u002Freact'",[85,8483,416],{"class":109},[85,8485,8486,8488,8490,8492,8494],{"class":87,"line":113},[85,8487,404],{"class":98},[85,8489,2815],{"class":109},[85,8491,410],{"class":98},[85,8493,2820],{"class":125},[85,8495,416],{"class":109},[85,8497,8498],{"class":87,"line":119},[85,8499,449],{"emptyLinePlaceholder":327},[85,8501,8502,8504,8506,8509,8512,8514],{"class":87,"line":132},[85,8503,2975],{"class":138},[85,8505,476],{"class":109},[85,8507,8508],{"class":125},"'WeatherCard'",[85,8510,8511],{"class":109},", () ",[85,8513,214],{"class":98},[85,8515,110],{"class":109},[85,8517,8518,8521,8523,8526,8528,8530],{"class":87,"line":145},[85,8519,8520],{"class":138},"  test",[85,8522,476],{"class":109},[85,8524,8525],{"class":125},"'renders city, temperature, and conditions'",[85,8527,8511],{"class":109},[85,8529,214],{"class":98},[85,8531,110],{"class":109},[85,8533,8534,8537],{"class":87,"line":157},[85,8535,8536],{"class":138},"    render",[85,8538,5628],{"class":109},[85,8540,8541,8543,8545,8547,8549,8552,8555,8557,8559,8562,8564,8566,8568,8571,8574,8576,8578,8581],{"class":87,"line":181},[85,8542,2133],{"class":109},[85,8544,248],{"class":102},[85,8546,598],{"class":138},[85,8548,253],{"class":98},[85,8550,8551],{"class":125},"\"Paris\"",[85,8553,8554],{"class":138}," temperature",[85,8556,253],{"class":98},[85,8558,256],{"class":109},[85,8560,8561],{"class":102},"22",[85,8563,606],{"class":109},[85,8565,579],{"class":138},[85,8567,253],{"class":98},[85,8569,8570],{"class":125},"\"Partly cloudy\"",[85,8572,8573],{"class":138}," humidity",[85,8575,253],{"class":98},[85,8577,256],{"class":109},[85,8579,8580],{"class":102},"45",[85,8582,262],{"class":109},[85,8584,8585],{"class":87,"line":187},[85,8586,8587],{"class":109},"    );\n",[85,8589,8590,8593,8596,8599,8601,8604,8607,8610],{"class":87,"line":219},[85,8591,8592],{"class":138},"    expect",[85,8594,8595],{"class":109},"(screen.",[85,8597,8598],{"class":138},"getByText",[85,8600,476],{"class":109},[85,8602,8603],{"class":125},"'Paris'",[85,8605,8606],{"class":109},")).",[85,8608,8609],{"class":138},"toBeInTheDocument",[85,8611,3652],{"class":109},[85,8613,8614,8616,8618,8620,8622,8625,8627,8629],{"class":87,"line":239},[85,8615,8592],{"class":138},[85,8617,8595],{"class":109},[85,8619,8598],{"class":138},[85,8621,476],{"class":109},[85,8623,8624],{"class":125},"'22°C'",[85,8626,8606],{"class":109},[85,8628,8609],{"class":138},[85,8630,3652],{"class":109},[85,8632,8633,8635,8637,8639,8641,8644,8646,8648],{"class":87,"line":265},[85,8634,8592],{"class":138},[85,8636,8595],{"class":109},[85,8638,8598],{"class":138},[85,8640,476],{"class":109},[85,8642,8643],{"class":125},"'Partly cloudy'",[85,8645,8606],{"class":109},[85,8647,8609],{"class":138},[85,8649,3652],{"class":109},[85,8651,8652],{"class":87,"line":271},[85,8653,3327],{"class":109},[85,8655,8656],{"class":87,"line":277},[85,8657,449],{"emptyLinePlaceholder":327},[85,8659,8660,8662,8664,8667,8669,8671],{"class":87,"line":552},[85,8661,8520],{"class":138},[85,8663,476],{"class":109},[85,8665,8666],{"class":125},"'handles negative temperatures'",[85,8668,8511],{"class":109},[85,8670,214],{"class":98},[85,8672,110],{"class":109},[85,8674,8675,8677],{"class":87,"line":558},[85,8676,8536],{"class":138},[85,8678,5628],{"class":109},[85,8680,8681,8683,8685,8687,8689,8692,8694,8696,8698,8701,8704,8706,8708,8710,8713,8715,8717,8719,8722],{"class":87,"line":588},[85,8682,2133],{"class":109},[85,8684,248],{"class":102},[85,8686,598],{"class":138},[85,8688,253],{"class":98},[85,8690,8691],{"class":125},"\"Oslo\"",[85,8693,8554],{"class":138},[85,8695,253],{"class":98},[85,8697,256],{"class":109},[85,8699,8700],{"class":98},"-",[85,8702,8703],{"class":102},"8",[85,8705,606],{"class":109},[85,8707,579],{"class":138},[85,8709,253],{"class":98},[85,8711,8712],{"class":125},"\"Snow\"",[85,8714,8573],{"class":138},[85,8716,253],{"class":98},[85,8718,256],{"class":109},[85,8720,8721],{"class":102},"80",[85,8723,262],{"class":109},[85,8725,8726],{"class":87,"line":627},[85,8727,8587],{"class":109},[85,8729,8730,8732,8734,8736,8738,8741,8743,8745],{"class":87,"line":633},[85,8731,8592],{"class":138},[85,8733,8595],{"class":109},[85,8735,8598],{"class":138},[85,8737,476],{"class":109},[85,8739,8740],{"class":125},"'-8°C'",[85,8742,8606],{"class":109},[85,8744,8609],{"class":138},[85,8746,3652],{"class":109},[85,8748,8749],{"class":87,"line":638},[85,8750,3327],{"class":109},[85,8752,8753],{"class":87,"line":644},[85,8754,449],{"emptyLinePlaceholder":327},[85,8756,8757,8759,8761,8764,8766,8768],{"class":87,"line":654},[85,8758,8520],{"class":138},[85,8760,476],{"class":109},[85,8762,8763],{"class":125},"'renders humidity as percentage'",[85,8765,8511],{"class":109},[85,8767,214],{"class":98},[85,8769,110],{"class":109},[85,8771,8772,8774],{"class":87,"line":663},[85,8773,8536],{"class":138},[85,8775,5628],{"class":109},[85,8777,8778,8780,8782,8784,8786,8789,8791,8793,8795,8798,8800,8802,8804,8807,8809,8811,8813,8816],{"class":87,"line":695},[85,8779,2133],{"class":109},[85,8781,248],{"class":102},[85,8783,598],{"class":138},[85,8785,253],{"class":98},[85,8787,8788],{"class":125},"\"London\"",[85,8790,8554],{"class":138},[85,8792,253],{"class":98},[85,8794,256],{"class":109},[85,8796,8797],{"class":102},"15",[85,8799,606],{"class":109},[85,8801,579],{"class":138},[85,8803,253],{"class":98},[85,8805,8806],{"class":125},"\"Foggy\"",[85,8808,8573],{"class":138},[85,8810,253],{"class":98},[85,8812,256],{"class":109},[85,8814,8815],{"class":102},"92",[85,8817,262],{"class":109},[85,8819,8820],{"class":87,"line":700},[85,8821,8587],{"class":109},[85,8823,8824,8826,8828,8830,8832,8835,8839,8841,8843,8845],{"class":87,"line":719},[85,8825,8592],{"class":138},[85,8827,8595],{"class":109},[85,8829,8598],{"class":138},[85,8831,476],{"class":109},[85,8833,8834],{"class":125},"\u002F",[85,8836,8838],{"class":8837},"sA_wV","92%",[85,8840,8834],{"class":125},[85,8842,8606],{"class":109},[85,8844,8609],{"class":138},[85,8846,3652],{"class":109},[85,8848,8849],{"class":87,"line":737},[85,8850,3327],{"class":109},[85,8852,8853],{"class":87,"line":742},[85,8854,755],{"class":109},[17,8856,8857],{},"This layer should have high coverage — 80%+ branch coverage is realistic. Every component variation, edge case, and error state should have tests here. These tests are fast, deterministic, and give you confidence that the components themselves work correctly regardless of what the AI passes to them.",[12,8859,8861],{"id":8860},"layer-2-schema-validation-testing","Layer 2: Schema Validation Testing",[17,8863,8864],{},"Test that your Zod schemas accept valid inputs and reject invalid ones. This is the contract between your AI tool definitions and your components.",[75,8866,8868],{"className":77,"code":8867,"language":79,"meta":80,"style":80},"\u002F\u002F __tests__\u002Ftool-schemas.test.ts\nimport { tools } from '@\u002Flib\u002Fgenui-registry';\n\ndescribe('weatherCard schema', () => {\n  test('accepts valid parameters', () => {\n    const result = tools.showWeather.parameters.safeParse({\n      city: 'London',\n      temperature: 18,\n      conditions: 'Cloudy',\n      humidity: 65,\n    });\n    expect(result.success).toBe(true);\n  });\n\n  test('rejects non-numeric temperature', () => {\n    const result = tools.showWeather.parameters.safeParse({\n      city: 'London',\n      temperature: 'warm',\n      conditions: 'Cloudy',\n      humidity: 65,\n    });\n    expect(result.success).toBe(false);\n  });\n\n  test('rejects humidity outside 0-100 range', () => {\n    const result = tools.showWeather.parameters.safeParse({\n      city: 'London',\n      temperature: 18,\n      conditions: 'Cloudy',\n      humidity: 150,\n    });\n    expect(result.success).toBe(false);\n  });\n});\n\ndescribe('dataTable schema', () => {\n  test('accepts valid columns and rows', () => {\n    const result = tools.dataTable.parameters.safeParse({\n      columns: [\n        { key: 'name', label: 'Name' },\n        { key: 'value', label: 'Value', numeric: true },\n      ],\n      rows: [{ name: 'Alpha', value: '100' }],\n    });\n    expect(result.success).toBe(true);\n  });\n\n  test('accepts rows without numeric columns', () => {\n    const result = tools.dataTable.parameters.safeParse({\n      columns: [{ key: 'label', label: 'Category' }],\n      rows: [{ label: 'A' }, { label: 'B' }],\n    });\n    expect(result.success).toBe(true);\n  });\n});\n",[82,8869,8870,8875,8887,8891,8906,8921,8937,8947,8957,8967,8977,8982,8998,9002,9006,9021,9035,9043,9052,9060,9068,9072,9086,9090,9094,9109,9123,9131,9139,9147,9156,9160,9174,9178,9182,9186,9201,9216,9231,9236,9252,9271,9276,9293,9297,9311,9315,9319,9334,9348,9363,9379,9383,9397,9401],{"__ignoreMap":80},[85,8871,8872],{"class":87,"line":88},[85,8873,8874],{"class":91},"\u002F\u002F __tests__\u002Ftool-schemas.test.ts\n",[85,8876,8877,8879,8881,8883,8885],{"class":87,"line":95},[85,8878,404],{"class":98},[85,8880,5579],{"class":109},[85,8882,410],{"class":98},[85,8884,6004],{"class":125},[85,8886,416],{"class":109},[85,8888,8889],{"class":87,"line":113},[85,8890,449],{"emptyLinePlaceholder":327},[85,8892,8893,8895,8897,8900,8902,8904],{"class":87,"line":119},[85,8894,2975],{"class":138},[85,8896,476],{"class":109},[85,8898,8899],{"class":125},"'weatherCard schema'",[85,8901,8511],{"class":109},[85,8903,214],{"class":98},[85,8905,110],{"class":109},[85,8907,8908,8910,8912,8915,8917,8919],{"class":87,"line":132},[85,8909,8520],{"class":138},[85,8911,476],{"class":109},[85,8913,8914],{"class":125},"'accepts valid parameters'",[85,8916,8511],{"class":109},[85,8918,214],{"class":98},[85,8920,110],{"class":109},[85,8922,8923,8925,8927,8929,8932,8935],{"class":87,"line":145},[85,8924,3690],{"class":98},[85,8926,456],{"class":102},[85,8928,106],{"class":98},[85,8930,8931],{"class":109}," tools.showWeather.parameters.",[85,8933,8934],{"class":138},"safeParse",[85,8936,142],{"class":109},[85,8938,8939,8942,8945],{"class":87,"line":157},[85,8940,8941],{"class":109},"      city: ",[85,8943,8944],{"class":125},"'London'",[85,8946,129],{"class":109},[85,8948,8949,8952,8955],{"class":87,"line":181},[85,8950,8951],{"class":109},"      temperature: ",[85,8953,8954],{"class":102},"18",[85,8956,129],{"class":109},[85,8958,8959,8962,8965],{"class":87,"line":187},[85,8960,8961],{"class":109},"      conditions: ",[85,8963,8964],{"class":125},"'Cloudy'",[85,8966,129],{"class":109},[85,8968,8969,8972,8975],{"class":87,"line":219},[85,8970,8971],{"class":109},"      humidity: ",[85,8973,8974],{"class":102},"65",[85,8976,129],{"class":109},[85,8978,8979],{"class":87,"line":239},[85,8980,8981],{"class":109},"    });\n",[85,8983,8984,8986,8989,8992,8994,8996],{"class":87,"line":265},[85,8985,8592],{"class":138},[85,8987,8988],{"class":109},"(result.success).",[85,8990,8991],{"class":138},"toBe",[85,8993,476],{"class":109},[85,8995,3719],{"class":102},[85,8997,3529],{"class":109},[85,8999,9000],{"class":87,"line":271},[85,9001,3327],{"class":109},[85,9003,9004],{"class":87,"line":277},[85,9005,449],{"emptyLinePlaceholder":327},[85,9007,9008,9010,9012,9015,9017,9019],{"class":87,"line":552},[85,9009,8520],{"class":138},[85,9011,476],{"class":109},[85,9013,9014],{"class":125},"'rejects non-numeric temperature'",[85,9016,8511],{"class":109},[85,9018,214],{"class":98},[85,9020,110],{"class":109},[85,9022,9023,9025,9027,9029,9031,9033],{"class":87,"line":558},[85,9024,3690],{"class":98},[85,9026,456],{"class":102},[85,9028,106],{"class":98},[85,9030,8931],{"class":109},[85,9032,8934],{"class":138},[85,9034,142],{"class":109},[85,9036,9037,9039,9041],{"class":87,"line":588},[85,9038,8941],{"class":109},[85,9040,8944],{"class":125},[85,9042,129],{"class":109},[85,9044,9045,9047,9050],{"class":87,"line":627},[85,9046,8951],{"class":109},[85,9048,9049],{"class":125},"'warm'",[85,9051,129],{"class":109},[85,9053,9054,9056,9058],{"class":87,"line":633},[85,9055,8961],{"class":109},[85,9057,8964],{"class":125},[85,9059,129],{"class":109},[85,9061,9062,9064,9066],{"class":87,"line":638},[85,9063,8971],{"class":109},[85,9065,8974],{"class":102},[85,9067,129],{"class":109},[85,9069,9070],{"class":87,"line":644},[85,9071,8981],{"class":109},[85,9073,9074,9076,9078,9080,9082,9084],{"class":87,"line":654},[85,9075,8592],{"class":138},[85,9077,8988],{"class":109},[85,9079,8991],{"class":138},[85,9081,476],{"class":109},[85,9083,3609],{"class":102},[85,9085,3529],{"class":109},[85,9087,9088],{"class":87,"line":663},[85,9089,3327],{"class":109},[85,9091,9092],{"class":87,"line":695},[85,9093,449],{"emptyLinePlaceholder":327},[85,9095,9096,9098,9100,9103,9105,9107],{"class":87,"line":700},[85,9097,8520],{"class":138},[85,9099,476],{"class":109},[85,9101,9102],{"class":125},"'rejects humidity outside 0-100 range'",[85,9104,8511],{"class":109},[85,9106,214],{"class":98},[85,9108,110],{"class":109},[85,9110,9111,9113,9115,9117,9119,9121],{"class":87,"line":719},[85,9112,3690],{"class":98},[85,9114,456],{"class":102},[85,9116,106],{"class":98},[85,9118,8931],{"class":109},[85,9120,8934],{"class":138},[85,9122,142],{"class":109},[85,9124,9125,9127,9129],{"class":87,"line":737},[85,9126,8941],{"class":109},[85,9128,8944],{"class":125},[85,9130,129],{"class":109},[85,9132,9133,9135,9137],{"class":87,"line":742},[85,9134,8951],{"class":109},[85,9136,8954],{"class":102},[85,9138,129],{"class":109},[85,9140,9141,9143,9145],{"class":87,"line":747},[85,9142,8961],{"class":109},[85,9144,8964],{"class":125},[85,9146,129],{"class":109},[85,9148,9149,9151,9154],{"class":87,"line":752},[85,9150,8971],{"class":109},[85,9152,9153],{"class":102},"150",[85,9155,129],{"class":109},[85,9157,9158],{"class":87,"line":3110},[85,9159,8981],{"class":109},[85,9161,9162,9164,9166,9168,9170,9172],{"class":87,"line":3116},[85,9163,8592],{"class":138},[85,9165,8988],{"class":109},[85,9167,8991],{"class":138},[85,9169,476],{"class":109},[85,9171,3609],{"class":102},[85,9173,3529],{"class":109},[85,9175,9176],{"class":87,"line":3135},[85,9177,3327],{"class":109},[85,9179,9180],{"class":87,"line":3141},[85,9181,755],{"class":109},[85,9183,9184],{"class":87,"line":3146},[85,9185,449],{"emptyLinePlaceholder":327},[85,9187,9188,9190,9192,9195,9197,9199],{"class":87,"line":3152},[85,9189,2975],{"class":138},[85,9191,476],{"class":109},[85,9193,9194],{"class":125},"'dataTable schema'",[85,9196,8511],{"class":109},[85,9198,214],{"class":98},[85,9200,110],{"class":109},[85,9202,9203,9205,9207,9210,9212,9214],{"class":87,"line":3168},[85,9204,8520],{"class":138},[85,9206,476],{"class":109},[85,9208,9209],{"class":125},"'accepts valid columns and rows'",[85,9211,8511],{"class":109},[85,9213,214],{"class":98},[85,9215,110],{"class":109},[85,9217,9218,9220,9222,9224,9227,9229],{"class":87,"line":3177},[85,9219,3690],{"class":98},[85,9221,456],{"class":102},[85,9223,106],{"class":98},[85,9225,9226],{"class":109}," tools.dataTable.parameters.",[85,9228,8934],{"class":138},[85,9230,142],{"class":109},[85,9232,9233],{"class":87,"line":3196},[85,9234,9235],{"class":109},"      columns: [\n",[85,9237,9238,9241,9244,9247,9250],{"class":87,"line":3215},[85,9239,9240],{"class":109},"        { key: ",[85,9242,9243],{"class":125},"'name'",[85,9245,9246],{"class":109},", label: ",[85,9248,9249],{"class":125},"'Name'",[85,9251,7705],{"class":109},[85,9253,9254,9256,9259,9261,9264,9267,9269],{"class":87,"line":3234},[85,9255,9240],{"class":109},[85,9257,9258],{"class":125},"'value'",[85,9260,9246],{"class":109},[85,9262,9263],{"class":125},"'Value'",[85,9265,9266],{"class":109},", numeric: ",[85,9268,3719],{"class":102},[85,9270,7705],{"class":109},[85,9272,9273],{"class":87,"line":3253},[85,9274,9275],{"class":109},"      ],\n",[85,9277,9278,9281,9284,9287,9290],{"class":87,"line":3258},[85,9279,9280],{"class":109},"      rows: [{ name: ",[85,9282,9283],{"class":125},"'Alpha'",[85,9285,9286],{"class":109},", value: ",[85,9288,9289],{"class":125},"'100'",[85,9291,9292],{"class":109}," }],\n",[85,9294,9295],{"class":87,"line":3275},[85,9296,8981],{"class":109},[85,9298,9299,9301,9303,9305,9307,9309],{"class":87,"line":3293},[85,9300,8592],{"class":138},[85,9302,8988],{"class":109},[85,9304,8991],{"class":138},[85,9306,476],{"class":109},[85,9308,3719],{"class":102},[85,9310,3529],{"class":109},[85,9312,9313],{"class":87,"line":3309},[85,9314,3327],{"class":109},[85,9316,9317],{"class":87,"line":3314},[85,9318,449],{"emptyLinePlaceholder":327},[85,9320,9321,9323,9325,9328,9330,9332],{"class":87,"line":3319},[85,9322,8520],{"class":138},[85,9324,476],{"class":109},[85,9326,9327],{"class":125},"'accepts rows without numeric columns'",[85,9329,8511],{"class":109},[85,9331,214],{"class":98},[85,9333,110],{"class":109},[85,9335,9336,9338,9340,9342,9344,9346],{"class":87,"line":3324},[85,9337,3690],{"class":98},[85,9339,456],{"class":102},[85,9341,106],{"class":98},[85,9343,9226],{"class":109},[85,9345,8934],{"class":138},[85,9347,142],{"class":109},[85,9349,9350,9353,9356,9358,9361],{"class":87,"line":3330},[85,9351,9352],{"class":109},"      columns: [{ key: ",[85,9354,9355],{"class":125},"'label'",[85,9357,9246],{"class":109},[85,9359,9360],{"class":125},"'Category'",[85,9362,9292],{"class":109},[85,9364,9365,9368,9371,9374,9377],{"class":87,"line":3335},[85,9366,9367],{"class":109},"      rows: [{ label: ",[85,9369,9370],{"class":125},"'A'",[85,9372,9373],{"class":109}," }, { label: ",[85,9375,9376],{"class":125},"'B'",[85,9378,9292],{"class":109},[85,9380,9381],{"class":87,"line":3343},[85,9382,8981],{"class":109},[85,9384,9385,9387,9389,9391,9393,9395],{"class":87,"line":3995},[85,9386,8592],{"class":138},[85,9388,8988],{"class":109},[85,9390,8991],{"class":138},[85,9392,476],{"class":109},[85,9394,3719],{"class":102},[85,9396,3529],{"class":109},[85,9398,9399],{"class":87,"line":4021},[85,9400,3327],{"class":109},[85,9402,9403],{"class":87,"line":4029},[85,9404,755],{"class":109},[17,9406,9407],{},"This layer is fast and free (no AI inference). It runs on every commit and catches the most common class of GenUI bugs: the AI generating a parameter value of the wrong type.",[12,9409,9411],{"id":9410},"layer-3-ai-output-validation-property-based","Layer 3: AI Output Validation (Property-Based)",[17,9413,9414],{},"When you do run the AI in tests, assert structural properties rather than exact content. This is the key insight that makes Generative UI testable.",[17,9416,9417],{},"You do not care whether the AI says Paris is 22° or 23°. You care that:",[33,9419,9420,9423,9426],{},[36,9421,9422],{},"The AI called at least one tool",[36,9424,9425],{},"The tool called is in your registry",[36,9427,9428],{},"The parameters passed to that tool are valid per its schema",[75,9430,9432],{"className":77,"code":9431,"language":79,"meta":80,"style":80},"\u002F\u002F __tests__\u002Fgenui-integration.test.ts\nimport { generateDashboard } from '@\u002Flib\u002Fstream-with-tools';\nimport { tools } from '@\u002Flib\u002Fgenui-registry';\n\n\u002F\u002F These tests hit the real AI — run nightly, not on every PR\ndescribe('generateDashboard integration', () => {\n  test('responds to weather query with showWeather tool', async () => {\n    const result = await generateDashboard('What is the weather in Paris?');\n\n    \u002F\u002F Assert structural properties\n    expect(result.toolCalls.length).toBeGreaterThan(0);\n\n    \u002F\u002F Every tool call should be in our registry\n    const unknownTools = result.toolCalls.filter(\n      call => !Object.keys(tools).includes(call.toolName)\n    );\n    expect(unknownTools).toHaveLength(0);\n\n    \u002F\u002F Validate each tool call's parameters against its schema\n    for (const call of result.toolCalls) {\n      const tool = tools[call.toolName as keyof typeof tools];\n      const validation = tool.parameters.safeParse(call.parameters);\n      expect(validation.success).toBe(true,\n        `Tool ${call.toolName} received invalid parameters: ${JSON.stringify(call.parameters)}`\n      );\n    }\n  });\n\n  test('responds to multi-part query with multiple tools', async () => {\n    const result = await generateDashboard(\n      'Show me the weather in London and New York'\n    );\n\n    \u002F\u002F Expect multiple tool calls for a multi-part question\n    expect(result.toolCalls.length).toBeGreaterThanOrEqual(2);\n  });\n\n  test('responds to stock query without weather tool', async () => {\n    const result = await generateDashboard('Show me Apple stock price');\n    const toolNames = result.toolCalls.map(c => c.toolName);\n\n    \u002F\u002F Should not use weather tool for a stock query\n    expect(toolNames).not.toContain('showWeather');\n  });\n});\n",[82,9433,9434,9439,9453,9465,9469,9474,9489,9508,9527,9531,9536,9557,9561,9566,9583,9607,9611,9627,9631,9636,9654,9676,9693,9709,9748,9752,9756,9760,9764,9783,9797,9802,9806,9810,9815,9834,9838,9842,9861,9880,9903,9907,9912,9929,9933],{"__ignoreMap":80},[85,9435,9436],{"class":87,"line":88},[85,9437,9438],{"class":91},"\u002F\u002F __tests__\u002Fgenui-integration.test.ts\n",[85,9440,9441,9443,9446,9448,9451],{"class":87,"line":95},[85,9442,404],{"class":98},[85,9444,9445],{"class":109}," { generateDashboard } ",[85,9447,410],{"class":98},[85,9449,9450],{"class":125}," '@\u002Flib\u002Fstream-with-tools'",[85,9452,416],{"class":109},[85,9454,9455,9457,9459,9461,9463],{"class":87,"line":113},[85,9456,404],{"class":98},[85,9458,5579],{"class":109},[85,9460,410],{"class":98},[85,9462,6004],{"class":125},[85,9464,416],{"class":109},[85,9466,9467],{"class":87,"line":119},[85,9468,449],{"emptyLinePlaceholder":327},[85,9470,9471],{"class":87,"line":132},[85,9472,9473],{"class":91},"\u002F\u002F These tests hit the real AI — run nightly, not on every PR\n",[85,9475,9476,9478,9480,9483,9485,9487],{"class":87,"line":145},[85,9477,2975],{"class":138},[85,9479,476],{"class":109},[85,9481,9482],{"class":125},"'generateDashboard integration'",[85,9484,8511],{"class":109},[85,9486,214],{"class":98},[85,9488,110],{"class":109},[85,9490,9491,9493,9495,9498,9500,9502,9504,9506],{"class":87,"line":157},[85,9492,8520],{"class":138},[85,9494,476],{"class":109},[85,9496,9497],{"class":125},"'responds to weather query with showWeather tool'",[85,9499,172],{"class":109},[85,9501,196],{"class":98},[85,9503,6393],{"class":109},[85,9505,214],{"class":98},[85,9507,110],{"class":109},[85,9509,9510,9512,9514,9516,9518,9520,9522,9525],{"class":87,"line":181},[85,9511,3690],{"class":98},[85,9513,456],{"class":102},[85,9515,106],{"class":98},[85,9517,230],{"class":98},[85,9519,5773],{"class":138},[85,9521,476],{"class":109},[85,9523,9524],{"class":125},"'What is the weather in Paris?'",[85,9526,3529],{"class":109},[85,9528,9529],{"class":87,"line":187},[85,9530,449],{"emptyLinePlaceholder":327},[85,9532,9533],{"class":87,"line":219},[85,9534,9535],{"class":91},"    \u002F\u002F Assert structural properties\n",[85,9537,9538,9540,9543,9546,9548,9551,9553,9555],{"class":87,"line":239},[85,9539,8592],{"class":138},[85,9541,9542],{"class":109},"(result.toolCalls.",[85,9544,9545],{"class":102},"length",[85,9547,3038],{"class":109},[85,9549,9550],{"class":138},"toBeGreaterThan",[85,9552,476],{"class":109},[85,9554,3035],{"class":102},[85,9556,3529],{"class":109},[85,9558,9559],{"class":87,"line":265},[85,9560,449],{"emptyLinePlaceholder":327},[85,9562,9563],{"class":87,"line":271},[85,9564,9565],{"class":91},"    \u002F\u002F Every tool call should be in our registry\n",[85,9567,9568,9570,9573,9575,9578,9581],{"class":87,"line":277},[85,9569,3690],{"class":98},[85,9571,9572],{"class":102}," unknownTools",[85,9574,106],{"class":98},[85,9576,9577],{"class":109}," result.toolCalls.",[85,9579,9580],{"class":138},"filter",[85,9582,5628],{"class":109},[85,9584,9585,9588,9590,9592,9595,9598,9601,9604],{"class":87,"line":552},[85,9586,9587],{"class":202},"      call",[85,9589,3754],{"class":98},[85,9591,4118],{"class":98},[85,9593,9594],{"class":109},"Object.",[85,9596,9597],{"class":138},"keys",[85,9599,9600],{"class":109},"(tools).",[85,9602,9603],{"class":138},"includes",[85,9605,9606],{"class":109},"(call.toolName)\n",[85,9608,9609],{"class":87,"line":558},[85,9610,8587],{"class":109},[85,9612,9613,9615,9618,9621,9623,9625],{"class":87,"line":588},[85,9614,8592],{"class":138},[85,9616,9617],{"class":109},"(unknownTools).",[85,9619,9620],{"class":138},"toHaveLength",[85,9622,476],{"class":109},[85,9624,3035],{"class":102},[85,9626,3529],{"class":109},[85,9628,9629],{"class":87,"line":627},[85,9630,449],{"emptyLinePlaceholder":327},[85,9632,9633],{"class":87,"line":633},[85,9634,9635],{"class":91},"    \u002F\u002F Validate each tool call's parameters against its schema\n",[85,9637,9638,9641,9643,9645,9648,9651],{"class":87,"line":638},[85,9639,9640],{"class":98},"    for",[85,9642,3077],{"class":109},[85,9644,99],{"class":98},[85,9646,9647],{"class":102}," call",[85,9649,9650],{"class":98}," of",[85,9652,9653],{"class":109}," result.toolCalls) {\n",[85,9655,9656,9658,9661,9663,9666,9669,9671,9673],{"class":87,"line":644},[85,9657,222],{"class":98},[85,9659,9660],{"class":102}," tool",[85,9662,106],{"class":98},[85,9664,9665],{"class":109}," tools[call.toolName ",[85,9667,9668],{"class":98},"as",[85,9670,5503],{"class":98},[85,9672,5506],{"class":98},[85,9674,9675],{"class":109}," tools];\n",[85,9677,9678,9680,9683,9685,9688,9690],{"class":87,"line":654},[85,9679,222],{"class":98},[85,9681,9682],{"class":102}," validation",[85,9684,106],{"class":98},[85,9686,9687],{"class":109}," tool.parameters.",[85,9689,8934],{"class":138},[85,9691,9692],{"class":109},"(call.parameters);\n",[85,9694,9695,9698,9701,9703,9705,9707],{"class":87,"line":663},[85,9696,9697],{"class":138},"      expect",[85,9699,9700],{"class":109},"(validation.success).",[85,9702,8991],{"class":138},[85,9704,476],{"class":109},[85,9706,3719],{"class":102},[85,9708,129],{"class":109},[85,9710,9711,9714,9717,9719,9722,9725,9728,9730,9733,9735,9737,9739,9742,9745],{"class":87,"line":695},[85,9712,9713],{"class":125},"        `Tool ${",[85,9715,9716],{"class":109},"call",[85,9718,3578],{"class":125},[85,9720,9721],{"class":109},"toolName",[85,9723,9724],{"class":125},"} received invalid parameters: ${",[85,9726,9727],{"class":102},"JSON",[85,9729,3578],{"class":125},[85,9731,9732],{"class":138},"stringify",[85,9734,476],{"class":125},[85,9736,9716],{"class":109},[85,9738,3578],{"class":125},[85,9740,9741],{"class":109},"parameters",[85,9743,9744],{"class":125},")",[85,9746,9747],{"class":125},"}`\n",[85,9749,9750],{"class":87,"line":700},[85,9751,4731],{"class":109},[85,9753,9754],{"class":87,"line":719},[85,9755,4736],{"class":109},[85,9757,9758],{"class":87,"line":737},[85,9759,3327],{"class":109},[85,9761,9762],{"class":87,"line":742},[85,9763,449],{"emptyLinePlaceholder":327},[85,9765,9766,9768,9770,9773,9775,9777,9779,9781],{"class":87,"line":747},[85,9767,8520],{"class":138},[85,9769,476],{"class":109},[85,9771,9772],{"class":125},"'responds to multi-part query with multiple tools'",[85,9774,172],{"class":109},[85,9776,196],{"class":98},[85,9778,6393],{"class":109},[85,9780,214],{"class":98},[85,9782,110],{"class":109},[85,9784,9785,9787,9789,9791,9793,9795],{"class":87,"line":752},[85,9786,3690],{"class":98},[85,9788,456],{"class":102},[85,9790,106],{"class":98},[85,9792,230],{"class":98},[85,9794,5773],{"class":138},[85,9796,5628],{"class":109},[85,9798,9799],{"class":87,"line":3110},[85,9800,9801],{"class":125},"      'Show me the weather in London and New York'\n",[85,9803,9804],{"class":87,"line":3116},[85,9805,8587],{"class":109},[85,9807,9808],{"class":87,"line":3135},[85,9809,449],{"emptyLinePlaceholder":327},[85,9811,9812],{"class":87,"line":3141},[85,9813,9814],{"class":91},"    \u002F\u002F Expect multiple tool calls for a multi-part question\n",[85,9816,9817,9819,9821,9823,9825,9828,9830,9832],{"class":87,"line":3146},[85,9818,8592],{"class":138},[85,9820,9542],{"class":109},[85,9822,9545],{"class":102},[85,9824,3038],{"class":109},[85,9826,9827],{"class":138},"toBeGreaterThanOrEqual",[85,9829,476],{"class":109},[85,9831,2533],{"class":102},[85,9833,3529],{"class":109},[85,9835,9836],{"class":87,"line":3152},[85,9837,3327],{"class":109},[85,9839,9840],{"class":87,"line":3168},[85,9841,449],{"emptyLinePlaceholder":327},[85,9843,9844,9846,9848,9851,9853,9855,9857,9859],{"class":87,"line":3177},[85,9845,8520],{"class":138},[85,9847,476],{"class":109},[85,9849,9850],{"class":125},"'responds to stock query without weather tool'",[85,9852,172],{"class":109},[85,9854,196],{"class":98},[85,9856,6393],{"class":109},[85,9858,214],{"class":98},[85,9860,110],{"class":109},[85,9862,9863,9865,9867,9869,9871,9873,9875,9878],{"class":87,"line":3196},[85,9864,3690],{"class":98},[85,9866,456],{"class":102},[85,9868,106],{"class":98},[85,9870,230],{"class":98},[85,9872,5773],{"class":138},[85,9874,476],{"class":109},[85,9876,9877],{"class":125},"'Show me Apple stock price'",[85,9879,3529],{"class":109},[85,9881,9882,9884,9887,9889,9891,9893,9895,9898,9900],{"class":87,"line":3215},[85,9883,3690],{"class":98},[85,9885,9886],{"class":102}," toolNames",[85,9888,106],{"class":98},[85,9890,9577],{"class":109},[85,9892,3892],{"class":138},[85,9894,476],{"class":109},[85,9896,9897],{"class":202},"c",[85,9899,3754],{"class":98},[85,9901,9902],{"class":109}," c.toolName);\n",[85,9904,9905],{"class":87,"line":3234},[85,9906,449],{"emptyLinePlaceholder":327},[85,9908,9909],{"class":87,"line":3253},[85,9910,9911],{"class":91},"    \u002F\u002F Should not use weather tool for a stock query\n",[85,9913,9914,9916,9919,9922,9924,9927],{"class":87,"line":3258},[85,9915,8592],{"class":138},[85,9917,9918],{"class":109},"(toolNames).not.",[85,9920,9921],{"class":138},"toContain",[85,9923,476],{"class":109},[85,9925,9926],{"class":125},"'showWeather'",[85,9928,3529],{"class":109},[85,9930,9931],{"class":87,"line":3275},[85,9932,3327],{"class":109},[85,9934,9935],{"class":87,"line":3293},[85,9936,755],{"class":109},[12,9938,9940],{"id":9939},"layer-4-mocked-ai-for-ci-speed","Layer 4: Mocked AI for CI Speed",[17,9942,9943],{},"For tests that need to validate the rendering pipeline without live AI inference, mock the AI to return deterministic tool call sequences:",[75,9945,9947],{"className":77,"code":9946,"language":79,"meta":80,"style":80},"\u002F\u002F __tests__\u002Frendering-pipeline.test.tsx\nimport { render } from '@testing-library\u002Freact';\nimport { mockStreamUI } from '@\u002Ftest\u002Fmocks\u002Fstream-ui';\n\n\u002F\u002F Mock the streamUI function to return a deterministic sequence\njest.mock('ai\u002Frsc', () => ({\n  streamUI: jest.fn(),\n}));\n\nimport { streamUI } from 'ai\u002Frsc';\n\nbeforeEach(() => {\n  (streamUI as jest.Mock).mockResolvedValue({\n    value: (\n      \u003C>\n        \u003CMockWeatherCard city=\"Paris\" temperature={22} conditions=\"Sunny\" humidity={40} \u002F>\n        \u003CMockStockTicker symbol=\"AAPL\" price={189.50} change={2.30} changePercent={1.23} \u002F>\n      \u003C\u002F>\n    ),\n    toolCalls: [\n      { toolName: 'showWeather', parameters: { city: 'Paris', temperature: 22, conditions: 'Sunny', humidity: 40 } },\n      { toolName: 'showStock', parameters: { symbol: 'AAPL', price: 189.50, change: 2.30, changePercent: 1.23 } },\n    ],\n  });\n});\n\ntest('renders AI output in the page', async () => {\n  render(\u003CDemoPage \u002F>);\n  await userEvent.type(screen.getByPlaceholderText(\u002Fask anything\u002Fi), 'Show me weather and Apple stock');\n  await userEvent.click(screen.getByRole('button', { name: \u002Fask\u002Fi }));\n\n  await waitFor(() => {\n    expect(screen.getByText('Paris')).toBeInTheDocument();\n    expect(screen.getByText('AAPL')).toBeInTheDocument();\n  });\n});\n",[82,9948,9949,9954,9966,9980,9984,9989,10009,10019,10024,10028,10040,10044,10056,10078,10086,10091,10132,10178,10183,10188,10193,10224,10254,10258,10262,10266,10270,10290,10303,10338,10373,10377,10390,10408,10426,10430],{"__ignoreMap":80},[85,9950,9951],{"class":87,"line":88},[85,9952,9953],{"class":91},"\u002F\u002F __tests__\u002Frendering-pipeline.test.tsx\n",[85,9955,9956,9958,9960,9962,9964],{"class":87,"line":95},[85,9957,404],{"class":98},[85,9959,8186],{"class":109},[85,9961,410],{"class":98},[85,9963,8481],{"class":125},[85,9965,416],{"class":109},[85,9967,9968,9970,9973,9975,9978],{"class":87,"line":113},[85,9969,404],{"class":98},[85,9971,9972],{"class":109}," { mockStreamUI } ",[85,9974,410],{"class":98},[85,9976,9977],{"class":125}," '@\u002Ftest\u002Fmocks\u002Fstream-ui'",[85,9979,416],{"class":109},[85,9981,9982],{"class":87,"line":119},[85,9983,449],{"emptyLinePlaceholder":327},[85,9985,9986],{"class":87,"line":132},[85,9987,9988],{"class":91},"\u002F\u002F Mock the streamUI function to return a deterministic sequence\n",[85,9990,9991,9994,9997,9999,10002,10004,10006],{"class":87,"line":145},[85,9992,9993],{"class":109},"jest.",[85,9995,9996],{"class":138},"mock",[85,9998,476],{"class":109},[85,10000,10001],{"class":125},"'ai\u002Frsc'",[85,10003,8511],{"class":109},[85,10005,214],{"class":98},[85,10007,10008],{"class":109}," ({\n",[85,10010,10011,10014,10017],{"class":87,"line":157},[85,10012,10013],{"class":109},"  streamUI: jest.",[85,10015,10016],{"class":138},"fn",[85,10018,154],{"class":109},[85,10020,10021],{"class":87,"line":181},[85,10022,10023],{"class":109},"}));\n",[85,10025,10026],{"class":87,"line":187},[85,10027,449],{"emptyLinePlaceholder":327},[85,10029,10030,10032,10034,10036,10038],{"class":87,"line":219},[85,10031,404],{"class":98},[85,10033,407],{"class":109},[85,10035,410],{"class":98},[85,10037,413],{"class":125},[85,10039,416],{"class":109},[85,10041,10042],{"class":87,"line":239},[85,10043,449],{"emptyLinePlaceholder":327},[85,10045,10046,10049,10052,10054],{"class":87,"line":265},[85,10047,10048],{"class":138},"beforeEach",[85,10050,10051],{"class":109},"(() ",[85,10053,214],{"class":98},[85,10055,110],{"class":109},[85,10057,10058,10061,10063,10066,10068,10071,10073,10076],{"class":87,"line":271},[85,10059,10060],{"class":109},"  (streamUI ",[85,10062,9668],{"class":98},[85,10064,10065],{"class":138}," jest",[85,10067,3578],{"class":109},[85,10069,10070],{"class":138},"Mock",[85,10072,3038],{"class":109},[85,10074,10075],{"class":138},"mockResolvedValue",[85,10077,142],{"class":109},[85,10079,10080,10083],{"class":87,"line":277},[85,10081,10082],{"class":138},"    value",[85,10084,10085],{"class":109},": (\n",[85,10087,10088],{"class":87,"line":552},[85,10089,10090],{"class":98},"      \u003C>\n",[85,10092,10093,10095,10098,10100,10102,10104,10106,10108,10110,10113,10115,10118,10120,10122,10124,10127,10129],{"class":87,"line":558},[85,10094,2169],{"class":98},[85,10096,10097],{"class":109},"MockWeatherCard city",[85,10099,253],{"class":98},[85,10101,8551],{"class":125},[85,10103,8554],{"class":109},[85,10105,253],{"class":98},[85,10107,256],{"class":109},[85,10109,8561],{"class":102},[85,10111,10112],{"class":109},"} conditions",[85,10114,253],{"class":98},[85,10116,10117],{"class":125},"\"Sunny\"",[85,10119,8573],{"class":109},[85,10121,253],{"class":98},[85,10123,256],{"class":109},[85,10125,10126],{"class":102},"40",[85,10128,606],{"class":109},[85,10130,10131],{"class":98},"\u002F>\n",[85,10133,10134,10136,10139,10141,10144,10147,10149,10151,10154,10157,10159,10161,10164,10167,10169,10171,10174,10176],{"class":87,"line":588},[85,10135,2169],{"class":98},[85,10137,10138],{"class":109},"MockStockTicker symbol",[85,10140,253],{"class":98},[85,10142,10143],{"class":125},"\"AAPL\"",[85,10145,10146],{"class":109}," price",[85,10148,253],{"class":98},[85,10150,256],{"class":109},[85,10152,10153],{"class":102},"189.50",[85,10155,10156],{"class":109},"} change",[85,10158,253],{"class":98},[85,10160,256],{"class":109},[85,10162,10163],{"class":102},"2.30",[85,10165,10166],{"class":109},"} changePercent",[85,10168,253],{"class":98},[85,10170,256],{"class":109},[85,10172,10173],{"class":102},"1.23",[85,10175,606],{"class":109},[85,10177,10131],{"class":98},[85,10179,10180],{"class":87,"line":627},[85,10181,10182],{"class":98},"      \u003C\u002F>\n",[85,10184,10185],{"class":87,"line":633},[85,10186,10187],{"class":109},"    ),\n",[85,10189,10190],{"class":87,"line":638},[85,10191,10192],{"class":109},"    toolCalls: [\n",[85,10194,10195,10198,10200,10203,10205,10208,10210,10213,10216,10219,10221],{"class":87,"line":644},[85,10196,10197],{"class":109},"      { toolName: ",[85,10199,9926],{"class":125},[85,10201,10202],{"class":109},", parameters: { city: ",[85,10204,8603],{"class":125},[85,10206,10207],{"class":109},", temperature: ",[85,10209,8561],{"class":102},[85,10211,10212],{"class":109},", conditions: ",[85,10214,10215],{"class":125},"'Sunny'",[85,10217,10218],{"class":109},", humidity: ",[85,10220,10126],{"class":102},[85,10222,10223],{"class":109}," } },\n",[85,10225,10226,10228,10231,10234,10237,10240,10242,10245,10247,10250,10252],{"class":87,"line":654},[85,10227,10197],{"class":109},[85,10229,10230],{"class":125},"'showStock'",[85,10232,10233],{"class":109},", parameters: { symbol: ",[85,10235,10236],{"class":125},"'AAPL'",[85,10238,10239],{"class":109},", price: ",[85,10241,10153],{"class":102},[85,10243,10244],{"class":109},", change: ",[85,10246,10163],{"class":102},[85,10248,10249],{"class":109},", changePercent: ",[85,10251,10173],{"class":102},[85,10253,10223],{"class":109},[85,10255,10256],{"class":87,"line":663},[85,10257,7723],{"class":109},[85,10259,10260],{"class":87,"line":695},[85,10261,3327],{"class":109},[85,10263,10264],{"class":87,"line":700},[85,10265,755],{"class":109},[85,10267,10268],{"class":87,"line":719},[85,10269,449],{"emptyLinePlaceholder":327},[85,10271,10272,10275,10277,10280,10282,10284,10286,10288],{"class":87,"line":737},[85,10273,10274],{"class":138},"test",[85,10276,476],{"class":109},[85,10278,10279],{"class":125},"'renders AI output in the page'",[85,10281,172],{"class":109},[85,10283,196],{"class":98},[85,10285,6393],{"class":109},[85,10287,214],{"class":98},[85,10289,110],{"class":109},[85,10291,10292,10294,10297,10300],{"class":87,"line":742},[85,10293,4655],{"class":138},[85,10295,10296],{"class":109},"(\u003C",[85,10298,10299],{"class":138},"DemoPage",[85,10301,10302],{"class":109}," \u002F>);\n",[85,10304,10305,10308,10311,10314,10316,10319,10321,10323,10326,10328,10330,10333,10336],{"class":87,"line":747},[85,10306,10307],{"class":98},"  await",[85,10309,10310],{"class":109}," userEvent.",[85,10312,10313],{"class":138},"type",[85,10315,8595],{"class":109},[85,10317,10318],{"class":138},"getByPlaceholderText",[85,10320,476],{"class":109},[85,10322,8834],{"class":125},[85,10324,10325],{"class":8837},"ask anything",[85,10327,8834],{"class":125},[85,10329,4227],{"class":98},[85,10331,10332],{"class":109},"), ",[85,10334,10335],{"class":125},"'Show me weather and Apple stock'",[85,10337,3529],{"class":109},[85,10339,10340,10342,10344,10347,10349,10352,10354,10357,10360,10363,10366,10368,10370],{"class":87,"line":752},[85,10341,10307],{"class":98},[85,10343,10310],{"class":109},[85,10345,10346],{"class":138},"click",[85,10348,8595],{"class":109},[85,10350,10351],{"class":138},"getByRole",[85,10353,476],{"class":109},[85,10355,10356],{"class":125},"'button'",[85,10358,10359],{"class":109},", { name:",[85,10361,10362],{"class":125}," \u002F",[85,10364,10365],{"class":8837},"ask",[85,10367,8834],{"class":125},[85,10369,4227],{"class":98},[85,10371,10372],{"class":109}," }));\n",[85,10374,10375],{"class":87,"line":3110},[85,10376,449],{"emptyLinePlaceholder":327},[85,10378,10379,10381,10384,10386,10388],{"class":87,"line":3116},[85,10380,10307],{"class":98},[85,10382,10383],{"class":138}," waitFor",[85,10385,10051],{"class":109},[85,10387,214],{"class":98},[85,10389,110],{"class":109},[85,10391,10392,10394,10396,10398,10400,10402,10404,10406],{"class":87,"line":3135},[85,10393,8592],{"class":138},[85,10395,8595],{"class":109},[85,10397,8598],{"class":138},[85,10399,476],{"class":109},[85,10401,8603],{"class":125},[85,10403,8606],{"class":109},[85,10405,8609],{"class":138},[85,10407,3652],{"class":109},[85,10409,10410,10412,10414,10416,10418,10420,10422,10424],{"class":87,"line":3141},[85,10411,8592],{"class":138},[85,10413,8595],{"class":109},[85,10415,8598],{"class":138},[85,10417,476],{"class":109},[85,10419,10236],{"class":125},[85,10421,8606],{"class":109},[85,10423,8609],{"class":138},[85,10425,3652],{"class":109},[85,10427,10428],{"class":87,"line":3146},[85,10429,3327],{"class":109},[85,10431,10432],{"class":87,"line":3152},[85,10433,755],{"class":109},[17,10435,10436],{},"With mocked AI, you test the rendering pipeline, error boundaries, loading states, and UI interactions without any inference cost or latency.",[12,10438,10440],{"id":10439},"the-ci-budget-problem","The CI Budget Problem",[17,10442,10443],{},"Real AI inference in test suites is expensive and slow. Left unmanaged, it will blow your CI budget and slow your pipeline to a crawl.",[17,10445,10446],{},[39,10447,10448],{},"Practical CI setup:",[75,10450,10454],{"className":10451,"code":10452,"language":10453,"meta":80,"style":80},"language-yaml shiki shiki-themes github-light github-dark","# .github\u002Fworkflows\u002Fci.yml\nname: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - run: npm ci\n      # Fast, deterministic — runs on every PR\n      - run: npm run test:unit\n        name: Unit tests (components + schemas)\n      # Rendering pipeline with mocked AI — runs on every PR\n      - run: npm run test:integration:mocked\n        name: Integration tests (mocked AI)\n\n  test-ai:\n    # Only runs on main branch push, not on every PR\n    if: github.ref == 'refs\u002Fheads\u002Fmain'\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - run: npm ci\n      - run: npm run test:integration:ai\n        env:\n          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n        name: Integration tests (real AI)\n","yaml",[82,10455,10456,10461,10470,10474,10482,10489,10502,10509,10513,10520,10526,10536,10543,10556,10568,10573,10584,10594,10599,10610,10619,10623,10630,10635,10644,10652,10658,10668,10678,10689,10696,10706],{"__ignoreMap":80},[85,10457,10458],{"class":87,"line":88},[85,10459,10460],{"class":91},"# .github\u002Fworkflows\u002Fci.yml\n",[85,10462,10463,10465,10467],{"class":87,"line":95},[85,10464,5640],{"class":2116},[85,10466,193],{"class":109},[85,10468,10469],{"class":125},"CI\n",[85,10471,10472],{"class":87,"line":113},[85,10473,449],{"emptyLinePlaceholder":327},[85,10475,10476,10479],{"class":87,"line":119},[85,10477,10478],{"class":102},"on",[85,10480,10481],{"class":109},":\n",[85,10483,10484,10487],{"class":87,"line":132},[85,10485,10486],{"class":2116},"  push",[85,10488,10481],{"class":109},[85,10490,10491,10494,10497,10499],{"class":87,"line":145},[85,10492,10493],{"class":2116},"    branches",[85,10495,10496],{"class":109},": [",[85,10498,3793],{"class":125},[85,10500,10501],{"class":109},"]\n",[85,10503,10504,10507],{"class":87,"line":157},[85,10505,10506],{"class":2116},"  pull_request",[85,10508,10481],{"class":109},[85,10510,10511],{"class":87,"line":181},[85,10512,449],{"emptyLinePlaceholder":327},[85,10514,10515,10518],{"class":87,"line":187},[85,10516,10517],{"class":2116},"jobs",[85,10519,10481],{"class":109},[85,10521,10522,10524],{"class":87,"line":219},[85,10523,8520],{"class":2116},[85,10525,10481],{"class":109},[85,10527,10528,10531,10533],{"class":87,"line":239},[85,10529,10530],{"class":2116},"    runs-on",[85,10532,193],{"class":109},[85,10534,10535],{"class":125},"ubuntu-latest\n",[85,10537,10538,10541],{"class":87,"line":265},[85,10539,10540],{"class":2116},"    steps",[85,10542,10481],{"class":109},[85,10544,10545,10548,10551,10553],{"class":87,"line":271},[85,10546,10547],{"class":109},"      - ",[85,10549,10550],{"class":2116},"uses",[85,10552,193],{"class":109},[85,10554,10555],{"class":125},"actions\u002Fcheckout@v4\n",[85,10557,10558,10560,10563,10565],{"class":87,"line":277},[85,10559,10547],{"class":109},[85,10561,10562],{"class":2116},"run",[85,10564,193],{"class":109},[85,10566,10567],{"class":125},"npm ci\n",[85,10569,10570],{"class":87,"line":552},[85,10571,10572],{"class":91},"      # Fast, deterministic — runs on every PR\n",[85,10574,10575,10577,10579,10581],{"class":87,"line":558},[85,10576,10547],{"class":109},[85,10578,10562],{"class":2116},[85,10580,193],{"class":109},[85,10582,10583],{"class":125},"npm run test:unit\n",[85,10585,10586,10589,10591],{"class":87,"line":588},[85,10587,10588],{"class":2116},"        name",[85,10590,193],{"class":109},[85,10592,10593],{"class":125},"Unit tests (components + schemas)\n",[85,10595,10596],{"class":87,"line":627},[85,10597,10598],{"class":91},"      # Rendering pipeline with mocked AI — runs on every PR\n",[85,10600,10601,10603,10605,10607],{"class":87,"line":633},[85,10602,10547],{"class":109},[85,10604,10562],{"class":2116},[85,10606,193],{"class":109},[85,10608,10609],{"class":125},"npm run test:integration:mocked\n",[85,10611,10612,10614,10616],{"class":87,"line":638},[85,10613,10588],{"class":2116},[85,10615,193],{"class":109},[85,10617,10618],{"class":125},"Integration tests (mocked AI)\n",[85,10620,10621],{"class":87,"line":644},[85,10622,449],{"emptyLinePlaceholder":327},[85,10624,10625,10628],{"class":87,"line":654},[85,10626,10627],{"class":2116},"  test-ai",[85,10629,10481],{"class":109},[85,10631,10632],{"class":87,"line":663},[85,10633,10634],{"class":91},"    # Only runs on main branch push, not on every PR\n",[85,10636,10637,10639,10641],{"class":87,"line":695},[85,10638,3657],{"class":2116},[85,10640,193],{"class":109},[85,10642,10643],{"class":125},"github.ref == 'refs\u002Fheads\u002Fmain'\n",[85,10645,10646,10648,10650],{"class":87,"line":700},[85,10647,10530],{"class":2116},[85,10649,193],{"class":109},[85,10651,10535],{"class":125},[85,10653,10654,10656],{"class":87,"line":719},[85,10655,10540],{"class":2116},[85,10657,10481],{"class":109},[85,10659,10660,10662,10664,10666],{"class":87,"line":737},[85,10661,10547],{"class":109},[85,10663,10550],{"class":2116},[85,10665,193],{"class":109},[85,10667,10555],{"class":125},[85,10669,10670,10672,10674,10676],{"class":87,"line":742},[85,10671,10547],{"class":109},[85,10673,10562],{"class":2116},[85,10675,193],{"class":109},[85,10677,10567],{"class":125},[85,10679,10680,10682,10684,10686],{"class":87,"line":747},[85,10681,10547],{"class":109},[85,10683,10562],{"class":2116},[85,10685,193],{"class":109},[85,10687,10688],{"class":125},"npm run test:integration:ai\n",[85,10690,10691,10694],{"class":87,"line":752},[85,10692,10693],{"class":2116},"        env",[85,10695,10481],{"class":109},[85,10697,10698,10701,10703],{"class":87,"line":3110},[85,10699,10700],{"class":2116},"          OPENAI_API_KEY",[85,10702,193],{"class":109},[85,10704,10705],{"class":125},"${{ secrets.OPENAI_API_KEY }}\n",[85,10707,10708,10710,10712],{"class":87,"line":3116},[85,10709,10588],{"class":2116},[85,10711,193],{"class":109},[85,10713,10714],{"class":125},"Integration tests (real AI)\n",[17,10716,10717,10720,10721,10723,10724,10726],{},[39,10718,10719],{},"Model selection for tests."," When running real AI integration tests, use ",[82,10722,4807],{}," instead of ",[82,10725,4803],{},". For the kinds of assertions you make in tests (did the right tool get called?), the smaller model is adequate and costs 10x less.",[17,10728,10729,10732],{},[39,10730,10731],{},"Cache AI responses."," Hash the prompt and cache the response for stable test runs:",[75,10734,10736],{"className":77,"code":10735,"language":79,"meta":80,"style":80},"import { createHash } from 'crypto';\nimport { readFileSync, writeFileSync, existsSync } from 'fs';\n\nasync function cachedGenerateUI(prompt: string) {\n  const hash = createHash('md5').update(prompt).digest('hex');\n  const cachePath = `.test-cache\u002Fai-${hash}.json`;\n\n  if (existsSync(cachePath)) {\n    return JSON.parse(readFileSync(cachePath, 'utf-8'));\n  }\n\n  const result = await generateUI(prompt);\n  writeFileSync(cachePath, JSON.stringify(result));\n  return result;\n}\n",[82,10737,10738,10752,10766,10770,10789,10824,10844,10848,10861,10887,10891,10895,10910,10926,10933],{"__ignoreMap":80},[85,10739,10740,10742,10745,10747,10750],{"class":87,"line":88},[85,10741,404],{"class":98},[85,10743,10744],{"class":109}," { createHash } ",[85,10746,410],{"class":98},[85,10748,10749],{"class":125}," 'crypto'",[85,10751,416],{"class":109},[85,10753,10754,10756,10759,10761,10764],{"class":87,"line":95},[85,10755,404],{"class":98},[85,10757,10758],{"class":109}," { readFileSync, writeFileSync, existsSync } ",[85,10760,410],{"class":98},[85,10762,10763],{"class":125}," 'fs'",[85,10765,416],{"class":109},[85,10767,10768],{"class":87,"line":113},[85,10769,449],{"emptyLinePlaceholder":327},[85,10771,10772,10774,10776,10779,10781,10783,10785,10787],{"class":87,"line":119},[85,10773,196],{"class":98},[85,10775,1426],{"class":98},[85,10777,10778],{"class":138}," cachedGenerateUI",[85,10780,476],{"class":109},[85,10782,2870],{"class":202},[85,10784,1967],{"class":98},[85,10786,2021],{"class":102},[85,10788,2101],{"class":109},[85,10790,10791,10793,10796,10798,10801,10803,10806,10808,10811,10814,10817,10819,10822],{"class":87,"line":132},[85,10792,1443],{"class":98},[85,10794,10795],{"class":102}," hash",[85,10797,106],{"class":98},[85,10799,10800],{"class":138}," createHash",[85,10802,476],{"class":109},[85,10804,10805],{"class":125},"'md5'",[85,10807,3038],{"class":109},[85,10809,10810],{"class":138},"update",[85,10812,10813],{"class":109},"(prompt).",[85,10815,10816],{"class":138},"digest",[85,10818,476],{"class":109},[85,10820,10821],{"class":125},"'hex'",[85,10823,3529],{"class":109},[85,10825,10826,10828,10831,10833,10836,10839,10842],{"class":87,"line":145},[85,10827,1443],{"class":98},[85,10829,10830],{"class":102}," cachePath",[85,10832,106],{"class":98},[85,10834,10835],{"class":125}," `.test-cache\u002Fai-${",[85,10837,10838],{"class":109},"hash",[85,10840,10841],{"class":125},"}.json`",[85,10843,416],{"class":109},[85,10845,10846],{"class":87,"line":157},[85,10847,449],{"emptyLinePlaceholder":327},[85,10849,10850,10853,10855,10858],{"class":87,"line":181},[85,10851,10852],{"class":98},"  if",[85,10854,3077],{"class":109},[85,10856,10857],{"class":138},"existsSync",[85,10859,10860],{"class":109},"(cachePath)) {\n",[85,10862,10863,10865,10868,10870,10873,10875,10878,10881,10884],{"class":87,"line":187},[85,10864,4635],{"class":98},[85,10866,10867],{"class":102}," JSON",[85,10869,3578],{"class":109},[85,10871,10872],{"class":138},"parse",[85,10874,476],{"class":109},[85,10876,10877],{"class":138},"readFileSync",[85,10879,10880],{"class":109},"(cachePath, ",[85,10882,10883],{"class":125},"'utf-8'",[85,10885,10886],{"class":109},"));\n",[85,10888,10889],{"class":87,"line":219},[85,10890,3776],{"class":109},[85,10892,10893],{"class":87,"line":239},[85,10894,449],{"emptyLinePlaceholder":327},[85,10896,10897,10899,10901,10903,10905,10907],{"class":87,"line":265},[85,10898,1443],{"class":98},[85,10900,456],{"class":102},[85,10902,106],{"class":98},[85,10904,230],{"class":98},[85,10906,2865],{"class":138},[85,10908,10909],{"class":109},"(prompt);\n",[85,10911,10912,10915,10917,10919,10921,10923],{"class":87,"line":271},[85,10913,10914],{"class":138},"  writeFileSync",[85,10916,10880],{"class":109},[85,10918,9727],{"class":102},[85,10920,3578],{"class":109},[85,10922,9732],{"class":138},[85,10924,10925],{"class":109},"(result));\n",[85,10927,10928,10930],{"class":87,"line":277},[85,10929,1460],{"class":98},[85,10931,10932],{"class":109}," result;\n",[85,10934,10935],{"class":87,"line":552},[85,10936,280],{"class":109},[17,10938,10939],{},"Commit the cache to version control and only regenerate when prompts change. This makes AI integration tests fast and free after the first run.",[12,10941,10943],{"id":10942},"accessibility-testing","Accessibility Testing",[17,10945,10946],{},"Every generated component combination needs accessibility verification. The AI will not add ARIA labels, manage focus, or maintain heading hierarchy — those must be built into the components themselves.",[75,10948,10950],{"className":77,"code":10949,"language":79,"meta":80,"style":80},"import { axe, toHaveNoViolations } from 'jest-axe';\nexpect.extend(toHaveNoViolations);\n\ntest('generated weather card has no accessibility violations', async () => {\n  const { container } = render(\n    \u003CWeatherCard city=\"Paris\" temperature={22} conditions=\"Sunny\" humidity={40} \u002F>\n  );\n  const results = await axe(container);\n  expect(results).toHaveNoViolations();\n});\n\ntest('generated data table has no accessibility violations', async () => {\n  const { container } = render(\n    \u003CDataTable\n      columns={[{ key: 'name', label: 'Name' }, { key: 'value', label: 'Value', numeric: true }]}\n      rows={[{ name: 'Alpha', value: '100' }]}\n      caption=\"Sample data\"\n    \u002F>\n  );\n  const results = await axe(container);\n  expect(results).toHaveNoViolations();\n});\n",[82,10951,10952,10966,10977,10981,11000,11018,11055,11059,11076,11089,11093,11097,11116,11132,11139,11171,11184,11189,11193,11197,11202,11207],{"__ignoreMap":80},[85,10953,10954,10956,10959,10961,10964],{"class":87,"line":88},[85,10955,404],{"class":98},[85,10957,10958],{"class":109}," { axe, toHaveNoViolations } ",[85,10960,410],{"class":98},[85,10962,10963],{"class":125}," 'jest-axe'",[85,10965,416],{"class":109},[85,10967,10968,10971,10974],{"class":87,"line":95},[85,10969,10970],{"class":109},"expect.",[85,10972,10973],{"class":138},"extend",[85,10975,10976],{"class":109},"(toHaveNoViolations);\n",[85,10978,10979],{"class":87,"line":113},[85,10980,449],{"emptyLinePlaceholder":327},[85,10982,10983,10985,10987,10990,10992,10994,10996,10998],{"class":87,"line":119},[85,10984,10274],{"class":138},[85,10986,476],{"class":109},[85,10988,10989],{"class":125},"'generated weather card has no accessibility violations'",[85,10991,172],{"class":109},[85,10993,196],{"class":98},[85,10995,6393],{"class":109},[85,10997,214],{"class":98},[85,10999,110],{"class":109},[85,11001,11002,11004,11006,11009,11012,11014,11016],{"class":87,"line":132},[85,11003,1443],{"class":98},[85,11005,2699],{"class":109},[85,11007,11008],{"class":102},"container",[85,11010,11011],{"class":109}," } ",[85,11013,253],{"class":98},[85,11015,8204],{"class":138},[85,11017,5628],{"class":109},[85,11019,11020,11022,11025,11027,11029,11031,11033,11035,11037,11039,11041,11043,11045,11047,11049,11051,11053],{"class":87,"line":145},[85,11021,2113],{"class":98},[85,11023,11024],{"class":109},"WeatherCard city",[85,11026,253],{"class":98},[85,11028,8551],{"class":125},[85,11030,8554],{"class":109},[85,11032,253],{"class":98},[85,11034,256],{"class":109},[85,11036,8561],{"class":102},[85,11038,10112],{"class":109},[85,11040,253],{"class":98},[85,11042,10117],{"class":125},[85,11044,8573],{"class":109},[85,11046,253],{"class":98},[85,11048,256],{"class":109},[85,11050,10126],{"class":102},[85,11052,606],{"class":109},[85,11054,10131],{"class":98},[85,11056,11057],{"class":87,"line":157},[85,11058,2256],{"class":109},[85,11060,11061,11063,11066,11068,11070,11073],{"class":87,"line":181},[85,11062,1443],{"class":98},[85,11064,11065],{"class":102}," results",[85,11067,106],{"class":98},[85,11069,230],{"class":98},[85,11071,11072],{"class":138}," axe",[85,11074,11075],{"class":109},"(container);\n",[85,11077,11078,11081,11084,11087],{"class":87,"line":187},[85,11079,11080],{"class":138},"  expect",[85,11082,11083],{"class":109},"(results).",[85,11085,11086],{"class":138},"toHaveNoViolations",[85,11088,3652],{"class":109},[85,11090,11091],{"class":87,"line":219},[85,11092,755],{"class":109},[85,11094,11095],{"class":87,"line":239},[85,11096,449],{"emptyLinePlaceholder":327},[85,11098,11099,11101,11103,11106,11108,11110,11112,11114],{"class":87,"line":265},[85,11100,10274],{"class":138},[85,11102,476],{"class":109},[85,11104,11105],{"class":125},"'generated data table has no accessibility violations'",[85,11107,172],{"class":109},[85,11109,196],{"class":98},[85,11111,6393],{"class":109},[85,11113,214],{"class":98},[85,11115,110],{"class":109},[85,11117,11118,11120,11122,11124,11126,11128,11130],{"class":87,"line":271},[85,11119,1443],{"class":98},[85,11121,2699],{"class":109},[85,11123,11008],{"class":102},[85,11125,11011],{"class":109},[85,11127,253],{"class":98},[85,11129,8204],{"class":138},[85,11131,5628],{"class":109},[85,11133,11134,11136],{"class":87,"line":277},[85,11135,2113],{"class":98},[85,11137,11138],{"class":109},"DataTable\n",[85,11140,11141,11144,11146,11149,11151,11153,11155,11158,11160,11162,11164,11166,11168],{"class":87,"line":552},[85,11142,11143],{"class":109},"      columns",[85,11145,253],{"class":98},[85,11147,11148],{"class":109},"{[{ key: ",[85,11150,9243],{"class":125},[85,11152,9246],{"class":109},[85,11154,9249],{"class":125},[85,11156,11157],{"class":109}," }, { key: ",[85,11159,9258],{"class":125},[85,11161,9246],{"class":109},[85,11163,9263],{"class":125},[85,11165,9266],{"class":109},[85,11167,3719],{"class":102},[85,11169,11170],{"class":109}," }]}\n",[85,11172,11173,11176,11178,11180,11182],{"class":87,"line":558},[85,11174,11175],{"class":109},"      rows={[{ name: ",[85,11177,9283],{"class":125},[85,11179,9286],{"class":109},[85,11181,9289],{"class":125},[85,11183,11170],{"class":109},[85,11185,11186],{"class":87,"line":588},[85,11187,11188],{"class":109},"      caption=\"Sample data\"\n",[85,11190,11191],{"class":87,"line":627},[85,11192,6189],{"class":109},[85,11194,11195],{"class":87,"line":633},[85,11196,2256],{"class":109},[85,11198,11199],{"class":87,"line":638},[85,11200,11201],{"class":109},"  const results = await axe(container);\n",[85,11203,11204],{"class":87,"line":644},[85,11205,11206],{"class":109},"  expect(results).toHaveNoViolations();\n",[85,11208,11209],{"class":87,"line":654},[85,11210,755],{"class":109},[17,11212,11213],{},"Run axe tests on every component in isolation. If a component passes axe individually, its composition in a generated layout will also be accessible (assuming the layout itself is accessible — test that too).",[12,11215,11217],{"id":11216},"summary","Summary",[17,11219,11220],{},"The testing strategy for Generative UI:",[63,11222,11223,11229,11235,11241,11247,11253],{},[36,11224,11225,11228],{},[39,11226,11227],{},"Test components in isolation"," with high coverage. These are deterministic and fast.",[36,11230,11231,11234],{},[39,11232,11233],{},"Test Zod schemas"," to validate the AI–component contract. Also deterministic and fast.",[36,11236,11237,11240],{},[39,11238,11239],{},"Mock the AI in CI"," for rendering pipeline tests. No inference cost.",[36,11242,11243,11246],{},[39,11244,11245],{},"Run real AI integration tests nightly"," with property-based assertions, not exact content.",[36,11248,11249,11252],{},[39,11250,11251],{},"Cache AI responses"," in test runs to avoid repeating inference for stable prompts.",[36,11254,11255,11258],{},[39,11256,11257],{},"Run axe on every component"," to catch accessibility violations before they reach production.",[17,11260,11261],{},"This pyramid gives you fast CI on every commit, strong confidence in the component layer, and weekly validation that the AI makes reasonable choices.",[1138,11263],{},[17,11265,11266],{},[28,11267,11268,11269,11272],{},"Struggling with testing your Generative UI implementation? ",[291,11270,11271],{"href":308},"Get expert help"," on setting up a robust testing strategy.",[312,11274,11275],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":80,"searchDepth":95,"depth":95,"links":11277},[11278,11279,11280,11281,11282,11283,11284,11285,11286],{"id":8417,"depth":95,"text":8418},{"id":8430,"depth":95,"text":8431},{"id":8455,"depth":95,"text":8456},{"id":8860,"depth":95,"text":8861},{"id":9410,"depth":95,"text":9411},{"id":9939,"depth":95,"text":9940},{"id":10439,"depth":95,"text":10440},{"id":10942,"depth":95,"text":10943},{"id":11216,"depth":95,"text":11217},"2026-02-07","Strategies for testing AI-generated interfaces: from unit tests to visual regression and determinism.",{"featured":326},"14 min read",{"title":7055,"description":11288},"learn\u002Ftesting-generative-ui-applications",[11294,11295,11296,333],"testing","quality","ci-cd","EvPddcOAQShbUBoMviDmwzSnbSyRO_wYQCTRcc8Kqpo",{"id":11299,"title":11300,"author":7,"body":11301,"category":1164,"date":13431,"description":13432,"extension":324,"meta":13433,"navigation":327,"path":13434,"readTime":13435,"seo":13436,"stem":13437,"tags":13438,"__hash__":13441},"content\u002Flearn\u002Fperformance-optimization-genui.md","Performance Optimization for Generative UI",{"type":9,"value":11302,"toc":13419},[11303,11307,11310,11313,11316,11320,11323,11329,11335,11341,11347,11351,11354,11502,11507,11513,11614,11618,11624,11627,11916,11923,11927,11930,11933,12318,12321,12327,12331,12334,12405,12408,12504,12508,12511,12516,12580,12585,12691,12701,12705,12708,12993,12996,13000,13003,13009,13019,13025,13034,13200,13204,13207,13402,13405,13407,13416],[12,11304,11306],{"id":11305},"the-performance-challenge","The Performance Challenge",[17,11308,11309],{},"Generative UI has an inherent latency penalty: before any UI can render, an LLM must run inference and decide what to generate. That process takes 200–800ms for a simple response and can stretch to several seconds for complex multi-tool responses.",[17,11311,11312],{},"Traditional UI optimization — caching, CDN, SSG — cannot eliminate this latency. The LLM decision step is on the critical path for every request.",[17,11314,11315],{},"But \"slower than static HTML\" does not mean \"must feel slow.\" The right architecture makes 500ms feel like instant. The wrong architecture makes 300ms feel like an eternity. This guide covers the techniques that bridge that gap.",[12,11317,11319],{"id":11318},"the-metrics-that-matter","The Metrics That Matter",[17,11321,11322],{},"Before optimizing, define what you are measuring:",[17,11324,11325,11328],{},[39,11326,11327],{},"Time to First Component (TTFC):"," How long until the user sees any AI-generated element, even a loading state. Target: under 200ms. This is achievable by streaming the skeleton immediately while inference runs.",[17,11330,11331,11334],{},[39,11332,11333],{},"Time to Interactive Component (TTIC):"," How long until the first real, data-populated component appears. Target: under 800ms. This is the end of LLM inference for the first tool call.",[17,11336,11337,11340],{},[39,11338,11339],{},"Streaming Completion Time:"," How long until all generated components have loaded. This varies with the number of tool calls. With streaming, this is less important than TTFC and TTIC.",[17,11342,11343,11346],{},[39,11344,11345],{},"Layout Shift Score (CLS):"," Generated components should not shift the page layout as they load. Skeletons must match the size of the final component.",[12,11348,11350],{"id":11349},"strategy-1-stream-skeletons-immediately","Strategy 1: Stream Skeletons Immediately",[17,11352,11353],{},"The single highest-impact optimization is streaming a loading skeleton before the LLM resolves the first parameter. The Vercel AI SDK's generator pattern enables this directly:",[75,11355,11357],{"className":77,"code":11356,"language":79,"meta":80,"style":80},"tools: {\n  revenueChart: {\n    description: 'Display a revenue chart',\n    parameters: z.object({\n      period: z.string(),\n      data: z.array(z.object({ date: z.string(), value: z.number() })),\n    }),\n    generate: async function* (params) {\n      \u002F\u002F This yields IMMEDIATELY — before params are resolved\n      \u002F\u002F The skeleton appears at time zero\n      yield \u003CChartSkeleton \u002F>;\n\n      \u002F\u002F Optionally fetch real data while the AI resolves params\n      \u002F\u002F The component appears when both are ready\n      return \u003CRevenueChart {...params} \u002F>;\n    },\n  },\n}\n",[82,11358,11359,11365,11371,11381,11391,11399,11419,11423,11439,11444,11449,11460,11464,11469,11474,11490,11494,11498],{"__ignoreMap":80},[85,11360,11361,11363],{"class":87,"line":88},[85,11362,1481],{"class":138},[85,11364,1484],{"class":109},[85,11366,11367,11369],{"class":87,"line":95},[85,11368,1489],{"class":138},[85,11370,1484],{"class":109},[85,11372,11373,11375,11377,11379],{"class":87,"line":113},[85,11374,1496],{"class":138},[85,11376,193],{"class":109},[85,11378,7339],{"class":125},[85,11380,129],{"class":109},[85,11382,11383,11385,11387,11389],{"class":87,"line":119},[85,11384,1508],{"class":138},[85,11386,1511],{"class":109},[85,11388,139],{"class":138},[85,11390,142],{"class":109},[85,11392,11393,11395,11397],{"class":87,"line":132},[85,11394,5098],{"class":109},[85,11396,151],{"class":138},[85,11398,154],{"class":109},[85,11400,11401,11403,11405,11407,11409,11411,11413,11415,11417],{"class":87,"line":145},[85,11402,5278],{"class":109},[85,11404,669],{"class":138},[85,11406,672],{"class":109},[85,11408,139],{"class":138},[85,11410,5365],{"class":109},[85,11412,151],{"class":138},[85,11414,5292],{"class":109},[85,11416,538],{"class":138},[85,11418,692],{"class":109},[85,11420,11421],{"class":87,"line":157},[85,11422,184],{"class":109},[85,11424,11425,11427,11429,11431,11433,11435,11437],{"class":87,"line":181},[85,11426,190],{"class":138},[85,11428,193],{"class":109},[85,11430,196],{"class":98},[85,11432,3074],{"class":98},[85,11434,3077],{"class":109},[85,11436,1435],{"class":202},[85,11438,2101],{"class":109},[85,11440,11441],{"class":87,"line":187},[85,11442,11443],{"class":91},"      \u002F\u002F This yields IMMEDIATELY — before params are resolved\n",[85,11445,11446],{"class":87,"line":219},[85,11447,11448],{"class":91},"      \u002F\u002F The skeleton appears at time zero\n",[85,11450,11451,11454,11456,11458],{"class":87,"line":239},[85,11452,11453],{"class":98},"      yield",[85,11455,245],{"class":109},[85,11457,7389],{"class":138},[85,11459,3107],{"class":109},[85,11461,11462],{"class":87,"line":265},[85,11463,449],{"emptyLinePlaceholder":327},[85,11465,11466],{"class":87,"line":271},[85,11467,11468],{"class":91},"      \u002F\u002F Optionally fetch real data while the AI resolves params\n",[85,11470,11471],{"class":87,"line":277},[85,11472,11473],{"class":91},"      \u002F\u002F The component appears when both are ready\n",[85,11475,11476,11478,11480,11482,11484,11486,11488],{"class":87,"line":552},[85,11477,242],{"class":98},[85,11479,245],{"class":109},[85,11481,1580],{"class":138},[85,11483,3126],{"class":109},[85,11485,3129],{"class":98},[85,11487,1435],{"class":138},[85,11489,624],{"class":109},[85,11491,11492],{"class":87,"line":558},[85,11493,268],{"class":109},[85,11495,11496],{"class":87,"line":588},[85,11497,274],{"class":109},[85,11499,11500],{"class":87,"line":627},[85,11501,280],{"class":109},[17,11503,1945,11504,11506],{},[82,11505,3359],{}," statement runs synchronously. The user sees the skeleton in the same round trip as the initial request. LLM inference happens in parallel. This is why TTFC can be under 200ms even when TTIC is 800ms.",[17,11508,11509,11512],{},[39,11510,11511],{},"Critical detail:"," The skeleton must match the final component's dimensions. If the skeleton is 100px tall and the loaded component is 300px, you have layout shift that hurts CLS and feels jarring.",[75,11514,11516],{"className":1992,"code":11515,"language":1994,"meta":80,"style":80},"\u002F\u002F Bad: generic skeleton that mismatches component size\nyield \u003Cdiv className=\"h-8 animate-pulse bg-muted rounded\" \u002F>;\n\n\u002F\u002F Good: skeleton that matches the component\nyield (\n  \u003Cdiv className=\"rounded-lg border p-6 h-64\">\n    \u003Cdiv className=\"h-4 w-32 animate-pulse bg-muted rounded mb-4\" \u002F>\n    \u003Cdiv className=\"h-48 w-full animate-pulse bg-muted rounded\" \u002F>\n  \u003C\u002Fdiv>\n);\n",[82,11517,11518,11523,11540,11544,11549,11555,11571,11586,11601,11610],{"__ignoreMap":80},[85,11519,11520],{"class":87,"line":88},[85,11521,11522],{"class":91},"\u002F\u002F Bad: generic skeleton that mismatches component size\n",[85,11524,11525,11527,11529,11531,11533,11535,11538],{"class":87,"line":95},[85,11526,3359],{"class":98},[85,11528,245],{"class":109},[85,11530,2117],{"class":2116},[85,11532,2120],{"class":138},[85,11534,253],{"class":98},[85,11536,11537],{"class":125},"\"h-8 animate-pulse bg-muted rounded\"",[85,11539,3107],{"class":109},[85,11541,11542],{"class":87,"line":113},[85,11543,449],{"emptyLinePlaceholder":327},[85,11545,11546],{"class":87,"line":119},[85,11547,11548],{"class":91},"\u002F\u002F Good: skeleton that matches the component\n",[85,11550,11551,11553],{"class":87,"line":132},[85,11552,3359],{"class":98},[85,11554,2108],{"class":109},[85,11556,11557,11560,11562,11564,11566,11569],{"class":87,"line":145},[85,11558,11559],{"class":109},"  \u003C",[85,11561,2117],{"class":2116},[85,11563,2120],{"class":138},[85,11565,253],{"class":98},[85,11567,11568],{"class":125},"\"rounded-lg border p-6 h-64\"",[85,11570,2128],{"class":109},[85,11572,11573,11575,11577,11579,11581,11584],{"class":87,"line":157},[85,11574,2113],{"class":109},[85,11576,2117],{"class":2116},[85,11578,2120],{"class":138},[85,11580,253],{"class":98},[85,11582,11583],{"class":125},"\"h-4 w-32 animate-pulse bg-muted rounded mb-4\"",[85,11585,6253],{"class":109},[85,11587,11588,11590,11592,11594,11596,11599],{"class":87,"line":181},[85,11589,2113],{"class":109},[85,11591,2117],{"class":2116},[85,11593,2120],{"class":138},[85,11595,253],{"class":98},[85,11597,11598],{"class":125},"\"h-48 w-full animate-pulse bg-muted rounded\"",[85,11600,6253],{"class":109},[85,11602,11603,11606,11608],{"class":87,"line":187},[85,11604,11605],{"class":109},"  \u003C\u002F",[85,11607,2117],{"class":2116},[85,11609,2128],{"class":109},[85,11611,11612],{"class":87,"line":219},[85,11613,3529],{"class":109},[12,11615,11617],{"id":11616},"strategy-2-parallel-tool-calls","Strategy 2: Parallel Tool Calls",[17,11619,11620,11621,11623],{},"When the AI needs to call multiple tools, they should execute in parallel. The Vercel AI SDK handles this automatically — multiple tool calls in a single response run their ",[82,11622,1404],{}," functions concurrently.",[17,11625,11626],{},"But your component's data fetching must not block:",[75,11628,11630],{"className":77,"code":11629,"language":79,"meta":80,"style":80},"\u002F\u002F Slow: sequential data fetching inside generate\ngenerate: async function* ({ userId, period }) {\n  yield \u003CDashboardSkeleton \u002F>;\n  const revenue = await fetchRevenue(userId, period);      \u002F\u002F 200ms\n  const users = await fetchUsers(userId, period);          \u002F\u002F 150ms\n  const conversions = await fetchConversions(userId);      \u002F\u002F 100ms\n  \u002F\u002F Total: ~450ms\n  return \u003CDashboard revenue={revenue} users={users} conversions={conversions} \u002F>;\n},\n\n\u002F\u002F Fast: parallel data fetching\ngenerate: async function* ({ userId, period }) {\n  yield \u003CDashboardSkeleton \u002F>;\n  const [revenue, users, conversions] = await Promise.all([\n    fetchRevenue(userId, period),\n    fetchUsers(userId, period),\n    fetchConversions(userId),\n  ]);\n  \u002F\u002F Total: ~200ms (longest fetch wins)\n  return \u003CDashboard revenue={revenue} users={users} conversions={conversions} \u002F>;\n},\n",[82,11631,11632,11637,11658,11670,11690,11710,11730,11735,11770,11775,11779,11784,11804,11814,11847,11855,11862,11870,11875,11880,11912],{"__ignoreMap":80},[85,11633,11634],{"class":87,"line":88},[85,11635,11636],{"class":91},"\u002F\u002F Slow: sequential data fetching inside generate\n",[85,11638,11639,11641,11643,11645,11647,11649,11652,11654,11656],{"class":87,"line":95},[85,11640,1404],{"class":138},[85,11642,193],{"class":109},[85,11644,196],{"class":98},[85,11646,3074],{"class":98},[85,11648,199],{"class":109},[85,11650,11651],{"class":202},"userId",[85,11653,172],{"class":109},[85,11655,1539],{"class":202},[85,11657,1438],{"class":109},[85,11659,11660,11663,11665,11668],{"class":87,"line":113},[85,11661,11662],{"class":98},"  yield",[85,11664,245],{"class":109},[85,11666,11667],{"class":138},"DashboardSkeleton",[85,11669,3107],{"class":109},[85,11671,11672,11674,11677,11679,11681,11684,11687],{"class":87,"line":119},[85,11673,1443],{"class":98},[85,11675,11676],{"class":102}," revenue",[85,11678,106],{"class":98},[85,11680,230],{"class":98},[85,11682,11683],{"class":138}," fetchRevenue",[85,11685,11686],{"class":109},"(userId, period);      ",[85,11688,11689],{"class":91},"\u002F\u002F 200ms\n",[85,11691,11692,11694,11697,11699,11701,11704,11707],{"class":87,"line":132},[85,11693,1443],{"class":98},[85,11695,11696],{"class":102}," users",[85,11698,106],{"class":98},[85,11700,230],{"class":98},[85,11702,11703],{"class":138}," fetchUsers",[85,11705,11706],{"class":109},"(userId, period);          ",[85,11708,11709],{"class":91},"\u002F\u002F 150ms\n",[85,11711,11712,11714,11717,11719,11721,11724,11727],{"class":87,"line":145},[85,11713,1443],{"class":98},[85,11715,11716],{"class":102}," conversions",[85,11718,106],{"class":98},[85,11720,230],{"class":98},[85,11722,11723],{"class":138}," fetchConversions",[85,11725,11726],{"class":109},"(userId);      ",[85,11728,11729],{"class":91},"\u002F\u002F 100ms\n",[85,11731,11732],{"class":87,"line":157},[85,11733,11734],{"class":91},"  \u002F\u002F Total: ~450ms\n",[85,11736,11737,11739,11741,11743,11745,11747,11750,11752,11755,11757,11759,11761,11764,11766,11768],{"class":87,"line":181},[85,11738,1460],{"class":98},[85,11740,245],{"class":109},[85,11742,7871],{"class":138},[85,11744,11676],{"class":138},[85,11746,601],{"class":109},[85,11748,11749],{"class":202},"revenue",[85,11751,606],{"class":109},[85,11753,11754],{"class":138},"users",[85,11756,601],{"class":109},[85,11758,11754],{"class":202},[85,11760,606],{"class":109},[85,11762,11763],{"class":138},"conversions",[85,11765,601],{"class":109},[85,11767,11763],{"class":202},[85,11769,624],{"class":109},[85,11771,11772],{"class":87,"line":187},[85,11773,11774],{"class":109},"},\n",[85,11776,11777],{"class":87,"line":219},[85,11778,449],{"emptyLinePlaceholder":327},[85,11780,11781],{"class":87,"line":239},[85,11782,11783],{"class":91},"\u002F\u002F Fast: parallel data fetching\n",[85,11785,11786,11788,11790,11792,11794,11796,11798,11800,11802],{"class":87,"line":265},[85,11787,1404],{"class":138},[85,11789,193],{"class":109},[85,11791,196],{"class":98},[85,11793,3074],{"class":98},[85,11795,199],{"class":109},[85,11797,11651],{"class":202},[85,11799,172],{"class":109},[85,11801,1539],{"class":202},[85,11803,1438],{"class":109},[85,11805,11806,11808,11810,11812],{"class":87,"line":271},[85,11807,11662],{"class":98},[85,11809,245],{"class":109},[85,11811,11667],{"class":138},[85,11813,3107],{"class":109},[85,11815,11816,11818,11820,11822,11824,11826,11828,11830,11832,11834,11836,11839,11841,11844],{"class":87,"line":277},[85,11817,1443],{"class":98},[85,11819,3506],{"class":109},[85,11821,11749],{"class":102},[85,11823,172],{"class":109},[85,11825,11754],{"class":102},[85,11827,172],{"class":109},[85,11829,11763],{"class":102},[85,11831,3516],{"class":109},[85,11833,253],{"class":98},[85,11835,230],{"class":98},[85,11837,11838],{"class":102}," Promise",[85,11840,3578],{"class":109},[85,11842,11843],{"class":138},"all",[85,11845,11846],{"class":109},"([\n",[85,11848,11849,11852],{"class":87,"line":552},[85,11850,11851],{"class":138},"    fetchRevenue",[85,11853,11854],{"class":109},"(userId, period),\n",[85,11856,11857,11860],{"class":87,"line":558},[85,11858,11859],{"class":138},"    fetchUsers",[85,11861,11854],{"class":109},[85,11863,11864,11867],{"class":87,"line":588},[85,11865,11866],{"class":138},"    fetchConversions",[85,11868,11869],{"class":109},"(userId),\n",[85,11871,11872],{"class":87,"line":627},[85,11873,11874],{"class":109},"  ]);\n",[85,11876,11877],{"class":87,"line":633},[85,11878,11879],{"class":91},"  \u002F\u002F Total: ~200ms (longest fetch wins)\n",[85,11881,11882,11884,11886,11888,11890,11892,11894,11896,11898,11900,11902,11904,11906,11908,11910],{"class":87,"line":638},[85,11883,1460],{"class":98},[85,11885,245],{"class":109},[85,11887,7871],{"class":138},[85,11889,11676],{"class":138},[85,11891,601],{"class":109},[85,11893,11749],{"class":202},[85,11895,606],{"class":109},[85,11897,11754],{"class":138},[85,11899,601],{"class":109},[85,11901,11754],{"class":202},[85,11903,606],{"class":109},[85,11905,11763],{"class":138},[85,11907,601],{"class":109},[85,11909,11763],{"class":202},[85,11911,624],{"class":109},[85,11913,11914],{"class":87,"line":644},[85,11915,11774],{"class":109},[17,11917,11918,11919,11922],{},"For independent data sources, ",[82,11920,11921],{},"Promise.all"," is always faster than sequential awaits.",[12,11924,11926],{"id":11925},"strategy-3-response-caching","Strategy 3: Response Caching",[17,11928,11929],{},"Many Generative UI queries are repeated. \"Show me this month's revenue dashboard\" runs dozens of times per day for the same user with the same underlying data.",[17,11931,11932],{},"Cache at the LLM response level, keyed by a hash of the prompt and relevant context:",[75,11934,11936],{"className":77,"code":11935,"language":79,"meta":80,"style":80},"import { createHash } from 'crypto';\n\ninterface CacheEntry {\n  value: React.ReactNode;\n  cachedAt: number;\n  ttlMs: number;\n}\n\nconst responseCache = new Map\u003Cstring, CacheEntry>();\n\nfunction getCacheKey(prompt: string, context: object): string {\n  return createHash('md5')\n    .update(prompt + JSON.stringify(context))\n    .digest('hex');\n}\n\nexport async function generateUIWithCache(\n  prompt: string,\n  context: object = {},\n  ttlMs: number = 5 * 60 * 1000  \u002F\u002F 5 minutes default\n) {\n  const key = getCacheKey(prompt, context);\n  const cached = responseCache.get(key);\n\n  if (cached && Date.now() - cached.cachedAt \u003C cached.ttlMs) {\n    return cached.value;\n  }\n\n  const result = await streamUI({ \u002F* ... *\u002F });\n  responseCache.set(key, { value: result.value, cachedAt: Date.now(), ttlMs });\n  return result.value;\n}\n",[82,11937,11938,11950,11954,11963,11978,11989,12000,12004,12008,12035,12039,12072,12085,12107,12119,12123,12127,12140,12151,12165,12192,12196,12209,12227,12231,12259,12266,12270,12274,12292,12308,12314],{"__ignoreMap":80},[85,11939,11940,11942,11944,11946,11948],{"class":87,"line":88},[85,11941,404],{"class":98},[85,11943,10744],{"class":109},[85,11945,410],{"class":98},[85,11947,10749],{"class":125},[85,11949,416],{"class":109},[85,11951,11952],{"class":87,"line":95},[85,11953,449],{"emptyLinePlaceholder":327},[85,11955,11956,11958,11961],{"class":87,"line":113},[85,11957,2006],{"class":98},[85,11959,11960],{"class":138}," CacheEntry",[85,11962,110],{"class":109},[85,11964,11965,11968,11970,11972,11974,11976],{"class":87,"line":119},[85,11966,11967],{"class":202},"  value",[85,11969,1967],{"class":98},[85,11971,3575],{"class":138},[85,11973,3578],{"class":109},[85,11975,3581],{"class":138},[85,11977,416],{"class":109},[85,11979,11980,11983,11985,11987],{"class":87,"line":132},[85,11981,11982],{"class":202},"  cachedAt",[85,11984,1967],{"class":98},[85,11986,2033],{"class":102},[85,11988,416],{"class":109},[85,11990,11991,11994,11996,11998],{"class":87,"line":145},[85,11992,11993],{"class":202},"  ttlMs",[85,11995,1967],{"class":98},[85,11997,2033],{"class":102},[85,11999,416],{"class":109},[85,12001,12002],{"class":87,"line":157},[85,12003,280],{"class":109},[85,12005,12006],{"class":87,"line":181},[85,12007,449],{"emptyLinePlaceholder":327},[85,12009,12010,12012,12015,12017,12020,12023,12025,12027,12029,12032],{"class":87,"line":187},[85,12011,99],{"class":98},[85,12013,12014],{"class":102}," responseCache",[85,12016,106],{"class":98},[85,12018,12019],{"class":98}," new",[85,12021,12022],{"class":138}," Map",[85,12024,3552],{"class":109},[85,12026,151],{"class":102},[85,12028,172],{"class":109},[85,12030,12031],{"class":138},"CacheEntry",[85,12033,12034],{"class":109},">();\n",[85,12036,12037],{"class":87,"line":219},[85,12038,449],{"emptyLinePlaceholder":327},[85,12040,12041,12043,12046,12048,12050,12052,12054,12056,12059,12061,12064,12066,12068,12070],{"class":87,"line":239},[85,12042,5600],{"class":98},[85,12044,12045],{"class":138}," getCacheKey",[85,12047,476],{"class":109},[85,12049,2870],{"class":202},[85,12051,1967],{"class":98},[85,12053,2021],{"class":102},[85,12055,172],{"class":109},[85,12057,12058],{"class":202},"context",[85,12060,1967],{"class":98},[85,12062,12063],{"class":102}," object",[85,12065,9744],{"class":109},[85,12067,1967],{"class":98},[85,12069,2021],{"class":102},[85,12071,110],{"class":109},[85,12073,12074,12076,12078,12080,12082],{"class":87,"line":265},[85,12075,1460],{"class":98},[85,12077,10800],{"class":138},[85,12079,476],{"class":109},[85,12081,10805],{"class":125},[85,12083,12084],{"class":109},")\n",[85,12086,12087,12090,12092,12095,12098,12100,12102,12104],{"class":87,"line":271},[85,12088,12089],{"class":109},"    .",[85,12091,10810],{"class":138},[85,12093,12094],{"class":109},"(prompt ",[85,12096,12097],{"class":98},"+",[85,12099,10867],{"class":102},[85,12101,3578],{"class":109},[85,12103,9732],{"class":138},[85,12105,12106],{"class":109},"(context))\n",[85,12108,12109,12111,12113,12115,12117],{"class":87,"line":277},[85,12110,12089],{"class":109},[85,12112,10816],{"class":138},[85,12114,476],{"class":109},[85,12116,10821],{"class":125},[85,12118,3529],{"class":109},[85,12120,12121],{"class":87,"line":552},[85,12122,280],{"class":109},[85,12124,12125],{"class":87,"line":558},[85,12126,449],{"emptyLinePlaceholder":327},[85,12128,12129,12131,12133,12135,12138],{"class":87,"line":588},[85,12130,1420],{"class":98},[85,12132,1423],{"class":98},[85,12134,1426],{"class":98},[85,12136,12137],{"class":138}," generateUIWithCache",[85,12139,5628],{"class":109},[85,12141,12142,12145,12147,12149],{"class":87,"line":627},[85,12143,12144],{"class":202},"  prompt",[85,12146,1967],{"class":98},[85,12148,2021],{"class":102},[85,12150,129],{"class":109},[85,12152,12153,12156,12158,12160,12162],{"class":87,"line":633},[85,12154,12155],{"class":202},"  context",[85,12157,1967],{"class":98},[85,12159,12063],{"class":102},[85,12161,106],{"class":98},[85,12163,12164],{"class":109}," {},\n",[85,12166,12167,12169,12171,12173,12175,12178,12181,12184,12186,12189],{"class":87,"line":638},[85,12168,11993],{"class":202},[85,12170,1967],{"class":98},[85,12172,2033],{"class":102},[85,12174,106],{"class":98},[85,12176,12177],{"class":102}," 5",[85,12179,12180],{"class":98}," *",[85,12182,12183],{"class":102}," 60",[85,12185,12180],{"class":98},[85,12187,12188],{"class":102}," 1000",[85,12190,12191],{"class":91},"  \u002F\u002F 5 minutes default\n",[85,12193,12194],{"class":87,"line":644},[85,12195,2101],{"class":109},[85,12197,12198,12200,12202,12204,12206],{"class":87,"line":654},[85,12199,1443],{"class":98},[85,12201,4244],{"class":102},[85,12203,106],{"class":98},[85,12205,12045],{"class":138},[85,12207,12208],{"class":109},"(prompt, context);\n",[85,12210,12211,12213,12216,12218,12221,12224],{"class":87,"line":663},[85,12212,1443],{"class":98},[85,12214,12215],{"class":102}," cached",[85,12217,106],{"class":98},[85,12219,12220],{"class":109}," responseCache.",[85,12222,12223],{"class":138},"get",[85,12225,12226],{"class":109},"(key);\n",[85,12228,12229],{"class":87,"line":695},[85,12230,449],{"emptyLinePlaceholder":327},[85,12232,12233,12235,12238,12241,12244,12247,12249,12251,12254,12256],{"class":87,"line":700},[85,12234,10852],{"class":98},[85,12236,12237],{"class":109}," (cached ",[85,12239,12240],{"class":98},"&&",[85,12242,12243],{"class":109}," Date.",[85,12245,12246],{"class":138},"now",[85,12248,3671],{"class":109},[85,12250,8700],{"class":98},[85,12252,12253],{"class":109}," cached.cachedAt ",[85,12255,3552],{"class":98},[85,12257,12258],{"class":109}," cached.ttlMs) {\n",[85,12260,12261,12263],{"class":87,"line":719},[85,12262,4635],{"class":98},[85,12264,12265],{"class":109}," cached.value;\n",[85,12267,12268],{"class":87,"line":737},[85,12269,3776],{"class":109},[85,12271,12272],{"class":87,"line":742},[85,12273,449],{"emptyLinePlaceholder":327},[85,12275,12276,12278,12280,12282,12284,12286,12288,12290],{"class":87,"line":747},[85,12277,1443],{"class":98},[85,12279,456],{"class":102},[85,12281,106],{"class":98},[85,12283,230],{"class":98},[85,12285,463],{"class":138},[85,12287,1432],{"class":109},[85,12289,6848],{"class":91},[85,12291,7614],{"class":109},[85,12293,12294,12297,12300,12303,12305],{"class":87,"line":752},[85,12295,12296],{"class":109},"  responseCache.",[85,12298,12299],{"class":138},"set",[85,12301,12302],{"class":109},"(key, { value: result.value, cachedAt: Date.",[85,12304,12246],{"class":138},[85,12306,12307],{"class":109},"(), ttlMs });\n",[85,12309,12310,12312],{"class":87,"line":3110},[85,12311,1460],{"class":98},[85,12313,3340],{"class":109},[85,12315,12316],{"class":87,"line":3116},[85,12317,280],{"class":109},[17,12319,12320],{},"For production, use Redis instead of an in-memory Map. Consider using Vercel KV or Upstash Redis for edge-compatible caching.",[17,12322,12323,12326],{},[39,12324,12325],{},"Important:"," Cache invalidation must match your data's update frequency. A revenue dashboard that caches for 5 minutes is fine. A real-time stock ticker that caches for 5 minutes is wrong.",[12,12328,12330],{"id":12329},"strategy-4-model-selection","Strategy 4: Model Selection",[17,12332,12333],{},"Not every query needs GPT-4o. Model selection is the highest-leverage cost and latency optimization available.",[875,12335,12336,12351],{},[878,12337,12338],{},[881,12339,12340,12343,12345,12348],{},[884,12341,12342],{},"Model",[884,12344,1641],{},[884,12346,12347],{},"Cost",[884,12349,12350],{},"Quality",[894,12352,12353,12366,12380,12393],{},[881,12354,12355,12358,12361,12363],{},[899,12356,12357],{},"GPT-4o",[899,12359,12360],{},"400–800ms",[899,12362,7205],{},[899,12364,12365],{},"Best",[881,12367,12368,12371,12374,12377],{},[899,12369,12370],{},"GPT-4o-mini",[899,12372,12373],{},"200–400ms",[899,12375,12376],{},"10x cheaper",[899,12378,12379],{},"Good",[881,12381,12382,12385,12388,12391],{},[899,12383,12384],{},"Claude Haiku",[899,12386,12387],{},"150–300ms",[899,12389,12390],{},"5x cheaper",[899,12392,12379],{},[881,12394,12395,12398,12401,12403],{},[899,12396,12397],{},"Gemini Flash",[899,12399,12400],{},"100–200ms",[899,12402,12390],{},[899,12404,12379],{},[17,12406,12407],{},"For most Generative UI tool selection tasks, GPT-4o-mini or Claude Haiku produces results indistinguishable from GPT-4o. Reserve the frontier models for complex reasoning tasks.",[75,12409,12411],{"className":77,"code":12410,"language":79,"meta":80,"style":80},"\u002F\u002F Route to appropriate model based on query complexity\nfunction selectModel(toolCount: number, contextLength: number) {\n  if (toolCount \u003C= 5 && contextLength \u003C 500) {\n    return openai('gpt-4o-mini');\n  }\n  return openai('gpt-4o');\n}\n",[82,12412,12413,12418,12445,12470,12484,12488,12500],{"__ignoreMap":80},[85,12414,12415],{"class":87,"line":88},[85,12416,12417],{"class":91},"\u002F\u002F Route to appropriate model based on query complexity\n",[85,12419,12420,12422,12425,12427,12430,12432,12434,12436,12439,12441,12443],{"class":87,"line":95},[85,12421,5600],{"class":98},[85,12423,12424],{"class":138}," selectModel",[85,12426,476],{"class":109},[85,12428,12429],{"class":202},"toolCount",[85,12431,1967],{"class":98},[85,12433,2033],{"class":102},[85,12435,172],{"class":109},[85,12437,12438],{"class":202},"contextLength",[85,12440,1967],{"class":98},[85,12442,2033],{"class":102},[85,12444,2101],{"class":109},[85,12446,12447,12449,12452,12455,12457,12460,12463,12465,12468],{"class":87,"line":113},[85,12448,10852],{"class":98},[85,12450,12451],{"class":109}," (toolCount ",[85,12453,12454],{"class":98},"\u003C=",[85,12456,12177],{"class":102},[85,12458,12459],{"class":98}," &&",[85,12461,12462],{"class":109}," contextLength ",[85,12464,3552],{"class":98},[85,12466,12467],{"class":102}," 500",[85,12469,2101],{"class":109},[85,12471,12472,12474,12477,12479,12482],{"class":87,"line":119},[85,12473,4635],{"class":98},[85,12475,12476],{"class":138}," openai",[85,12478,476],{"class":109},[85,12480,12481],{"class":125},"'gpt-4o-mini'",[85,12483,3529],{"class":109},[85,12485,12486],{"class":87,"line":132},[85,12487,3776],{"class":109},[85,12489,12490,12492,12494,12496,12498],{"class":87,"line":145},[85,12491,1460],{"class":98},[85,12493,12476],{"class":138},[85,12495,476],{"class":109},[85,12497,479],{"class":125},[85,12499,3529],{"class":109},[85,12501,12502],{"class":87,"line":157},[85,12503,280],{"class":109},[12,12505,12507],{"id":12506},"strategy-5-bundle-optimization","Strategy 5: Bundle Optimization",[17,12509,12510],{},"Generative UI component libraries can grow large. Every component in your tool registry ships to the browser. Manage this actively.",[17,12512,12513],{},[39,12514,12515],{},"Lazy load non-critical components:",[75,12517,12519],{"className":77,"code":12518,"language":79,"meta":80,"style":80},"\u002F\u002F Only import heavy chart components when needed\nconst HeavyChartComponent = dynamic(\n  () => import('@\u002Fcomponents\u002Fheavy-chart'),\n  { loading: () => \u003CChartSkeleton \u002F> }\n);\n",[82,12520,12521,12526,12540,12557,12576],{"__ignoreMap":80},[85,12522,12523],{"class":87,"line":88},[85,12524,12525],{"class":91},"\u002F\u002F Only import heavy chart components when needed\n",[85,12527,12528,12530,12533,12535,12538],{"class":87,"line":95},[85,12529,99],{"class":98},[85,12531,12532],{"class":102}," HeavyChartComponent",[85,12534,106],{"class":98},[85,12536,12537],{"class":138}," dynamic",[85,12539,5628],{"class":109},[85,12541,12542,12545,12547,12550,12552,12555],{"class":87,"line":113},[85,12543,12544],{"class":109},"  () ",[85,12546,214],{"class":98},[85,12548,12549],{"class":98}," import",[85,12551,476],{"class":109},[85,12553,12554],{"class":125},"'@\u002Fcomponents\u002Fheavy-chart'",[85,12556,482],{"class":109},[85,12558,12559,12562,12564,12567,12569,12571,12573],{"class":87,"line":119},[85,12560,12561],{"class":109},"  { ",[85,12563,3593],{"class":138},[85,12565,12566],{"class":109},": () ",[85,12568,214],{"class":98},[85,12570,245],{"class":109},[85,12572,7389],{"class":138},[85,12574,12575],{"class":109}," \u002F> }\n",[85,12577,12578],{"class":87,"line":132},[85,12579,3529],{"class":109},[17,12581,12582],{},[39,12583,12584],{},"Separate the component bundle from the tool registry:",[75,12586,12588],{"className":77,"code":12587,"language":79,"meta":80,"style":80},"\u002F\u002F Tool registry: lightweight, shipped early\nexport const toolDefinitions = {\n  revenueChart: {\n    description: '...',\n    parameters: z.object({ ... }),\n  },\n};\n\n\u002F\u002F Component implementations: lazy loaded when needed\nexport const toolComponents = {\n  revenueChart: dynamic(() => import('@\u002Fcomponents\u002Frevenue-chart')),\n};\n",[82,12589,12590,12595,12608,12613,12622,12635,12639,12643,12647,12652,12665,12687],{"__ignoreMap":80},[85,12591,12592],{"class":87,"line":88},[85,12593,12594],{"class":91},"\u002F\u002F Tool registry: lightweight, shipped early\n",[85,12596,12597,12599,12601,12604,12606],{"class":87,"line":95},[85,12598,1420],{"class":98},[85,12600,5011],{"class":98},[85,12602,12603],{"class":102}," toolDefinitions",[85,12605,106],{"class":98},[85,12607,110],{"class":109},[85,12609,12610],{"class":87,"line":113},[85,12611,12612],{"class":109},"  revenueChart: {\n",[85,12614,12615,12617,12620],{"class":87,"line":119},[85,12616,122],{"class":109},[85,12618,12619],{"class":125},"'...'",[85,12621,129],{"class":109},[85,12623,12624,12626,12628,12630,12632],{"class":87,"line":132},[85,12625,135],{"class":109},[85,12627,139],{"class":138},[85,12629,1432],{"class":109},[85,12631,3129],{"class":98},[85,12633,12634],{"class":109}," }),\n",[85,12636,12637],{"class":87,"line":145},[85,12638,274],{"class":109},[85,12640,12641],{"class":87,"line":157},[85,12642,5484],{"class":109},[85,12644,12645],{"class":87,"line":181},[85,12646,449],{"emptyLinePlaceholder":327},[85,12648,12649],{"class":87,"line":187},[85,12650,12651],{"class":91},"\u002F\u002F Component implementations: lazy loaded when needed\n",[85,12653,12654,12656,12658,12661,12663],{"class":87,"line":219},[85,12655,1420],{"class":98},[85,12657,5011],{"class":98},[85,12659,12660],{"class":102}," toolComponents",[85,12662,106],{"class":98},[85,12664,110],{"class":109},[85,12666,12667,12670,12673,12675,12677,12679,12681,12684],{"class":87,"line":239},[85,12668,12669],{"class":109},"  revenueChart: ",[85,12671,12672],{"class":138},"dynamic",[85,12674,10051],{"class":109},[85,12676,214],{"class":98},[85,12678,12549],{"class":98},[85,12680,476],{"class":109},[85,12682,12683],{"class":125},"'@\u002Fcomponents\u002Frevenue-chart'",[85,12685,12686],{"class":109},")),\n",[85,12688,12689],{"class":87,"line":265},[85,12690,5484],{"class":109},[17,12692,12693,12696,12697,12700],{},[39,12694,12695],{},"Measure your bundle."," Run ",[82,12698,12699],{},"npx @next\u002Fbundle-analyzer"," and look for components that are disproportionately large. A single charting library can add 50KB+ to your bundle.",[12,12702,12704],{"id":12703},"strategy-6-optimistic-ui","Strategy 6: Optimistic UI",[17,12706,12707],{},"For queries the system can predict, show an optimistic UI before the AI responds:",[75,12709,12711],{"className":77,"code":12710,"language":79,"meta":80,"style":80},"export function useGenerativeUI() {\n  const [ui, setUI] = useState\u003CReact.ReactNode>(null);\n  const [optimisticUI, setOptimisticUI] = useState\u003CReact.ReactNode>(null);\n\n  async function generate(prompt: string) {\n    \u002F\u002F Immediately show a plausible skeleton based on query type\n    if (prompt.toLowerCase().includes('weather')) {\n      setOptimisticUI(\u003CWeatherCardSkeleton \u002F>);\n    } else if (prompt.toLowerCase().includes('stock') || prompt.toLowerCase().includes('price')) {\n      setOptimisticUI(\u003CStockTickerSkeleton \u002F>);\n    } else {\n      setOptimisticUI(\u003CGenericSkeleton \u002F>);\n    }\n\n    const result = await generateUI(prompt);\n    setOptimisticUI(null);\n    setUI(result);\n  }\n\n  return { ui: optimisticUI ?? ui, generate };\n}\n",[82,12712,12713,12724,12758,12792,12796,12815,12820,12842,12854,12898,12909,12917,12928,12932,12936,12950,12961,12969,12973,12977,12989],{"__ignoreMap":80},[85,12714,12715,12717,12719,12722],{"class":87,"line":88},[85,12716,1420],{"class":98},[85,12718,1426],{"class":98},[85,12720,12721],{"class":138}," useGenerativeUI",[85,12723,3499],{"class":109},[85,12725,12726,12728,12730,12732,12734,12737,12739,12741,12743,12745,12748,12750,12752,12754,12756],{"class":87,"line":95},[85,12727,1443],{"class":98},[85,12729,3506],{"class":109},[85,12731,3570],{"class":102},[85,12733,172],{"class":109},[85,12735,12736],{"class":102},"setUI",[85,12738,3516],{"class":109},[85,12740,253],{"class":98},[85,12742,3521],{"class":138},[85,12744,3552],{"class":109},[85,12746,12747],{"class":138},"React",[85,12749,3578],{"class":109},[85,12751,3581],{"class":138},[85,12753,6722],{"class":109},[85,12755,4601],{"class":102},[85,12757,3529],{"class":109},[85,12759,12760,12762,12764,12767,12769,12772,12774,12776,12778,12780,12782,12784,12786,12788,12790],{"class":87,"line":113},[85,12761,1443],{"class":98},[85,12763,3506],{"class":109},[85,12765,12766],{"class":102},"optimisticUI",[85,12768,172],{"class":109},[85,12770,12771],{"class":102},"setOptimisticUI",[85,12773,3516],{"class":109},[85,12775,253],{"class":98},[85,12777,3521],{"class":138},[85,12779,3552],{"class":109},[85,12781,12747],{"class":138},[85,12783,3578],{"class":109},[85,12785,3581],{"class":138},[85,12787,6722],{"class":109},[85,12789,4601],{"class":102},[85,12791,3529],{"class":109},[85,12793,12794],{"class":87,"line":119},[85,12795,449],{"emptyLinePlaceholder":327},[85,12797,12798,12800,12802,12805,12807,12809,12811,12813],{"class":87,"line":132},[85,12799,3620],{"class":98},[85,12801,1426],{"class":98},[85,12803,12804],{"class":138}," generate",[85,12806,476],{"class":109},[85,12808,2870],{"class":202},[85,12810,1967],{"class":98},[85,12812,2021],{"class":102},[85,12814,2101],{"class":109},[85,12816,12817],{"class":87,"line":145},[85,12818,12819],{"class":91},"    \u002F\u002F Immediately show a plausible skeleton based on query type\n",[85,12821,12822,12824,12827,12830,12832,12834,12836,12839],{"class":87,"line":157},[85,12823,3657],{"class":98},[85,12825,12826],{"class":109}," (prompt.",[85,12828,12829],{"class":138},"toLowerCase",[85,12831,2972],{"class":109},[85,12833,9603],{"class":138},[85,12835,476],{"class":109},[85,12837,12838],{"class":125},"'weather'",[85,12840,12841],{"class":109},")) {\n",[85,12843,12844,12847,12849,12852],{"class":87,"line":181},[85,12845,12846],{"class":138},"      setOptimisticUI",[85,12848,10296],{"class":109},[85,12850,12851],{"class":138},"WeatherCardSkeleton",[85,12853,10302],{"class":109},[85,12855,12856,12859,12862,12865,12867,12869,12871,12873,12875,12878,12880,12882,12885,12887,12889,12891,12893,12896],{"class":87,"line":187},[85,12857,12858],{"class":109},"    } ",[85,12860,12861],{"class":98},"else",[85,12863,12864],{"class":98}," if",[85,12866,12826],{"class":109},[85,12868,12829],{"class":138},[85,12870,2972],{"class":109},[85,12872,9603],{"class":138},[85,12874,476],{"class":109},[85,12876,12877],{"class":125},"'stock'",[85,12879,4230],{"class":109},[85,12881,3674],{"class":98},[85,12883,12884],{"class":109}," prompt.",[85,12886,12829],{"class":138},[85,12888,2972],{"class":109},[85,12890,9603],{"class":138},[85,12892,476],{"class":109},[85,12894,12895],{"class":125},"'price'",[85,12897,12841],{"class":109},[85,12899,12900,12902,12904,12907],{"class":87,"line":219},[85,12901,12846],{"class":138},[85,12903,10296],{"class":109},[85,12905,12906],{"class":138},"StockTickerSkeleton",[85,12908,10302],{"class":109},[85,12910,12911,12913,12915],{"class":87,"line":239},[85,12912,12858],{"class":109},[85,12914,12861],{"class":98},[85,12916,110],{"class":109},[85,12918,12919,12921,12923,12926],{"class":87,"line":265},[85,12920,12846],{"class":138},[85,12922,10296],{"class":109},[85,12924,12925],{"class":138},"GenericSkeleton",[85,12927,10302],{"class":109},[85,12929,12930],{"class":87,"line":271},[85,12931,4736],{"class":109},[85,12933,12934],{"class":87,"line":277},[85,12935,449],{"emptyLinePlaceholder":327},[85,12937,12938,12940,12942,12944,12946,12948],{"class":87,"line":552},[85,12939,3690],{"class":98},[85,12941,456],{"class":102},[85,12943,106],{"class":98},[85,12945,230],{"class":98},[85,12947,2865],{"class":138},[85,12949,10909],{"class":109},[85,12951,12952,12955,12957,12959],{"class":87,"line":558},[85,12953,12954],{"class":138},"    setOptimisticUI",[85,12956,476],{"class":109},[85,12958,4601],{"class":102},[85,12960,3529],{"class":109},[85,12962,12963,12966],{"class":87,"line":588},[85,12964,12965],{"class":138},"    setUI",[85,12967,12968],{"class":109},"(result);\n",[85,12970,12971],{"class":87,"line":627},[85,12972,3776],{"class":109},[85,12974,12975],{"class":87,"line":633},[85,12976,449],{"emptyLinePlaceholder":327},[85,12978,12979,12981,12984,12986],{"class":87,"line":638},[85,12980,1460],{"class":98},[85,12982,12983],{"class":109}," { ui: optimisticUI ",[85,12985,6159],{"class":98},[85,12987,12988],{"class":109}," ui, generate };\n",[85,12990,12991],{"class":87,"line":644},[85,12992,280],{"class":109},[17,12994,12995],{},"Simple keyword matching on the client is zero-latency. Showing a weather skeleton the instant the user submits a weather query feels significantly faster than waiting for the server round-trip.",[12,12997,12999],{"id":12998},"core-web-vitals-impact","Core Web Vitals Impact",[17,13001,13002],{},"Generative UI affects your Core Web Vitals. Here is what to watch:",[17,13004,13005,13008],{},[39,13006,13007],{},"Largest Contentful Paint (LCP):"," If your main content is AI-generated, LCP will reflect the full generation time. Mitigate by generating above-the-fold content first and using streaming to progressively paint the page.",[17,13010,13011,13014,13015,13018],{},[39,13012,13013],{},"Cumulative Layout Shift (CLS):"," The biggest risk. If your skeletons do not match component sizes, every component load causes layout shift. Use ",[82,13016,13017],{},"min-height"," on skeleton containers to reserve space.",[17,13020,13021,13024],{},[39,13022,13023],{},"Interaction to Next Paint (INP):"," Make sure AI generation is triggered by user actions (button clicks, form submits), not passive page load. Passive generation can block interaction handling.",[17,13026,13027,13030,13031,13033],{},[39,13028,13029],{},"First Input Delay \u002F INP:"," Do not run ",[82,13032,781],{}," directly in a React event handler. It is a long-running async operation. Keep the event handler fast:",[75,13035,13037],{"className":77,"code":13036,"language":79,"meta":80,"style":80},"\u002F\u002F Potentially slow: streamUI blocks the handler\nasync function handleSubmit(e: React.FormEvent) {\n  e.preventDefault();\n  const result = await streamUI({ ... }); \u002F\u002F blocks\n  setUI(result.value);\n}\n\n\u002F\u002F Better: kick off async, update state when ready\nfunction handleSubmit(e: React.FormEvent) {\n  e.preventDefault();\n  setLoading(true);\n  generateUI(prompt).then(ui => {\n    setUI(ui);\n    setLoading(false);\n  });\n}\n",[82,13038,13039,13044,13066,13075,13097,13105,13109,13113,13118,13138,13146,13157,13175,13182,13192,13196],{"__ignoreMap":80},[85,13040,13041],{"class":87,"line":88},[85,13042,13043],{"class":91},"\u002F\u002F Potentially slow: streamUI blocks the handler\n",[85,13045,13046,13048,13050,13052,13054,13056,13058,13060,13062,13064],{"class":87,"line":95},[85,13047,196],{"class":98},[85,13049,1426],{"class":98},[85,13051,3625],{"class":138},[85,13053,476],{"class":109},[85,13055,3630],{"class":202},[85,13057,1967],{"class":98},[85,13059,3575],{"class":138},[85,13061,3578],{"class":109},[85,13063,3639],{"class":138},[85,13065,2101],{"class":109},[85,13067,13068,13071,13073],{"class":87,"line":113},[85,13069,13070],{"class":109},"  e.",[85,13072,3649],{"class":138},[85,13074,3652],{"class":109},[85,13076,13077,13079,13081,13083,13085,13087,13089,13091,13094],{"class":87,"line":119},[85,13078,1443],{"class":98},[85,13080,456],{"class":102},[85,13082,106],{"class":98},[85,13084,230],{"class":98},[85,13086,463],{"class":138},[85,13088,1432],{"class":109},[85,13090,3129],{"class":98},[85,13092,13093],{"class":109}," }); ",[85,13095,13096],{"class":91},"\u002F\u002F blocks\n",[85,13098,13099,13102],{"class":87,"line":132},[85,13100,13101],{"class":138},"  setUI",[85,13103,13104],{"class":109},"(result.value);\n",[85,13106,13107],{"class":87,"line":145},[85,13108,280],{"class":109},[85,13110,13111],{"class":87,"line":157},[85,13112,449],{"emptyLinePlaceholder":327},[85,13114,13115],{"class":87,"line":181},[85,13116,13117],{"class":91},"\u002F\u002F Better: kick off async, update state when ready\n",[85,13119,13120,13122,13124,13126,13128,13130,13132,13134,13136],{"class":87,"line":187},[85,13121,5600],{"class":98},[85,13123,3625],{"class":138},[85,13125,476],{"class":109},[85,13127,3630],{"class":202},[85,13129,1967],{"class":98},[85,13131,3575],{"class":138},[85,13133,3578],{"class":109},[85,13135,3639],{"class":138},[85,13137,2101],{"class":109},[85,13139,13140,13142,13144],{"class":87,"line":219},[85,13141,13070],{"class":109},[85,13143,3649],{"class":138},[85,13145,3652],{"class":109},[85,13147,13148,13151,13153,13155],{"class":87,"line":239},[85,13149,13150],{"class":138},"  setLoading",[85,13152,476],{"class":109},[85,13154,3719],{"class":102},[85,13156,3529],{"class":109},[85,13158,13159,13162,13164,13167,13169,13171,13173],{"class":87,"line":265},[85,13160,13161],{"class":138},"  generateUI",[85,13163,10813],{"class":109},[85,13165,13166],{"class":138},"then",[85,13168,476],{"class":109},[85,13170,3570],{"class":202},[85,13172,3754],{"class":98},[85,13174,110],{"class":109},[85,13176,13177,13179],{"class":87,"line":271},[85,13178,12965],{"class":138},[85,13180,13181],{"class":109},"(ui);\n",[85,13183,13184,13186,13188,13190],{"class":87,"line":277},[85,13185,3714],{"class":138},[85,13187,476],{"class":109},[85,13189,3609],{"class":102},[85,13191,3529],{"class":109},[85,13193,13194],{"class":87,"line":552},[85,13195,3327],{"class":109},[85,13197,13198],{"class":87,"line":558},[85,13199,280],{"class":109},[12,13201,13203],{"id":13202},"measuring-what-youre-optimizing","Measuring What You're Optimizing",[17,13205,13206],{},"Without measurement, optimization is guesswork. Add performance tracking from the start:",[75,13208,13210],{"className":77,"code":13209,"language":79,"meta":80,"style":80},"export async function generateUIWithMetrics(prompt: string) {\n  const startTime = performance.now();\n\n  const result = await streamUI({\n    \u002F* ... *\u002F\n    onFinish: ({ toolCalls }) => {\n      const totalTime = performance.now() - startTime;\n\n      \u002F\u002F Send to your analytics \u002F observability platform\n      track('genui.generation_complete', {\n        prompt_length: prompt.length,\n        tool_calls_count: toolCalls.length,\n        total_ms: Math.round(totalTime),\n        tools_used: toolCalls.map(c => c.toolName),\n      });\n    },\n  });\n\n  return result.value;\n}\n",[82,13211,13212,13233,13249,13253,13267,13272,13288,13308,13312,13317,13330,13339,13348,13359,13375,13380,13384,13388,13392,13398],{"__ignoreMap":80},[85,13213,13214,13216,13218,13220,13223,13225,13227,13229,13231],{"class":87,"line":88},[85,13215,1420],{"class":98},[85,13217,1423],{"class":98},[85,13219,1426],{"class":98},[85,13221,13222],{"class":138}," generateUIWithMetrics",[85,13224,476],{"class":109},[85,13226,2870],{"class":202},[85,13228,1967],{"class":98},[85,13230,2021],{"class":102},[85,13232,2101],{"class":109},[85,13234,13235,13237,13240,13242,13245,13247],{"class":87,"line":95},[85,13236,1443],{"class":98},[85,13238,13239],{"class":102}," startTime",[85,13241,106],{"class":98},[85,13243,13244],{"class":109}," performance.",[85,13246,12246],{"class":138},[85,13248,3652],{"class":109},[85,13250,13251],{"class":87,"line":113},[85,13252,449],{"emptyLinePlaceholder":327},[85,13254,13255,13257,13259,13261,13263,13265],{"class":87,"line":119},[85,13256,1443],{"class":98},[85,13258,456],{"class":102},[85,13260,106],{"class":98},[85,13262,230],{"class":98},[85,13264,463],{"class":138},[85,13266,142],{"class":109},[85,13268,13269],{"class":87,"line":132},[85,13270,13271],{"class":91},"    \u002F* ... *\u002F\n",[85,13273,13274,13277,13279,13282,13284,13286],{"class":87,"line":145},[85,13275,13276],{"class":138},"    onFinish",[85,13278,7731],{"class":109},[85,13280,13281],{"class":202},"toolCalls",[85,13283,211],{"class":109},[85,13285,214],{"class":98},[85,13287,110],{"class":109},[85,13289,13290,13292,13295,13297,13299,13301,13303,13305],{"class":87,"line":157},[85,13291,222],{"class":98},[85,13293,13294],{"class":102}," totalTime",[85,13296,106],{"class":98},[85,13298,13244],{"class":109},[85,13300,12246],{"class":138},[85,13302,3671],{"class":109},[85,13304,8700],{"class":98},[85,13306,13307],{"class":109}," startTime;\n",[85,13309,13310],{"class":87,"line":181},[85,13311,449],{"emptyLinePlaceholder":327},[85,13313,13314],{"class":87,"line":187},[85,13315,13316],{"class":91},"      \u002F\u002F Send to your analytics \u002F observability platform\n",[85,13318,13319,13322,13324,13327],{"class":87,"line":219},[85,13320,13321],{"class":138},"      track",[85,13323,476],{"class":109},[85,13325,13326],{"class":125},"'genui.generation_complete'",[85,13328,13329],{"class":109},", {\n",[85,13331,13332,13335,13337],{"class":87,"line":239},[85,13333,13334],{"class":109},"        prompt_length: prompt.",[85,13336,9545],{"class":102},[85,13338,129],{"class":109},[85,13340,13341,13344,13346],{"class":87,"line":265},[85,13342,13343],{"class":109},"        tool_calls_count: toolCalls.",[85,13345,9545],{"class":102},[85,13347,129],{"class":109},[85,13349,13350,13353,13356],{"class":87,"line":271},[85,13351,13352],{"class":109},"        total_ms: Math.",[85,13354,13355],{"class":138},"round",[85,13357,13358],{"class":109},"(totalTime),\n",[85,13360,13361,13364,13366,13368,13370,13372],{"class":87,"line":277},[85,13362,13363],{"class":109},"        tools_used: toolCalls.",[85,13365,3892],{"class":138},[85,13367,476],{"class":109},[85,13369,9897],{"class":202},[85,13371,3754],{"class":98},[85,13373,13374],{"class":109}," c.toolName),\n",[85,13376,13377],{"class":87,"line":552},[85,13378,13379],{"class":109},"      });\n",[85,13381,13382],{"class":87,"line":558},[85,13383,268],{"class":109},[85,13385,13386],{"class":87,"line":588},[85,13387,3327],{"class":109},[85,13389,13390],{"class":87,"line":627},[85,13391,449],{"emptyLinePlaceholder":327},[85,13393,13394,13396],{"class":87,"line":633},[85,13395,1460],{"class":98},[85,13397,3340],{"class":109},[85,13399,13400],{"class":87,"line":638},[85,13401,280],{"class":109},[17,13403,13404],{},"Track TTFC and TTIC separately by timing the skeleton yield and the final component return. After a week of data, you will have a clear picture of where time is actually going.",[1138,13406],{},[17,13408,13409],{},[28,13410,13411,13412,13415],{},"Working on GenUI performance challenges? ",[291,13413,13414],{"href":308},"Let's talk"," — optimization across the full stack is a specialty.",[312,13417,13418],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":80,"searchDepth":95,"depth":95,"links":13420},[13421,13422,13423,13424,13425,13426,13427,13428,13429,13430],{"id":11305,"depth":95,"text":11306},{"id":11318,"depth":95,"text":11319},{"id":11349,"depth":95,"text":11350},{"id":11616,"depth":95,"text":11617},{"id":11925,"depth":95,"text":11926},{"id":12329,"depth":95,"text":12330},{"id":12506,"depth":95,"text":12507},{"id":12703,"depth":95,"text":12704},{"id":12998,"depth":95,"text":12999},{"id":13202,"depth":95,"text":13203},"2026-01-30","How to keep AI-powered interfaces fast: streaming strategies, bundle optimization, and rendering patterns.",{"featured":326},"\u002Flearn\u002Fperformance-optimization-genui","11 min read",{"title":11300,"description":13432},"learn\u002Fperformance-optimization-genui",[13439,13440,4885,333],"performance","optimization","evO5tUbl1P_NLEGj-lG9YpyalyeecHYinBi8dLok14c",{"id":13443,"title":13444,"author":7,"body":13445,"category":321,"date":15295,"description":15296,"extension":324,"meta":15297,"navigation":327,"path":15298,"readTime":15299,"seo":15300,"stem":15301,"tags":15302,"__hash__":15306},"content\u002Flearn\u002Fgenerative-ui-accessibility-guide.md","Generative UI Accessibility: Making AI Interfaces Inclusive",{"type":9,"value":13446,"toc":15284},[13447,13451,13454,13457,13460,13463,13467,13470,13492,13558,13568,13579,13763,13769,13775,13779,13782,13789,13927,13930,13958,13961,14052,14061,14065,14068,14073,14266,14275,14280,14283,14287,14290,14300,14310,14642,14652,14656,14659,14732,14742,14762,14770,14774,14777,14780,14964,14967,15072,15076,15079,15085,15207,15213,15219,15225,15229,15232,15268,15271,15273,15281],[12,13448,13450],{"id":13449},"why-accessibility-is-harder-with-generative-ui","Why Accessibility is Harder with Generative UI",[17,13452,13453],{},"In a traditional UI, an engineer can audit every screen and verify it meets WCAG requirements. The screen count is finite. The accessibility team knows exactly what to test.",[17,13455,13456],{},"Generative UI breaks this model. The set of possible interfaces is not enumerable — the AI can compose components in ways no human explicitly designed. A screen that passes accessibility review today might combine with a newly added component tomorrow to produce an inaccessible layout.",[17,13458,13459],{},"The solution is to push accessibility requirements down to the component level. If every component in your library is individually accessible, any composition of them will be accessible — provided the composition itself is structured correctly (more on that below).",[17,13461,13462],{},"This is actually a cleaner model than manually auditing every screen. It is also non-negotiable: the AI will not add ARIA labels or manage focus for you. The component library is your only leverage point.",[12,13464,13466],{"id":13465},"the-component-level-baseline","The Component-Level Baseline",[17,13468,13469],{},"Every component in your generative UI tool registry must meet these requirements independently:",[17,13471,13472,13475,13476,13479,13480,13483,13484,13487,13488,13491],{},[39,13473,13474],{},"Semantic HTML first."," Use ",[82,13477,13478],{},"\u003Cbutton>"," for buttons, ",[82,13481,13482],{},"\u003Cnav>"," for navigation, ",[82,13485,13486],{},"\u003Ctable>"," for tabular data. Do not use ",[82,13489,13490],{},"\u003Cdiv onClick={...}>"," when a semantic element works.",[75,13493,13495],{"className":1992,"code":13494,"language":1994,"meta":80,"style":80},"\u002F\u002F Wrong: div masquerading as a button\n\u003Cdiv className=\"button\" onClick={handleClick}>Submit\u003C\u002Fdiv>\n\n\u002F\u002F Right: actual button element\n\u003Cbutton type=\"button\" onClick={handleClick}>Submit\u003C\u002Fbutton>\n",[82,13496,13497,13502,13527,13531,13536],{"__ignoreMap":80},[85,13498,13499],{"class":87,"line":88},[85,13500,13501],{"class":91},"\u002F\u002F Wrong: div masquerading as a button\n",[85,13503,13504,13506,13508,13510,13512,13515,13518,13520,13523,13525],{"class":87,"line":95},[85,13505,3552],{"class":109},[85,13507,2117],{"class":2116},[85,13509,2120],{"class":138},[85,13511,253],{"class":98},[85,13513,13514],{"class":125},"\"button\"",[85,13516,13517],{"class":138}," onClick",[85,13519,253],{"class":98},[85,13521,13522],{"class":109},"{handleClick}>Submit\u003C\u002F",[85,13524,2117],{"class":2116},[85,13526,2128],{"class":109},[85,13528,13529],{"class":87,"line":113},[85,13530,449],{"emptyLinePlaceholder":327},[85,13532,13533],{"class":87,"line":119},[85,13534,13535],{"class":91},"\u002F\u002F Right: actual button element\n",[85,13537,13538,13540,13542,13544,13546,13548,13550,13552,13554,13556],{"class":87,"line":132},[85,13539,3552],{"class":109},[85,13541,3964],{"class":2116},[85,13543,5495],{"class":138},[85,13545,253],{"class":98},[85,13547,13514],{"class":125},[85,13549,13517],{"class":138},[85,13551,253],{"class":98},[85,13553,13522],{"class":109},[85,13555,3964],{"class":2116},[85,13557,2128],{"class":109},[17,13559,13560,13563,13564,13567],{},[39,13561,13562],{},"All images have alt text."," For decorative images: ",[82,13565,13566],{},"alt=\"\"",". For informational images, write a description.",[17,13569,13570,13573,13574,7436,13576,13578],{},[39,13571,13572],{},"Color is not the only signal."," A chart that shows positive values in green and negative in red needs another indicator for users who cannot distinguish red from green — a ",[82,13575,12097],{},[82,13577,8700],{}," sign, an icon, or a text label.",[75,13580,13582],{"className":1992,"code":13581,"language":1994,"meta":80,"style":80},"function TrendIndicator({ value }: { value: number }) {\n  const isPositive = value >= 0;\n  return (\n    \u003Cspan\n      className={isPositive ? 'text-green-600' : 'text-red-600'}\n      aria-label={isPositive ? `Up ${Math.abs(value)}%` : `Down ${Math.abs(value)}%`}\n    >\n      {\u002F* Icon provides visual signal beyond color *\u002F}\n      {isPositive ? '↑' : '↓'} {Math.abs(value)}%\n    \u003C\u002Fspan>\n  );\n}\n",[82,13583,13584,13610,13627,13633,13640,13659,13710,13715,13724,13747,13755,13759],{"__ignoreMap":80},[85,13585,13586,13588,13591,13593,13596,13598,13600,13602,13604,13606,13608],{"class":87,"line":88},[85,13587,5600],{"class":98},[85,13589,13590],{"class":138}," TrendIndicator",[85,13592,1432],{"class":109},[85,13594,13595],{"class":202},"value",[85,13597,2094],{"class":109},[85,13599,1967],{"class":98},[85,13601,2699],{"class":109},[85,13603,13595],{"class":202},[85,13605,1967],{"class":98},[85,13607,2033],{"class":102},[85,13609,1438],{"class":109},[85,13611,13612,13614,13616,13618,13621,13623,13625],{"class":87,"line":95},[85,13613,1443],{"class":98},[85,13615,2375],{"class":102},[85,13617,106],{"class":98},[85,13619,13620],{"class":109}," value ",[85,13622,2383],{"class":98},[85,13624,2386],{"class":102},[85,13626,416],{"class":109},[85,13628,13629,13631],{"class":87,"line":113},[85,13630,1460],{"class":98},[85,13632,2108],{"class":109},[85,13634,13635,13637],{"class":87,"line":119},[85,13636,2113],{"class":109},[85,13638,13639],{"class":2116},"span\n",[85,13641,13642,13644,13646,13649,13651,13653,13655,13657],{"class":87,"line":132},[85,13643,6140],{"class":138},[85,13645,253],{"class":98},[85,13647,13648],{"class":109},"{isPositive ",[85,13650,2403],{"class":98},[85,13652,2430],{"class":125},[85,13654,2409],{"class":98},[85,13656,2435],{"class":125},[85,13658,280],{"class":109},[85,13660,13661,13663,13665,13667,13669,13672,13675,13677,13680,13682,13684,13686,13689,13691,13694,13696,13698,13700,13702,13704,13706,13708],{"class":87,"line":145},[85,13662,6169],{"class":138},[85,13664,253],{"class":98},[85,13666,13648],{"class":109},[85,13668,2403],{"class":98},[85,13670,13671],{"class":125}," `Up ${",[85,13673,13674],{"class":109},"Math",[85,13676,3578],{"class":125},[85,13678,13679],{"class":138},"abs",[85,13681,476],{"class":125},[85,13683,13595],{"class":109},[85,13685,9744],{"class":125},[85,13687,13688],{"class":125},"}%`",[85,13690,2409],{"class":98},[85,13692,13693],{"class":125}," `Down ${",[85,13695,13674],{"class":109},[85,13697,3578],{"class":125},[85,13699,13679],{"class":138},[85,13701,476],{"class":125},[85,13703,13595],{"class":109},[85,13705,9744],{"class":125},[85,13707,13688],{"class":125},[85,13709,280],{"class":109},[85,13711,13712],{"class":87,"line":157},[85,13713,13714],{"class":109},"    >\n",[85,13716,13717,13719,13722],{"class":87,"line":181},[85,13718,3859],{"class":109},[85,13720,13721],{"class":91},"\u002F* Icon provides visual signal beyond color *\u002F",[85,13723,280],{"class":109},[85,13725,13726,13729,13731,13734,13736,13739,13742,13744],{"class":87,"line":187},[85,13727,13728],{"class":109},"      {isPositive ",[85,13730,2403],{"class":98},[85,13732,13733],{"class":125}," '↑'",[85,13735,2409],{"class":98},[85,13737,13738],{"class":125}," '↓'",[85,13740,13741],{"class":109},"} {Math.",[85,13743,13679],{"class":138},[85,13745,13746],{"class":109},"(value)}%\n",[85,13748,13749,13751,13753],{"class":87,"line":219},[85,13750,2247],{"class":109},[85,13752,85],{"class":2116},[85,13754,2128],{"class":109},[85,13756,13757],{"class":87,"line":239},[85,13758,2256],{"class":109},[85,13760,13761],{"class":87,"line":265},[85,13762,280],{"class":109},[17,13764,13765,13768],{},[39,13766,13767],{},"Interactive elements are keyboard reachable."," Every button, link, and form control in your components must be focusable and operable with keyboard alone.",[17,13770,13771,13774],{},[39,13772,13773],{},"Touch targets are large enough."," WCAG 2.5.5 (AA) requires 44×44 CSS pixels for interactive elements. On mobile, small tap targets are a primary source of accessibility failures.",[12,13776,13778],{"id":13777},"aria-live-regions-for-streaming-content","ARIA Live Regions for Streaming Content",[17,13780,13781],{},"Streaming is the defining feature of Generative UI — components appear progressively as the AI generates them. Screen readers do not automatically announce content that appears dynamically. You must tell them.",[17,13783,13784,13785,13788],{},"Use ",[82,13786,13787],{},"aria-live"," to announce when new generated content arrives:",[75,13790,13792],{"className":1992,"code":13791,"language":1994,"meta":80,"style":80},"\u002F\u002F components\u002Fgenui-output-region.tsx\nexport function GenUIOutputRegion({ children, isLoading }: {\n  children: React.ReactNode;\n  isLoading: boolean;\n}) {\n  return (\n    \u003Cdiv\n      aria-live=\"polite\"\n      aria-busy={isLoading}\n      aria-label=\"AI-generated content\"\n      aria-atomic=\"false\"\n    >\n      {children}\n    \u003C\u002Fdiv>\n  );\n}\n",[82,13793,13794,13799,13823,13838,13849,13853,13859,13865,13875,13884,13893,13903,13907,13911,13919,13923],{"__ignoreMap":80},[85,13795,13796],{"class":87,"line":88},[85,13797,13798],{"class":91},"\u002F\u002F components\u002Fgenui-output-region.tsx\n",[85,13800,13801,13803,13805,13808,13810,13812,13814,13817,13819,13821],{"class":87,"line":95},[85,13802,1420],{"class":98},[85,13804,1426],{"class":98},[85,13806,13807],{"class":138}," GenUIOutputRegion",[85,13809,1432],{"class":109},[85,13811,4480],{"class":202},[85,13813,172],{"class":109},[85,13815,13816],{"class":202},"isLoading",[85,13818,2094],{"class":109},[85,13820,1967],{"class":98},[85,13822,110],{"class":109},[85,13824,13825,13828,13830,13832,13834,13836],{"class":87,"line":113},[85,13826,13827],{"class":202},"  children",[85,13829,1967],{"class":98},[85,13831,3575],{"class":138},[85,13833,3578],{"class":109},[85,13835,3581],{"class":138},[85,13837,416],{"class":109},[85,13839,13840,13843,13845,13847],{"class":87,"line":119},[85,13841,13842],{"class":202},"  isLoading",[85,13844,1967],{"class":98},[85,13846,4505],{"class":102},[85,13848,416],{"class":109},[85,13850,13851],{"class":87,"line":132},[85,13852,6405],{"class":109},[85,13854,13855,13857],{"class":87,"line":145},[85,13856,1460],{"class":98},[85,13858,2108],{"class":109},[85,13860,13861,13863],{"class":87,"line":157},[85,13862,2113],{"class":109},[85,13864,6135],{"class":2116},[85,13866,13867,13870,13872],{"class":87,"line":181},[85,13868,13869],{"class":138},"      aria-live",[85,13871,253],{"class":98},[85,13873,13874],{"class":125},"\"polite\"\n",[85,13876,13877,13879,13881],{"class":87,"line":187},[85,13878,6179],{"class":138},[85,13880,253],{"class":98},[85,13882,13883],{"class":109},"{isLoading}\n",[85,13885,13886,13888,13890],{"class":87,"line":219},[85,13887,6169],{"class":138},[85,13889,253],{"class":98},[85,13891,13892],{"class":125},"\"AI-generated content\"\n",[85,13894,13895,13898,13900],{"class":87,"line":239},[85,13896,13897],{"class":138},"      aria-atomic",[85,13899,253],{"class":98},[85,13901,13902],{"class":125},"\"false\"\n",[85,13904,13905],{"class":87,"line":265},[85,13906,13714],{"class":109},[85,13908,13909],{"class":87,"line":271},[85,13910,6594],{"class":109},[85,13912,13913,13915,13917],{"class":87,"line":277},[85,13914,2247],{"class":109},[85,13916,2117],{"class":2116},[85,13918,2128],{"class":109},[85,13920,13921],{"class":87,"line":552},[85,13922,2256],{"class":109},[85,13924,13925],{"class":87,"line":558},[85,13926,280],{"class":109},[17,13928,13929],{},"Key choices here:",[33,13931,13932,13942,13952],{},[36,13933,13934,13937,13938,13941],{},[82,13935,13936],{},"aria-live=\"polite\""," announces new content at the next idle moment — not interrupting the user mid-sentence like ",[82,13939,13940],{},"assertive"," would.",[36,13943,13944,13947,13948,13951],{},[82,13945,13946],{},"aria-busy={isLoading}"," tells assistive tech the region is updating. Screen readers hold announcements until ",[82,13949,13950],{},"aria-busy"," becomes false.",[36,13953,13954,13957],{},[82,13955,13956],{},"aria-atomic=\"false\""," announces individual additions as they arrive, rather than re-reading the entire region each time.",[17,13959,13960],{},"For the loading skeleton state:",[75,13962,13964],{"className":1992,"code":13963,"language":1994,"meta":80,"style":80},"function LoadingSkeleton({ label }: { label: string }) {\n  return (\n    \u003Cdiv\n      role=\"status\"\n      aria-label={`Loading ${label}`}\n      className=\"animate-pulse rounded-lg bg-muted h-32\"\n    \u002F>\n  );\n}\n",[82,13965,13966,13992,13998,14004,14014,14031,14040,14044,14048],{"__ignoreMap":80},[85,13967,13968,13970,13973,13975,13978,13980,13982,13984,13986,13988,13990],{"class":87,"line":88},[85,13969,5600],{"class":98},[85,13971,13972],{"class":138}," LoadingSkeleton",[85,13974,1432],{"class":109},[85,13976,13977],{"class":202},"label",[85,13979,2094],{"class":109},[85,13981,1967],{"class":98},[85,13983,2699],{"class":109},[85,13985,13977],{"class":202},[85,13987,1967],{"class":98},[85,13989,2021],{"class":102},[85,13991,1438],{"class":109},[85,13993,13994,13996],{"class":87,"line":95},[85,13995,1460],{"class":98},[85,13997,2108],{"class":109},[85,13999,14000,14002],{"class":87,"line":113},[85,14001,2113],{"class":109},[85,14003,6135],{"class":2116},[85,14005,14006,14009,14011],{"class":87,"line":119},[85,14007,14008],{"class":138},"      role",[85,14010,253],{"class":98},[85,14012,14013],{"class":125},"\"status\"\n",[85,14015,14016,14018,14020,14022,14025,14027,14029],{"class":87,"line":132},[85,14017,6169],{"class":138},[85,14019,253],{"class":98},[85,14021,256],{"class":109},[85,14023,14024],{"class":125},"`Loading ${",[85,14026,13977],{"class":109},[85,14028,2517],{"class":125},[85,14030,280],{"class":109},[85,14032,14033,14035,14037],{"class":87,"line":145},[85,14034,6140],{"class":138},[85,14036,253],{"class":98},[85,14038,14039],{"class":125},"\"animate-pulse rounded-lg bg-muted h-32\"\n",[85,14041,14042],{"class":87,"line":157},[85,14043,6189],{"class":109},[85,14045,14046],{"class":87,"line":181},[85,14047,2256],{"class":109},[85,14049,14050],{"class":87,"line":187},[85,14051,280],{"class":109},[17,14053,14054,14057,14058,14060],{},[82,14055,14056],{},"role=\"status\""," is an implicit ",[82,14059,13936],{}," region for short status messages. It announces when it appears without interrupting current speech.",[12,14062,14064],{"id":14063},"focus-management","Focus Management",[17,14066,14067],{},"When generated content appears, keyboard focus stays where it was. Usually this is correct — you do not want focus jumping around as the AI streams in components. But for some interactions, you need to move focus explicitly.",[17,14069,14070],{},[39,14071,14072],{},"After a form submission that replaces page content:",[75,14074,14076],{"className":1992,"code":14075,"language":1994,"meta":80,"style":80},"const outputRef = useRef\u003CHTMLDivElement>(null);\n\nasync function handleSubmit(e: React.FormEvent) {\n  e.preventDefault();\n  const ui = await generateUI(prompt);\n  setGeneratedUI(ui);\n\n  \u002F\u002F Move focus to the output area after generation completes\n  \u002F\u002F Small timeout ensures the DOM has updated\n  setTimeout(() => {\n    outputRef.current?.focus();\n  }, 50);\n}\n\n\u002F\u002F Add tabIndex to make the div focusable\n\u003Cdiv ref={outputRef} tabIndex={-1} aria-label=\"Generated results\">\n  {generatedUI}\n\u003C\u002Fdiv>\n",[82,14077,14078,14101,14105,14127,14135,14149,14156,14160,14165,14170,14181,14191,14201,14205,14209,14214,14252,14257],{"__ignoreMap":80},[85,14079,14080,14082,14085,14087,14090,14092,14095,14097,14099],{"class":87,"line":88},[85,14081,99],{"class":98},[85,14083,14084],{"class":102}," outputRef",[85,14086,106],{"class":98},[85,14088,14089],{"class":138}," useRef",[85,14091,3552],{"class":109},[85,14093,14094],{"class":138},"HTMLDivElement",[85,14096,6722],{"class":109},[85,14098,4601],{"class":102},[85,14100,3529],{"class":109},[85,14102,14103],{"class":87,"line":95},[85,14104,449],{"emptyLinePlaceholder":327},[85,14106,14107,14109,14111,14113,14115,14117,14119,14121,14123,14125],{"class":87,"line":113},[85,14108,196],{"class":98},[85,14110,1426],{"class":98},[85,14112,3625],{"class":138},[85,14114,476],{"class":109},[85,14116,3630],{"class":202},[85,14118,1967],{"class":98},[85,14120,3575],{"class":138},[85,14122,3578],{"class":109},[85,14124,3639],{"class":138},[85,14126,2101],{"class":109},[85,14128,14129,14131,14133],{"class":87,"line":119},[85,14130,13070],{"class":109},[85,14132,3649],{"class":138},[85,14134,3652],{"class":109},[85,14136,14137,14139,14141,14143,14145,14147],{"class":87,"line":132},[85,14138,1443],{"class":98},[85,14140,3732],{"class":102},[85,14142,106],{"class":98},[85,14144,230],{"class":98},[85,14146,2865],{"class":138},[85,14148,10909],{"class":109},[85,14150,14151,14154],{"class":87,"line":145},[85,14152,14153],{"class":138},"  setGeneratedUI",[85,14155,13181],{"class":109},[85,14157,14158],{"class":87,"line":157},[85,14159,449],{"emptyLinePlaceholder":327},[85,14161,14162],{"class":87,"line":181},[85,14163,14164],{"class":91},"  \u002F\u002F Move focus to the output area after generation completes\n",[85,14166,14167],{"class":87,"line":187},[85,14168,14169],{"class":91},"  \u002F\u002F Small timeout ensures the DOM has updated\n",[85,14171,14172,14175,14177,14179],{"class":87,"line":219},[85,14173,14174],{"class":138},"  setTimeout",[85,14176,10051],{"class":109},[85,14178,214],{"class":98},[85,14180,110],{"class":109},[85,14182,14183,14186,14189],{"class":87,"line":239},[85,14184,14185],{"class":109},"    outputRef.current?.",[85,14187,14188],{"class":138},"focus",[85,14190,3652],{"class":109},[85,14192,14193,14196,14199],{"class":87,"line":265},[85,14194,14195],{"class":109},"  }, ",[85,14197,14198],{"class":102},"50",[85,14200,3529],{"class":109},[85,14202,14203],{"class":87,"line":271},[85,14204,280],{"class":109},[85,14206,14207],{"class":87,"line":277},[85,14208,449],{"emptyLinePlaceholder":327},[85,14210,14211],{"class":87,"line":552},[85,14212,14213],{"class":91},"\u002F\u002F Add tabIndex to make the div focusable\n",[85,14215,14216,14218,14220,14223,14225,14228,14231,14233,14235,14237,14240,14242,14245,14247,14250],{"class":87,"line":558},[85,14217,3552],{"class":109},[85,14219,2117],{"class":2116},[85,14221,14222],{"class":138}," ref",[85,14224,253],{"class":98},[85,14226,14227],{"class":109},"{outputRef} ",[85,14229,14230],{"class":138},"tabIndex",[85,14232,253],{"class":98},[85,14234,256],{"class":109},[85,14236,8700],{"class":98},[85,14238,14239],{"class":102},"1",[85,14241,606],{"class":109},[85,14243,14244],{"class":138},"aria-label",[85,14246,253],{"class":98},[85,14248,14249],{"class":125},"\"Generated results\"",[85,14251,2128],{"class":109},[85,14253,14254],{"class":87,"line":588},[85,14255,14256],{"class":109},"  {generatedUI}\n",[85,14258,14259,14262,14264],{"class":87,"line":627},[85,14260,14261],{"class":109},"\u003C\u002F",[85,14263,2117],{"class":2116},[85,14265,2128],{"class":109},[17,14267,14268,14271,14272,3578],{},[82,14269,14270],{},"tabIndex={-1}"," makes the element programmatically focusable without adding it to the tab order. The user can tab past it naturally, but you can focus it with ",[82,14273,14274],{},".focus()",[17,14276,14277],{},[39,14278,14279],{},"After dialog or panel opens with generated content:",[17,14281,14282],{},"Move focus to the first focusable element inside the panel, or to the panel's heading. Return focus to the trigger element when the panel closes.",[12,14284,14286],{"id":14285},"keyboard-navigation-in-generated-components","Keyboard Navigation in Generated Components",[17,14288,14289],{},"Components that appear in generated layouts must be fully keyboard navigable. Audit each component:",[17,14291,14292,14295,14296,14299],{},[39,14293,14294],{},"Tables:"," Arrow key navigation within table cells is expected by screen reader users. If your ",[82,14297,14298],{},"DataTable"," component does not implement this, it is a keyboard barrier for complex tables.",[17,14301,14302,14305,14306,14309],{},[39,14303,14304],{},"Charts:"," Provide a tabular alternative. SVG charts are visually rich but nearly meaningless to screen readers. Add a ",[82,14307,14308],{},"\u003Cdetails>"," element or a visually-hidden table with the chart data.",[75,14311,14313],{"className":1992,"code":14312,"language":1994,"meta":80,"style":80},"function BarChart({ title, data }: BarChartProps) {\n  return (\n    \u003Cdiv>\n      \u003Ch3>{title}\u003C\u002Fh3>\n      {\u002F* Visual chart *\u002F}\n      \u003Csvg aria-hidden=\"true\">\n        {\u002F* ... chart rendering ... *\u002F}\n      \u003C\u002Fsvg>\n      {\u002F* Accessible data table, visually hidden *\u002F}\n      \u003Cdetails className=\"sr-only\">\n        \u003Csummary>View data as table\u003C\u002Fsummary>\n        \u003Ctable>\n          \u003Ccaption>{title}\u003C\u002Fcaption>\n          \u003Cthead>\n            \u003Ctr>\u003Cth>Category\u003C\u002Fth>\u003Cth>Value\u003C\u002Fth>\u003C\u002Ftr>\n          \u003C\u002Fthead>\n          \u003Ctbody>\n            {data.map(({ label, value }) => (\n              \u003Ctr key={label}>\n                \u003Ctd>{label}\u003C\u002Ftd>\n                \u003Ctd>{value}\u003C\u002Ftd>\n              \u003C\u002Ftr>\n            ))}\n          \u003C\u002Ftbody>\n        \u003C\u002Ftable>\n      \u003C\u002Fdetails>\n    \u003C\u002Fdiv>\n  );\n}\n",[82,14314,14315,14340,14346,14354,14367,14376,14393,14402,14410,14419,14435,14448,14456,14469,14477,14509,14517,14525,14547,14561,14575,14588,14597,14602,14610,14618,14626,14634,14638],{"__ignoreMap":80},[85,14316,14317,14319,14322,14324,14327,14329,14331,14333,14335,14338],{"class":87,"line":88},[85,14318,5600],{"class":98},[85,14320,14321],{"class":138}," BarChart",[85,14323,1432],{"class":109},[85,14325,14326],{"class":202},"title",[85,14328,172],{"class":109},[85,14330,259],{"class":202},[85,14332,2094],{"class":109},[85,14334,1967],{"class":98},[85,14336,14337],{"class":138}," BarChartProps",[85,14339,2101],{"class":109},[85,14341,14342,14344],{"class":87,"line":95},[85,14343,1460],{"class":98},[85,14345,2108],{"class":109},[85,14347,14348,14350,14352],{"class":87,"line":113},[85,14349,2113],{"class":109},[85,14351,2117],{"class":2116},[85,14353,2128],{"class":109},[85,14355,14356,14358,14360,14363,14365],{"class":87,"line":119},[85,14357,2133],{"class":109},[85,14359,773],{"class":2116},[85,14361,14362],{"class":109},">{title}\u003C\u002F",[85,14364,773],{"class":2116},[85,14366,2128],{"class":109},[85,14368,14369,14371,14374],{"class":87,"line":132},[85,14370,3859],{"class":109},[85,14372,14373],{"class":91},"\u002F* Visual chart *\u002F",[85,14375,280],{"class":109},[85,14377,14378,14380,14383,14386,14388,14391],{"class":87,"line":145},[85,14379,2133],{"class":109},[85,14381,14382],{"class":2116},"svg",[85,14384,14385],{"class":138}," aria-hidden",[85,14387,253],{"class":98},[85,14389,14390],{"class":125},"\"true\"",[85,14392,2128],{"class":109},[85,14394,14395,14397,14400],{"class":87,"line":157},[85,14396,3884],{"class":109},[85,14398,14399],{"class":91},"\u002F* ... chart rendering ... *\u002F",[85,14401,280],{"class":109},[85,14403,14404,14406,14408],{"class":87,"line":181},[85,14405,2210],{"class":109},[85,14407,14382],{"class":2116},[85,14409,2128],{"class":109},[85,14411,14412,14414,14417],{"class":87,"line":187},[85,14413,3859],{"class":109},[85,14415,14416],{"class":91},"\u002F* Accessible data table, visually hidden *\u002F",[85,14418,280],{"class":109},[85,14420,14421,14423,14426,14428,14430,14433],{"class":87,"line":219},[85,14422,2133],{"class":109},[85,14424,14425],{"class":2116},"details",[85,14427,2120],{"class":138},[85,14429,253],{"class":98},[85,14431,14432],{"class":125},"\"sr-only\"",[85,14434,2128],{"class":109},[85,14436,14437,14439,14441,14444,14446],{"class":87,"line":239},[85,14438,2169],{"class":109},[85,14440,11216],{"class":2116},[85,14442,14443],{"class":109},">View data as table\u003C\u002F",[85,14445,11216],{"class":2116},[85,14447,2128],{"class":109},[85,14449,14450,14452,14454],{"class":87,"line":265},[85,14451,2169],{"class":109},[85,14453,875],{"class":2116},[85,14455,2128],{"class":109},[85,14457,14458,14460,14463,14465,14467],{"class":87,"line":271},[85,14459,3905],{"class":109},[85,14461,14462],{"class":2116},"caption",[85,14464,14362],{"class":109},[85,14466,14462],{"class":2116},[85,14468,2128],{"class":109},[85,14470,14471,14473,14475],{"class":87,"line":277},[85,14472,3905],{"class":109},[85,14474,878],{"class":2116},[85,14476,2128],{"class":109},[85,14478,14479,14481,14483,14486,14488,14491,14493,14495,14497,14500,14502,14505,14507],{"class":87,"line":552},[85,14480,4255],{"class":109},[85,14482,881],{"class":2116},[85,14484,14485],{"class":109},">\u003C",[85,14487,884],{"class":2116},[85,14489,14490],{"class":109},">Category\u003C\u002F",[85,14492,884],{"class":2116},[85,14494,14485],{"class":109},[85,14496,884],{"class":2116},[85,14498,14499],{"class":109},">Value\u003C\u002F",[85,14501,884],{"class":2116},[85,14503,14504],{"class":109},">\u003C\u002F",[85,14506,881],{"class":2116},[85,14508,2128],{"class":109},[85,14510,14511,14513,14515],{"class":87,"line":558},[85,14512,3961],{"class":109},[85,14514,878],{"class":2116},[85,14516,2128],{"class":109},[85,14518,14519,14521,14523],{"class":87,"line":588},[85,14520,3905],{"class":109},[85,14522,894],{"class":2116},[85,14524,2128],{"class":109},[85,14526,14527,14530,14532,14535,14537,14539,14541,14543,14545],{"class":87,"line":627},[85,14528,14529],{"class":109},"            {data.",[85,14531,3892],{"class":138},[85,14533,14534],{"class":109},"(({ ",[85,14536,13977],{"class":202},[85,14538,172],{"class":109},[85,14540,13595],{"class":202},[85,14542,211],{"class":109},[85,14544,214],{"class":98},[85,14546,2108],{"class":109},[85,14548,14549,14552,14554,14556,14558],{"class":87,"line":633},[85,14550,14551],{"class":109},"              \u003C",[85,14553,881],{"class":2116},[85,14555,4244],{"class":138},[85,14557,253],{"class":98},[85,14559,14560],{"class":109},"{label}>\n",[85,14562,14563,14566,14568,14571,14573],{"class":87,"line":638},[85,14564,14565],{"class":109},"                \u003C",[85,14567,899],{"class":2116},[85,14569,14570],{"class":109},">{label}\u003C\u002F",[85,14572,899],{"class":2116},[85,14574,2128],{"class":109},[85,14576,14577,14579,14581,14584,14586],{"class":87,"line":644},[85,14578,14565],{"class":109},[85,14580,899],{"class":2116},[85,14582,14583],{"class":109},">{value}\u003C\u002F",[85,14585,899],{"class":2116},[85,14587,2128],{"class":109},[85,14589,14590,14593,14595],{"class":87,"line":654},[85,14591,14592],{"class":109},"              \u003C\u002F",[85,14594,881],{"class":2116},[85,14596,2128],{"class":109},[85,14598,14599],{"class":87,"line":663},[85,14600,14601],{"class":109},"            ))}\n",[85,14603,14604,14606,14608],{"class":87,"line":695},[85,14605,3961],{"class":109},[85,14607,894],{"class":2116},[85,14609,2128],{"class":109},[85,14611,14612,14614,14616],{"class":87,"line":700},[85,14613,2541],{"class":109},[85,14615,875],{"class":2116},[85,14617,2128],{"class":109},[85,14619,14620,14622,14624],{"class":87,"line":719},[85,14621,2210],{"class":109},[85,14623,14425],{"class":2116},[85,14625,2128],{"class":109},[85,14627,14628,14630,14632],{"class":87,"line":737},[85,14629,2247],{"class":109},[85,14631,2117],{"class":2116},[85,14633,2128],{"class":109},[85,14635,14636],{"class":87,"line":742},[85,14637,2256],{"class":109},[85,14639,14640],{"class":87,"line":747},[85,14641,280],{"class":109},[17,14643,1945,14644,14647,14648,14651],{},[82,14645,14646],{},"sr-only"," class hides the table visually while keeping it in the accessibility tree. ",[82,14649,14650],{},"aria-hidden=\"true\""," on the SVG prevents screen readers from trying to interpret the raw SVG markup.",[12,14653,14655],{"id":14654},"reduced-motion","Reduced Motion",[17,14657,14658],{},"Some users configure their operating system to prefer reduced motion — because animations cause physical discomfort for people with vestibular disorders. Loading skeletons and transition animations must respect this preference.",[75,14660,14664],{"className":14661,"code":14662,"language":14663,"meta":80,"style":80},"language-css shiki shiki-themes github-light github-dark","\u002F* In your global CSS or Tailwind config *\u002F\n@media (prefers-reduced-motion: reduce) {\n  .animate-pulse {\n    animation: none;\n  }\n\n  .transition-all {\n    transition: none;\n  }\n}\n","css",[82,14665,14666,14671,14679,14686,14698,14702,14706,14713,14724,14728],{"__ignoreMap":80},[85,14667,14668],{"class":87,"line":88},[85,14669,14670],{"class":91},"\u002F* In your global CSS or Tailwind config *\u002F\n",[85,14672,14673,14676],{"class":87,"line":95},[85,14674,14675],{"class":98},"@media",[85,14677,14678],{"class":109}," (prefers-reduced-motion: reduce) {\n",[85,14680,14681,14684],{"class":87,"line":113},[85,14682,14683],{"class":138},"  .animate-pulse",[85,14685,110],{"class":109},[85,14687,14688,14691,14693,14696],{"class":87,"line":119},[85,14689,14690],{"class":102},"    animation",[85,14692,193],{"class":109},[85,14694,14695],{"class":102},"none",[85,14697,416],{"class":109},[85,14699,14700],{"class":87,"line":132},[85,14701,3776],{"class":109},[85,14703,14704],{"class":87,"line":145},[85,14705,449],{"emptyLinePlaceholder":327},[85,14707,14708,14711],{"class":87,"line":157},[85,14709,14710],{"class":138},"  .transition-all",[85,14712,110],{"class":109},[85,14714,14715,14718,14720,14722],{"class":87,"line":181},[85,14716,14717],{"class":102},"    transition",[85,14719,193],{"class":109},[85,14721,14695],{"class":102},[85,14723,416],{"class":109},[85,14725,14726],{"class":87,"line":187},[85,14727,3776],{"class":109},[85,14729,14730],{"class":87,"line":219},[85,14731,280],{"class":109},[17,14733,14734,14735,802,14738,14741],{},"In Tailwind, you can use the ",[82,14736,14737],{},"motion-safe:",[82,14739,14740],{},"motion-reduce:"," variants:",[75,14743,14745],{"className":1992,"code":14744,"language":1994,"meta":80,"style":80},"\u003Cdiv className=\"motion-safe:animate-pulse motion-reduce:opacity-50 bg-muted rounded-lg h-32\" \u002F>\n",[82,14746,14747],{"__ignoreMap":80},[85,14748,14749,14751,14753,14755,14757,14760],{"class":87,"line":88},[85,14750,3552],{"class":109},[85,14752,2117],{"class":2116},[85,14754,2120],{"class":138},[85,14756,253],{"class":98},[85,14758,14759],{"class":125},"\"motion-safe:animate-pulse motion-reduce:opacity-50 bg-muted rounded-lg h-32\"",[85,14761,6253],{"class":109},[17,14763,14764,14766,14767,14769],{},[82,14765,14737],{}," applies only when the user has not requested reduced motion. ",[82,14768,14740],{}," applies when they have. For loading states, a static slightly-dimmed placeholder is a good reduced-motion alternative to the pulsing animation.",[12,14771,14773],{"id":14772},"heading-hierarchy-in-composed-layouts","Heading Hierarchy in Composed Layouts",[17,14775,14776],{},"The AI composes components into layouts. Each component may have its own heading. When multiple components appear together, their headings must form a coherent hierarchy — not a soup of disconnected H2s.",[17,14778,14779],{},"This is a composition problem that cannot be solved at the individual component level. Each component needs to accept a heading level prop:",[75,14781,14783],{"className":1992,"code":14782,"language":1994,"meta":80,"style":80},"interface MetricCardProps {\n  label: string;\n  value: string;\n  change: number;\n  headingLevel?: 'h2' | 'h3' | 'h4';  \u002F\u002F default to h3\n}\n\nfunction MetricCard({ label, value, change, headingLevel: Heading = 'h3' }: MetricCardProps) {\n  return (\n    \u003Cdiv className=\"rounded-lg border p-6\">\n      \u003CHeading className=\"text-sm font-medium text-muted-foreground\">{label}\u003C\u002FHeading>\n      {\u002F* ... *\u002F}\n    \u003C\u002Fdiv>\n  );\n}\n",[82,14784,14785,14794,14805,14815,14825,14851,14855,14859,14900,14906,14921,14940,14948,14956,14960],{"__ignoreMap":80},[85,14786,14787,14789,14792],{"class":87,"line":88},[85,14788,2006],{"class":98},[85,14790,14791],{"class":138}," MetricCardProps",[85,14793,110],{"class":109},[85,14795,14796,14799,14801,14803],{"class":87,"line":95},[85,14797,14798],{"class":202},"  label",[85,14800,1967],{"class":98},[85,14802,2021],{"class":102},[85,14804,416],{"class":109},[85,14806,14807,14809,14811,14813],{"class":87,"line":113},[85,14808,11967],{"class":202},[85,14810,1967],{"class":98},[85,14812,2021],{"class":102},[85,14814,416],{"class":109},[85,14816,14817,14819,14821,14823],{"class":87,"line":119},[85,14818,2306],{"class":202},[85,14820,1967],{"class":98},[85,14822,2033],{"class":102},[85,14824,416],{"class":109},[85,14826,14827,14830,14832,14835,14837,14840,14842,14845,14848],{"class":87,"line":132},[85,14828,14829],{"class":202},"  headingLevel",[85,14831,2704],{"class":98},[85,14833,14834],{"class":125}," 'h2'",[85,14836,4518],{"class":98},[85,14838,14839],{"class":125}," 'h3'",[85,14841,4518],{"class":98},[85,14843,14844],{"class":125}," 'h4'",[85,14846,14847],{"class":109},";  ",[85,14849,14850],{"class":91},"\u002F\u002F default to h3\n",[85,14852,14853],{"class":87,"line":145},[85,14854,280],{"class":109},[85,14856,14857],{"class":87,"line":157},[85,14858,449],{"emptyLinePlaceholder":327},[85,14860,14861,14863,14866,14868,14870,14872,14874,14876,14878,14880,14883,14885,14888,14890,14892,14894,14896,14898],{"class":87,"line":181},[85,14862,5600],{"class":98},[85,14864,14865],{"class":138}," MetricCard",[85,14867,1432],{"class":109},[85,14869,13977],{"class":202},[85,14871,172],{"class":109},[85,14873,13595],{"class":202},[85,14875,172],{"class":109},[85,14877,2355],{"class":202},[85,14879,172],{"class":109},[85,14881,14882],{"class":202},"headingLevel",[85,14884,193],{"class":109},[85,14886,14887],{"class":202},"Heading",[85,14889,106],{"class":98},[85,14891,14839],{"class":125},[85,14893,2094],{"class":109},[85,14895,1967],{"class":98},[85,14897,14791],{"class":138},[85,14899,2101],{"class":109},[85,14901,14902,14904],{"class":87,"line":187},[85,14903,1460],{"class":98},[85,14905,2108],{"class":109},[85,14907,14908,14910,14912,14914,14916,14919],{"class":87,"line":219},[85,14909,2113],{"class":109},[85,14911,2117],{"class":2116},[85,14913,2120],{"class":138},[85,14915,253],{"class":98},[85,14917,14918],{"class":125},"\"rounded-lg border p-6\"",[85,14920,2128],{"class":109},[85,14922,14923,14925,14927,14929,14931,14934,14936,14938],{"class":87,"line":239},[85,14924,2133],{"class":109},[85,14926,14887],{"class":102},[85,14928,2120],{"class":138},[85,14930,253],{"class":98},[85,14932,14933],{"class":125},"\"text-sm font-medium text-muted-foreground\"",[85,14935,14570],{"class":109},[85,14937,14887],{"class":102},[85,14939,2128],{"class":109},[85,14941,14942,14944,14946],{"class":87,"line":265},[85,14943,3859],{"class":109},[85,14945,6848],{"class":91},[85,14947,280],{"class":109},[85,14949,14950,14952,14954],{"class":87,"line":271},[85,14951,2247],{"class":109},[85,14953,2117],{"class":2116},[85,14955,2128],{"class":109},[85,14957,14958],{"class":87,"line":277},[85,14959,2256],{"class":109},[85,14961,14962],{"class":87,"line":552},[85,14963,280],{"class":109},[17,14965,14966],{},"In your tool definition, include heading level as a parameter the AI can set:",[75,14968,14970],{"className":77,"code":14969,"language":79,"meta":80,"style":80},"metricCard: {\n  description: 'Display a KPI metric. Use headingLevel h2 for the first metric in a section, h3 for subsequent metrics.',\n  parameters: z.object({\n    label: z.string(),\n    value: z.string(),\n    change: z.number(),\n    headingLevel: z.enum(['h2', 'h3', 'h4']).default('h3'),\n  }),\n}\n",[82,14971,14972,14979,14991,15002,15011,15020,15029,15063,15068],{"__ignoreMap":80},[85,14973,14974,14977],{"class":87,"line":88},[85,14975,14976],{"class":138},"metricCard",[85,14978,1484],{"class":109},[85,14980,14981,14984,14986,14989],{"class":87,"line":95},[85,14982,14983],{"class":138},"  description",[85,14985,193],{"class":109},[85,14987,14988],{"class":125},"'Display a KPI metric. Use headingLevel h2 for the first metric in a section, h3 for subsequent metrics.'",[85,14990,129],{"class":109},[85,14992,14993,14996,14998,15000],{"class":87,"line":113},[85,14994,14995],{"class":138},"  parameters",[85,14997,1511],{"class":109},[85,14999,139],{"class":138},[85,15001,142],{"class":109},[85,15003,15004,15007,15009],{"class":87,"line":119},[85,15005,15006],{"class":109},"    label: z.",[85,15008,151],{"class":138},[85,15010,154],{"class":109},[85,15012,15013,15016,15018],{"class":87,"line":132},[85,15014,15015],{"class":109},"    value: z.",[85,15017,151],{"class":138},[85,15019,154],{"class":109},[85,15021,15022,15025,15027],{"class":87,"line":145},[85,15023,15024],{"class":109},"    change: z.",[85,15026,538],{"class":138},[85,15028,154],{"class":109},[85,15030,15031,15034,15036,15038,15041,15043,15046,15048,15051,15054,15057,15059,15061],{"class":87,"line":157},[85,15032,15033],{"class":109},"    headingLevel: z.",[85,15035,163],{"class":138},[85,15037,166],{"class":109},[85,15039,15040],{"class":125},"'h2'",[85,15042,172],{"class":109},[85,15044,15045],{"class":125},"'h3'",[85,15047,172],{"class":109},[85,15049,15050],{"class":125},"'h4'",[85,15052,15053],{"class":109},"]).",[85,15055,15056],{"class":138},"default",[85,15058,476],{"class":109},[85,15060,15045],{"class":125},[85,15062,482],{"class":109},[85,15064,15065],{"class":87,"line":181},[85,15066,15067],{"class":109},"  }),\n",[85,15069,15070],{"class":87,"line":187},[85,15071,280],{"class":109},[12,15073,15075],{"id":15074},"testing-tools","Testing Tools",[17,15077,15078],{},"Use these tools to audit your component library and generated outputs:",[17,15080,15081,15084],{},[39,15082,15083],{},"axe-core:"," Automated accessibility testing that catches ~30% of accessibility issues automatically. Integrate with jest-axe for unit test coverage.",[75,15086,15088],{"className":77,"code":15087,"language":79,"meta":80,"style":80},"import { axe, toHaveNoViolations } from 'jest-axe';\nexpect.extend(toHaveNoViolations);\n\ntest('MetricCard has no accessibility violations', async () => {\n  const { container } = render(\n    \u003CMetricCard label=\"Revenue\" value=\"$84,200\" change={12.4} \u002F>\n  );\n  expect(await axe(container)).toHaveNoViolations();\n});\n",[82,15089,15090,15102,15110,15114,15133,15149,15181,15185,15203],{"__ignoreMap":80},[85,15091,15092,15094,15096,15098,15100],{"class":87,"line":88},[85,15093,404],{"class":98},[85,15095,10958],{"class":109},[85,15097,410],{"class":98},[85,15099,10963],{"class":125},[85,15101,416],{"class":109},[85,15103,15104,15106,15108],{"class":87,"line":95},[85,15105,10970],{"class":109},[85,15107,10973],{"class":138},[85,15109,10976],{"class":109},[85,15111,15112],{"class":87,"line":113},[85,15113,449],{"emptyLinePlaceholder":327},[85,15115,15116,15118,15120,15123,15125,15127,15129,15131],{"class":87,"line":119},[85,15117,10274],{"class":138},[85,15119,476],{"class":109},[85,15121,15122],{"class":125},"'MetricCard has no accessibility violations'",[85,15124,172],{"class":109},[85,15126,196],{"class":98},[85,15128,6393],{"class":109},[85,15130,214],{"class":98},[85,15132,110],{"class":109},[85,15134,15135,15137,15139,15141,15143,15145,15147],{"class":87,"line":132},[85,15136,1443],{"class":98},[85,15138,2699],{"class":109},[85,15140,11008],{"class":102},[85,15142,11011],{"class":109},[85,15144,253],{"class":98},[85,15146,8204],{"class":138},[85,15148,5628],{"class":109},[85,15150,15151,15153,15156,15158,15161,15164,15166,15168,15171,15173,15175,15177,15179],{"class":87,"line":145},[85,15152,2113],{"class":98},[85,15154,15155],{"class":109},"MetricCard label",[85,15157,253],{"class":98},[85,15159,15160],{"class":125},"\"Revenue\"",[85,15162,15163],{"class":109}," value",[85,15165,253],{"class":98},[85,15167,8079],{"class":125},[85,15169,15170],{"class":109}," change",[85,15172,253],{"class":98},[85,15174,256],{"class":109},[85,15176,8089],{"class":102},[85,15178,606],{"class":109},[85,15180,10131],{"class":98},[85,15182,15183],{"class":87,"line":157},[85,15184,2256],{"class":109},[85,15186,15187,15189,15191,15194,15196,15199,15201],{"class":87,"line":181},[85,15188,11080],{"class":138},[85,15190,476],{"class":109},[85,15192,15193],{"class":98},"await",[85,15195,11072],{"class":138},[85,15197,15198],{"class":109},"(container)).",[85,15200,11086],{"class":138},[85,15202,3652],{"class":109},[85,15204,15205],{"class":87,"line":187},[85,15206,755],{"class":109},[17,15208,15209,15212],{},[39,15210,15211],{},"Storybook Accessibility addon:"," Run axe checks directly in Storybook during development. Catches issues before they reach tests.",[17,15214,15215,15218],{},[39,15216,15217],{},"Screen reader testing:"," NVDA (Windows, free) and VoiceOver (macOS, built-in) are essential for testing the experience that automated tools cannot measure — how understandable is the generated content when read aloud?",[17,15220,15221,15224],{},[39,15222,15223],{},"Keyboard-only navigation:"," Unplug your mouse and navigate your application using only Tab, Shift+Tab, Enter, Space, and arrow keys. This is the fastest way to find keyboard traps.",[12,15226,15228],{"id":15227},"the-non-negotiables-summary","The Non-Negotiables Summary",[17,15230,15231],{},"Before shipping a Generative UI feature:",[33,15233,15234,15237,15240,15243,15248,15256,15259,15265],{},[36,15235,15236],{},"Every component in the tool registry passes axe with no violations",[36,15238,15239],{},"All interactive elements are keyboard reachable and operable",[36,15241,15242],{},"Color is never the sole signal for meaning",[36,15244,15245,15247],{},[82,15246,13787],{}," region wraps streamed output",[36,15249,15250,15251,15253,15254],{},"Skeletons have ",[82,15252,14056],{}," and descriptive ",[82,15255,14244],{},[36,15257,15258],{},"SVG charts have a tabular data alternative",[36,15260,15261,15262],{},"All animations respect ",[82,15263,15264],{},"prefers-reduced-motion",[36,15266,15267],{},"Heading levels are parameterized on components, not hardcoded",[17,15269,15270],{},"Accessibility built into the component library is not a burden — it is what makes the \"AI can compose anything\" promise true for all users.",[1138,15272],{},[17,15274,15275],{},[28,15276,15277,15278,3578],{},"Building accessible Generative UI for a complex application? ",[291,15279,15280],{"href":308},"Let's work through the specifics together",[312,15282,15283],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":80,"searchDepth":95,"depth":95,"links":15285},[15286,15287,15288,15289,15290,15291,15292,15293,15294],{"id":13449,"depth":95,"text":13450},{"id":13465,"depth":95,"text":13466},{"id":13777,"depth":95,"text":13778},{"id":14063,"depth":95,"text":14064},{"id":14285,"depth":95,"text":14286},{"id":14654,"depth":95,"text":14655},{"id":14772,"depth":95,"text":14773},{"id":15074,"depth":95,"text":15075},{"id":15227,"depth":95,"text":15228},"2026-01-22","A practical guide to making generative interfaces accessible to all users, including screen readers and keyboard navigation.",{"featured":326},"\u002Flearn\u002Fgenerative-ui-accessibility-guide","9 min read",{"title":13444,"description":15296},"learn\u002Fgenerative-ui-accessibility-guide",[15303,15304,333,15305],"accessibility","wcag","inclusive-design","OLBtGhQh2WD182VDVXrNlpyMrJl3LZvzXHBJO9hykps",{"id":15308,"title":15309,"author":7,"body":15310,"category":15445,"date":15446,"description":15447,"extension":324,"meta":15448,"navigation":327,"path":15449,"readTime":15450,"seo":15451,"stem":15452,"tags":15453,"__hash__":15456},"content\u002Flearn\u002Fweekly-genui-news-digest-1.md","Weekly AI & GenUI News Digest #1",{"type":9,"value":15311,"toc":15439},[15312,15315,15317,15321,15324,15344,15350,15356,15365,15373,15375,15379,15382,15385,15388,15391,15393,15397,15400,15403,15406,15409,15411,15415,15418,15421,15424,15427,15429],[17,15313,15314],{},"Welcome to the first edition of the GenerativeUI.ai weekly digest — a curated roundup of what is happening across the AI interface space. I am filtering for signal over noise: framework releases, funding that signals direction, and patterns worth knowing about.",[1138,15316],{},[12,15318,15320],{"id":15319},"vercel-ai-sdk-reaches-40","Vercel AI SDK Reaches 4.0",[17,15322,15323],{},"Vercel released AI SDK 4.0 this week, the most significant version since the library's introduction. The headline changes:",[17,15325,15326,15329,15330,172,15332,15335,15336,15339,15340,15343],{},[39,15327,15328],{},"Unified streaming API."," The previously separate ",[82,15331,781],{},[82,15333,15334],{},"streamText",", and ",[82,15337,15338],{},"streamObject"," APIs are now unified under a single ",[82,15341,15342],{},"generateStream"," call that handles all three output types. You can now mix streaming text and streaming components in a single response, which opens up a more natural pattern: the AI narrates what it is building while the components appear.",[17,15345,15346,15349],{},[39,15347,15348],{},"Edge runtime stability."," AI SDK 4.0 declares full Edge runtime support as stable. Cold start times drop significantly when functions run on the edge.",[17,15351,15352,15355],{},[39,15353,15354],{},"Provider expansion."," Amazon Bedrock and Cohere join the official provider list. The SDK now covers the major model providers without requiring community wrappers.",[17,15357,15358,15361,15362,15364],{},[39,15359,15360],{},"What this means in practice:"," Existing ",[82,15363,781],{}," code migrates with minimal changes. The unified API is a quality-of-life improvement. The edge stability is the change that will matter most for production applications — lower latency, lower cost at scale.",[17,15366,15367],{},[291,15368,15372],{"href":15369,"rel":15370},"https:\u002F\u002Fsdk.vercel.ai\u002Fdocs\u002Fchangelog",[15371],"nofollow","Vercel AI SDK changelog",[1138,15374],{},[12,15376,15378],{"id":15377},"copilotkit-raises-20m-series-a","CopilotKit Raises $20M Series A",[17,15380,15381],{},"CopilotKit announced a $20M Series A led by Andreessen Horowitz, valuing the company at approximately $140M. This is notable for a few reasons.",[17,15383,15384],{},"The company's focus has been on the \"copilot\" pattern — AI that assists within an existing UI rather than replacing it — and investors appear to be betting that this is the primary commercial form that enterprise AI interfaces will take in the near term. The copilot pattern is lower risk for enterprises than fully generative interfaces: the existing UI still works, the AI is additive.",[17,15386,15387],{},"The funding will go toward CopilotKit Cloud, a managed backend service that removes the need to run your own AI infrastructure. For teams building copilot features, this reduces the ops burden significantly.",[17,15389,15390],{},"For the open-source project, this likely means faster development, more documentation, and a maintained enterprise offering. The risk is the usual one with VC-backed open-source: divergence between the open-source and commercial versions. Worth watching.",[1138,15392],{},[12,15394,15396],{"id":15395},"thesys-hits-13k-stars-in-90-days","Thesys Hits 13K Stars in 90 Days",[17,15398,15399],{},"The json-render project (now branded as Thesys) has had one of the fastest GitHub star trajectories in the developer tooling space, reaching 13K stars in its first three months. The project launched in January 2026 with a clear pitch: AI outputs JSON, JSON renders UI, the same JSON works anywhere.",[17,15401,15402],{},"This week they shipped Vue and Angular renderers alongside the existing React one, making good on the framework-agnostic promise. The community reception for the Vue renderer has been particularly strong — Vue developers have had limited options for Generative UI until now.",[17,15404,15405],{},"The JSON schema format is also attracting interest from mobile teams. A React Native renderer is in active development. The vision of \"one AI response, every client\" is becoming more concrete.",[17,15407,15408],{},"The project is still early. Production deployments are sparse compared to Vercel AI SDK and CopilotKit. But the growth rate suggests real demand for the JSON approach, particularly in teams that do not live in the Next.js ecosystem.",[1138,15410],{},[12,15412,15414],{"id":15413},"pattern-watch-the-confirmation-step","Pattern Watch: The Confirmation Step",[17,15416,15417],{},"An emerging pattern worth tracking: inserting a confirmation step between AI component generation and rendering.",[17,15419,15420],{},"The flow: user asks a question, AI generates a proposed UI, user sees a preview with \"Render this?\" and an explanation of what the AI is about to show. One click renders the final interface.",[17,15422,15423],{},"This pattern has appeared in a handful of internal tools and was written up by the Clerk engineering blog this week. The motivations are partly UX (users feel more in control) and partly practical (the confirmation step lets the user reject a bad AI decision without it disrupting their workflow).",[17,15425,15426],{},"Whether this pattern has legs for consumer-facing products is unclear — adding a confirmation step to every AI response is friction most users would not want. But for enterprise tools and admin interfaces, where the consequences of a wrong AI decision matter, it looks promising.",[1138,15428],{},[17,15430,15431],{},[28,15432,15433,15434,15438],{},"That's the digest for week one. If you have news tips or project announcements, send them to the address on the ",[291,15435,15437],{"href":15436},"\u002Fabout","about page",". Next edition lands next Thursday.",{"title":80,"searchDepth":95,"depth":95,"links":15440},[15441,15442,15443,15444],{"id":15319,"depth":95,"text":15320},{"id":15377,"depth":95,"text":15378},{"id":15395,"depth":95,"text":15396},{"id":15413,"depth":95,"text":15414},"news","2026-01-15","This week in Generative UI: Vercel AI SDK 4.0 release, CopilotKit funding, and emerging patterns.",{"featured":326},"\u002Flearn\u002Fweekly-genui-news-digest-1","5 min read",{"title":15309,"description":15447},"learn\u002Fweekly-genui-news-digest-1",[15445,15454,15455],"weekly-digest","industry","xocX55FhsxVxhm5atnaZDRjNR_0_BAH2Evzxbj630YA",1776470174495]