99 lines
6.5 KiB
JavaScript
99 lines
6.5 KiB
JavaScript
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")));
|
|
if (result.summary) {
|
|
const forget = actionButton("Forget running summary", () => run(put(`/chats/${id}/summary`, {summary: "", previousSummary: result.summary}), "summary"));
|
|
forget.title = "Remove the automatic summary from conversation memory";
|
|
panel.content.append(forget);
|
|
const sourceHeading = document.createElement("h4"); sourceHeading.textContent = "Messages included in this summary";
|
|
panel.content.append(sourceHeading);
|
|
for (const source of result.summarySources || []) {
|
|
const item = document.createElement("details"); item.className = "memory-summary-source";
|
|
const label = document.createElement("summary");
|
|
const excerpt = source.content.length > 160 ? `${source.content.slice(0, 157)}...` : source.content;
|
|
label.textContent = `#${source.id} · ${source.role === "user" ? "You" : "Assistant"}: ${excerpt}`;
|
|
const content = document.createElement("p"); content.textContent = source.content;
|
|
item.append(label, content); panel.content.append(item);
|
|
}
|
|
}
|
|
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();
|
|
}};
|
|
}
|