← Back to list

GenUI Beyond Chat: Replaying and Reviewing A2UI Cache with Widgetbook — Part 3

How I use deterministic component stories to design the grammar catalog, add a second localization dimension for generated lessons, and…

Cagatay Ulusoy · 2026-07-20 19:31 · 2 claps · 14.1 min read
#genuis #generative-ui #ai #flutter #gemini
Open on Medium ↗
Wiki topics: LLM · Large Language Models AI · AI · General 📱 · Mobile Development 🥊 · Combat Sports

GenUI Beyond Chat: Replaying and Reviewing A2UI Cache with Widgetbook — Part 3

How I use deterministic component stories to design the grammar catalog, add a second localization dimension for generated lessons, and replay cached A2UI from Firestore in Widgetbook.

In Part 1, I defined the curriculum, the teaching system, and the catalog boundary around lesson generation.

In Part 2, I cached the completed A2UI output in Firestore and replayed it without another Gemini request.

That left me with a practical problem.

The grammar catalog contains about forty components. I need to inspect each one with short and long content, different screen sizes, light and dark themes, right-to-left layouts, large text, and combinations of generated data. I also need to review complete lessons in every language in which they have been generated.

Asking Gemini to generate a new lesson whenever I want to see one widget is a terrible development loop. It is slow, probabilistic, and consumes tokens. More importantly, it gives the model control over which component I get to inspect.

I needed a deterministic workbench for the catalog and a live inspection tool for the generated and cached artifacts.

That is where Widgetbook fits into this architecture.

A catalog is a teaching vocabulary

In a conventional design system, a component catalog describes the visual language of the application: buttons, cards, inputs, navigation, and typography.

A GenUI catalog has another responsibility. It also describes what the model is able to say through the interface.

If the catalog contains only text blocks, Gemini is limited to generating a polished wall of text. While text can describe grammar rules, it cannot render a visual, spatial layout of the Finnish local cases, guide a learner through an interactive decision tree, or embed a hands-on exercise to construct an inflected word. The prompt can request these specific teaching moves, but the catalog must supply the specialized custom widgets required to bring those interactive UI experiences to the screen.

This led me to distinguish between a static grammar book and a generative grammar book.

A static grammar book locks the page

A carefully edited static, hard-copy grammar book has important advantages over generated lessons.

Its author and editors decide exactly what every page says. The facts, terminology, examples, teaching order, and exercises can be reviewed before publication. Every learner receives the same explanation, so teachers can refer to a page, compare answers, and build a course around a stable source. Once published, the book is inexpensive to distribute, works without a generation service, and behaves predictably.

This is a one-to-many relationship: one editorial team creates one book for many learners. That scale is one of the static book’s greatest strengths.

It is also its main limitation.

The book must teach an imagined average reader. It cannot know whether the learner already understands case endings through Turkish, has never encountered them in English, or needs a visual explanation before reading the rule. The examples, pace, explanation language, and exercise order are fixed in advance.

A digital edition can translate labels and paragraphs, but localization normally changes the words inside an already-authored page. It does not reconsider how the idea should be taught. If the printed layout is transferred directly to a phone, its weakest form is a wall of text on a smaller screen.

Updating a hard-copy book is predictable but slow. A better explanation, a new exercise, or a more accessible layout must be edited and distributed as another version of the page.

A generative grammar book defines the boundaries

A generative book’s main advantage is adaptation. The explanation can begin from the learner’s language, choose a visual structure that fits the grammar point, and place interaction beside the rule instead of at the end of a chapter. Flutter can also adapt the same semantic component to the available width, text length, theme, text scale, and reading direction.

For example, the lesson can use:

  • InflectionBreakdown to reveal a stem and suffix in stages;
  • LocalCaseScene to present inside, out of, into, on, off, and onto as one spatial system;
  • SentencePattern to show parallel example sentences of one construction
  • QuickCheck, ClozeSentence, and Flashcard for recognition, production, and recall.

The learner is no longer only scrolling through an explanation. The interface reveals a pattern, asks for a decision, and responds to an action.

The current implementation does not generate a private lesson for every individual learner. It personalizes at the explanation-language level and then caches that lesson-language artifact for reuse. The result sits between one-to-many publishing and one-to-one tutoring: English and Turkish learners can receive different teaching bridges, while every compatible request in the same language can replay the same reviewed A2UI.

