From d5959968660cd44da5452001117b97a973a87d60 Mon Sep 17 00:00:00 2001 From: Dorian Date: Tue, 3 Mar 2026 23:32:42 +0000 Subject: [PATCH] feat(chat): add AI memory panel with persistent facts (M9.7) Collapsible memory panel below persona selector. Add/edit/delete persistent facts (max 20) injected into every system prompt. Facts stored in localStorage, shown as collapsed "Memory (N/20)" toggle. Co-Authored-By: Claude Opus 4.6 --- .../app/src/components/chat/ChatWindow.vue | 2 + .../app/src/components/chat/MemoryPanel.vue | 122 ++++++++++++++++++ packages/app/src/composables/useAI.ts | 7 +- packages/app/src/stores/memory.ts | 85 ++++++++++++ 4 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 packages/app/src/components/chat/MemoryPanel.vue create mode 100644 packages/app/src/stores/memory.ts diff --git a/packages/app/src/components/chat/ChatWindow.vue b/packages/app/src/components/chat/ChatWindow.vue index cfb0a787..91d0c3dd 100644 --- a/packages/app/src/components/chat/ChatWindow.vue +++ b/packages/app/src/components/chat/ChatWindow.vue @@ -21,6 +21,7 @@ + +
+ + + + +
+
+
+ + +
+ +
+ + +
+ + +
+

+ Maximum 20 memories reached +

+
+
+ + + diff --git a/packages/app/src/composables/useAI.ts b/packages/app/src/composables/useAI.ts index 5363c841..0cde55ad 100644 --- a/packages/app/src/composables/useAI.ts +++ b/packages/app/src/composables/useAI.ts @@ -4,6 +4,7 @@ import { searchWeb } from '@/composables/useWebSearch' import { getApiKey } from '@/utils/key-vault' import type { ImageAttachment } from '@aiui/core/types/message' import { usePersonaStore } from '@/stores/personas' +import { useMemoryStore } from '@/stores/memory' type Provider = 'claude' | 'openrouter' | 'mock' @@ -304,7 +305,7 @@ function formatWebSearchContext(results: { title: string; url: string; content?: - You MAY add [[podcast_ext:...]] or [[film_ext:...]] tags for "to learn more" recommendations after your answer.\n\n${lines.join('\n')}` } -/** Build the system prompt, incorporating persona if active */ +/** Build the system prompt, incorporating persona and memory */ function buildSystemPrompt(chatStore: ReturnType): string { let prompt = SYSTEM_PROMPT @@ -318,6 +319,10 @@ function buildSystemPrompt(chatStore: ReturnType): string { } } + // Append memory facts + const memoryStore = useMemoryStore() + prompt += memoryStore.buildMemoryContext() + if (chatStore.webSearchEnabled) { prompt += ` diff --git a/packages/app/src/stores/memory.ts b/packages/app/src/stores/memory.ts new file mode 100644 index 00000000..d2c99b72 --- /dev/null +++ b/packages/app/src/stores/memory.ts @@ -0,0 +1,85 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' + +export interface MemoryItem { + id: string + text: string +} + +const STORAGE_KEY = 'aiui-memory' +const MAX_ITEMS = 20 + +function generateId(): string { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID() + } + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0 + return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16) + }) +} + +function loadFromStorage(): MemoryItem[] { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (!raw) return [] + return JSON.parse(raw) + } catch { + return [] + } +} + +function saveToStorage(items: MemoryItem[]) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(items)) +} + +export const useMemoryStore = defineStore('memory', () => { + const items = ref(loadFromStorage()) + + const isFull = computed(() => items.value.length >= MAX_ITEMS) + + function persist() { + saveToStorage(items.value) + } + + function addItem(text: string): MemoryItem | null { + if (items.value.length >= MAX_ITEMS) return null + const trimmed = text.trim() + if (!trimmed) return null + const item: MemoryItem = { id: generateId(), text: trimmed } + items.value.push(item) + persist() + return item + } + + function updateItem(id: string, text: string) { + const item = items.value.find(x => x.id === id) + if (!item) return + item.text = text.trim() + persist() + } + + function deleteItem(id: string) { + const idx = items.value.findIndex(x => x.id === id) + if (idx !== -1) { + items.value.splice(idx, 1) + persist() + } + } + + /** Build the memory section for the system prompt */ + function buildMemoryContext(): string { + if (items.value.length === 0) return '' + const facts = items.value.map(i => `- ${i.text}`).join('\n') + return `\n\n**User memory (always remember these facts):**\n${facts}` + } + + return { + items, + isFull, + addItem, + updateItem, + deleteItem, + buildMemoryContext, + } +})