This pattern works well for AI answers generated in stages: send an initial status, replace content when data is ready, then add follow-up suggestions.
1. Initial message
import { AIRich, Enums } from './lib/MessageBuilder.js';
const rich = new AIRich(sock, { dynamic: false })
.addText('Preparing an answer…', { id: 'answer' })
.addImage('https://example.com/placeholder.png', {
id: 'image',
status: Enums.IMAGINE_STATUS.GENERATING,
update_text: 'Generating illustration',
})
.addMetadata('0 seconds', { id: 'duration' });
const sent = await rich.send(jid);
const messageId = sent.key.id;
dynamic: false keeps responseId stable across edits. With dynamic: true, IDs are regenerated on every build().
2. Replace blocks
rich
.addText(
'Here are the three main points from the article:\n\n' +
'1. Rich responses carry sections and submessages.\n' +
'2. Every block can be assigned an ID.\n' +
'3. Edits use protocolMessage type 14.',
{ replace: 'answer' }
)
.addImage('https://example.com/chart.png', {
replace: 'image',
status: Enums.IMAGINE_STATUS.READY,
width: 640,
height: 360,
})
.addMetadata('Finished in 1.8 seconds', { replace: 'duration' });
await rich.sendEdit(jid, messageId);
replace finds a node by ID. The new node takes the old node’s position. You do not need to delete and re-add it.
3. Add suggestions
rich
.addSuggest(['Summarize in three sentences', 'Translate to English'], {
id: 'suggestions',
})
.addFooterAction(
[
{ text: 'Open article', type: 'OPEN_FULL_VIEW', url: 'https://example.com/article' },
],
{ id: 'action' }
);
await rich.sendEdit(jid, messageId);
4. Handle failures
try {
await rich.send(jid);
} catch (err) {
if (err.code === 'DUPLICATE_ID') {
console.error('ID already in use:', err.message);
} else {
console.error('Failed to send rich response:', err);
}
}
Wrap sendEdit() in its own try/catch. If the edit fails, the initial message remains and you can retry without sending a new one.
Notes
- Use stable, descriptive IDs. Avoid timestamp-based IDs that change on every render.
- Never combine
replaceandinsertAtin one call. send()already performs the initial sync. CallsendEdit()only for later changes.unifiedResponseis base64-encoded; large payloads increase message size. Limit block count and code length.unsupportedTypeAlert: trueadds fallbacks for unsupported types. Disable it only when you know the target supports them.
Flow summary
send() → initial message + sync edit
replace → swap content by id
sendEdit → send protocolMessage type 14