That flexibility has a cost.

A generative book is harder to predict and review because there is no longer only one canonical page. Model output can vary, a structurally valid lesson can still contain a factual error, and every language-specific tree adds another artifact that may need human inspection. The first generation also introduces latency and token cost. After generation, the application must cache the result, preserve catalog compatibility, validate the A2UI, and decide what happens when an old artifact can no longer be replayed.

The quality ceiling is higher, but so is the engineering and editorial responsibility.

A static grammar book locks the page; a generative grammar book defines the boundaries. That is why ‘generative’ does not mean ‘unbounded.’ Gemini isn’t inventing Flutter widgets or rendering arbitrary layouts. It composes a lesson from a strictly controlled teaching vocabulary entirely owned by the application.

I do not see the two approaches as competitors. I want the editorial discipline of a static book and the adaptive composition of a generative one.

A static, hard copy book is optimized for editorial certainty and one-to-many distribution. A generative book accepts more generation and review complexity so the explanation, interaction, and widget tree can adapt.

Plan teaching, not decorative variations

The goal of my catalog was not to collect as many attractive widgets as possible. It was to cover the teaching moves that Finnish grammar explanations require.

That difference affects component design.

CaseCard, EndingRules, and InflectionBreakdown can all display an ending, but they do not have the same job:

  • CaseCard introduces or recaps one grammatical case.
  • EndingRules explains a rule that branches according to the word ending.
  • InflectionBreakdown shows how one word changes through several stages.

If their descriptions were all “shows grammar information in a card,” the model would have to guess between them. The catalog would look varied to a developer while remaining ambiguous to Gemini.

The CaseCard schema therefore states its editorial role and its boundaries. It says to use the component for one case and points the model to EndingRules or InflectionBreakdown when the explanation needs a different teaching move.

That description is not documentation added after the widget is finished. It is part of the model’s input. Catalog clarity affects component selection, consistency, and input-token cost.

Widgetbook makes the same description visible to the human reviewer. The story uses a small documentation adapter:

final component = ComponentMeta(
  name: 'CaseCard',
  path: '[Grammar]/Lesson',
  docsBuilder: genUiCatalogDocsBuilder('CaseCard'),
);

genUiCatalogDocsBuilder finds the registered CatalogItem and reads the description from its JSON schema. The model and the developer therefore see the same definition of the component’s purpose.

This became one of the most useful catalog-planning checks:

If I cannot explain when a component should be used — and when a neighboring component should be used instead — the catalog item is not ready.

Test the custom widgets before Gemini uses them

Each catalog component has deterministic Widgetbook stories built with ordinary Dart fixtures.

For CaseCard, one story shows a short Inessive example. Another deliberately uses a long case name, a long compound word, and a long English gloss. For GrammarDecisionTree, separate stories cover one decision, three decisions, and asymmetric option labels that wrap onto multiple lines. Interactive components have fixtures for their answer and feedback behavior.

// Widgetbook's generator reads this `.stories.dart` library and writes the
// private `_Story` type and public component registration into the part file.
// The generated file is build output; story definitions stay in this file.
part 'grammar_decision_tree.stories.g.dart';

/// Component-level information consumed by Widgetbook code generation.
///
/// The generator finds the single top-level [ComponentMeta] and copies its
/// name, navigation path, and docs builder into the generated
/// `GrammarDecisionTreeStoryHostComponent`. The docs builder reuses the
/// `GrammarDecisionTree` GenUI schema description, so Widgetbook shows the same
/// component guidance that Gemini receives in the catalog.
final component = ComponentMeta(
  name: 'GrammarDecisionTree',
  path: '[Grammar]/Lesson',
  docsBuilder: genUiCatalogDocsBuilder('GrammarDecisionTree'),
);

/// Defines the widget constructor and argument model used by the generator.
///
/// [Meta] is required in every stories file. Its constructor tear-off tells
/// Widgetbook which widget type the generated component represents. This file
/// uses a small host widget and a custom, empty args class because each story
/// supplies a fixed regression fixture instead of editable story arguments.
const meta = Meta(
  GrammarDecisionTreeStoryHost.new,
  argsType: GrammarDecisionTreeStoryHostArgs.new,
);

