mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-26 18:12:21 +02:00
Squash Odysseus development history
This commit is contained in:
+307
-39
@@ -1,11 +1,14 @@
|
||||
// Memory Management Functions
|
||||
// This module handles all memory-related operations
|
||||
|
||||
import uiModule from './ui.js';
|
||||
import uiModule from './ui.js?v=20260908weekhoverfix1';
|
||||
import sessionModule from './sessions.js';
|
||||
import spinnerModule from './spinner.js';
|
||||
import { makeWindowDraggable } from './windowDrag.js';
|
||||
import { snapModalToZone } from './tileManager.js';
|
||||
import { snapModalToZone } from './tileManager.js?v=20260910responsivebounds1';
|
||||
import { topPortalZ } from './toolWindowZOrder.js';
|
||||
import { orderActionMenuItems, actionMenuRank, SELECT_MENU_ICON } from './actionMenuOrder.js';
|
||||
import { bindMenuDismiss } from './escMenuStack.js';
|
||||
|
||||
var escapeHtml = uiModule.esc;
|
||||
|
||||
@@ -14,10 +17,108 @@ let activeCategory = 'all';
|
||||
let sortOrder = 'newest';
|
||||
let selectMode = false;
|
||||
let selectedIds = new Set();
|
||||
let memoriesLoading = false;
|
||||
let activeMemoryModalMode = 'memory';
|
||||
|
||||
|
||||
const MEMORY_CATEGORIES = ['fact', 'identity', 'preference', 'contact', 'project', 'goal', 'task'];
|
||||
|
||||
// Sort-option icons for the custom Memory sort picker (and Skills picker
|
||||
// once it reuses the same markup). Each value maps to a 13px Feather-style
|
||||
// SVG so the icon visually distinguishes Newest / Oldest / A-Z / Most used.
|
||||
const _MEMORY_SORT_ICONS = {
|
||||
newest: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>',
|
||||
oldest: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><polyline points="3 3 3 8 8 8"/><polyline points="12 7 12 12 16 14"/></svg>',
|
||||
alpha: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4h6"/><path d="M3 10h6"/><path d="M3 16h4"/><path d="M14 4l4 12"/><path d="M16 12h4"/><polyline points="17 18 21 14 17 10"/><line x1="21" y1="14" x2="13" y2="14"/></svg>',
|
||||
uses: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></svg>',
|
||||
};
|
||||
|
||||
function _memorySortIcon(value) {
|
||||
return _MEMORY_SORT_ICONS[value] || _MEMORY_SORT_ICONS.newest;
|
||||
}
|
||||
|
||||
function _renderMemorySortPickerCurrent() {
|
||||
const sel = document.getElementById('memory-sort');
|
||||
const btn = document.getElementById('memory-sort-btn');
|
||||
if (!sel || !btn) return;
|
||||
const value = sel.value || 'newest';
|
||||
const opt = sel.querySelector(`option[value="${CSS.escape(value)}"]`);
|
||||
const label = opt ? opt.textContent : value;
|
||||
const iconWrap = btn.querySelector('.memory-sort-icon-cur');
|
||||
const labelEl = btn.querySelector('.memory-sort-label');
|
||||
if (iconWrap) iconWrap.innerHTML = _memorySortIcon(value);
|
||||
if (labelEl) labelEl.textContent = label;
|
||||
}
|
||||
|
||||
function _initMemorySortPicker() {
|
||||
const sel = document.getElementById('memory-sort');
|
||||
const picker = document.getElementById('memory-sort-picker');
|
||||
const btn = document.getElementById('memory-sort-btn');
|
||||
const menu = document.getElementById('memory-sort-menu');
|
||||
if (!sel || !picker || !btn || !menu || picker._wired) return;
|
||||
picker._wired = true;
|
||||
|
||||
const items = Array.from(sel.children)
|
||||
.filter(o => o.tagName === 'OPTION')
|
||||
.map(o => ({ value: o.value, label: o.textContent }));
|
||||
|
||||
menu.innerHTML = items.map(it => `
|
||||
<button type="button" role="option" class="memory-sort-item" data-value="${it.value}">
|
||||
<span class="memory-sort-item-icon">${_memorySortIcon(it.value)}</span>
|
||||
<span class="memory-sort-item-label">${it.label}</span>
|
||||
</button>
|
||||
`).join('');
|
||||
|
||||
let unregister = () => {};
|
||||
const close = () => {
|
||||
menu.hidden = true;
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
unregister();
|
||||
unregister = () => {};
|
||||
};
|
||||
const open = () => { menu.hidden = false; btn.setAttribute('aria-expanded', 'true'); };
|
||||
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (menu.hidden) {
|
||||
open();
|
||||
unregister = bindMenuDismiss(menu, close, e => picker.contains(e.target));
|
||||
} else close();
|
||||
});
|
||||
menu.addEventListener('click', (e) => {
|
||||
const item = e.target.closest('.memory-sort-item');
|
||||
if (!item) return;
|
||||
sel.value = item.dataset.value;
|
||||
sel.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
_renderMemorySortPickerCurrent();
|
||||
close();
|
||||
});
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!menu.hidden && !picker.contains(e.target)) close();
|
||||
});
|
||||
_renderMemorySortPickerCurrent();
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -68,16 +169,25 @@ function buildCategoryChips() {
|
||||
|
||||
const cats = new Set(memories.map(m => m.category || 'fact'));
|
||||
const sorted = ['all', ...Array.from(cats).sort()];
|
||||
const counts = memories.reduce((result, memory) => {
|
||||
const category = memory.category || 'fact';
|
||||
result[category] = (result[category] || 0) + 1;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
container.innerHTML = '';
|
||||
sorted.forEach(cat => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'memory-cat-chip' + (cat === activeCategory ? ' active' : '');
|
||||
btn.className = 'skills-summary-chip memory-filter-chip' + (cat === activeCategory ? ' active' : '');
|
||||
btn.dataset.cat = cat;
|
||||
btn.textContent = cat;
|
||||
const label = document.createElement('span');
|
||||
label.textContent = cat === 'all' ? 'All' : cat;
|
||||
const count = document.createElement('strong');
|
||||
count.textContent = String(cat === 'all' ? memories.length : counts[cat] || 0);
|
||||
btn.replaceChildren(label, count);
|
||||
btn.addEventListener('click', () => {
|
||||
activeCategory = cat;
|
||||
container.querySelectorAll('.memory-cat-chip').forEach(b => b.classList.remove('active'));
|
||||
container.querySelectorAll('[data-cat]').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
renderMemoryList();
|
||||
updateMemoryCount();
|
||||
@@ -119,7 +229,10 @@ async function syncToggles() {
|
||||
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'; };
|
||||
const applyDim = () => {
|
||||
if (skillsPanel) skillsPanel.style.opacity = skillsToggle.checked ? '' : '0.3';
|
||||
reflectSkillsToggleInSidebar(skillsToggle.checked);
|
||||
};
|
||||
applyDim();
|
||||
if (!skillsToggle.dataset.boundUx) {
|
||||
skillsToggle.dataset.boundUx = '1';
|
||||
@@ -128,9 +241,96 @@ async function syncToggles() {
|
||||
}
|
||||
}
|
||||
|
||||
function _applyMemoryModalMode(mode) {
|
||||
activeMemoryModalMode = mode === 'skills' ? 'skills' : 'memory';
|
||||
const modal = document.getElementById('memory-modal');
|
||||
if (!modal) return;
|
||||
modal.dataset.memoryMode = activeMemoryModalMode;
|
||||
modal.setAttribute('aria-label', activeMemoryModalMode === 'skills' ? 'Skills' : 'Memory');
|
||||
const titleText = document.getElementById('memory-modal-title-text');
|
||||
if (titleText) titleText.textContent = activeMemoryModalMode === 'skills' ? 'Skills' : 'Memory';
|
||||
modal.querySelectorAll('[data-memory-scope]').forEach(el => {
|
||||
const scope = el.dataset.memoryScope;
|
||||
const scopedOut = !!scope && scope !== activeMemoryModalMode;
|
||||
el.hidden = scopedOut;
|
||||
el.classList.toggle('hidden', scopedOut);
|
||||
el.setAttribute('aria-hidden', scopedOut ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
export function openMemoryModal(tabName = 'browse') {
|
||||
const modal = document.getElementById('memory-modal');
|
||||
if (!modal) return;
|
||||
const mode = tabName === 'skills' ? 'skills' : 'memory';
|
||||
_applyMemoryModalMode(mode);
|
||||
modal.classList.remove('hidden', 'modal-minimized');
|
||||
modal.style.display = '';
|
||||
if (renderMemoryList) renderMemoryList();
|
||||
if (updateMemoryCount) updateMemoryCount();
|
||||
const target = mode === 'skills' ? 'skills' : 'browse';
|
||||
const tab = document.querySelector(`.memory-tab[data-memory-tab="${target}"]`);
|
||||
if (tab) {
|
||||
tab.click();
|
||||
} else if (target === 'skills') {
|
||||
import('./skills.js?v=20260909kebabconsistency1').then(m => {
|
||||
if (m.loadSkills) m.loadSkills(true);
|
||||
else if (m.default?.loadSkills) m.default.loadSkills(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function _decodeMemoryAnchorId(memoryId) {
|
||||
try { return decodeURIComponent(String(memoryId || '').replace(/\+/g, ' ')).trim(); }
|
||||
catch (_) { return String(memoryId || '').trim(); }
|
||||
}
|
||||
|
||||
function _resetMemoryDeepLinkFilters() {
|
||||
activeCategory = 'all';
|
||||
const search = document.getElementById('memory-search');
|
||||
if (search && search.value) search.value = '';
|
||||
document.querySelectorAll('#memory-category-filters [data-cat]').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.cat === 'all');
|
||||
});
|
||||
}
|
||||
|
||||
// Open the memory panel and focus the memory referenced by a chat tool link.
|
||||
// Memory search output may expose a shortened id, so prefix matching is
|
||||
// intentional here.
|
||||
export async function openMemory(memoryId, attempt = 0) {
|
||||
openMemoryModal('browse');
|
||||
_resetMemoryDeepLinkFilters();
|
||||
await loadMemories();
|
||||
if (renderMemoryList) renderMemoryList();
|
||||
const wanted = _decodeMemoryAnchorId(memoryId);
|
||||
if (!wanted) return;
|
||||
const item = Array.from(document.querySelectorAll('.memory-item[data-memory-id]'))
|
||||
.find(el => String(el.dataset.memoryId || '') === wanted)
|
||||
|| Array.from(document.querySelectorAll('.memory-item[data-memory-id]'))
|
||||
.find(el => String(el.dataset.memoryId || '').startsWith(wanted));
|
||||
if (!item) {
|
||||
if (attempt < 12) setTimeout(() => openMemory(wanted, attempt + 1), 120);
|
||||
return;
|
||||
}
|
||||
item.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
|
||||
item.classList.add('memory-item-focus');
|
||||
setTimeout(() => item.classList.remove('memory-item-focus'), 2200);
|
||||
}
|
||||
|
||||
function reflectMemoryToggleInSidebar(enabled) {
|
||||
const btn = document.getElementById('tool-memory-btn');
|
||||
if (btn) btn.classList.toggle('tool-disabled', !enabled);
|
||||
document.body.classList.toggle('memory-disabled', !enabled);
|
||||
if (!enabled) document.getElementById('synapse-canvas')?.remove();
|
||||
window.dispatchEvent(new CustomEvent('memory-visual-state', { detail: { enabled } }));
|
||||
['tool-memory-btn', 'rail-memory'].forEach(id => {
|
||||
const btn = document.getElementById(id);
|
||||
if (btn) btn.classList.toggle('tool-disabled', !enabled);
|
||||
});
|
||||
}
|
||||
|
||||
function reflectSkillsToggleInSidebar(enabled) {
|
||||
['tool-skills-btn', 'rail-skills'].forEach(id => {
|
||||
const btn = document.getElementById(id);
|
||||
if (btn) btn.classList.toggle('tool-disabled', !enabled);
|
||||
});
|
||||
}
|
||||
|
||||
function syncToggleDim(toggle) {
|
||||
@@ -274,12 +474,17 @@ async function syncPrefToggle(elementId, prefKey, onMsg, offMsg, dimBelow = true
|
||||
}
|
||||
|
||||
export async function loadMemories() {
|
||||
_ensureNewMemoryCategorySelect();
|
||||
memoriesLoading = true;
|
||||
renderMemoryList();
|
||||
updateMemoryCount();
|
||||
try {
|
||||
const response = await fetch(`${window.location.origin}/api/memory`);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Memory fetch failed with status:', response.status);
|
||||
memories = [];
|
||||
memoriesLoading = false;
|
||||
buildCategoryChips();
|
||||
renderMemoryList();
|
||||
updateMemoryCount();
|
||||
@@ -297,12 +502,14 @@ export async function loadMemories() {
|
||||
memories = [];
|
||||
}
|
||||
|
||||
memoriesLoading = false;
|
||||
buildCategoryChips();
|
||||
renderMemoryList();
|
||||
updateMemoryCount();
|
||||
} catch (error) {
|
||||
console.error('Failed to load memories:', error);
|
||||
memories = [];
|
||||
memoriesLoading = false;
|
||||
buildCategoryChips();
|
||||
renderMemoryList();
|
||||
updateMemoryCount();
|
||||
@@ -313,13 +520,16 @@ export async function loadMemories() {
|
||||
|
||||
// ---- Bulk select mode ----
|
||||
|
||||
const _SELECT_BTN_DOT_SVG = '<svg class="memory-select-btn-icon" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="11" r="3" fill="currentColor" stroke="none"/></svg>';
|
||||
const _SELECT_BTN_X_SVG = '<svg class="memory-select-btn-icon" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" style="vertical-align:-2px;margin-right:3px;"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
|
||||
|
||||
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'; }
|
||||
if (selectBtn) { selectBtn.classList.add('active'); selectBtn.innerHTML = _SELECT_BTN_X_SVG + 'Cancel'; }
|
||||
updateBulkCount();
|
||||
renderMemoryList();
|
||||
}
|
||||
@@ -331,7 +541,7 @@ function exitSelectMode() {
|
||||
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 (selectBtn) { selectBtn.classList.remove('active'); selectBtn.innerHTML = _SELECT_BTN_DOT_SVG + 'Select'; }
|
||||
if (selectAll) selectAll.checked = false;
|
||||
renderMemoryList();
|
||||
}
|
||||
@@ -428,7 +638,7 @@ export async function tidyMemories() {
|
||||
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'; }
|
||||
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;color:var(--accent, var(--red));"><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;
|
||||
}
|
||||
@@ -471,7 +681,7 @@ export async function tidyMemories() {
|
||||
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';
|
||||
tidyBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor" style="vertical-align:-1px;margin-right:2px;color:var(--accent, var(--red));"><path d="M12 0L14.59 8.41L23 12L14.59 15.59L12 24L9.41 15.59L1 12L9.41 8.41Z"/></svg> Tidy';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -587,6 +797,15 @@ export function renderMemoryList() {
|
||||
memoryList.innerHTML = '';
|
||||
|
||||
if (filtered.length === 0) {
|
||||
const selectBtn = document.getElementById('memory-select-btn');
|
||||
if (selectBtn) selectBtn.disabled = true;
|
||||
if (selectMode) exitSelectMode();
|
||||
if (memoriesLoading) {
|
||||
const row = spinnerModule.createLoadingRow('Loading memories...', 14);
|
||||
row.classList.add('memory-empty');
|
||||
memoryList.replaceChildren(row);
|
||||
return;
|
||||
}
|
||||
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') {
|
||||
@@ -606,6 +825,9 @@ export function renderMemoryList() {
|
||||
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';
|
||||
@@ -701,7 +923,7 @@ export function renderMemoryList() {
|
||||
menuBtn.title = 'Actions';
|
||||
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'memory-item-dropdown';
|
||||
dropdown.className = 'dropdown session-dropdown-menu memory-item-dropdown';
|
||||
|
||||
// Pin / Unpin — bookmark icon matches the chat-session "Favorite" SVG.
|
||||
// Filled when pinned, outlined when not.
|
||||
@@ -716,19 +938,19 @@ export function renderMemoryList() {
|
||||
|
||||
const editItem = document.createElement('div');
|
||||
editItem.className = 'dropdown-item-compact';
|
||||
editItem.textContent = '✎ Edit';
|
||||
editItem.innerHTML = '<span class="dropdown-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></span><span>Edit</span>';
|
||||
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.className = 'dropdown-item-compact dropdown-item-danger';
|
||||
deleteItem.innerHTML = '<span class="dropdown-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/></svg></span><span>Delete</span>';
|
||||
deleteItem.addEventListener('click', () => { dropdown.style.display = 'none'; deleteMemory(memory.id); });
|
||||
|
||||
// Select — enters bulk-select mode and pre-selects this memory. Same
|
||||
// pattern as the email/documents/skills Select item.
|
||||
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.innerHTML = `<span class="dropdown-icon">${SELECT_MENU_ICON}</span><span>Select</span>`;
|
||||
selectItem.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (dropdown.parentNode) dropdown.remove();
|
||||
@@ -743,32 +965,61 @@ export function renderMemoryList() {
|
||||
// dismisses cleanly.
|
||||
const cancelItem = document.createElement('div');
|
||||
cancelItem.className = 'dropdown-item-compact dropdown-cancel-mobile';
|
||||
cancelItem.textContent = '✕ Cancel';
|
||||
cancelItem.innerHTML = '<span class="dropdown-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></span><span>Cancel</span>';
|
||||
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);
|
||||
const orderedMenuItems = orderActionMenuItems([
|
||||
{ label: 'Edit', node: editItem },
|
||||
{ label: 'Select', node: selectItem },
|
||||
{ label: memory.pinned ? 'Unpin' : 'Pin', node: pinItem },
|
||||
{ label: 'Delete', node: deleteItem },
|
||||
{ label: 'Cancel', node: cancelItem },
|
||||
]);
|
||||
let addedActionDivider = false;
|
||||
orderedMenuItems.forEach((entry, index) => {
|
||||
if (!addedActionDivider && index > 0 && actionMenuRank(entry) >= 700) {
|
||||
const divider = document.createElement('div');
|
||||
divider.className = 'dropdown-divider';
|
||||
dropdown.appendChild(divider);
|
||||
addedActionDivider = true;
|
||||
}
|
||||
dropdown.appendChild(entry.node);
|
||||
});
|
||||
|
||||
menuBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const openForButton = [...document.querySelectorAll('.memory-item-dropdown')]
|
||||
.find(menu => menu._anchor === menuBtn);
|
||||
if (openForButton) {
|
||||
if (typeof openForButton._dismiss === 'function') openForButton._dismiss();
|
||||
else openForButton.remove();
|
||||
return;
|
||||
}
|
||||
// Close any other open dropdowns
|
||||
document.querySelectorAll('.memory-item-dropdown').forEach(d => d.remove());
|
||||
document.querySelectorAll('.memory-item-dropdown').forEach(d => {
|
||||
if (typeof d._dismiss === 'function') d._dismiss();
|
||||
else d.remove();
|
||||
});
|
||||
dropdown._anchor = menuBtn;
|
||||
const rect = menuBtn.getBoundingClientRect();
|
||||
dropdown.style.position = 'fixed';
|
||||
dropdown.style.top = rect.bottom + 2 + 'px';
|
||||
dropdown.style.top = rect.bottom + 4 + 'px';
|
||||
dropdown.style.right = (window.innerWidth - rect.right) + 'px';
|
||||
dropdown.style.left = 'auto';
|
||||
dropdown.style.zIndex = '10001';
|
||||
// Portaled to <body>, so it must outrank the Memory modal it belongs to.
|
||||
// Tool modals get a monotonically increasing z-index from modalManager's
|
||||
// bring-to-front counter, which climbs unbounded over a long session —
|
||||
// once it passed the old hardcoded 10001 the menu rendered behind the
|
||||
// panel (#4720). topPortalZ() derives the value from the live tool-window
|
||||
// stack so the menu always sits just above, however high it has climbed.
|
||||
dropdown.style.zIndex = String(topPortalZ());
|
||||
dropdown.style.display = 'block';
|
||||
document.body.appendChild(dropdown);
|
||||
// Keep on-screen (mobile): flip above the button if it overflows the
|
||||
// bottom, clamp the left edge, cap height as a last resort.
|
||||
const dr = dropdown.getBoundingClientRect();
|
||||
if (dr.bottom > window.innerHeight - 6) {
|
||||
dropdown.style.top = Math.max(6, rect.top - dr.height - 2) + 'px';
|
||||
dropdown.style.top = Math.max(6, rect.top - dr.height - 4) + 'px';
|
||||
}
|
||||
if (dr.left < 6) {
|
||||
dropdown.style.right = Math.max(6, window.innerWidth - 6 - dr.width) + 'px';
|
||||
@@ -848,8 +1099,17 @@ export function renderMemoryList() {
|
||||
item.addEventListener('pointercancel', _lpCancel);
|
||||
}
|
||||
|
||||
// Close dropdown on outside click
|
||||
document.addEventListener('click', () => { if (dropdown.parentNode) dropdown.remove(); }, { once: false });
|
||||
// Close dropdown on an outside click. Delay registration so the opening
|
||||
// tap cannot immediately close its own menu.
|
||||
const closeDropdown = () => {
|
||||
document.removeEventListener('click', onOutsideClick, true);
|
||||
if (dropdown.parentNode) dropdown.remove();
|
||||
};
|
||||
const onOutsideClick = (event) => {
|
||||
if (!dropdown.contains(event.target) && !menuBtn.contains(event.target)) closeDropdown();
|
||||
};
|
||||
dropdown._dismiss = closeDropdown;
|
||||
setTimeout(() => document.addEventListener('click', onOutsideClick, true), 0);
|
||||
}
|
||||
|
||||
memoryList.appendChild(item);
|
||||
@@ -954,6 +1214,11 @@ export function updateMemoryCount() {
|
||||
const h2Count = document.getElementById('memory-count-h2');
|
||||
const tabCount = document.getElementById('memory-count'); // optional (may be absent)
|
||||
if (!h2Count && !tabCount) return;
|
||||
if (memoriesLoading) {
|
||||
if (h2Count) h2Count.textContent = 'loading...';
|
||||
if (tabCount) tabCount.textContent = '...';
|
||||
return;
|
||||
}
|
||||
|
||||
const searchInput = document.getElementById('memory-search');
|
||||
const searchTerm = searchInput ? searchInput.value.toLowerCase().trim() : '';
|
||||
@@ -977,6 +1242,7 @@ export function updateMemoryCount() {
|
||||
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');
|
||||
@@ -991,6 +1257,7 @@ export async function addNewMemory() {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: text,
|
||||
category: category,
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1127,7 +1394,7 @@ export async function extractMemory(sessionId) {
|
||||
});
|
||||
}
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
openMemoryModal('browse');
|
||||
}
|
||||
|
||||
// ---- Export ----
|
||||
@@ -1160,10 +1427,6 @@ async function handleImportFile(file) {
|
||||
if (!file) return;
|
||||
|
||||
const sessionId = sessionModule?.getCurrentSessionId?.();
|
||||
if (!sessionId) {
|
||||
showError('Open a session first — import needs an AI model');
|
||||
return;
|
||||
}
|
||||
|
||||
const importBtn = document.getElementById('memory-import-btn');
|
||||
const _origImportHtml = importBtn ? importBtn.innerHTML : '';
|
||||
@@ -1180,7 +1443,9 @@ async function handleImportFile(file) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('session', sessionId);
|
||||
if (sessionId) {
|
||||
formData.append('session', sessionId);
|
||||
}
|
||||
|
||||
const res = await fetch(`${window.location.origin}/api/memory/import`, {
|
||||
method: 'POST',
|
||||
@@ -1313,8 +1578,7 @@ async function handleImportFile(file) {
|
||||
});
|
||||
}
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
document.querySelector('.memory-tab[data-memory-tab="browse"]')?.click();
|
||||
openMemoryModal('browse');
|
||||
} catch (error) {
|
||||
console.error('Import failed:', error);
|
||||
showError('Import failed — ' + error.message);
|
||||
@@ -1341,6 +1605,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Memory modal tabs
|
||||
document.querySelectorAll('.memory-tab[data-memory-tab]').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
if (tab.hidden || tab.classList.contains('hidden')) return;
|
||||
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 => {
|
||||
@@ -1348,7 +1613,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
// Lazy-load skills tab (cascade=true → play the domino-in entrance)
|
||||
if (target === 'skills') {
|
||||
import('./skills.js').then(m => { if (m.loadSkills) m.loadSkills(true); else if (m.default?.loadSkills) m.default.loadSkills(true); });
|
||||
import('./skills.js?v=20260909kebabconsistency1').then(m => { if (m.loadSkills) m.loadSkills(true); else if (m.default?.loadSkills) m.default.loadSkills(true); });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1360,6 +1625,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
renderMemoryList();
|
||||
});
|
||||
}
|
||||
_initMemorySortPicker();
|
||||
|
||||
const tidyBtn = document.getElementById('memory-tidy-btn');
|
||||
if (tidyBtn) tidyBtn.addEventListener('click', tidyMemories);
|
||||
@@ -1413,7 +1679,9 @@ const memoryModule = {
|
||||
buildCategoryChips,
|
||||
tidyMemories,
|
||||
importMemories,
|
||||
exportMemories
|
||||
exportMemories,
|
||||
openMemoryModal,
|
||||
openMemory
|
||||
};
|
||||
|
||||
export default memoryModule;
|
||||
|
||||
Reference in New Issue
Block a user