Add support for message history pagination, preserve scroll in rendering, and improve draft/message workflows.
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import {get, post, put, del} from "./api.js";
|
||||
import {createConversationPanel, actionButton} from "./conversationPanel.js";
|
||||
|
||||
export function createMemoryPanel({state}) {
|
||||
const panel = createConversationPanel("memoryPanel", "Conversation memory");
|
||||
let chatId, request = 0;
|
||||
function field(label, value = "", maxLength = 2000, key = "") {
|
||||
const wrapper = document.createElement("label"); wrapper.className = "field";
|
||||
const caption = document.createElement("span"); caption.textContent = label;
|
||||
const input = document.createElement("textarea"); input.value = value; input.maxLength = maxLength; input.rows = 3;
|
||||
input.dataset.draftKey = key; input.dataset.savedValue = value;
|
||||
wrapper.append(caption, input);
|
||||
return {wrapper, input};
|
||||
}
|
||||
async function load(edits = new Map()) {
|
||||
const current = ++request, id = chatId;
|
||||
panel.notice("Loading memory...");
|
||||
const result = await get(`/chats/${id}/memories`);
|
||||
if (current !== request) return;
|
||||
if (result.error) { panel.notice(result.error, true); return result; }
|
||||
panel.notice();
|
||||
panel.content.replaceChildren();
|
||||
const usage = document.createElement("p"); usage.className = "memory-context subtitle";
|
||||
const context = result.context;
|
||||
usage.textContent = `Estimated context: ${context.estimatedTokens.toLocaleString()} tokens${context.contextLength ? ` of ${context.contextLength.toLocaleString()}` : " (model context size not set)"}. Includes saved facts, summary, instructions and up to ${context.recentMessageLimit} recent messages. Token counts are approximate; your next message and new search results add to this.`;
|
||||
panel.content.append(usage);
|
||||
if (context.contextLength) {
|
||||
const meter = document.createElement("meter"); meter.min = 0; meter.max = context.contextLength;
|
||||
meter.value = Math.min(context.estimatedTokens, context.contextLength);
|
||||
meter.setAttribute("aria-label", "Estimated context usage"); panel.content.append(meter);
|
||||
}
|
||||
const run = async (operation, savedKey) => {
|
||||
const unsaved = new Map([...panel.content.querySelectorAll("textarea")]
|
||||
.filter(input => input.value !== input.dataset.savedValue).map(input => [input.dataset.draftKey, input.value]));
|
||||
const controls = [...panel.content.querySelectorAll("button, textarea")];
|
||||
controls.forEach(control => { control.disabled = true; });
|
||||
const response = await operation;
|
||||
if (current !== request) return;
|
||||
if (response.error) {
|
||||
controls.forEach(control => { control.disabled = false; });
|
||||
return panel.notice(response.error, true);
|
||||
}
|
||||
unsaved.delete(savedKey);
|
||||
const refreshed = await load(unsaved);
|
||||
if (refreshed?.error) {
|
||||
controls.forEach(control => { control.disabled = false; });
|
||||
return panel.notice(`Memory saved, but it could not be refreshed: ${refreshed.error}`, true);
|
||||
}
|
||||
panel.notice("Memory saved.");
|
||||
};
|
||||
const summary = field("Running summary", result.summary, result.summaryMaxChars, "summary");
|
||||
summary.input.rows = 6;
|
||||
const summaryHelp = document.createElement("p"); summaryHelp.className = "subtitle";
|
||||
summaryHelp.textContent = "Correct outdated information here. Future automatic summaries build on your corrections. Clear the field and save to remove the summary.";
|
||||
panel.content.append(summary.wrapper, summaryHelp, actionButton("Save summary", () => run(put(`/chats/${id}/summary`, {summary: summary.input.value, previousSummary: result.summary}), "summary")));
|
||||
const heading = document.createElement("h4"); heading.textContent = "Saved facts";
|
||||
const hint = document.createElement("p"); hint.className = "subtitle";
|
||||
hint.textContent = `The ${context.memoryFactLimit} most recently updated facts are included in context. Edit or remove facts that are no longer accurate.`;
|
||||
panel.content.append(heading, hint);
|
||||
for (const [index, memory] of result.memories.entries()) {
|
||||
const row = document.createElement("section"); row.className = "memory-fact";
|
||||
const key = `fact-${memory.id}`;
|
||||
const fact = field(`Fact ${index + 1}${index >= context.memoryFactLimit ? " (outside current context)" : ""}`, memory.fact, 800, key);
|
||||
const actions = document.createElement("div"); actions.className = "panel-actions";
|
||||
actions.append(actionButton("Save fact", () => run(put(`/chats/${id}/memories/${memory.id}`, {fact: fact.input.value}), key)),
|
||||
actionButton("Remove fact", () => run(del(`/chats/${id}/memories/${memory.id}`), key)));
|
||||
row.append(fact.wrapper, actions); panel.content.append(row);
|
||||
}
|
||||
const fresh = field("New fact", "", 800, "new");
|
||||
panel.content.append(fresh.wrapper, actionButton("Add fact", () => run(post(`/chats/${id}/memories`, {fact: fresh.input.value}), "new")));
|
||||
for (const input of panel.content.querySelectorAll("textarea")) {
|
||||
if (edits.has(input.dataset.draftKey)) input.value = edits.get(input.dataset.draftKey);
|
||||
}
|
||||
return {ok: true};
|
||||
}
|
||||
return {async open() {
|
||||
if (!state.activeChatId) return;
|
||||
chatId = state.activeChatId;
|
||||
panel.content.replaceChildren(); panel.open();
|
||||
await load();
|
||||
}};
|
||||
}
|
||||
Reference in New Issue
Block a user