Menu
Public repository / 439f0dcabf5cdf2b120c7724be895cf1df30507f
AkurAI-Framework / site/frontend/ai/chat/actionbar.js
Latest change f78c65c17425a60672d123a11f89e8211c141393 - feat(ai/chat): native-ESM port of assistant-ui chat primitives by Ólafur Búi Ólafsson
// actionbar.js — ActionBar primitive (native port of assistant-ui's ActionBar).
//
// A row of per-message actions: copy, retry, edit. The primitive makes no
// backend assumptions — it dispatches a `chat:action` CustomEvent on its root
// element (bubbling) and also invokes an optional `onAction(name, detail)`
// callback. Consumers decide what copy/retry/edit actually do.
//
// Native ES module — pure DOM, zero dependencies.
//
// Public API:
// createActionBar({ actions, message, onAction }) -> handle
// handle.el the action-bar element
// handle.setVisible(bool) show/hide the bar
// actions defaults to ["copy", "retry", "edit"]; pass a subset to trim it.
// The "copy" action copies the message's text to the clipboard itself (best
// effort) and still emits the event so callers can react (e.g. toast).
const ICONS = {
copy: "⧉",
retry: "↻",
edit: "✎",
};
const TITLES = {
copy: "Copy message",
retry: "Retry",
edit: "Edit",
};
function createActionBar(options = {}) {
const actions = Array.isArray(options.actions) && options.actions.length
? options.actions
: ["copy", "retry", "edit"];
const message = options.message || null;
const onAction = typeof options.onAction === "function" ? options.onAction : null;
const el = document.createElement("div");
el.className = "chat-actionbar";
el.setAttribute("role", "toolbar");
el.setAttribute("aria-label", "Message actions");
for (const name of actions) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "btn btn-icon btn-icon-sm btn-ghost chat-action";
btn.dataset.action = name;
btn.title = TITLES[name] || name;
btn.setAttribute("aria-label", TITLES[name] || name);
btn.textContent = ICONS[name] || name;
btn.addEventListener("click", () => fire(name, btn));
el.appendChild(btn);
}
function fire(name, btn) {
const detail = { action: name, message, text: message ? message.getText() : undefined };
if (name === "copy" && detail.text != null) {
copyText(detail.text).then((ok) => {
if (ok) flash(btn, "✓");
console.log("[chat] copy", ok ? "ok" : "failed");
});
}
el.dispatchEvent(new CustomEvent("chat:action", { detail, bubbles: true }));
if (onAction) onAction(name, detail);
}
return {
el,
setVisible(on) {
el.classList.toggle("is-hidden", !on);
},
};
}
function flash(btn, glyph) {
const original = btn.textContent;
btn.textContent = glyph;
setTimeout(() => {
btn.textContent = original;
}, 1100);
}
function copyText(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text).then(
() => true,
() => false
);
}
// Fallback for non-secure contexts: a throwaway textarea + execCommand.
try {
const ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
const ok = document.execCommand("copy");
document.body.removeChild(ta);
return Promise.resolve(ok);
} catch {
return Promise.resolve(false);
}
}
export { createActionBar };
export default createActionBar;