Skip to content

Retry, edit and switch between versions of an answer

Let people retry an answer without losing the first one, edit what they sent, and move between the versions with the built-in picker.

On this page
Open
Retry the reply, then walk the versions with the arrows. The model here is scripted: the first two retries each get a new answer.
ts
import { aparteGlobalConfig } from '@aparte/core';

aparteGlobalConfig.setBubbleActions({ retry: true, edit: true });

Both buttons are off until you turn them on: retry shows on replies, edit on the person’s own messages. With AparteClient running — useAparteClient in React and Vue, createAparteClient in Svelte and provideAparte() in Angular start one — that line is all. Buttons lists the others and their order.

  1. YouMy dashboard query is slow. What should I do?
    1. Assistant Cache it: the query is the same on every load… version 1, kept
    2. Assistant Batch it: send the ten lookups as one request… ‹ 2 / 2 ›, on screen
A conversation is a tree. Retry adds a sibling under the same message; the chat shows one path through the tree.

A retry asks the model again from the same point and puts the new answer beside the old one, not over it. As soon as a message has two versions, its footer shows the ‹ 2 / 2 › picker, and its arrows switch the version on screen with nothing for you to wire. AparteClient asks again at a sampling temperature of 0.4, whatever temperature the app set, so the new answer usually differs. Offered on the person’s own messages too — setBubbleActions({ user: ['edit', 'retry', 'copy'] }) — a retry answers that message afresh.

The edit button opens the message in an editor: Enter saves, Shift+Enter starts a new line and Escape cancels, whatever the composer’s submit-on-enter. Saving a changed text sends aparte-edit with { messageId, content, targetId } — an unchanged or empty one just closes the editor — and AparteClient changes the message in place, drops the replies under it and asks again. An edit does not branch: the old text and its answers are gone. To keep them, handle aparte-edit yourself and keep the old text in your own data; the built-in picker shows only on replies, so a switch between versions of a question is yours to draw.

ts
import type { AparteChatViewport, AparteMessage } from '@aparte/core';

declare const viewport: AparteChatViewport;
declare function streamAnswer(history: AparteMessage[]): AsyncIterable<string>;   // your model

async function answerInto(id: string): Promise<void> {
  // The path on screen, ending in the new empty reply: everything before it is the history.
  const history = viewport.getMessages().slice(0, -1);
  for await (const chunk of streamAnswer(history)) viewport.appendToken(id, chunk);
  viewport.completeMessage(id);
}

document.addEventListener('aparte-retry', (event) => {
  const id = viewport.addSiblingOf(event.detail.messageId, {
    id: crypto.randomUUID(), role: 'assistant', content: '', timestamp: Date.now(),
  });
  if (id) void answerInto(id);
});

document.addEventListener('aparte-edit', (event) => {
  const { messageId, content } = event.detail;
  viewport.updateMessage(messageId, { content });
  viewport.truncateResponsesAfter(messageId);
  const id = viewport.addSiblingOf(messageId, {
    id: crypto.randomUUID(), role: 'assistant', content: '', timestamp: Date.now(),
  });
  if (id) void answerInto(id);
});

The buttons only send their event; without a handler, an edit puts the original text back. addSiblingOf reads the role of the message you give it: beside a reply it adds a version of that reply, under one of the person’s messages it adds an answer to it — which is how the edit handler gets its fresh reply. truncateResponsesAfter drops every version of the replies under the edited message, so the new one starts alone. In React, Vue, Svelte or Angular, the framework package owns the messages: call addSiblingOf, updateMessage and truncateResponsesAfter on the chat’s ref, and stream with its injectTokenStream(id, tokens) in place of answerInto.

The version arrows, and retry and edit on every message, are disabled until the reply is done; Stop stays live. <aparte-chat-viewport> carries data-busy meanwhile, for controls of your own: aparte-chat:has(aparte-chat-viewport[data-busy]) .my-button { opacity: 0.5; }.

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

aparteGlobalConfig.setBubbleActions({ retry: false });     // keep edit, drop retry
aparteGlobalConfig.setBubbleActions({ user: ['copy'] });   // your messages: copy only

A user or assistant list replaces the flags for that role: to drop retry there, leave it out of the list. The picker needs no switch of its own: it shows only on a message that has versions. Its counter can be drawn your way, dots for instance, with setSiblingNavRenderer.