Menu
Public repository / 91bd2a37654909c5a3aef6ddd614991bd378ccb8
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;