Add support for message history pagination, preserve scroll in rendering, and improve draft/message workflows.
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import {get, post} from "./api.js";
|
||||
import {createConversationPanel, actionButton} from "./conversationPanel.js";
|
||||
|
||||
export function createBranchExplorer({state, setActiveChat, setMessageContent}) {
|
||||
const panel = createConversationPanel("branchExplorer", "Branches & answer comparison");
|
||||
let request = 0;
|
||||
return {async open() {
|
||||
const chatId = state.activeChatId;
|
||||
if (!chatId) return;
|
||||
const current = ++request;
|
||||
panel.open(); panel.notice("Loading branches..."); panel.content.replaceChildren();
|
||||
const nodes = [], cache = new Map();
|
||||
let next = 0, activeLeafId;
|
||||
const layout = document.createElement("div"); layout.className = "branch-layout";
|
||||
const sidebar = document.createElement("div"), comparison = document.createElement("div");
|
||||
sidebar.className = "branch-sidebar"; comparison.className = "answer-comparison";
|
||||
layout.append(sidebar, comparison); panel.content.append(layout);
|
||||
const tree = document.createElement("ol"); tree.className = "branch-tree"; tree.setAttribute("aria-label", "Conversation branches");
|
||||
const linked = state.chatSessions.filter(chat => chat.source_chat_id === chatId || chat.id === state.chatSessions.find(item => item.id === chatId)?.source_chat_id);
|
||||
if (linked.length) {
|
||||
const heading = document.createElement("h4"); heading.textContent = "Related conversations"; sidebar.append(heading);
|
||||
for (const chat of linked) sidebar.append(actionButton(chat.title, async () => {
|
||||
const result = await setActiveChat(chat.id);
|
||||
if (!result?.error) panel.close(); else panel.notice(result.error, true);
|
||||
}));
|
||||
}
|
||||
const hint = document.createElement("p"); hint.className = "subtitle";
|
||||
hint.textContent = "Select a message to compare answers or versions. Opening this panel keeps your active branch unchanged.";
|
||||
sidebar.append(hint, tree);
|
||||
const panes = ["Left answer", "Right answer"].map(label => {
|
||||
const section = document.createElement("section"); section.className = "comparison-pane";
|
||||
const heading = document.createElement("h4"); heading.textContent = label;
|
||||
const message = document.createElement("select"); message.setAttribute("aria-label", `${label} message`);
|
||||
const version = document.createElement("select"); version.setAttribute("aria-label", `${label} version`);
|
||||
const body = document.createElement("div"); body.className = "msg-content";
|
||||
let revision = 0;
|
||||
const render = async () => {
|
||||
const token = ++revision, id = Number(message.value);
|
||||
use.disabled = true; activate.disabled = true; version.disabled = true;
|
||||
body.replaceChildren(); version.replaceChildren();
|
||||
if (!id) return;
|
||||
let data = cache.get(id);
|
||||
if (!data) {
|
||||
const loaded = await get(`/chats/${chatId}/messages/${id}/versions`);
|
||||
if (token !== revision || current !== request) return;
|
||||
if (loaded.error) return panel.notice(loaded.error, true);
|
||||
data = loaded.message; cache.set(id, data);
|
||||
}
|
||||
const versions = data.responseVersions?.length ? data.responseVersions : [{content: data.content, modelName: data.modelName}];
|
||||
use.hidden = data.role !== "bot" || versions.length < 2;
|
||||
use.disabled = false; activate.disabled = false; version.disabled = false;
|
||||
version.replaceChildren(...versions.map((item, index) => {
|
||||
const option = document.createElement("option"); option.value = index;
|
||||
option.textContent = `Version ${index + 1}${item.modelName ? ` · ${item.modelName}` : ""}${index === data.versionIndex ? " (selected)" : ""}`;
|
||||
return option;
|
||||
}));
|
||||
version.value = String(label === "Left answer" && versions.length > 1 ? 0 : data.versionIndex || 0);
|
||||
const show = () => setMessageContent(body, versions[Number(version.value)]?.content || "");
|
||||
version.onchange = show; show();
|
||||
};
|
||||
message.onchange = render;
|
||||
const activate = actionButton("Open this branch", async () => {
|
||||
const result = await post(`/chats/${chatId}/branches/${message.value}/activate`, {includeDescendants: true});
|
||||
if (result.error) return panel.notice(result.error, true);
|
||||
const opened = await setActiveChat(chatId);
|
||||
if (opened?.error) return panel.notice(opened.error, true);
|
||||
panel.close();
|
||||
});
|
||||
const use = actionButton("Use this version", async () => {
|
||||
const result = await post(`/chats/${chatId}/messages/${message.value}/versions/${version.value}`, {});
|
||||
if (result.error) return panel.notice(result.error, true);
|
||||
cache.delete(Number(message.value));
|
||||
const refreshed = await setActiveChat(chatId);
|
||||
if (refreshed?.error) return panel.notice(refreshed.error, true);
|
||||
panel.notice("Selected answer version saved.");
|
||||
});
|
||||
const actions = document.createElement("div"); actions.className = "panel-actions"; actions.append(activate, use);
|
||||
section.append(heading, message, version, actions, body); comparison.append(section);
|
||||
return {message, render};
|
||||
});
|
||||
const loadMore = actionButton("Load more branches", async () => {
|
||||
const result = await get(`/chats/${chatId}/branches?after=${next}`);
|
||||
if (current !== request) return;
|
||||
if (result.error) return panel.notice(result.error, true);
|
||||
nodes.push(...result.nodes); next = result.next; activeLeafId = result.activeLeafId;
|
||||
tree.replaceChildren();
|
||||
const known = new Set(nodes.map(node => node.id)), children = new Map();
|
||||
for (const node of nodes) {
|
||||
const parent = known.has(node.parentMessageId) ? node.parentMessageId : null;
|
||||
children.set(parent, [...(children.get(parent) || []), node]);
|
||||
}
|
||||
const stack = (children.get(null) || []).map(node => ({node, level: 0})).reverse();
|
||||
while (stack.length) {
|
||||
const {node, level} = stack.pop();
|
||||
stack.push(...(children.get(node.id) || []).map(child => ({node: child, level: level + 1})).reverse());
|
||||
const row = document.createElement("li"); row.style.setProperty("--branch-depth", Math.min(level, 8));
|
||||
row.title = node.parentMessageId ? `Continues message #${node.parentMessageId}` : "Conversation root";
|
||||
const button = actionButton(`${node.role === "bot" ? "Assistant" : "You"} #${node.id}${node.id === activeLeafId ? " · active tip" : ""} — ${node.preview}`, async () => {
|
||||
panes[1].message.value = String(node.id); await panes[1].render();
|
||||
});
|
||||
if (node.id === activeLeafId) button.setAttribute("aria-current", "true");
|
||||
row.append(button); tree.append(row);
|
||||
}
|
||||
for (const pane of panes) {
|
||||
const selected = pane.message.value;
|
||||
pane.message.replaceChildren(...nodes.map(node => {
|
||||
const option = document.createElement("option"); option.value = node.id;
|
||||
option.textContent = `${node.role === "bot" ? "Assistant" : "You"} #${node.id} — ${node.preview.slice(0, 65)}`;
|
||||
return option;
|
||||
}));
|
||||
pane.message.value = selected || String(nodes.filter(node => node.role === "bot").at(-1)?.id || nodes.at(-1)?.id || "");
|
||||
await pane.render();
|
||||
}
|
||||
loadMore.hidden = next == null;
|
||||
panel.notice(nodes.length ? "" : "Send a message to start a conversation branch.");
|
||||
});
|
||||
sidebar.append(loadMore);
|
||||
await loadMore.onclick();
|
||||
}};
|
||||
}
|
||||
Reference in New Issue
Block a user