/// No story-specific controls are needed; viewports, themes, locales, and text
/// scale still come from Widgetbook's global add-ons.
class GrammarDecisionTreeStoryHostArgs {
  const GrammarDecisionTreeStoryHostArgs();
}

/// Groups the decision-tree fixtures as one generated Widgetbook component.
class GrammarDecisionTreeStoryHost extends StatelessWidget {
  const GrammarDecisionTreeStoryHost({required this.child, super.key});

  final Widget child;

  @override
  Widget build(BuildContext context) => child;
}

/// A story is one named, deterministic state listed under the component.
///
/// The `$` prefix makes this top-level variable discoverable by Widgetbook's
/// generator. `_Story` comes from the generated part file, while `builder`
/// returns the widget shown in Widgetbook's canvas.
final $grammarDecisionTreeUseCase = _Story(
  name: 'object case (3 steps)',
  builder: (context, args) =>
      const GrammarDecisionTreeStoryHost(child: _previewObjectCase),
);

// A compact fixture verifies that the component also works with one decision.
final $grammarDecisionTreeShortUseCase = _Story(
  name: 'complement (1 step)',
  builder: (context, args) =>
      const GrammarDecisionTreeStoryHost(child: _previewComplement),
);

// A regression story isolates wrapping and equal-height option behavior.
final $grammarDecisionTreeLongOptionsUseCase = _Story(
  name: 'long option labels',
  builder: (context, args) =>
      const GrammarDecisionTreeStoryHost(child: _previewLongOptions),
);

// Typical content fixture: the selected options show one completed path.
const GrammarDecisionTree _previewObjectCase = GrammarDecisionTree(
  prompt: 'Which object case?',
  subtitle: 'Follow the logic to determine the correct case.',
  steps: [
    DecisionStep(
      question: 'Is the sentence negative?',
      options: [
        DecisionOption(label: 'Yes', selected: true),
        DecisionOption(label: 'No'),
      ],
    ),
    DecisionStep(
      question: 'Is it a total object?',
      options: [
        DecisionOption(label: 'Yes (setting off / departure)'),
        DecisionOption(label: 'No (normal completed action)', selected: true),
      ],
    ),
    DecisionStep(
      question: 'Is the action complete?',
      options: [
        DecisionOption(label: 'Yes', selected: true),
        DecisionOption(label: 'No'),
      ],
    ),
  ],
  result: DecisionResult(
    label: 'Partitiivi',
    example: 'Minä rakastan **suomea**.',
    gloss: 'I love Finnish.',
  ),
);

// Minimal content fixture: useful for spotting unnecessary empty space.
const GrammarDecisionTree _previewComplement = GrammarDecisionTree(
  prompt: 'Which complement case?',
  subtitle: 'Choosing the predicative after "X on ___".',
  steps: [
    DecisionStep(
      question: 'Is the subject a countable, whole thing?',
      options: [
        DecisionOption(label: 'Yes', selected: true),
        DecisionOption(label: 'No'),
      ],
    ),
  ],
  result: DecisionResult(
    label: 'Nominatiivi',
    example: 'Talo on **iso**.',
    gloss: 'The house is big.',
  ),
);

// Edge case: long, asymmetric answer labels. Verifies the option buttons wrap
// instead of truncating, and that both options keep the same height even when
// one wraps to more lines than the other (the short "No" stretches to match).
const GrammarDecisionTree _previewLongOptions = GrammarDecisionTree(
  prompt: 'Which form of the object?',
  subtitle: 'Long answer labels must wrap and stay readable, not get cut off.',
  steps: [
    DecisionStep(
      question:
          'Is the verb negative, or does it express an ongoing, incomplete '
          'action?',
      options: [
        DecisionOption(
          label: 'Yes — negation or an ongoing, incomplete action',
          selected: true,
        ),
        DecisionOption(label: 'No'),
      ],
    ),
    DecisionStep(
      question: 'Does the sentence describe a complete, bounded result?',
      options: [
        DecisionOption(label: 'Yes (whole, completed result)'),
        DecisionOption(
          label: 'No (partial, unbounded or irresultative action)',
          selected: true,
        ),
      ],
    ),
  ],
  result: DecisionResult(
    label: 'Partitiivi',
    example: 'En lukenut **kirjaa** loppuun.',
    gloss: 'I did not read the book to the end.',
  ),
);

