AkurAI Build
Menu

Public repository / 439f0dcabf5cdf2b120c7724be895cf1df30507f

AkurAI-Framework / site/frontend/ai/runtime/threadlist.js

Latest change 22a320a8e40a1822b386e2d5fd9bcf3537282876 - feat(ai): generative-UI / tool-call runtime + primitives by Ólafur Búi Ólafsson

// threadlist.js — switch among conversation threads.
//
// In-memory list mirrored to localStorage so threads survive reloads. Selecting
// a thread emits a `thread:select` CustomEvent (detail = the thread) on both the
// element and document, and calls opts.onSelect(thread) if provided.
//
// createThreadList(opts) -> {
//   element,                         // <nav class="toolui-threadlist">
//   add(thread), remove(id), select(id), rename(id, title),
//   list(), getSelected(),
// }
//   opts = {
//     threads = [],                  // [{ id, title, ts }]
//     selectedId,
//     storageKey = "toolui:threads", // localStorage key (null disables persist)
//     onSelect(thread),
//   }

const DEFAULT_KEY = "toolui:threads";

function loadStored(key) {
  if (!key || typeof localStorage === "undefined") return null;
  try {
    const raw = localStorage.getItem(key);
    return raw ? JSON.parse(raw) : null;
  } catch (err) {
    console.warn("[runtime] threadlist: bad stored data", err);
    return null;
  }
}

function persist(key, state) {
  if (!key || typeof localStorage === "undefined") return;
  try {
    localStorage.setItem(key, JSON.stringify(state));
  } catch (err) {
    console.warn("[runtime] threadlist: persist failed", err);
  }
}

function createThreadList(opts) {
  const o = opts || {};
  const storageKey = o.storageKey === null ? null : o.storageKey || DEFAULT_KEY;

  const stored = loadStored(storageKey);
  let threads = (stored && stored.threads) || o.threads || [];
  let selectedId = (stored && stored.selectedId) || o.selectedId || (threads[0] && threads[0].id) || null;

  const element = document.createElement("nav");
  element.className = "toolui-threadlist";
  element.setAttribute("aria-label", "Conversations");

  function save() {
    persist(storageKey, { threads, selectedId });
  }

  function render() {
    element.textContent = "";
    if (!threads.length) {
      const empty = document.createElement("p");
      empty.className = "small muted";
      empty.style.padding = "8px 4px";
      empty.textContent = "No conversations yet.";
      element.appendChild(empty);
      return;
    }
    for (const t of threads) {
      const item = document.createElement("button");
      item.type = "button";
      item.className = "btn btn-ghost btn-sm toolui-thread-item";
      item.style.display = "block";
      item.style.width = "100%";
      item.style.textAlign = "left";
      item.dataset.threadId = t.id;
      item.setAttribute("aria-current", t.id === selectedId ? "true" : "false");
      if (t.id === selectedId) item.classList.add("is-active");
      item.textContent = t.title || t.id;
      item.addEventListener("click", () => select(t.id));
      element.appendChild(item);
    }
  }

  function emitSelect(thread) {
    const ev = new CustomEvent("thread:select", { detail: thread, bubbles: true });
    element.dispatchEvent(ev);
    if (element !== document) document.dispatchEvent(new CustomEvent("thread:select", { detail: thread }));
    if (typeof o.onSelect === "function") o.onSelect(thread);
    console.log("[runtime] thread:select", thread && thread.id);
  }

  function select(id) {
    const thread = threads.find((t) => t.id === id);
    if (!thread) return null;
    selectedId = id;
    save();
    render();
    emitSelect(thread);
    return thread;
  }

  function add(thread) {
    const t = {
      id: thread.id || `t_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
      title: thread.title || "New conversation",
      ts: thread.ts || Date.now(),
    };
    threads = [t, ...threads];
    if (!selectedId) selectedId = t.id;
    save();
    render();
    return t;
  }

  function remove(id) {
    threads = threads.filter((t) => t.id !== id);
    if (selectedId === id) selectedId = threads[0] ? threads[0].id : null;
    save();
    render();
  }

  function rename(id, title) {
    const t = threads.find((x) => x.id === id);
    if (t) {
      t.title = title;
      save();
      render();
    }
  }

  render();

  return {
    element,
    add,
    remove,
    select,
    rename,
    list: () => threads.slice(),
    getSelected: () => threads.find((t) => t.id === selectedId) || null,
  };
}

const ToolUiThreadList = { createThreadList };
if (typeof window !== "undefined") window.ToolUiThreadList = ToolUiThreadList;

export { createThreadList };
export default ToolUiThreadList;