<bmx-chat>
A conversation, for an AI assistant or a support desk: answers written as they stream in, in Markdown with highlighted, copyable code, tables, cited sources and the steps the assistant took; files sent with a message; suggestions; asking again, editing and resending, rating an answer; and the whole conversation saved as Markdown, text or JSON.
25 properties · 7 events · 14 methods · 9 parts
Example
Show markup
<bmx-chat
id="ex-chat"
lang="en-GB"
label="Support assistant"
heading="How can I help?"
description="Ask about orders, refunds or the API. This demonstration answers from the page itself - point the handler at your own service."
assistant-name="Nova"
suggestions="How do refunds work?,Show me the API in TypeScript,Compare the plans"
attachments
style="--bmx-chat-height: 34rem"
></bmx-chat>
<div class="row" style="margin-block-start: 1rem; gap: 0.5rem; flex-wrap: wrap">
<button type="button" id="ex-chat-md">Save as Markdown</button>
<button type="button" id="ex-chat-clear">Clear</button>
</div>
<script type="module">
await customElements.whenDefined('bmx-chat');
const chat = document.getElementById('ex-chat');
// A stand-in for your own service: it streams a canned answer a few characters at a time.
// In an application: `chat.handler = ({ messages, signal }) => fetch('/api/chat', { method: 'POST', body: JSON.stringify(messages), signal });`
const answers = [
{
match: /refund/i,
tools: [{ name: 'search', status: 'done', summary: 'Searched the help centre', detail: 'query: "refund policy"\n3 articles found' }],
sources: [
{ title: 'Refunds and returns', url: 'https://binarymission.co.uk/support.html', snippet: 'Refunds are paid within 30 days of the return arriving.' },
{ title: 'Licence terms', url: 'https://binarymission.co.uk/eula.html' },
],
text: 'Refunds are paid to the card you used, **within 30 days** of the return arriving [1].\n\n1. Open *Orders* and choose the order.\n2. Press **Return items** and pick a reason.\n3. Print the label and send the parcel.\n\nSoftware licences follow the licence terms instead [2].',
},
{
match: /api|typescript|code/i,
text: 'Here is the whole round trip in TypeScript:\n\n```ts\nconst chat = document.querySelector("bmx-chat");\nchat.handler = async function* ({ messages, signal }) {\n const res = await fetch("/api/chat", { method: "POST", body: JSON.stringify(messages), signal });\n // Stream the body as it arrives.\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n yield decoder.decode(value, { stream: true });\n }\n};\n```\n\nThe chat calls it for every message and **Stop** aborts the `signal`.',
},
{
match: /plan|compare|price/i,
text: '| Plan | Components | Source licence |\n| --- | --- | --- |\n| Web | 43 | Separate |\n| Pro | 54 | Separate |\n| Gold | All | Separate |\n\nEvery plan includes the same support; Gold adds the grids, canvas and logs.',
},
];
chat.handler = async function* ({ text, signal, update }) {
const answer = answers.find(a => a.match.test(text)) ?? { text: `You asked: *${text.replace(/[*_`]/g, '')}*. Try one of the suggestions to see sources, code and tables.` };
if (answer.tools) {
update({ tools: answer.tools.map(t => ({ ...t, status: 'running' })) });
await new Promise(r => setTimeout(r, 700));
update({ tools: answer.tools });
}
if (answer.sources) update({ sources: answer.sources });
for (let i = 0; i < answer.text.length; i += 6) {
if (signal.aborted) return;
await new Promise(r => setTimeout(r, 18));
yield answer.text.slice(i, i + 6);
}
};
document.getElementById('ex-chat-md').addEventListener('click', () => chat.download('md', 'support-conversation'));
document.getElementById('ex-chat-clear').addEventListener('click', () => chat.clear());
</script>
The chat holds no model and sends nothing anywhere. Give it a handler
that asks your own service - your server, or a model you host - and
returns the reply as text, a stream or a fetch response; the chat calls
it for each message and streams what comes back. Or leave the handler out,
listen for bmxChatSend, and write replies with startReply,
appendToMessage and finishReply.
chat.handler = async function* ({ messages, signal }) { const res = await fetch('/my/chat', { method: 'POST', body: JSON.stringify(messages), signal }); yield* readMyStream(res); };
Answers are sanitised before they are shown, and images in them are left
out unless allow-images is set, so an answer cannot load anything.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
accept |
accept |
string |
— | The files allowed, as an <input type="file"> accept ("image/*,.pdf"). |
allowImages |
allow-images |
boolean |
false |
Show images an answer's Markdown points to. Off by default, so an answer cannot load anything. |
assistantAvatar |
assistant-avatar |
string |
— | The assistant's picture. Default: a mark drawn by the library. |
assistantName |
assistant-name |
string |
— | The assistant's name. |
attachments |
attachments |
boolean |
false |
Let the reader send files. |
busy |
busy |
boolean |
false |
A reply is being written. Set by the chat while its handler runs; set it yourself when writing replies without one. |
description |
description |
string |
— | A line under the title. |
disabled |
disabled |
boolean |
false |
Show the text box, but disabled. |
feedback |
feedback |
boolean |
true |
Offer good and poor buttons on answers. |
handler |
property only | BmxChatHandler |
— | Writes each reply: given the conversation and a signal, returns text, a stream, or a fetch response. |
heading |
heading |
string |
— | A title shown before the first message ("How can I help?"). |
label |
label |
string |
— | What the conversation is, for screen readers. |
locale |
locale |
string |
— | A BCP 47 locale for times and dates. Default: the page's lang. |
maxAttachmentSize |
max-attachment-size |
number |
20 * 1024 * 1024 |
The largest file, in bytes. Default 20 MB. |
maxAttachments |
max-attachments |
number |
5 |
At most this many files per message. |
maxLength |
max-length |
number |
— | At most this many characters in a message. |
messages |
messages |
readonly BmxChatMessage[] | string |
[] |
The conversation. JSON in markup. Updated as messages are sent and answered. |
placeholder |
placeholder |
string |
— | Shown in the text box while it is empty. |
readonly |
readonly |
boolean |
false |
Show the conversation without the text box. |
sendOnEnter |
send-on-enter |
boolean |
true |
Enter sends (Shift+Enter is a new line). Off: Ctrl+Enter or Cmd+Enter sends. |
strings |
strings |
Partial<BmxChatStrings> | string |
— | Words the chat shows or says: any of BmxChatStrings. JSON in markup. |
suggestions |
suggestions |
readonly string[] | string |
— | Messages the reader can send with one click: a list, or comma-separated in markup. |
userAvatar |
user-avatar |
string |
— | The reader's picture. |
userName |
user-name |
string |
— | The reader's name, on their own messages. |
voice |
voice |
boolean |
false |
Offer a microphone button that writes what is said, using the browser's speech recognition (some browsers send the audio to their maker's service). |
Events
| Event | Detail | Description |
|---|---|---|
bmxChatError |
BmxChatErrorDetail |
A reply failed. |
bmxChatFeedback |
BmxChatFeedbackDetail |
The reader rated an answer. |
bmxChatMessagesChange |
BmxChatMessagesDetail |
The conversation changed. messages is the whole new list. |
bmxChatReply |
BmxChatMessageDetail |
A reply was finished. |
bmxChatRetry |
BmxChatMessageDetail |
The reader asked for an answer again (with no handler, write it yourself). |
bmxChatSend |
BmxChatSendDetail |
The reader sent a message (cancelable). |
bmxChatStop |
BmxChatMessageDetail | null |
The reader pressed Stop. |
Methods
| Method | Signature | Description |
|---|---|---|
addMessage |
addMessage(message: Partial<BmxChatMessage>) => Promise<string> |
Adds a message (any role) and returns its id. |
appendToMessage |
appendToMessage(id: string, text: string) => Promise<void> |
Adds text to the end of a message, as a reply streams in. |
clear |
clear() => Promise<void> |
Empties the conversation. |
download |
download(format?: "md" | "txt" | "json", filename?: string) => Promise<void> |
Saves the conversation as md, txt or json. |
finishReply |
finishReply(id: string, status?: "done" | "error" | "stopped", error?: string) => Promise<void> |
Ends a reply started with startReply. |
getMessages |
getMessages() => Promise<readonly BmxChatMessage[]> |
The conversation. |
scrollToLatest |
scrollToLatest() => Promise<void> |
Scrolls to the latest message. |
send |
send(text: string, files?: readonly File[]) => Promise<void> |
Sends a message as the reader, as if typed. Resolves when its reply (if any) is done. |
setFocus |
setFocus() => Promise<void> |
Moves focus to the text box. |
startReply |
startReply(message?: Partial<BmxChatMessage>) => Promise<string> |
Starts a reply written by the application (with no handler): returns its id, and marks the chat busy. |
stop |
stop() => Promise<void> |
Stops the reply being written. |
toMarkdown |
toMarkdown() => Promise<string> |
The conversation as Markdown. |
toText |
toText() => Promise<string> |
The conversation as plain text. |
updateMessage |
updateMessage(id: string, patch: Partial<BmxChatMessage>) => Promise<void> |
Changes a message. |
CSS shadow parts
| Part | Description |
|---|---|
avatar |
an author's picture or initials. |
bubble |
a message's text. |
composer |
where the reader writes. |
empty |
what shows before the first message. |
input |
the text box. |
log |
the conversation. |
message |
one message. |
send |
the send (and stop) button. |
suggestions |
the suggested messages. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-chat-background |
Behind the conversation. |
--bmx-chat-bubble |
Everyone else's messages. |
--bmx-chat-height |
How tall the chat is. Default 32rem. |
--bmx-chat-max-width |
The widest a message grows. Default 46rem. |
--bmx-chat-user-bubble |
The reader's own messages. |
--bmx-chat-user-text |
Text on the reader's messages. |