No model call is required. I can select the exact state I want, reproduce a layout problem, change the widget, and see the same input again.

The global Widgetbook add-ons then apply the environmental variations around that fixture:

  • iOS and Android viewports;
  • light and dark themes;
  • application locale and text direction;
  • text scale and accessibility checks;
  • alignment, zoom, grid, and semantics inspection.

GrammarDecisionTree with dark theme, and 1.5 text scale in iPad

GrammarDecisionTree with dark theme, and 1.5 text scale in iPad

This is where catalog planning becomes concrete. A component may look distinct in a design mock-up and still fail as a catalog item. Its purpose may overlap with another component. Its schema may require data that a lesson cannot reliably provide. A wide table may work on desktop but become unreadable on mobile.

As mentioned in the previous article, in that last case, I do not necessarily add a second catalog item and ask Gemini to choose between “table” and “mobile cards.” That would add another schema and another description to every provider turn. A better option is often one semantic data component whose Flutter renderer chooses a table or stacked cards from the available width and content length.

Widgetbook lets me test that responsive decision in the client, where it belongs, without increasing the model’s catalog vocabulary or input-token cost.

Localization now has two dimensions

In a traditional app, localization means translating static UI strings. In a GenUI app, localization has two dimensions: the static app shell and the generative payload.

The widget catalog must be designed so that the LLM handles the content language, while Flutter handles the shell.

Widgetbook already has a locale add-on. In a traditional Flutter application, changing the locale usually keeps the story’s widget tree and replaces the strings supplied by the localization delegates. It may also change formatting and text direction, but the authored composition is still recognizably the same screen.

The cached English and Turkish versions of vowel_harmony are not one A2UI tree with translated labels. They are independent compositions generated from the same lesson identity and editorial brief.

Turkish and Finnish are not related languages, but both make extensive use of suffixes. A Turkish-speaking learner already has a useful mental model for case endings and vowel harmony. The generated lesson can begin with that structural bridge and use a comparison component early.

English has suffixes, but it does not normally express grammatical case through a productive series of noun endings in the way Finnish does. An English-speaking learner may first need a visual explanation of what it means to attach location and direction to the noun itself. That lesson can require a different sequence of callouts, case cards, breakdowns, and checks.

The facts about Finnish remain the same. The teaching bridge and the widget tree can change.

The standard locale add-on still tests the conventional localization layer: Widgetbook chrome around the story, app-owned labels, and right-to-left behavior. I added a separate Grammar lesson language add-on to select the generated artifact itself.

Grammar Lesson Language custom add on

Grammar Lesson Language custom add on

This is the GenUI localization shift I wanted the workbench to make visible:

In a conventional localized screen, language usually changes the words inside the tree. In a hyper-personalized GenUI lesson, language can change the tree.

A language add-on whose options come from Firestore

In traditional apps, Widgetbook is typically a static, synchronous environment. Fetching add-on options from Firestore introduces an asynchronous step into the catalog’s initialization.

One lesson may currently exist in English, Turkish, and Arabic. Another may only have an English artifact because no other user with a different language opened that lesson.. The language control should show what is actually stored under:

grammar_genui_lessons/{lessonId}/translations/{languageCode}

That created an interesting Widgetbook problem. An add-on defines its fields synchronously, but the available language documents arrive from an asynchronous Firestore query.

[embed]

My custom add-on starts with no fields. After the viewer loads the selected lesson’s translation documents, it publishes the available choices into a small shared catalog and updates the add-on’s query group:

grammarLessonLanguageCatalog.replace(options);

state.updateQueryGroup(
  grammarLessonLanguageAddon.groupName,
  grammarLessonLanguageAddon.valueToQueryGroup(options.first),
);

Widgetbook rebuilds the add-ons panel, the language dropdown appears, and the first available lesson is rendered immediately. The selected option is exposed to the viewer through an InheritedWidget scope.

Language is deliberately not a custom control inside the story canvas. It is an environmental mode, like viewport or theme. The value is serialized in Widgetbook’s URL, so a reviewer can share a link to the same lesson-language variant.

Replay the cached lesson, not a Widgetbook imitation

The catalog stories answer one question: Does this component work with controlled data?

I also need to answer another: What did Gemini actually compose, and what does the application render from the cached artifact?

