Files
KrisHD_AI/public/js/memoryPanel.js
T
Fretux f67d918d09
CI / check (22) (push) Failing after 16m26s
CI / check (24) (push) Failing after 15m49s
CI / check (22) (pull_request) Failing after 16m26s
CI / check (24) (pull_request) Failing after 15m50s
Allow message retries for "pending" status, refactor message context handling, remove workspace import/export routes, and add SQLite-backed rate limiting.
2026-10-09 19:28:43 +02:00

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();
}};
}