Skip to content

Blocks in a text answer

A backend that only sends text can still send a card or quick replies — a code block whose language names it. Cards, suggestions, and your own.

On this page

Some backends send only text: a workflow engine that runs its tools itself (n8n), a server that streams its answer and nothing else, a model with no tools. Their answer can still put something richer than prose on screen, with one rule:

A code block whose language aparté knows becomes part of the interface. Any other is shown as code.

md
Which day suits you?

```suggestions
["Tuesday 3", "Wednesday 4", "Thursday 5"]
```

The block’s body is JSON, written by your backend — build it from your tool’s real result (in n8n, a Code node) rather than asking a model to type JSON: it is exact every time. A chat that does not know the language, an email, or a model reading the history back sees the sentence and the block, which still reads as what it is.

LanguageWhat it becomesTurn it on
card-<name>that card: card-order, card-place, card-weather, your ownsetupCardFences() — cards
suggestionsquick replies above the composer, for the person to clicksetupSuggestionBlocks()

Each is off until you turn it on: once on, the block is no longer shown as code, so it is the app’s choice.

The replies the last answer offers, as buttons above the composer: the free times of a booking bot, “Return an item” after an order status, “Yes” / “No”. A click sends the reply as if it were typed. Nothing of the block shows in the message, and the replies go the moment a message is sent.

ts
import { setupSuggestionBlocks } from '@aparte/core';

setupSuggestionBlocks();
html
<aparte-composer>
  <aparte-suggestions answers></aparte-suggestions>
  <!-- the composer's input and send button -->
</aparte-composer>

The block holds an array of strings, each both the label and what is sent. A { "label", "prompt" } object is read for its label alone: the block arrives in the reply’s text, where a model can write it too, and a button must send what it shows. A page that wants a button to show one thing and send another writes it in the suggestions attribute of the element, which is the page’s own.

A reply is shown only if it is 1 to 100 characters long and holds nothing invisible: no zero-width, bidi or tag characters, no stray joiners or variation selectors (an emoji sequence such as 🏳️‍🌈 is fine). A reply that fails is left out, and the console says which one, once. A click on a reply sends its text alone, leaves what the person was typing and any files they attached where they were, and does nothing while a question holds the composer or a reply is streaming.

md
```suggestions
["Tuesday 3", "Another week"]
```

<aparte-suggestions answers> shows nothing until an answer offers replies, and nothing when the last message is the person’s. A block that is not a list of replies is shown as its source; a block still being written offers nothing until it closes. The fixed prompt starters of an empty chat are the same element without answers (empty-only); a page can have both.

The rule is one seam, public: registerCodeLanguageRenderer. Cards and suggestions are two users of it, and a language of yours is a third. A fenced block in a drawn language loses its code card, so what you draw stands for it (card: true keeps it, with its copy button, for a source worth taking); bare: true also drops the scroller around the drawing, for one that handles its own overflow, as a card’s carousel does:

ts
import { aparteGlobalConfig, escapeHtml } from '@aparte/core';

aparteGlobalConfig.registerCodeLanguageRenderer({
  languages: ['ticket'],
  bare: true,
  render: ({ source, open }) => {
    if (open) return '';                       // still being written
    const t = JSON.parse(source);
    return `<div class="ticket">Ticket ${escapeHtml(t.id)}: ${escapeHtml(t.subject)}</div>`;
  },
});

What render returns is inserted as it is (it is your code, like a segment renderer), so escape what comes from the block. For a block it cannot draw, answer with the source in an element of class aparte-code-error, which core shows as quiet code. A card with a layout, variants and buttons is easier with defineCard, whose block comes for free.