Appearance
Rich replies with plain fallbacks
Start with the stored message, not the card:
markdown
```compare
| Attribute | Rolling | Blue-green |
|---|---|---|
| Rollback | Redeploy | Switch traffic |
```That one string can feed the rich client, search, copy, export, summaries, and the next model turn. A JSON attachment cannot do that by itself. In production, the primary client may render a polished table while search indexes an opaque blob, copied text loses the values, and an older client shows nothing.
Store one plain Markdown message. Rich clients may recognize a small set of fenced blocks and render them as widgets. Every block must still read sensibly as ordinary Markdown. Presentation is an interpretation of the message, not a second source of truth.
Concept map
text
assistant Markdown
-> ordered segment parser
-> ordinary prose -> Markdown renderer
-> known valid fence -> widget renderer
-> unknown or malformed fence -> ordinary Markdown
same stored text
-> search
-> summaries
-> memory capture
-> model history
-> copy and export
-> simple clientsThe fallback path is the main compatibility path. It must work without JavaScript widget state or special database columns.
Choose formats with useful text bodies
Four formats cover many chat replies without becoming a general layout language:
- Timeline for ordered events with labels, titles, and descriptions.
- Slides for a small sequence of pages.
- Compare for options arranged in a table.
- Choices for suggested next messages.
Use fenced Markdown tags such as timeline, slides, compare, and choices. Inside the fence, accept constrained headings, lists, tables, and separators. Do not put arbitrary JSON in the body.
markdown
Here are the two deployment options.
```compare
| Attribute | Rolling | Blue-green |
|---|---|---|
| Extra capacity | Low | High |
| Rollback | Redeploy | Switch traffic |
| Best fit | Small changes | Risky releases |
```
The current capacity limit favors rolling deployment.Without a widget renderer, the reader still gets a table between two useful paragraphs. Search and summarization see the same words the user saw.
Parsing must fail open
Split content into ordered text and widget segments with a pure parser. Recognize only top-level fenced blocks whose info string exactly matches an allowed tag. Ordinary code fences, nested examples, unknown tags, tilde fences, and unclosed fences all need tests.
The parser must never throw while rendering a message. If a known widget body is malformed, return it to the ordinary Markdown renderer. A malformed comparison should become a basic table or text block, not break the whole chat bubble.
Apply strict caps. Limit slide count, choice count, option length, timeline rows, table width, and body size. These bounds protect layout and reduce the number of ambiguous parser states.
Raw HTML should remain disabled. Widget syntax is not a route around the Markdown security boundary.
Interaction should reuse the chat path
Suggested choices are not a new command type. When tapped, their label should travel through the same send function as typed text and become an ordinary user message. The model then sees a normal turn, and outbox, retry, unread, and analytics behavior remain unchanged.
Only choices under the newest assistant reply should be active. Older choices can remain visible but disabled. Removing them shifts history and makes old screenshots or references confusing.
Slides keep active-page state inside the component. That position does not need durable storage. Provide swipe, keyboard arrows, visible page count, and buttons for precise pointers. Respect reduced motion.
Comparison tables should use real table elements. On narrow screens, horizontal scrolling with a sticky attribute column keeps row meaning visible. Do not replace semantics with a grid of generic containers.
Timelines should use ordered lists. Dates or step labels cannot rely on color alone. Small details like these determine whether the widget remains understandable through keyboard, screen reader, and high-contrast use.
Keep the design vocabulary small
Rich replies should look like existing attachments, not a second design system. Reuse card width, separator, radius, surface, typography, focus ring, button sizes, and icons. Mobile and desktop can change width and controls without changing information.
Prompt guidance should also stay small. List exact fence syntax, say that prose is the default, limit widgets per reply, and require useful plain-text bodies. The model does not need every CSS rule or parser detail.
Two widgets per reply is a practical ceiling. A one-line answer needs none.
Terminology
- Source message is the Markdown string stored in the message row.
- Widget fence is a recognized fenced block with a constrained body.
- Segment is one ordered piece of prose or widget content.
- Progressive rendering adds interaction while preserving the base text.
- Fail-open parsing returns malformed rich content to ordinary Markdown.
- Suggested reply is a button that sends normal user text.
- Local widget state is interaction state that does not need persistence.
This language helps reviewers reject accidental dual storage. There should not be a hidden attachment payload that can drift from the message.
Decisions and alternatives declined
Markdown bodies win over JSON here. JSON validates more mechanically but fails as prose, copy, search input, and model history. The constrained Markdown grammar offers enough structure for these four widgets.
Tag names and prompt guidance belong in a shared schema module. Both the UI and server build need the same list. A server-only location can pass local tests and fail the production web image when that directory is absent.
Use pure parsing with component-specific rendering. A registry abstraction adds little when a direct switch has one caller and four cases. Add it when extension pressure appears.
No schema migration is needed. Richness lives in message text, so old clients and existing storage continue to work.
Rejected alternatives include raw HTML from the model, custom React component payloads, and a general JSON layout protocol. Raw HTML widens the security boundary. Component payloads bind messages to one client version. A general layout language creates an application platform inside the chat before there is evidence it is needed.
Failure modes
- A malformed widget crashes the entire message.
- Unknown fences disappear instead of rendering as text.
- The parser mistakes a widget example inside a code block for a live widget.
- Search indexes labels but not values stored in a separate payload.
- Suggested choices bypass the ordinary send and outbox path.
- Old choices remain active and send stale context.
- A comparison widens the page on mobile.
- Slide position is conveyed only by dot color.
- Prompt guidance encourages widgets for trivial answers.
- Server and UI disagree about allowed tags.
- The production image omits the shared schema file.
Field checklist
Rich presentation should respond to the task, not to a hidden guess about who the user is. The next chapter covers personalization based on explicit choices and observed behavior without demographic profiling: Personalization without a profile. For the message ordering that feeds these replies, see Chat that feels attentive.