AkurAI Build
Menu

Public repository / 91bd2a37654909c5a3aef6ddd614991bd378ccb8

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;