Menu
Public repository / 439f0dcabf5cdf2b120c7724be895cf1df30507f
AkurAI-Framework / site/frontend/ai/runtime/branch.js
Latest change 22a320a8e40a1822b386e2d5fd9bcf3537282876 - feat(ai): generative-UI / tool-call runtime + primitives by Ólafur Búi Ólafsson
// branch.js — navigate alternate message branches (regenerations / edits).
//
// A message can have N alternative versions. This picker shows "‹ k/N ›" and
// lets the user step prev/next. Each change emits a `branch:change` CustomEvent
// (detail = { index, total }) and calls opts.onChange(index, total).
//
// createBranchPicker(opts) -> {
// element, // <div class="toolui-branch">
// setIndex(i), next(), prev(),
// getIndex(), getTotal(), setTotal(n),
// }
// opts = { total = 1, index = 0, onChange(index, total) }
function createBranchPicker(opts) {
const o = opts || {};
let total = Math.max(1, o.total || 1);
let index = Math.min(Math.max(0, o.index || 0), total - 1);
const element = document.createElement("div");
element.className = "toolui-branch btn-group";
element.setAttribute("role", "group");
element.setAttribute("aria-label", "Message versions");
const prevBtn = document.createElement("button");
prevBtn.type = "button";
prevBtn.className = "btn btn-icon btn-icon-sm btn-ghost";
prevBtn.setAttribute("aria-label", "Previous version");
prevBtn.textContent = "‹";
const label = document.createElement("span");
label.className = "toolui-branch-label small muted";
label.style.padding = "0 8px";
label.setAttribute("aria-live", "polite");
const nextBtn = document.createElement("button");
nextBtn.type = "button";
nextBtn.className = "btn btn-icon btn-icon-sm btn-ghost";
nextBtn.setAttribute("aria-label", "Next version");
nextBtn.textContent = "›";
element.appendChild(prevBtn);
element.appendChild(label);
element.appendChild(nextBtn);
function paint() {
label.textContent = `${index + 1} / ${total}`;
prevBtn.disabled = index <= 0;
nextBtn.disabled = index >= total - 1;
}
function emit() {
const detail = { index, total };
element.dispatchEvent(new CustomEvent("branch:change", { detail, bubbles: true }));
document.dispatchEvent(new CustomEvent("branch:change", { detail }));
if (typeof o.onChange === "function") o.onChange(index, total);
console.log("[runtime] branch:change", detail);
}
function setIndex(i) {
const clamped = Math.min(Math.max(0, i), total - 1);
if (clamped === index) return index;
index = clamped;
paint();
emit();
return index;
}
function setTotal(n) {
total = Math.max(1, n);
if (index > total - 1) index = total - 1;
paint();
return total;
}
prevBtn.addEventListener("click", () => setIndex(index - 1));
nextBtn.addEventListener("click", () => setIndex(index + 1));
paint();
return {
element,
setIndex,
next: () => setIndex(index + 1),
prev: () => setIndex(index - 1),
getIndex: () => index,
getTotal: () => total,
setTotal,
};
}
const ToolUiBranch = { createBranchPicker };
if (typeof window !== "undefined") window.ToolUiBranch = ToolUiBranch;
export { createBranchPicker };
export default ToolUiBranch;