Because a generative UI defines the boundaries rather than hardcoding the page, we lose the predictability of traditional static routing. To allow a user to review a past lesson, we cannot simply reload a route. We must cache the exact JSON blueprint of the widgets Gemini selected, allowing us to accurately replay that specific generative state.

For that I added a live story under [Live]/Grammar.

Its flow is:

  • Connect
  • Load the canonical curriculum index
  • Select a lesson
  • Read its generated translation documents
  • Publish their languages to the custom add-on
  • Send the selected a2uiText to FirebaseGenUiSession.ingestA2uiText
  • Render the surfaces with the production GrammarLessonScreen

The viewer does not call Gemini. The selected Firestore document already contains the accumulated a2uiText and the authored surfaceIds described in Part 2.

The replay controller creates the production FirebaseGenUiSession with the real grammar catalog. It then calls ingestA2uiText, listens for materialized surfaces, and passes the result to the production GrammarLessonScreen.

I did not build a simplified A2UI renderer for Widgetbook. That would prove that my preview renderer works. Reusing the production session and screen lets me inspect what the application itself renders with the current catalog, theme, and Flutter code.

It also turns the viewer into a compatibility check. A cached lesson may have been generated months ago. If I rename a component, make a previously optional field required, or remove a catalog item, replay exposes the break before a learner finds it.

Keep the live viewer fenced off

Most of my Widgetbook remains an offline, deterministic component catalog. Opening a CaseCard or QuickCheck story must not initialize Firebase or perform a network read.

The live viewer is an explicit exception:

  • Firebase initialization happens only after the reviewer presses Connect.
  • The unauthenticated viewer has read-only access to the curriculum and generated translation documents; Widgetbook does not write lessons.

The separation matters. Deterministic stories are suitable for repeatable development and visual review. The live viewer is for diagnosis: it displays whatever generated artifacts currently exist in Firestore.

The review loop

Widgetbook now supports two complementary review loops.

For an individual catalog item, I ask:

  • Does it represent one clear teaching move?
  • Is its role distinct from neighboring components?
  • Does the schema description tell Gemini when to choose it?
  • Does it handle long content, small screens, large text, and RTL layouts?
  • Can responsive behavior stay in Flutter instead of becoming another catalog item?

For a complete generated lesson, I ask:

  • Did the model choose components that fit the grammar explanation?
  • Does the lesson move through concept, mechanism, use, and application rather than becoming a wall of text?
  • Are recognition, production, and recall genuinely interactive?
  • Does each explanation-language variant use an appropriate teaching bridge?
  • Does the cached A2UI still replay with the current catalog and production screen?

The first loop improves the catalog components before generation. The second reviews what Gemini said with that vocabulary.

The beauty of this isn’t just technical replayability; it is design verification. A designer can jump into Widgetbook and see exactly how the LLM is composing the screens using the design system components and populating them with varying text lengths and data structures. They can verify that the app’s visual identity holds up against unpredictable LLM output, all without needing to trigger live API calls or write Dart code.

Widgetbook makes the UI system inspectable at the two points where GenUI otherwise becomes difficult to reason about: before the model composes a screen and after the generated screen has been persisted.

Closing the loop

The three parts of this series now form one pipeline:

  • Part 1: curriculum + teaching contract + catalog boundaries
  • Part 2: generated A2UI + Firestore cache + production replay
  • Part 3: component workbench + language-variant inspection

The catalog is not only a registry of Flutter widgets. It is the application’s teaching vocabulary.

The cached A2UI is not only a model response. It is a durable lesson artifact.

Widgetbook is not only a component gallery. It is where I review the vocabulary before the model uses it and replay complete lessons after it has.

That is the development loop I was missing when I started building a grammar book with GenUI.


메타데이터
post_id
d1a12eaa4e37
slug
genui-beyond-chat-replaying-and-reviewing-a2ui-cache-with-widgetbook-part-3-d1a12eaa4e37
url
https://medium.com/@ulusoyca/genui-beyond-chat-replaying-and-reviewing-a2ui-cache-with-widgetbook-part-3-d1a12eaa4e37
canonical_url
https://medium.com/@ulusoyca/genui-beyond-chat-replaying-and-reviewing-a2ui-cache-with-widgetbook-part-3-d1a12eaa4e37
author_url
https://medium.com/@ulusoyca
status
ok
fetched_at
2026-09-12 15:47:29