|
|
|
|
|
|
| import uiModule from './ui.js';
|
| import sessionModule from './sessions.js';
|
| import spinnerModule from './spinner.js';
|
| import { makeWindowDraggable } from './windowDrag.js';
|
| import { snapModalToZone } from './tileManager.js';
|
|
|
| var escapeHtml = uiModule.esc;
|
|
|
| let memories = [];
|
| let activeCategory = 'all';
|
| let sortOrder = 'newest';
|
| let selectMode = false;
|
| let selectedIds = new Set();
|
|
|
|
|
| const MEMORY_CATEGORIES = ['fact', 'identity', 'preference', 'contact', 'project', 'goal', 'task'];
|
|
|
| function _ensureNewMemoryCategorySelect() {
|
| const sel = document.getElementById('new-memory-category');
|
| if (!sel || sel.dataset.wired === '1') return;
|
| sel.dataset.wired = '1';
|
| MEMORY_CATEGORIES.forEach(cat => {
|
| const opt = document.createElement('option');
|
| opt.value = cat;
|
| opt.textContent = cat;
|
| if (cat === 'fact') opt.selected = true;
|
| sel.appendChild(opt);
|
| });
|
| }
|
|
|
| function _readNewMemoryCategory() {
|
| _ensureNewMemoryCategorySelect();
|
| const sel = document.getElementById('new-memory-category');
|
| const cat = sel?.value || 'fact';
|
| return MEMORY_CATEGORIES.includes(cat) ? cat : 'fact';
|
| }
|
|
|
| let _memoryDragWired = false;
|
| function _wireMemoryDrag() {
|
| if (_memoryDragWired) return;
|
| const modal = document.getElementById('memory-modal');
|
| const content = modal && modal.querySelector('.modal-content');
|
| const header = modal && modal.querySelector('.modal-header');
|
| if (!modal || !content || !header) return;
|
| _memoryDragWired = true;
|
| makeWindowDraggable(modal, {
|
| content,
|
| header,
|
| skipSelector: 'button, input, select, label',
|
| enableDock: true,
|
| enableLeftDock: true,
|
| onEnterFullscreen: () => {
|
| snapModalToZone(modal, {
|
| name: 'fullscreen',
|
| rect: {
|
| left: 0,
|
| top: 0,
|
| width: window.innerWidth || document.documentElement.clientWidth || 0,
|
| height: window.innerHeight || document.documentElement.clientHeight || 0,
|
| },
|
| });
|
| },
|
| });
|
| }
|
|
|
| function relativeTime(timestamp) {
|
| const now = Math.floor(Date.now() / 1000);
|
| const diff = now - timestamp;
|
| if (diff < 60) return 'just now';
|
| if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
| if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
| if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
|
| if (diff < 2592000) return `${Math.floor(diff / 604800)}w ago`;
|
| if (diff < 31536000) return `${Math.floor(diff / 2592000)}mo ago`;
|
| return `${Math.floor(diff / 31536000)}y ago`;
|
| }
|
|
|
| function buildCategoryChips() {
|
| const container = document.getElementById('memory-category-filters');
|
| if (!container) return;
|
|
|
|
|
|
|
| if (!memories.length) { container.innerHTML = ''; return; }
|
|
|
| const cats = new Set(memories.map(m => m.category || 'fact'));
|
| const sorted = ['all', ...Array.from(cats).sort()];
|
|
|
| container.innerHTML = '';
|
| sorted.forEach(cat => {
|
| const btn = document.createElement('button');
|
| btn.className = 'memory-cat-chip' + (cat === activeCategory ? ' active' : '');
|
| btn.dataset.cat = cat;
|
| btn.textContent = cat;
|
| btn.addEventListener('click', () => {
|
| activeCategory = cat;
|
| container.querySelectorAll('.memory-cat-chip').forEach(b => b.classList.remove('active'));
|
| btn.classList.add('active');
|
| renderMemoryList();
|
| updateMemoryCount();
|
| });
|
| container.appendChild(btn);
|
| });
|
| }
|
|
|
| async function syncToggles() {
|
|
|
|
|
| await syncPrefToggle('memory-enabled-header-toggle', 'memory_enabled', 'Memory enabled', 'Memory disabled', false);
|
|
|
|
|
|
|
| await syncPrefToggle('skills-enabled-header-toggle', 'skills_enabled', 'Skills enabled', 'Skills disabled', false);
|
| await syncPrefToggle('auto-memory-toggle', 'auto_memory', 'Auto-extract memories enabled', 'Auto-extract memories disabled', false);
|
| await syncPrefToggle('auto-skills-toggle', 'auto_skills', 'Auto-extract skills enabled', 'Auto-extract skills disabled', false);
|
| await syncPrefToggle('auto-approve-skills-toggle', 'auto_approve_skills', 'Auto-approve skills enabled', 'Auto-approve skills disabled', false);
|
| await syncPrefSlider('skill-confidence-slider', 'skill_min_confidence', 'skill-confidence-label', 0.85);
|
| await syncPrefNumber('skill-max-input', 'skill_max_injected', 3);
|
|
|
|
|
| const headerToggle = document.getElementById('memory-enabled-header-toggle');
|
| if (headerToggle) {
|
| const modalBody = document.querySelector('.memory-modal-body');
|
| if (modalBody) modalBody.style.opacity = headerToggle.checked ? '' : '0.3';
|
| reflectMemoryToggleInSidebar(headerToggle.checked);
|
| if (!headerToggle.dataset.boundUx) {
|
| headerToggle.dataset.boundUx = '1';
|
| headerToggle.addEventListener('change', () => {
|
| if (modalBody) modalBody.style.opacity = headerToggle.checked ? '' : '0.3';
|
| reflectMemoryToggleInSidebar(headerToggle.checked);
|
| });
|
| }
|
| }
|
|
|
|
|
| const skillsToggle = document.getElementById('skills-enabled-header-toggle');
|
| if (skillsToggle) {
|
| const skillsPanel = document.querySelector('[data-memory-panel="skills"]');
|
| const applyDim = () => { if (skillsPanel) skillsPanel.style.opacity = skillsToggle.checked ? '' : '0.3'; };
|
| applyDim();
|
| if (!skillsToggle.dataset.boundUx) {
|
| skillsToggle.dataset.boundUx = '1';
|
| skillsToggle.addEventListener('change', applyDim);
|
| }
|
| }
|
| }
|
|
|
| function reflectMemoryToggleInSidebar(enabled) {
|
| const btn = document.getElementById('tool-memory-btn');
|
| if (btn) btn.classList.toggle('tool-disabled', !enabled);
|
| }
|
|
|
| function syncToggleDim(toggle) {
|
| const card = toggle.closest('.admin-card');
|
| if (!card) return;
|
| const toggleRow = toggle.closest('div[style*="justify-content"]');
|
| let sibling = toggleRow ? toggleRow.nextElementSibling : null;
|
| while (sibling) {
|
| sibling.style.opacity = toggle.checked ? '' : '0.35';
|
| sibling.style.pointerEvents = toggle.checked ? '' : 'none';
|
| sibling = sibling.nextElementSibling;
|
| }
|
| }
|
|
|
| |
| |
|
|
| async function syncPrefSlider(elementId, prefKey, labelId, defaultVal) {
|
| const slider = document.getElementById(elementId);
|
| if (!slider) return;
|
| const label = labelId ? document.getElementById(labelId) : null;
|
| const maxPos = Number(slider.max);
|
| const fmt = (pos) => (Number(pos) >= maxPos ? 'All' : `≥ ${pos}%`);
|
| try {
|
| const res = await fetch(`${window.location.origin}/api/prefs/${prefKey}`);
|
| if (res.ok) {
|
| const data = await res.json();
|
| let pref = (data.value === undefined || data.value === null) ? defaultVal : Number(data.value);
|
|
|
| let pos = (!pref || pref <= 0) ? maxPos : Math.round(pref * 100);
|
| pos = Math.max(Number(slider.min), Math.min(maxPos, pos));
|
| slider.value = String(pos);
|
| }
|
| } catch (e) {
|
| console.error(`Failed to load ${prefKey} pref:`, e);
|
| }
|
| if (label) label.textContent = fmt(slider.value);
|
| if (!slider.dataset.bound) {
|
| slider.dataset.bound = '1';
|
| slider.addEventListener('input', () => { if (label) label.textContent = fmt(slider.value); });
|
| slider.addEventListener('change', async () => {
|
| const pos = Number(slider.value);
|
| const pref = pos >= maxPos ? 0 : pos / 100;
|
| try {
|
| const res = await fetch(`${window.location.origin}/api/prefs/${prefKey}`, {
|
| method: 'PUT',
|
| headers: { 'Content-Type': 'application/json' },
|
| body: JSON.stringify({ value: pref })
|
| });
|
| if (!res.ok) { showError('Failed to save preference'); return; }
|
| showToast(pref === 0 ? 'Skill confidence: All' : `Skill confidence ≥ ${Math.round(pref * 100)}%`);
|
| } catch (e) {
|
| console.error(`Failed to save ${prefKey} pref:`, e);
|
| showError('Failed to save preference');
|
| }
|
| });
|
| }
|
| }
|
|
|
|
|
| async function syncPrefNumber(elementId, prefKey, defaultVal) {
|
| const input = document.getElementById(elementId);
|
| if (!input) return;
|
| const clamp = (raw) => {
|
| let v = parseInt(raw, 10);
|
| if (isNaN(v)) v = defaultVal;
|
| const lo = Number(input.min), hi = Number(input.max);
|
| if (!isNaN(lo)) v = Math.max(lo, v);
|
| if (!isNaN(hi)) v = Math.min(hi, v);
|
| return v;
|
| };
|
| try {
|
| const res = await fetch(`${window.location.origin}/api/prefs/${prefKey}`);
|
| if (res.ok) {
|
| const data = await res.json();
|
| input.value = String((data.value === undefined || data.value === null) ? defaultVal : clamp(data.value));
|
| }
|
| } catch (e) {
|
| console.error(`Failed to load ${prefKey} pref:`, e);
|
| }
|
| if (!input.dataset.bound) {
|
| input.dataset.bound = '1';
|
| input.addEventListener('change', async () => {
|
| const v = clamp(input.value);
|
| input.value = String(v);
|
| try {
|
| const res = await fetch(`${window.location.origin}/api/prefs/${prefKey}`, {
|
| method: 'PUT',
|
| headers: { 'Content-Type': 'application/json' },
|
| body: JSON.stringify({ value: v })
|
| });
|
| if (!res.ok) { showError('Failed to save preference'); return; }
|
| showToast(v === 0 ? 'No skills injected' : `Max injected skills: ${v}`);
|
| } catch (e) {
|
| console.error(`Failed to save ${prefKey} pref:`, e);
|
| showError('Failed to save preference');
|
| }
|
| });
|
| }
|
| }
|
|
|
| async function syncPrefToggle(elementId, prefKey, onMsg, offMsg, dimBelow = true) {
|
| const toggle = document.getElementById(elementId);
|
| if (!toggle) return;
|
| try {
|
| const res = await fetch(`${window.location.origin}/api/prefs/${prefKey}`);
|
| if (res.ok) {
|
| const data = await res.json();
|
| toggle.checked = data.value !== false;
|
| }
|
| } catch (e) {
|
| console.error(`Failed to load ${prefKey} pref:`, e);
|
| }
|
| if (dimBelow) syncToggleDim(toggle);
|
| if (!toggle.dataset.bound) {
|
| toggle.dataset.bound = '1';
|
| toggle.addEventListener('change', async () => {
|
| if (dimBelow) syncToggleDim(toggle);
|
| try {
|
| const res = await fetch(`${window.location.origin}/api/prefs/${prefKey}`, {
|
| method: 'PUT',
|
| headers: { 'Content-Type': 'application/json' },
|
| body: JSON.stringify({ value: toggle.checked })
|
| });
|
| if (!res.ok) {
|
| console.error(`PUT ${prefKey} returned ${res.status}`);
|
| toggle.checked = !toggle.checked;
|
| if (dimBelow) syncToggleDim(toggle);
|
| showError('Failed to save preference');
|
| return;
|
| }
|
| showToast(toggle.checked ? onMsg : offMsg);
|
| } catch (e) {
|
| console.error(`Failed to save ${prefKey} pref:`, e);
|
| toggle.checked = !toggle.checked;
|
| if (dimBelow) syncToggleDim(toggle);
|
| showError('Failed to save preference');
|
| }
|
| });
|
| }
|
| }
|
|
|
| export async function loadMemories() {
|
| _ensureNewMemoryCategorySelect();
|
| try {
|
| const response = await fetch(`${window.location.origin}/api/memory`);
|
|
|
| if (!response.ok) {
|
| console.error('Memory fetch failed with status:', response.status);
|
| memories = [];
|
| buildCategoryChips();
|
| renderMemoryList();
|
| updateMemoryCount();
|
| syncToggles();
|
| return;
|
| }
|
|
|
| const data = await response.json();
|
|
|
| if (data && data.memory) {
|
| memories = data.memory;
|
| } else if (Array.isArray(data)) {
|
| memories = data;
|
| } else {
|
| memories = [];
|
| }
|
|
|
| buildCategoryChips();
|
| renderMemoryList();
|
| updateMemoryCount();
|
| } catch (error) {
|
| console.error('Failed to load memories:', error);
|
| memories = [];
|
| buildCategoryChips();
|
| renderMemoryList();
|
| updateMemoryCount();
|
| }
|
|
|
| syncToggles();
|
| }
|
|
|
|
|
|
|
| function enterSelectMode() {
|
| selectMode = true;
|
| selectedIds.clear();
|
| const bulkBar = document.getElementById('memory-bulk-bar');
|
| const selectBtn = document.getElementById('memory-select-btn');
|
| if (bulkBar) bulkBar.classList.remove('hidden');
|
| if (selectBtn) { selectBtn.classList.add('active'); selectBtn.textContent = 'Cancel'; }
|
| updateBulkCount();
|
| renderMemoryList();
|
| }
|
|
|
| function exitSelectMode() {
|
| selectMode = false;
|
| selectedIds.clear();
|
| const bulkBar = document.getElementById('memory-bulk-bar');
|
| const selectBtn = document.getElementById('memory-select-btn');
|
| const selectAll = document.getElementById('memory-select-all');
|
| if (bulkBar) bulkBar.classList.add('hidden');
|
| if (selectBtn) { selectBtn.classList.remove('active'); selectBtn.textContent = 'Select'; }
|
| if (selectAll) selectAll.checked = false;
|
| renderMemoryList();
|
| }
|
|
|
| function toggleSelectItem(id) {
|
| if (selectedIds.has(id)) {
|
| selectedIds.delete(id);
|
| } else {
|
| selectedIds.add(id);
|
| }
|
| updateBulkCount();
|
| }
|
|
|
| function updateBulkCount() {
|
| const countEl = document.getElementById('memory-selected-count');
|
| const deleteBtn = document.getElementById('memory-bulk-delete');
|
| if (countEl) countEl.textContent = `${selectedIds.size} Selected`;
|
| if (deleteBtn) deleteBtn.disabled = selectedIds.size === 0;
|
| }
|
|
|
| function toggleSelectAll() {
|
| const selectAllEl = document.getElementById('memory-select-all');
|
| if (!selectAllEl) return;
|
|
|
| if (selectAllEl.checked) {
|
|
|
| const visible = getFilteredMemories();
|
| visible.forEach(m => selectedIds.add(m.id));
|
| } else {
|
| selectedIds.clear();
|
| }
|
| updateBulkCount();
|
| renderMemoryList();
|
| }
|
|
|
| async function bulkDelete() {
|
| if (selectedIds.size === 0) return;
|
| const count = selectedIds.size;
|
| if (!await uiModule.styledConfirm(`Delete ${count} ${count === 1 ? 'memory' : 'memories'}?`, { confirmText: 'Delete', danger: true })) return;
|
|
|
| let deleted = 0;
|
| const deletedIds = [];
|
| for (const id of selectedIds) {
|
| try {
|
| const res = await fetch(`${window.location.origin}/api/memory/${id}`, { method: 'DELETE' });
|
| if (res.ok) {
|
| deleted++;
|
| deletedIds.push(id);
|
| }
|
| } catch (e) {
|
| console.error('Failed to delete memory:', id, e);
|
| }
|
| }
|
|
|
| await animateMemoryRemoval(deletedIds);
|
| exitSelectMode();
|
| await loadMemories();
|
| showToast(`Deleted ${deleted} ${deleted === 1 ? 'memory' : 'memories'}`);
|
| }
|
|
|
|
|
|
|
| export async function tidyMemories() {
|
| const tidyBtn = document.getElementById('memory-tidy-btn');
|
| let tidySpinner = null;
|
| if (tidyBtn) {
|
| tidyBtn.disabled = true;
|
| tidyBtn.textContent = '';
|
|
|
|
|
| tidyBtn.style.border = 'none';
|
| tidyBtn.style.background = 'none';
|
| tidySpinner = spinnerModule.create('', 'clean', 'whirlpool');
|
| const _spEl = tidySpinner.createElement();
|
| _spEl.style.position = 'relative';
|
| _spEl.style.top = '1px';
|
| tidyBtn.appendChild(_spEl);
|
| tidySpinner.start();
|
| }
|
|
|
|
|
| const beforeMap = new Map(memories.map(m => [m.id, { ...m }]));
|
|
|
| try {
|
| const res = await fetch(`${window.location.origin}/api/memory/audit`, {
|
| method: 'POST',
|
| });
|
|
|
| if (!res.ok) {
|
| const err = await res.json().catch(() => ({}));
|
| throw new Error(err.detail || 'Audit failed');
|
| }
|
|
|
| const data = await res.json();
|
| if ((data.removed || 0) === 0) {
|
| if (tidySpinner) tidySpinner.destroy();
|
| if (tidyBtn) { tidyBtn.disabled = false; tidyBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor" style="vertical-align:-1px;margin-right:2px;"><path d="M12 0L14.59 8.41L23 12L14.59 15.59L12 24L9.41 15.59L1 12L9.41 8.41Z"/></svg> Tidy'; }
|
| showToast('Already clean');
|
| return;
|
| }
|
|
|
|
|
| const freshRes = await fetch(`${window.location.origin}/api/memory`);
|
| const freshData = await freshRes.json();
|
| const afterList = freshData.memory || freshData || [];
|
| const afterMap = new Map(afterList.map(m => [m.id, m]));
|
|
|
|
|
| const removed = [];
|
| const edited = [];
|
| for (const [id, oldMem] of beforeMap) {
|
| if (!afterMap.has(id)) {
|
| removed.push(id);
|
| } else if (afterMap.get(id).text !== oldMem.text) {
|
| edited.push({ id, oldText: oldMem.text, newText: afterMap.get(id).text });
|
| }
|
| }
|
|
|
| if (tidySpinner) tidySpinner.updateMessage('Tidying memories');
|
|
|
|
|
| await animateTidyDiff(removed, edited);
|
|
|
|
|
| memories = afterList;
|
| buildCategoryChips();
|
| renderMemoryList();
|
| updateMemoryCount();
|
|
|
| showToast(`Tidied: ${data.removed} removed (${data.before} \u2192 ${data.after})`);
|
| } catch (error) {
|
| console.error('Tidy failed:', error);
|
| showError('Tidy failed — check console');
|
| } finally {
|
| if (tidySpinner) tidySpinner.destroy();
|
| if (tidyBtn) {
|
| tidyBtn.disabled = false;
|
| tidyBtn.style.border = '';
|
| tidyBtn.style.background = '';
|
| tidyBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor" style="vertical-align:-1px;margin-right:2px;"><path d="M12 0L14.59 8.41L23 12L14.59 15.59L12 24L9.41 15.59L1 12L9.41 8.41Z"/></svg> Tidy';
|
| }
|
| }
|
| }
|
|
|
| function sleep(ms) {
|
| return new Promise(r => setTimeout(r, ms));
|
| }
|
|
|
| async function animateMemoryRemoval(ids) {
|
| const idSet = new Set([...ids].map(id => String(id)));
|
| const memoryList = document.getElementById('memory-list');
|
| if (!memoryList || !idSet.size) return;
|
| const items = Array.from(memoryList.querySelectorAll('.memory-item[data-memory-id]'))
|
| .filter(el => idSet.has(String(el.dataset.memoryId)));
|
| if (!items.length) return;
|
| for (const el of items) {
|
| el.style.maxHeight = `${Math.max(el.getBoundingClientRect().height, el.scrollHeight)}px`;
|
| el.classList.add('memory-tidy-removing');
|
| }
|
| await sleep(520);
|
| }
|
|
|
| async function animateTidyDiff(removedIds, editedItems) {
|
| const memoryList = document.getElementById('memory-list');
|
| if (!memoryList) return;
|
|
|
|
|
| const items = memoryList.querySelectorAll('.memory-item');
|
| const itemMap = new Map();
|
| const filtered = getFilteredMemories();
|
| items.forEach((el, i) => {
|
| if (filtered[i]) itemMap.set(filtered[i].id, el);
|
| });
|
|
|
|
|
| for (const { id, oldText, newText } of editedItems) {
|
| const el = itemMap.get(id);
|
| if (!el) continue;
|
|
|
| const textEl = el.querySelector('.memory-item-text');
|
| if (!textEl) continue;
|
|
|
| el.classList.add('memory-tidy-editing');
|
| textEl.classList.add('memory-tidy-text-old');
|
| await sleep(300);
|
|
|
| textEl.textContent = newText;
|
| textEl.classList.remove('memory-tidy-text-old');
|
| textEl.classList.add('memory-tidy-text-new');
|
| await sleep(400);
|
|
|
| el.classList.remove('memory-tidy-editing');
|
| textEl.classList.remove('memory-tidy-text-new');
|
| await sleep(100);
|
| }
|
|
|
|
|
| for (const id of removedIds) {
|
| const el = itemMap.get(id);
|
| if (!el) continue;
|
|
|
| el.classList.add('memory-tidy-removing');
|
| await sleep(200);
|
| }
|
|
|
|
|
| if (removedIds.length > 0) {
|
| await sleep(500);
|
| }
|
| }
|
|
|
|
|
|
|
| function getFilteredMemories() {
|
| const searchTerm = document.getElementById('memory-search')?.value?.toLowerCase().trim() || '';
|
|
|
| let filtered = searchTerm
|
| ? memories.filter(m => m.text && m.text.toLowerCase().includes(searchTerm))
|
| : [...memories];
|
|
|
| if (activeCategory !== 'all') {
|
| filtered = filtered.filter(m => (m.category || 'fact') === activeCategory);
|
| }
|
|
|
| const sortSelect = document.getElementById('memory-sort');
|
| const sort = sortSelect ? sortSelect.value : sortOrder;
|
| if (sort === 'newest') {
|
| filtered.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
|
| } else if (sort === 'oldest') {
|
| filtered.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
| } else if (sort === 'alpha') {
|
| filtered.sort((a, b) => (a.text || '').localeCompare(b.text || ''));
|
| } else if (sort === 'uses') {
|
| filtered.sort((a, b) => (b.uses || 0) - (a.uses || 0) || (b.timestamp || 0) - (a.timestamp || 0));
|
| }
|
|
|
|
|
| filtered.sort((a, b) => (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0));
|
|
|
| return filtered;
|
| }
|
|
|
|
|
|
|
| export function renderMemoryList() {
|
| const memoryList = document.getElementById('memory-list');
|
| if (!memoryList) {
|
| console.error('Memory list element not found');
|
| return;
|
| }
|
|
|
| const filtered = getFilteredMemories();
|
| memoryList.innerHTML = '';
|
|
|
| if (filtered.length === 0) {
|
| const selectBtn = document.getElementById('memory-select-btn');
|
| if (selectBtn) selectBtn.disabled = true;
|
| if (selectMode) exitSelectMode();
|
| const searchTerm = document.getElementById('memory-search')?.value?.trim() || '';
|
| const _smiley = '<span style="vertical-align:-3px;margin-left:6px;">' + uiModule.emptyStateIcon('smiley') + '</span>';
|
| if (searchTerm || activeCategory !== 'all') {
|
| memoryList.innerHTML = `<div class="memory-empty">No matches.</div>`;
|
| } else {
|
| memoryList.innerHTML = `<div class="memory-empty" style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;">
|
| <span>No memories yet${_smiley}</span>
|
| <span style="opacity:0.7;font-size:11px;display:block;">
|
| <a href="#" data-mem-goto-add style="color:var(--accent,var(--red));text-decoration:underline;">Import in Add tab</a>
|
| </span>
|
| </div>`;
|
| memoryList.querySelector('[data-mem-goto-add]')?.addEventListener('click', (e) => {
|
| e.preventDefault();
|
| document.querySelector('.memory-tab[data-memory-tab="add"]')?.click();
|
| });
|
| }
|
| return;
|
| }
|
|
|
| const selectBtn = document.getElementById('memory-select-btn');
|
| if (selectBtn) selectBtn.disabled = false;
|
|
|
| filtered.forEach(memory => {
|
| const item = document.createElement('div');
|
| item.className = 'memory-item';
|
| item.dataset.memoryId = String(memory.id);
|
|
|
|
|
| if (selectMode) {
|
| const cb = document.createElement('input');
|
| cb.type = 'checkbox';
|
| cb.className = 'memory-select-cb';
|
| cb.checked = selectedIds.has(memory.id);
|
| cb.addEventListener('change', () => {
|
| toggleSelectItem(memory.id);
|
| const selectAllEl = document.getElementById('memory-select-all');
|
| if (selectAllEl) selectAllEl.checked = filtered.every(m => selectedIds.has(m.id));
|
| });
|
| item.appendChild(cb);
|
| item.style.cursor = 'pointer';
|
| item.addEventListener('click', (e) => {
|
| if (e.target === cb) return;
|
| cb.checked = !cb.checked;
|
| cb.dispatchEvent(new Event('change'));
|
| });
|
| }
|
|
|
|
|
| const content = document.createElement('div');
|
| content.className = 'memory-item-content';
|
|
|
| const textSpan = document.createElement('span');
|
| textSpan.className = 'memory-item-text';
|
| textSpan.textContent = memory.text;
|
|
|
| const meta = document.createElement('div');
|
| meta.className = 'memory-item-meta';
|
|
|
| if (memory.pinned) {
|
| const pinBadge = document.createElement('span');
|
| pinBadge.className = 'memory-cat-badge memory-cat-pinned';
|
| pinBadge.textContent = 'pinned';
|
| meta.appendChild(pinBadge);
|
| }
|
|
|
| const catBadge = document.createElement('span');
|
| const cat = memory.category || 'fact';
|
| catBadge.className = 'memory-cat-badge memory-cat-' + cat;
|
| catBadge.textContent = cat;
|
| meta.appendChild(catBadge);
|
|
|
| const srcSpan = document.createElement('span');
|
| srcSpan.className = 'memory-item-source';
|
| srcSpan.textContent = memory.source === 'auto' ? 'auto' : 'manual';
|
| meta.appendChild(srcSpan);
|
|
|
| const uses = Number(memory.uses || 0);
|
| if (uses > 0) {
|
| const useSpan = document.createElement('span');
|
| useSpan.className = 'memory-item-uses';
|
| useSpan.textContent = `${uses}×`;
|
| useSpan.title = `Injected into chat context ${uses} time${uses === 1 ? '' : 's'}`;
|
| meta.appendChild(useSpan);
|
| }
|
|
|
| if (memory.timestamp) {
|
| const timeSpan = document.createElement('span');
|
| timeSpan.className = 'memory-item-time';
|
| timeSpan.textContent = relativeTime(memory.timestamp);
|
| timeSpan.title = new Date(memory.timestamp * 1000).toLocaleString();
|
| meta.appendChild(timeSpan);
|
| }
|
|
|
| content.appendChild(textSpan);
|
| content.appendChild(meta);
|
|
|
| if (memory.pinned) item.classList.add('memory-pinned');
|
|
|
| item.appendChild(content);
|
|
|
|
|
| if (!selectMode) {
|
| textSpan.addEventListener('dblclick', (e) => {
|
| e.stopPropagation();
|
| startInlineEdit(item, memory);
|
| });
|
| textSpan.style.cursor = 'text';
|
| }
|
|
|
|
|
| if (!selectMode) {
|
| const menuBtn = document.createElement('button');
|
| menuBtn.className = 'memory-menu-btn';
|
| menuBtn.innerHTML = '\u22EE';
|
| menuBtn.title = 'Actions';
|
|
|
| const dropdown = document.createElement('div');
|
| dropdown.className = 'memory-item-dropdown';
|
|
|
|
|
|
|
| const _bookmarkPath = '<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>';
|
| const _pinSvg = memory.pinned
|
| ? `<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${_bookmarkPath}</svg>`
|
| : `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${_bookmarkPath}</svg>`;
|
| const pinItem = document.createElement('div');
|
| pinItem.className = 'dropdown-item-compact';
|
| pinItem.innerHTML = `<span class="dropdown-icon">${_pinSvg}</span><span>${memory.pinned ? 'Unpin' : 'Pin'}</span>`;
|
| pinItem.addEventListener('click', () => { dropdown.style.display = 'none'; togglePin(memory.id, !memory.pinned); });
|
|
|
| const editItem = document.createElement('div');
|
| editItem.className = 'dropdown-item-compact';
|
| editItem.textContent = '✎ Edit';
|
| editItem.addEventListener('click', () => { dropdown.style.display = 'none'; startInlineEdit(item, memory); });
|
|
|
| const deleteItem = document.createElement('div');
|
| deleteItem.className = 'dropdown-item-compact memory-dropdown-delete';
|
| deleteItem.textContent = '✕ Delete';
|
| deleteItem.addEventListener('click', () => { dropdown.style.display = 'none'; deleteMemory(memory.id); });
|
|
|
|
|
|
|
| const selectItem = document.createElement('div');
|
| selectItem.className = 'dropdown-item-compact';
|
| selectItem.innerHTML = '<span class="dropdown-icon"><span style="font-size:16px;line-height:1;">●</span></span><span>Select</span>';
|
| selectItem.addEventListener('click', (e) => {
|
| e.stopPropagation();
|
| if (dropdown.parentNode) dropdown.remove();
|
| if (!selectMode) enterSelectMode();
|
| selectedIds.add(memory.id);
|
| updateBulkCount();
|
| renderMemoryList();
|
| });
|
|
|
|
|
|
|
|
|
| const cancelItem = document.createElement('div');
|
| cancelItem.className = 'dropdown-item-compact dropdown-cancel-mobile';
|
| cancelItem.textContent = '✕ Cancel';
|
| cancelItem.addEventListener('click', (e) => { e.stopPropagation(); if (dropdown.parentNode) dropdown.remove(); });
|
|
|
| dropdown.appendChild(pinItem);
|
| dropdown.appendChild(selectItem);
|
| dropdown.appendChild(editItem);
|
| dropdown.appendChild(deleteItem);
|
| dropdown.appendChild(cancelItem);
|
|
|
| menuBtn.addEventListener('click', (e) => {
|
| e.stopPropagation();
|
|
|
| document.querySelectorAll('.memory-item-dropdown').forEach(d => d.remove());
|
| const rect = menuBtn.getBoundingClientRect();
|
| dropdown.style.position = 'fixed';
|
| dropdown.style.top = rect.bottom + 2 + 'px';
|
| dropdown.style.right = (window.innerWidth - rect.right) + 'px';
|
| dropdown.style.left = 'auto';
|
| dropdown.style.zIndex = '10001';
|
| dropdown.style.display = 'block';
|
| document.body.appendChild(dropdown);
|
|
|
|
|
| const dr = dropdown.getBoundingClientRect();
|
| if (dr.bottom > window.innerHeight - 6) {
|
| dropdown.style.top = Math.max(6, rect.top - dr.height - 2) + 'px';
|
| }
|
| if (dr.left < 6) {
|
| dropdown.style.right = Math.max(6, window.innerWidth - 6 - dr.width) + 'px';
|
| }
|
| const dr2 = dropdown.getBoundingClientRect();
|
| if (dr2.bottom > window.innerHeight - 6) {
|
| dropdown.style.maxHeight = Math.max(80, window.innerHeight - 12 - dr2.top) + 'px';
|
| dropdown.style.overflowY = 'auto';
|
| }
|
|
|
|
|
|
|
|
|
| let _sw = null;
|
| let _swDy = 0;
|
| const _onTS = (ev) => {
|
| if (ev.touches.length !== 1) return;
|
| _sw = { x: ev.touches[0].clientX, y: ev.touches[0].clientY };
|
| _swDy = 0;
|
| dropdown.style.transition = '';
|
| };
|
| const _onTM = (ev) => {
|
| if (!_sw || ev.touches.length !== 1) return;
|
| const dx = ev.touches[0].clientX - _sw.x;
|
| const dy = ev.touches[0].clientY - _sw.y;
|
| if (Math.abs(dy) < Math.abs(dx)) { _sw = null; return; }
|
| if (dy > 0) {
|
| _swDy = dy;
|
| dropdown.style.transform = 'translateY(' + dy + 'px)';
|
| dropdown.style.opacity = String(Math.max(0.3, 1 - dy / 240));
|
| }
|
| };
|
| const _onTE = () => {
|
| if (!_sw) return;
|
| _sw = null;
|
| if (_swDy > 60) {
|
| dropdown.style.transition = 'transform 0.15s ease, opacity 0.15s ease';
|
| dropdown.style.transform = 'translateY(120px)';
|
| dropdown.style.opacity = '0';
|
| setTimeout(() => { if (dropdown.parentNode) dropdown.remove(); }, 160);
|
| } else {
|
| dropdown.style.transition = 'transform 0.18s ease, opacity 0.18s ease';
|
| dropdown.style.transform = '';
|
| dropdown.style.opacity = '';
|
| }
|
| };
|
| dropdown.addEventListener('touchstart', _onTS, { passive: true });
|
| dropdown.addEventListener('touchmove', _onTM, { passive: true });
|
| dropdown.addEventListener('touchend', _onTE);
|
| });
|
|
|
| item.appendChild(menuBtn);
|
|
|
|
|
|
|
|
|
| {
|
| let hold = null;
|
| let start = null;
|
| const _lpCancel = () => { if (hold) { clearTimeout(hold); hold = null; } start = null; };
|
| item.addEventListener('pointerdown', (e) => {
|
| if (e.target.closest('.memory-menu-btn, .memory-select-cb, button, input')) return;
|
| start = { x: e.clientX, y: e.clientY };
|
| hold = setTimeout(() => {
|
| hold = null;
|
| item._suppressNextClick = true;
|
| setTimeout(() => { item._suppressNextClick = false; }, 400);
|
| if (navigator.vibrate) try { navigator.vibrate(15); } catch {}
|
| menuBtn.click();
|
| }, 500);
|
| });
|
| item.addEventListener('pointermove', (e) => {
|
| if (!start) return;
|
| if (Math.hypot(e.clientX - start.x, e.clientY - start.y) > 10) _lpCancel();
|
| });
|
| item.addEventListener('pointerup', _lpCancel);
|
| item.addEventListener('pointercancel', _lpCancel);
|
| }
|
|
|
|
|
| document.addEventListener('click', () => { if (dropdown.parentNode) dropdown.remove(); }, { once: false });
|
| }
|
|
|
| memoryList.appendChild(item);
|
| });
|
|
|
| }
|
|
|
|
|
|
|
| function startInlineEdit(item, memory) {
|
| item.innerHTML = '';
|
| item.className = 'memory-item memory-item-editing';
|
|
|
| const editRow = document.createElement('div');
|
| editRow.className = 'memory-edit-row';
|
|
|
| const input = document.createElement('input');
|
| input.type = 'text';
|
| input.className = 'memory-item-edit-input';
|
| input.value = memory.text;
|
|
|
| const catSelect = document.createElement('select');
|
| catSelect.className = 'memory-edit-cat-select';
|
| MEMORY_CATEGORIES.forEach(cat => {
|
| const opt = document.createElement('option');
|
| opt.value = cat;
|
| opt.textContent = cat;
|
| if (cat === (memory.category || 'fact')) opt.selected = true;
|
| catSelect.appendChild(opt);
|
| });
|
|
|
| editRow.appendChild(input);
|
| editRow.appendChild(catSelect);
|
|
|
| const actions = document.createElement('div');
|
| actions.className = 'memory-item-actions';
|
| actions.style.opacity = '1';
|
|
|
| const saveBtn = document.createElement('button');
|
| saveBtn.className = 'memory-item-btn save';
|
| saveBtn.textContent = 'save';
|
| saveBtn.addEventListener('click', () => saveInlineEdit(memory.id, input.value, catSelect.value));
|
|
|
| const cancelBtn = document.createElement('button');
|
| cancelBtn.className = 'memory-item-btn';
|
| cancelBtn.textContent = 'cancel';
|
| cancelBtn.addEventListener('click', () => renderMemoryList());
|
|
|
| actions.appendChild(saveBtn);
|
| actions.appendChild(cancelBtn);
|
|
|
| item.appendChild(editRow);
|
| item.appendChild(actions);
|
|
|
| input.focus();
|
| input.select();
|
|
|
| input.addEventListener('keydown', (e) => {
|
| if (e.key === 'Enter') saveInlineEdit(memory.id, input.value, catSelect.value);
|
| if (e.key === 'Escape') {
|
| e.stopPropagation();
|
| e.stopImmediatePropagation();
|
| renderMemoryList();
|
| }
|
| });
|
| }
|
|
|
| async function saveInlineEdit(id, newText, newCategory) {
|
| newText = newText.trim();
|
| if (!newText) return;
|
|
|
| const memory = memories.find(m => m.id === id);
|
| const catChanged = newCategory && newCategory !== (memory?.category || 'fact');
|
| if (!memory || (newText === memory.text && !catChanged)) {
|
| renderMemoryList();
|
| return;
|
| }
|
|
|
| try {
|
| const params = new URLSearchParams({ text: newText });
|
| if (newCategory) params.append('category', newCategory);
|
|
|
| const response = await fetch(`${window.location.origin}/api/memory/${id}`, {
|
| method: 'PUT',
|
| body: params
|
| });
|
|
|
| if (response.ok) {
|
| await loadMemories();
|
| showToast('Memory updated');
|
| } else {
|
| const errorData = await response.json();
|
| throw new Error(errorData.detail || 'Failed to update memory');
|
| }
|
| } catch (error) {
|
| console.error('Error updating memory:', error);
|
| showError('Failed to update memory');
|
| }
|
| }
|
|
|
| export function updateMemoryCount() {
|
| const h2Count = document.getElementById('memory-count-h2');
|
| const tabCount = document.getElementById('memory-count');
|
| if (!h2Count && !tabCount) return;
|
|
|
| const searchInput = document.getElementById('memory-search');
|
| const searchTerm = searchInput ? searchInput.value.toLowerCase().trim() : '';
|
|
|
| let visible = memories;
|
| const scopeTotal = visible.length;
|
| if (searchTerm) {
|
| visible = visible.filter(m => m.text && m.text.toLowerCase().includes(searchTerm));
|
| }
|
| if (activeCategory !== 'all') {
|
| visible = visible.filter(m => (m.category || 'fact') === activeCategory);
|
| }
|
|
|
| const num = visible.length === scopeTotal ? `${scopeTotal}` : `${visible.length}/${scopeTotal}`;
|
|
|
|
|
| if (h2Count) h2Count.textContent = `${num} ${scopeTotal === 1 && visible.length === scopeTotal ? 'memory' : 'memories'}`;
|
| if (tabCount) tabCount.textContent = num;
|
| }
|
|
|
| export async function addNewMemory() {
|
| const input = document.getElementById('new-memory-input');
|
| const text = input.value.trim();
|
| const category = _readNewMemoryCategory();
|
|
|
| if (!text) {
|
| showError('Memory text cannot be empty');
|
| return;
|
| }
|
|
|
| try {
|
| const response = await fetch(`${window.location.origin}/api/memory/add`, {
|
| method: 'POST',
|
| headers: {
|
| 'Content-Type': 'application/json',
|
| },
|
| body: JSON.stringify({
|
| text: text,
|
| category: category,
|
| })
|
| });
|
|
|
| if (response.ok) {
|
| input.value = '';
|
| await loadMemories();
|
| showToast('Memory added');
|
| } else {
|
| const errorData = await response.json();
|
| console.error('Server error details:', errorData);
|
| throw new Error(errorData.detail || 'Failed to add memory');
|
| }
|
| } catch (error) {
|
| console.error('Error adding memory:', error);
|
| showError('Failed to add memory');
|
| }
|
| }
|
|
|
| export async function editMemory(id) {
|
| const memory = memories.find(m => m.id === id);
|
| if (!memory) return;
|
|
|
| const newText = prompt('Edit memory:', memory.text);
|
| if (!newText || newText === memory.text) return;
|
|
|
| await saveInlineEdit(id, newText);
|
| }
|
|
|
| async function togglePin(id, pinned) {
|
| try {
|
| const res = await fetch(`${window.location.origin}/api/memory/${id}/pin`, {
|
| method: 'POST',
|
| body: new URLSearchParams({ pinned: pinned.toString() })
|
| });
|
| if (res.ok) {
|
| const mem = memories.find(m => m.id === id);
|
| if (mem) mem.pinned = pinned;
|
| renderMemoryList();
|
| showToast(pinned ? 'Pinned — always in context' : 'Unpinned — RAG only');
|
| }
|
| } catch (e) {
|
| console.error('Failed to toggle pin:', e);
|
| showError('Failed to update pin');
|
| }
|
| }
|
|
|
| export async function deleteMemory(id) {
|
| const memory = memories.find(m => m.id === id);
|
| if (!memory) return;
|
|
|
| if (!await uiModule.styledConfirm(`Delete this memory?\n"${memory.text}"`, { confirmText: 'Delete', danger: true })) return;
|
|
|
| try {
|
| const response = await fetch(`${window.location.origin}/api/memory/${id}`, {
|
| method: 'DELETE'
|
| });
|
|
|
| if (response.ok) {
|
| await animateMemoryRemoval([id]);
|
| await loadMemories();
|
| showToast('Memory deleted');
|
| } else {
|
| throw new Error('Failed to delete');
|
| }
|
| } catch (error) {
|
| showError('Failed to delete memory');
|
| }
|
| }
|
|
|
| export async function extractMemory(sessionId) {
|
| const res = await fetch(`${window.location.origin}/api/memory/extract`, {
|
| method: 'POST',
|
| body: new URLSearchParams({ session: sessionId })
|
| });
|
| if (!res.ok) {
|
| showError('Failed to extract memory suggestions');
|
| return;
|
| }
|
| const data = await res.json();
|
| const suggestions = data.suggestions || [];
|
|
|
| const modal = document.getElementById('memory-modal');
|
| const body = document.getElementById('memory-suggestions-body');
|
| if (!body) {
|
| console.error('memory-suggestions-body element not found');
|
| return;
|
| }
|
|
|
| body.innerHTML = '';
|
| body.classList.remove('hidden');
|
|
|
| const memList = document.getElementById('memory-list');
|
| if (memList) memList.classList.add('hidden');
|
|
|
| if (suggestions.length === 0) {
|
| body.innerHTML = '<div class="memory-empty">No useful information detected.</div>';
|
| } else {
|
| const header = document.createElement('div');
|
| header.className = 'memory-suggestions-header';
|
| header.innerHTML = '<span>Suggested memories</span>';
|
| const backBtn = document.createElement('button');
|
| backBtn.className = 'memory-item-btn';
|
| backBtn.textContent = 'back';
|
| backBtn.addEventListener('click', () => {
|
| body.classList.add('hidden');
|
| body.innerHTML = '';
|
| if (memList) memList.classList.remove('hidden');
|
| });
|
| header.appendChild(backBtn);
|
| body.appendChild(header);
|
|
|
| suggestions.forEach(s => {
|
| const div = document.createElement('div');
|
| div.className = 'memory-suggestion-item';
|
| const txt = document.createElement('span');
|
| txt.className = 'memory-item-text';
|
| txt.textContent = s;
|
| const btn = document.createElement('button');
|
| btn.className = 'memory-item-btn save';
|
| btn.textContent = 'save';
|
| btn.addEventListener('click', async () => {
|
| await fetch(`${window.location.origin}/api/memory/add`, {
|
| method: 'POST',
|
| headers: { 'Content-Type': 'application/json' },
|
| body: JSON.stringify({ text: s })
|
| });
|
| btn.disabled = true;
|
| btn.textContent = 'saved';
|
| showToast('Saved to memory');
|
| });
|
| div.appendChild(txt);
|
| div.appendChild(btn);
|
| body.appendChild(div);
|
| });
|
| }
|
|
|
| modal.classList.remove('hidden');
|
| }
|
|
|
|
|
|
|
| export function exportMemories() {
|
| if (!memories || memories.length === 0) {
|
| showToast('No memories to export');
|
| return;
|
| }
|
| const data = JSON.stringify(memories, null, 2);
|
| const blob = new Blob([data], { type: 'application/json' });
|
| const url = URL.createObjectURL(blob);
|
| const a = document.createElement('a');
|
| a.href = url;
|
| a.download = 'memories.json';
|
| a.click();
|
| URL.revokeObjectURL(url);
|
| showToast(`Exported ${memories.length} memories`);
|
| }
|
|
|
|
|
|
|
| export async function importMemories() {
|
| const fileInput = document.getElementById('memory-import-file');
|
| if (!fileInput) return;
|
| fileInput.click();
|
| }
|
|
|
| async function handleImportFile(file) {
|
| if (!file) return;
|
|
|
| const sessionId = sessionModule?.getCurrentSessionId?.();
|
|
|
| const importBtn = document.getElementById('memory-import-btn');
|
| const _origImportHtml = importBtn ? importBtn.innerHTML : '';
|
| let importSpin = null;
|
| if (importBtn) {
|
| importBtn.disabled = true;
|
| importBtn.innerHTML = '';
|
| importSpin = spinnerModule.createWhirlpool(12);
|
| importSpin.element.style.cssText = 'width:12px;height:12px;margin:0 5px 0 0;display:inline-flex;vertical-align:-2px;transform:translateY(-1px);';
|
| importBtn.appendChild(importSpin.element);
|
| importBtn.appendChild(document.createTextNode('Importing'));
|
| }
|
|
|
| try {
|
| const formData = new FormData();
|
| formData.append('file', file);
|
| if (sessionId) {
|
| formData.append('session', sessionId);
|
| }
|
|
|
| const res = await fetch(`${window.location.origin}/api/memory/import`, {
|
| method: 'POST',
|
| body: formData
|
| });
|
|
|
| if (!res.ok) {
|
| const err = await res.json().catch(() => ({}));
|
| throw new Error(err.detail || 'Import failed');
|
| }
|
|
|
| const data = await res.json();
|
| const suggestions = data.suggestions || [];
|
|
|
|
|
| const modal = document.getElementById('memory-modal');
|
| const body = document.getElementById('memory-suggestions-body');
|
| if (!body) return;
|
|
|
| body.innerHTML = '';
|
| body.classList.remove('hidden');
|
|
|
| const memList = document.getElementById('memory-list');
|
| if (memList) memList.classList.add('hidden');
|
|
|
| if (suggestions.length === 0) {
|
| body.innerHTML = '<div class="memory-empty">No useful information found in file.</div>';
|
| } else {
|
| const reviewItems = suggestions
|
| .map((s) => ({
|
| text: typeof s === 'string' ? s : s.text,
|
| category: (typeof s === 'object' && s.category) || 'fact',
|
| active: true,
|
| }))
|
| .filter((s) => s.text);
|
| const header = document.createElement('div');
|
| header.className = 'memory-suggestions-header';
|
| const headerTitle = document.createElement('span');
|
| const updateHeaderTitle = () => {
|
| const remaining = reviewItems.filter((item) => item.active).length;
|
| headerTitle.textContent = `Imported from ${data.filename || file.name} (${remaining}) Review`;
|
| };
|
| updateHeaderTitle();
|
| const headerActions = document.createElement('div');
|
| headerActions.className = 'memory-suggestions-actions';
|
| const backBtn = document.createElement('button');
|
| backBtn.className = 'memory-item-btn';
|
| backBtn.textContent = 'back';
|
| backBtn.addEventListener('click', () => {
|
| body.classList.add('hidden');
|
| body.innerHTML = '';
|
| if (memList) memList.classList.remove('hidden');
|
| });
|
| const saveAllBtn = document.createElement('button');
|
| saveAllBtn.className = 'memory-item-btn save';
|
| saveAllBtn.textContent = 'save all';
|
| saveAllBtn.addEventListener('click', async () => {
|
| let saved = 0;
|
| for (const s of reviewItems) {
|
| if (!s.active || !s.text) continue;
|
| try {
|
| await fetch(`${window.location.origin}/api/memory/add`, {
|
| method: 'POST',
|
| headers: { 'Content-Type': 'application/json' },
|
| body: JSON.stringify({ text: s.text, category: s.category })
|
| });
|
| saved++;
|
| } catch (e) { }
|
| }
|
| body.classList.add('hidden');
|
| body.innerHTML = '';
|
| if (memList) memList.classList.remove('hidden');
|
| await loadMemories();
|
| document.querySelector('.memory-tab[data-memory-tab="browse"]')?.click();
|
| showToast(`Saved ${saved} memories`);
|
| });
|
| headerActions.appendChild(saveAllBtn);
|
| headerActions.appendChild(backBtn);
|
| header.appendChild(headerTitle);
|
| header.appendChild(headerActions);
|
| body.appendChild(header);
|
|
|
| reviewItems.forEach(item => {
|
| const div = document.createElement('div');
|
| div.className = 'memory-suggestion-item';
|
|
|
| const content = document.createElement('div');
|
| content.className = 'memory-item-content';
|
| const txt = document.createElement('span');
|
| txt.className = 'memory-item-text';
|
| txt.textContent = item.text;
|
| const catBadge = document.createElement('span');
|
| catBadge.className = 'memory-cat-badge memory-cat-' + item.category;
|
| catBadge.textContent = item.category;
|
| content.appendChild(txt);
|
| content.appendChild(catBadge);
|
|
|
| const actionWrap = document.createElement('div');
|
| actionWrap.className = 'memory-suggestion-actions';
|
| const btn = document.createElement('button');
|
| btn.className = 'memory-item-btn save';
|
| btn.textContent = 'save';
|
| btn.addEventListener('click', async () => {
|
| await fetch(`${window.location.origin}/api/memory/add`, {
|
| method: 'POST',
|
| headers: { 'Content-Type': 'application/json' },
|
| body: JSON.stringify({ text: item.text, category: item.category })
|
| });
|
| item.active = false;
|
| div.remove();
|
| updateHeaderTitle();
|
| btn.disabled = true;
|
| btn.textContent = 'saved';
|
| showToast('Saved to memory');
|
| });
|
| const deleteBtn = document.createElement('button');
|
| deleteBtn.className = 'memory-item-btn delete';
|
| deleteBtn.textContent = 'delete';
|
| deleteBtn.addEventListener('click', () => {
|
| item.active = false;
|
| div.remove();
|
| updateHeaderTitle();
|
| });
|
| actionWrap.appendChild(btn);
|
| actionWrap.appendChild(deleteBtn);
|
|
|
| div.appendChild(content);
|
| div.appendChild(actionWrap);
|
| body.appendChild(div);
|
| });
|
| }
|
|
|
| modal.classList.remove('hidden');
|
| document.querySelector('.memory-tab[data-memory-tab="browse"]')?.click();
|
| } catch (error) {
|
| console.error('Import failed:', error);
|
| showError('Import failed — ' + error.message);
|
| } finally {
|
| if (importSpin) importSpin.destroy();
|
| if (importBtn) {
|
| importBtn.disabled = false;
|
| importBtn.innerHTML = _origImportHtml;
|
| }
|
|
|
| const fileInput = document.getElementById('memory-import-file');
|
| if (fileInput) fileInput.value = '';
|
| }
|
| }
|
|
|
|
|
| var showToast = uiModule.showToast;
|
| var showError = uiModule.showError;
|
|
|
|
|
| document.addEventListener('DOMContentLoaded', () => {
|
| _wireMemoryDrag();
|
|
|
|
|
| document.querySelectorAll('.memory-tab[data-memory-tab]').forEach(tab => {
|
| tab.addEventListener('click', () => {
|
| const target = tab.dataset.memoryTab;
|
| document.querySelectorAll('.memory-tab').forEach(t => t.classList.toggle('active', t === tab));
|
| document.querySelectorAll('.memory-tab-panel[data-memory-panel]').forEach(p => {
|
| p.classList.toggle('hidden', p.dataset.memoryPanel !== target);
|
| });
|
|
|
| if (target === 'skills') {
|
| import('./skills.js').then(m => { if (m.loadSkills) m.loadSkills(true); else if (m.default?.loadSkills) m.default.loadSkills(true); });
|
| }
|
| });
|
| });
|
|
|
| const sortSelect = document.getElementById('memory-sort');
|
| if (sortSelect) {
|
| sortSelect.addEventListener('change', () => {
|
| sortOrder = sortSelect.value;
|
| renderMemoryList();
|
| });
|
| }
|
|
|
| const tidyBtn = document.getElementById('memory-tidy-btn');
|
| if (tidyBtn) tidyBtn.addEventListener('click', tidyMemories);
|
|
|
| const selectBtn = document.getElementById('memory-select-btn');
|
| if (selectBtn) selectBtn.addEventListener('click', () => {
|
| if (selectMode) exitSelectMode();
|
| else enterSelectMode();
|
| });
|
|
|
| const selectAll = document.getElementById('memory-select-all');
|
| if (selectAll) selectAll.addEventListener('change', toggleSelectAll);
|
|
|
| const bulkBar = document.getElementById('memory-bulk-bar');
|
| if (bulkBar) bulkBar.addEventListener('click', (e) => {
|
| if (e.target.closest('button') || e.target === selectAll) return;
|
| selectAll.checked = !selectAll.checked;
|
| selectAll.dispatchEvent(new Event('change'));
|
| });
|
|
|
| const bulkDeleteBtn = document.getElementById('memory-bulk-delete');
|
| if (bulkDeleteBtn) bulkDeleteBtn.addEventListener('click', bulkDelete);
|
|
|
| const bulkCancelBtn = document.getElementById('memory-bulk-cancel');
|
| if (bulkCancelBtn) bulkCancelBtn.addEventListener('click', exitSelectMode);
|
|
|
| const exportBtn = document.getElementById('memory-export-btn');
|
| if (exportBtn) exportBtn.addEventListener('click', exportMemories);
|
|
|
| const importBtn = document.getElementById('memory-import-btn');
|
| if (importBtn) importBtn.addEventListener('click', importMemories);
|
|
|
| const importFile = document.getElementById('memory-import-file');
|
| if (importFile) importFile.addEventListener('change', (e) => {
|
| if (e.target.files[0]) handleImportFile(e.target.files[0]);
|
| });
|
|
|
| window.addEventListener('memory-refresh', () => {
|
| loadMemories();
|
| });
|
| });
|
|
|
| const memoryModule = {
|
| loadMemories,
|
| renderMemoryList,
|
| updateMemoryCount,
|
| addNewMemory,
|
| editMemory,
|
| deleteMemory,
|
| extractMemory,
|
| buildCategoryChips,
|
| tidyMemories,
|
| importMemories,
|
| exportMemories
|
| };
|
|
|
| export default memoryModule;
|
| window.memoryModule = memoryModule;
|
|
|