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