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.
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.
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.
The languages aparté knows
Section titled “The languages aparté knows”| Language | What it becomes | Turn it on |
|---|---|---|
card-<name> | that card: card-order, card-place, card-weather, your own | setupCardFences() — cards |
suggestions | quick replies above the composer, for the person to click | setupSuggestionBlocks() |
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.
Quick replies — suggestions
Section titled “Quick replies — suggestions”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.
import { setupSuggestionBlocks } from '@aparte/core';
setupSuggestionBlocks();<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.
```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.
A language of your own
Section titled “A language of your own”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:
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.