mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-07 07:22:21 +02:00
Squash Odysseus development history
This commit is contained in:
+363
-127
@@ -5,13 +5,19 @@
|
||||
*/
|
||||
|
||||
import { topPortalZ } from './toolWindowZOrder.js';
|
||||
import uiModule from './ui.js';
|
||||
import uiModule from './ui.js?v=20260908weekhoverfix1';
|
||||
import sessionModule from './sessions.js';
|
||||
import spinnerModule from './spinner.js';
|
||||
import markdownModule from './markdown.js';
|
||||
import { makeWindowDraggable } from './windowDrag.js';
|
||||
import { langIcon } from './langIcons.js';
|
||||
import { registerMenuDismiss, dismissOrRemove } from './escMenuStack.js';
|
||||
import { langIcon } from './langIcons.js?v=20260831richtexttools91';
|
||||
import {
|
||||
registerMenuDismiss,
|
||||
dismissOrRemove,
|
||||
bindExpandedCardDismiss,
|
||||
unbindExpandedCardDismiss,
|
||||
} from './escMenuStack.js';
|
||||
import { orderActionMenuItems, actionMenuRank, SELECT_MENU_ICON } from './actionMenuOrder.js';
|
||||
|
||||
// ── Injected references from documentModule ──
|
||||
let API_BASE = '';
|
||||
@@ -21,11 +27,94 @@ let _isOpenFn; // () => boolean — is doc panel open
|
||||
let _createDocument;
|
||||
let _newDocument;
|
||||
let _loadDocument;
|
||||
let _prepareDocumentOpen;
|
||||
let _switchToDoc;
|
||||
let _openPanel;
|
||||
let _addDocToTabs;
|
||||
let _syncDocIndicator;
|
||||
|
||||
function _isEmailDocument(doc) {
|
||||
return String(doc?.language || '').toLowerCase() === 'email';
|
||||
}
|
||||
|
||||
function _documentExport(doc, extMap) {
|
||||
const email = _isEmailDocument(doc);
|
||||
const ext = email ? '.eml' : (extMap[doc?.language] || '.txt');
|
||||
const title = String(doc?.title || 'document').trim() || 'document';
|
||||
const filename = title.toLowerCase().endsWith(ext) ? title : title + ext;
|
||||
// Email drafts use an internal header/body separator for the editor. Turn it
|
||||
// into the blank line required by RFC 5322 when exporting as .eml.
|
||||
const content = email
|
||||
? String(doc?.current_content || '').replace(/\r?\n---\r?\n/, '\r\n\r\n')
|
||||
: String(doc?.current_content || '');
|
||||
return { filename, content, type: email ? 'message/rfc822' : 'text/plain;charset=utf-8' };
|
||||
}
|
||||
|
||||
const _LIBRARY_SORT_ICONS = {
|
||||
recent: '<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="9"/><polyline points="12 7 12 12 15 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"><polyline points="1 4 1 10 7 10"/><path d="M3.5 15a9 9 0 1 0 .7-9.4L1 10"/><polyline points="12 7 12 12 15 14"/></svg>',
|
||||
'most-messages': '<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="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M8 9h8M8 13h5"/></svg>',
|
||||
'most-sources': '<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="M10 13a5 5 0 0 0 7.1.1l2-2a5 5 0 0 0-7.1-7.1l-1.1 1.1"/><path d="M14 11a5 5 0 0 0-7.1-.1l-2 2A5 5 0 0 0 12 20l1.1-1.1"/></svg>',
|
||||
edits: '<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="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></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 16 4 4 4-4"/><path d="M7 20V4"/><path d="m15 4 3 4 3-4"/><path d="M18 8v12"/></svg>',
|
||||
};
|
||||
|
||||
function _librarySortIcon(value) {
|
||||
return _LIBRARY_SORT_ICONS[value] || _LIBRARY_SORT_ICONS.recent;
|
||||
}
|
||||
|
||||
function _enhanceLibrarySortSelect(select) {
|
||||
if (!select || select.dataset.enhanced === 'true') return;
|
||||
select.dataset.enhanced = 'true';
|
||||
const picker = document.createElement('div');
|
||||
picker.className = 'email-filter-picker library-sort-picker';
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'email-filter-btn library-sort-btn';
|
||||
button.setAttribute('aria-haspopup', 'listbox');
|
||||
button.setAttribute('aria-expanded', 'false');
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'email-filter-menu library-sort-menu';
|
||||
menu.setAttribute('role', 'listbox');
|
||||
menu.hidden = true;
|
||||
|
||||
const render = () => {
|
||||
const selected = select.options[select.selectedIndex] || select.options[0];
|
||||
button.innerHTML = `<span class="email-filter-current"><span class="email-filter-icon">${_librarySortIcon(selected?.value)}</span><span class="email-filter-label"></span></span><svg class="email-filter-caret" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>`;
|
||||
button.querySelector('.email-filter-label').textContent = selected?.textContent || 'Recent';
|
||||
menu.replaceChildren(...Array.from(select.options, option => {
|
||||
const item = document.createElement('button');
|
||||
item.type = 'button';
|
||||
item.className = 'email-filter-item library-sort-item';
|
||||
item.dataset.value = option.value;
|
||||
item.setAttribute('role', 'option');
|
||||
item.setAttribute('aria-selected', String(option.value === select.value));
|
||||
item.innerHTML = `<span class="email-filter-item-icon">${_librarySortIcon(option.value)}</span><span class="email-filter-item-label"></span>`;
|
||||
item.querySelector('.email-filter-item-label').textContent = option.textContent;
|
||||
item.addEventListener('click', () => {
|
||||
select.value = option.value;
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
menu.hidden = true;
|
||||
button.setAttribute('aria-expanded', 'false');
|
||||
render();
|
||||
});
|
||||
return item;
|
||||
}));
|
||||
};
|
||||
button.addEventListener('click', () => {
|
||||
const willOpen = menu.hidden;
|
||||
picker.closest('#doclib-modal')?.querySelectorAll('.library-sort-menu:not([hidden])').forEach(other => { other.hidden = true; });
|
||||
picker.closest('#doclib-modal')?.querySelectorAll('.library-sort-btn[aria-expanded="true"]').forEach(other => { other.setAttribute('aria-expanded', 'false'); });
|
||||
menu.hidden = !willOpen;
|
||||
button.setAttribute('aria-expanded', String(willOpen));
|
||||
});
|
||||
select.addEventListener('change', render);
|
||||
select.parentNode.insertBefore(picker, select);
|
||||
picker.append(select, button, menu);
|
||||
select.style.display = 'none';
|
||||
render();
|
||||
}
|
||||
|
||||
export function initLibrary(config) {
|
||||
API_BASE = config.apiBase;
|
||||
_esc = config.esc;
|
||||
@@ -34,6 +123,7 @@ export function initLibrary(config) {
|
||||
_createDocument = config.createDocument;
|
||||
_newDocument = config.newDocument;
|
||||
_loadDocument = config.loadDocument;
|
||||
_prepareDocumentOpen = config.prepareDocumentOpen;
|
||||
_switchToDoc = config.switchToDoc;
|
||||
_openPanel = config.openPanel;
|
||||
_addDocToTabs = config.addDocToTabs;
|
||||
@@ -95,12 +185,21 @@ let _libraryImportMode = false;
|
||||
let _libScrollBound = false; // infinite-scroll listener attached once
|
||||
let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
|
||||
function _setLibraryCountChipContent(chip, label, count) {
|
||||
const text = document.createElement('span');
|
||||
text.textContent = label;
|
||||
const value = document.createElement('strong');
|
||||
value.textContent = String(count);
|
||||
chip.replaceChildren(text, value);
|
||||
}
|
||||
|
||||
// ---- Library animation helpers ----
|
||||
|
||||
/** Collapse an expanded card */
|
||||
function _collapseExpandedCard(card) {
|
||||
const grid = card.closest('.doclib-grid');
|
||||
const instant = card?.dataset?.spaceToggle === '1';
|
||||
unbindExpandedCardDismiss(card);
|
||||
card.classList.remove('doclib-card-expanded');
|
||||
// Release the height lock so grid returns to natural size
|
||||
if (grid) {
|
||||
@@ -171,6 +270,17 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
card._suppressNextClick = true;
|
||||
setTimeout(() => { card._suppressNextClick = false; }, 400);
|
||||
if (navigator.vibrate) try { navigator.vibrate(15); } catch {}
|
||||
// On mobile, a hold is the direct shortcut into bulk selection. The
|
||||
// existing menu remains available from the kebab for other actions.
|
||||
if (window.innerWidth <= 768 && ('ontouchstart' in window || navigator.maxTouchPoints > 0)) {
|
||||
const selectBtn = [...document.querySelectorAll('[id$="-select-btn"]')]
|
||||
.find(btn => btn.offsetParent !== null);
|
||||
if (selectBtn) {
|
||||
if (!selectBtn.classList.contains('active')) selectBtn.click();
|
||||
setTimeout(() => card.querySelector('.memory-select-cb')?.click(), 40);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const btn = card.querySelector(menuSelector);
|
||||
if (btn) btn.click();
|
||||
}, 500);
|
||||
@@ -191,44 +301,51 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
archive: '<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="21 8 21 21 3 21 3 8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/></svg>',
|
||||
restore: '<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="M3 12a9 9 0 1 0 9-9"/><polyline points="3 4 3 9 8 9"/></svg>',
|
||||
delete: '<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 11v6"/><path d="M14 11v6"/></svg>',
|
||||
export: '<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="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>',
|
||||
clone: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>',
|
||||
copy: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>',
|
||||
};
|
||||
|
||||
function _showLibDropdown(anchor, items, opts) {
|
||||
opts = opts || {};
|
||||
const openForAnchor = [...document.querySelectorAll('._lib-dd')]
|
||||
.find(menu => menu._anchor === anchor);
|
||||
if (openForAnchor) {
|
||||
if (typeof openForAnchor._dismiss === 'function') openForAnchor._dismiss();
|
||||
else openForAnchor.remove();
|
||||
return;
|
||||
}
|
||||
document.querySelectorAll('._lib-dd').forEach(dismissOrRemove);
|
||||
const dd = document.createElement('div');
|
||||
dd.className = 'dropdown session-dropdown-menu _lib-dd';
|
||||
for (const item of items) {
|
||||
dd._anchor = anchor;
|
||||
const menuItems = [...items];
|
||||
if (typeof opts.onSelect === 'function') {
|
||||
menuItems.push({ label: 'Select', menuOrder: 650, action: opts.onSelect, iconSvg: SELECT_MENU_ICON });
|
||||
}
|
||||
menuItems.push({ label: 'Cancel', menuOrder: 1000, cancel: true, action: opts.onCancel,
|
||||
iconSvg: '<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>' });
|
||||
const orderedItems = orderActionMenuItems(menuItems);
|
||||
let addedActionDivider = false;
|
||||
for (const [index, item] of orderedItems.entries()) {
|
||||
if (!addedActionDivider && index > 0 && actionMenuRank(item) >= 700) {
|
||||
const divider = document.createElement('div');
|
||||
divider.className = 'dropdown-divider';
|
||||
dd.appendChild(divider);
|
||||
addedActionDivider = true;
|
||||
}
|
||||
const row = document.createElement('div');
|
||||
row.className = 'dropdown-item-compact' + (item.danger ? ' dropdown-item-danger' : '');
|
||||
row.className = 'dropdown-item-compact' + (item.danger ? ' dropdown-item-danger' : '') + (item.cancel ? ' dropdown-cancel-mobile' : '');
|
||||
const iconKey = item.icon || item.label.toLowerCase();
|
||||
const iconSvg = _LIB_DD_ICONS[iconKey] || '';
|
||||
const iconSvg = item.iconSvg || _LIB_DD_ICONS[iconKey] || '';
|
||||
row.innerHTML = (iconSvg ? '<span class="dropdown-icon">' + iconSvg + '</span>' : '') + '<span>' + item.label + '</span>';
|
||||
row.addEventListener('click', (e) => { e.stopPropagation(); teardown(); item.action(); });
|
||||
row.addEventListener('click', (e) => { e.stopPropagation(); teardown(); if (typeof item.action === 'function') item.action(); });
|
||||
dd.appendChild(row);
|
||||
}
|
||||
if (typeof opts.onSelect === 'function') {
|
||||
const sel = document.createElement('div');
|
||||
sel.className = 'dropdown-item-compact';
|
||||
sel.innerHTML =
|
||||
'<span class="dropdown-icon"><span style="font-size:16px;line-height:1;position:relative;top:-2px;">●</span></span>'
|
||||
+ '<span>Select</span>';
|
||||
sel.addEventListener('click', (e) => { e.stopPropagation(); teardown(); opts.onSelect(); });
|
||||
dd.appendChild(sel);
|
||||
}
|
||||
const cancel = document.createElement('div');
|
||||
cancel.className = 'dropdown-item-compact dropdown-cancel-mobile';
|
||||
cancel.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>';
|
||||
cancel.addEventListener('click', (e) => { e.stopPropagation(); teardown(); if (typeof opts.onCancel === 'function') opts.onCancel(); });
|
||||
dd.appendChild(cancel);
|
||||
document.body.appendChild(dd);
|
||||
const rect = anchor.getBoundingClientRect();
|
||||
dd.style.right = (window.innerWidth - rect.right) + 'px';
|
||||
dd.style.top = (rect.bottom + 2) + 'px';
|
||||
dd.style.top = (rect.bottom + 4) + 'px';
|
||||
dd.style.display = 'block';
|
||||
dd.style.zIndex = String(topPortalZ());
|
||||
requestAnimationFrame(() => {
|
||||
@@ -366,16 +483,15 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
function libraryRenderLangChips() {
|
||||
const wrap = document.getElementById('doclib-chips');
|
||||
if (!wrap) return;
|
||||
// Remove only language chip buttons, keep sort/select elements
|
||||
wrap.querySelectorAll('.memory-cat-chip').forEach(c => c.remove());
|
||||
wrap.replaceChildren();
|
||||
const totalAll = Object.values(_libraryLanguages).reduce((a, b) => a + b, 0);
|
||||
|
||||
// Hide the "all (0)" chip + lang chips entirely when there are no docs.
|
||||
if (totalAll === 0) return;
|
||||
|
||||
const allChip = document.createElement('button');
|
||||
allChip.className = 'memory-cat-chip' + (!_libraryActiveLanguage ? ' active' : '');
|
||||
allChip.textContent = `all (${totalAll})`;
|
||||
allChip.className = 'skills-summary-chip' + (!_libraryActiveLanguage ? ' active' : '');
|
||||
_setLibraryCountChipContent(allChip, 'All', totalAll);
|
||||
allChip.addEventListener('click', () => {
|
||||
if (_librarySelectMode) {
|
||||
_libraryDocs.forEach(d => _librarySelectedIds.add(d.id));
|
||||
@@ -393,8 +509,8 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
const sorted = Object.entries(_libraryLanguages).sort((a, b) => b[1] - a[1]);
|
||||
for (const [lang, count] of sorted) {
|
||||
const chip = document.createElement('button');
|
||||
chip.className = 'memory-cat-chip' + (_libraryActiveLanguage === lang ? ' active' : '');
|
||||
chip.textContent = `${lang} (${count})`;
|
||||
chip.className = 'skills-summary-chip' + (_libraryActiveLanguage === lang ? ' active' : '');
|
||||
_setLibraryCountChipContent(chip, lang, count);
|
||||
chip.addEventListener('click', () => {
|
||||
_libraryActiveLanguage = lang;
|
||||
libraryFetch(false);
|
||||
@@ -542,14 +658,14 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
|
||||
// Content wrapper
|
||||
const content = document.createElement('div');
|
||||
content.style.cssText = 'flex:1;min-width:0;padding-top:4px;';
|
||||
content.style.cssText = 'flex:1;min-width:0;';
|
||||
|
||||
// Title row with version badge
|
||||
const titleRow = document.createElement('div');
|
||||
titleRow.style.cssText = 'display:flex;align-items:center;gap:6px;width:100%;';
|
||||
const titleEl = document.createElement('span');
|
||||
titleEl.className = 'memory-item-title';
|
||||
titleEl.style.cssText = 'flex:0 1 auto;min-width:0;';
|
||||
titleEl.style.cssText = 'display:block;flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
|
||||
// Language-specific icon next to the title (matches the document's type:
|
||||
// markdown/csv/python/html/etc.). Falls back to the generic document icon
|
||||
// when the language has no dedicated glyph.
|
||||
@@ -563,14 +679,12 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
verBadge.style.cssText = 'font-size:9px;padding:1px 6px;border-radius:8px;background:color-mix(in srgb, var(--red) 15%, transparent);border:1px solid color-mix(in srgb, var(--red) 40%, transparent);color:var(--red);flex-shrink:0;';
|
||||
verBadge.textContent = 'v' + (doc.version_count || 1);
|
||||
titleRow.appendChild(verBadge);
|
||||
// Chevron pushed to the right end of the title row — collapsed
|
||||
// shows nothing, expanded reveals a downward chevron so the user
|
||||
// sees the card is open and can tap to close it.
|
||||
// The chevron is appended beside the flexible title/meta column below,
|
||||
// matching the structure used by chat library rows.
|
||||
const chevron = document.createElement('span');
|
||||
chevron.className = 'doclib-card-chevron';
|
||||
chevron.style.marginLeft = 'auto';
|
||||
chevron.style.cssText = 'margin-left:auto;flex:0 0 auto;';
|
||||
chevron.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>';
|
||||
titleRow.appendChild(chevron);
|
||||
content.appendChild(titleRow);
|
||||
|
||||
// Meta line: session → [lang-icon language] → time
|
||||
@@ -588,32 +702,39 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
pieces.push(`<span>${_esc(libraryRelativeTime(doc.updated_at))}</span>`);
|
||||
meta.innerHTML = pieces.join('<span style="opacity:0.5;">\u00b7</span>');
|
||||
content.appendChild(meta);
|
||||
card.appendChild(content);
|
||||
|
||||
// Header element (kept for expand/preview compatibility)
|
||||
const header = document.createElement('div');
|
||||
header.className = 'doclib-card-header';
|
||||
header.style.display = 'none';
|
||||
|
||||
// Action buttons — "..." menu
|
||||
const actionsWrap = document.createElement('div');
|
||||
actionsWrap.className = 'memory-item-actions';
|
||||
actionsWrap.style.cssText = 'flex:0 0 auto;margin-left:0;';
|
||||
const menuWrap = document.createElement('span');
|
||||
menuWrap.className = 'doclib-card-menu-wrap';
|
||||
menuWrap.style.position = 'relative';
|
||||
const menuBtn = document.createElement('button');
|
||||
menuBtn.className = 'memory-item-btn';
|
||||
menuBtn.title = 'Actions';
|
||||
menuBtn.style.position = 'relative';
|
||||
menuBtn.style.top = '-2px';
|
||||
menuBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/></svg>';
|
||||
menuBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
// In select mode every part of a document card selects the document.
|
||||
// Do not let the card action menu interrupt that workflow.
|
||||
if (_librarySelectMode) {
|
||||
libraryToggleCardSelection(card, doc.id);
|
||||
return;
|
||||
}
|
||||
// Mobile: the custom 5-item dropdown is too crowded — route through the
|
||||
// shared _showLibDropdown with a small set (Open, Clone) plus Select +
|
||||
// shared _showLibDropdown with clear open destinations plus Select +
|
||||
// Cancel. Heavier actions (Archive, Delete, Export) live in bulk mode.
|
||||
if (window.innerWidth <= 768) {
|
||||
const items = [];
|
||||
if (doc.session_id) items.push({ label: 'Open', action: () => libraryOpenInSession(doc) });
|
||||
items.push({ label: 'Clone', action: () => libraryImportDocument(doc) });
|
||||
const items = [
|
||||
{
|
||||
label: doc.session_id ? 'Open in original' : 'Open document',
|
||||
icon: 'open',
|
||||
action: () => doc.session_id ? libraryOpenInSession(doc) : libraryOpenDocument(doc),
|
||||
},
|
||||
{ label: 'Open in new chat', icon: 'copy', action: () => libraryImportDocument(doc, { newSession: true }) },
|
||||
];
|
||||
_showLibDropdown(menuBtn, items, { onSelect: () => {
|
||||
libraryEnterSelectMode();
|
||||
_librarySelectedIds.add(doc.id);
|
||||
@@ -655,7 +776,7 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
|
||||
// Dropdown menu
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'doclib-card-dropdown';
|
||||
dropdown.className = 'dropdown session-dropdown-menu doclib-card-dropdown';
|
||||
dropdown.style.cssText = 'display:none;position:absolute;top:100%;right:0;z-index:1000;min-width:0;width:max-content;padding:4px;background:var(--panel);border:1px solid var(--border);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.3);backdrop-filter:blur(12px);font-size:12px;';
|
||||
|
||||
// Single close path for the card action dropdown, shared by the toggle
|
||||
@@ -691,40 +812,61 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
}
|
||||
dropdown.appendChild(openItem);
|
||||
|
||||
// Select — same position and behavior as every other Library card menu.
|
||||
const selectItem = document.createElement('button');
|
||||
selectItem.className = 'dropdown-item-compact';
|
||||
selectItem.style.cssText = 'background:none;border:none;width:100%;';
|
||||
selectItem.innerHTML = _di(SELECT_MENU_ICON) + '<span>Select</span>';
|
||||
selectItem.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
hideCardDropdown();
|
||||
libraryEnterSelectMode();
|
||||
_librarySelectedIds.add(doc.id);
|
||||
libraryUpdateBulkCount();
|
||||
libraryRenderGrid();
|
||||
});
|
||||
|
||||
// Clone
|
||||
const _cloneIco = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
||||
const cloneItem = document.createElement('button');
|
||||
cloneItem.className = 'dropdown-item-compact';
|
||||
cloneItem.style.cssText = 'background:none;border:none;width:100%;';
|
||||
cloneItem.innerHTML = _di(_cloneIco) + '<span>Clone</span>';
|
||||
cloneItem.title = 'Clone to active session';
|
||||
cloneItem.innerHTML = _di(_cloneIco) + '<span>Copy to session</span>';
|
||||
cloneItem.title = 'Copy to current session';
|
||||
cloneItem.addEventListener('click', (e) => { e.stopPropagation(); hideCardDropdown(); libraryImportDocument(doc); });
|
||||
dropdown.appendChild(cloneItem);
|
||||
|
||||
// Export
|
||||
const _exportIco = '<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="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>';
|
||||
const exportItem = document.createElement('button');
|
||||
exportItem.type = 'button';
|
||||
exportItem.className = 'dropdown-item-compact';
|
||||
exportItem.style.cssText = 'background:none;border:none;width:100%;';
|
||||
exportItem.innerHTML = _di(_exportIco) + '<span>Export</span>';
|
||||
exportItem.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
const exportDocumentFile = async () => {
|
||||
hideCardDropdown();
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/document/${doc.id}`);
|
||||
if (!res.ok) throw new Error('Failed');
|
||||
const full = await res.json();
|
||||
const extMap = { javascript: '.js', python: '.py', html: '.html', css: '.css', markdown: '.md', json: '.json', yaml: '.yml', bash: '.sh', sql: '.sql', rust: '.rs', go: '.go', java: '.java', c: '.c', cpp: '.cpp', typescript: '.ts', ruby: '.rb', php: '.php', xml: '.xml', toml: '.toml', ini: '.ini' };
|
||||
const ext = extMap[full.language] || '.txt';
|
||||
const blob = new Blob([full.current_content || ''], { type: 'text/plain' });
|
||||
const extMap = { javascript: '.js', python: '.py', html: '.html', svg: '.svg', css: '.css', markdown: '.md', json: '.json', yaml: '.yml', bash: '.sh', sql: '.sql', rust: '.rs', go: '.go', java: '.java', c: '.c', cpp: '.cpp', typescript: '.ts', ruby: '.rb', php: '.php', xml: '.xml', toml: '.toml', ini: '.ini' };
|
||||
const exported = _documentExport(full, extMap);
|
||||
const blob = new Blob([exported.content], { type: exported.type });
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = (full.title || 'document') + ext;
|
||||
a.download = exported.filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
} catch { if (uiModule) uiModule.showError('Failed to export document'); }
|
||||
};
|
||||
exportItem.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
await exportDocumentFile();
|
||||
});
|
||||
dropdown.appendChild(exportItem);
|
||||
// Keep Select immediately before the archive/destructive section.
|
||||
dropdown.appendChild(selectItem);
|
||||
|
||||
// Archive / Restore — soft-archive a doc out of the main list, or bring it back.
|
||||
const _archiveIco = '<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="21 8 21 21 3 21 3 8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/></svg>';
|
||||
@@ -746,6 +888,9 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
if (uiModule) uiModule.showToast(toArchived ? 'Archived' : 'Restored');
|
||||
} catch { if (uiModule) uiModule.showError('Failed to ' + (toArchived ? 'archive' : 'restore')); }
|
||||
});
|
||||
const actionDivider = document.createElement('div');
|
||||
actionDivider.className = 'dropdown-divider';
|
||||
dropdown.appendChild(actionDivider);
|
||||
dropdown.appendChild(archiveItem);
|
||||
|
||||
// Delete
|
||||
@@ -759,16 +904,21 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
|
||||
menuWrap.appendChild(dropdown);
|
||||
actionsWrap.appendChild(menuWrap);
|
||||
card.appendChild(actionsWrap);
|
||||
|
||||
// Hidden header for expand/preview compatibility
|
||||
card.appendChild(header);
|
||||
// Match chat rows: a flexible title/meta column followed by the
|
||||
// chevron and kebab as sibling controls in one full-width header row.
|
||||
const visibleHeader = document.createElement('div');
|
||||
visibleHeader.className = 'doclib-document-header';
|
||||
visibleHeader.style.cssText = 'display:flex;align-items:center;width:100%;gap:6px;';
|
||||
visibleHeader.appendChild(content);
|
||||
visibleHeader.appendChild(actionsWrap);
|
||||
visibleHeader.appendChild(chevron);
|
||||
card.appendChild(visibleHeader);
|
||||
|
||||
// Inject library card hover styles once
|
||||
if (!document.getElementById('doclib-card-styles')) {
|
||||
const s = document.createElement('style');
|
||||
s.id = 'doclib-card-styles';
|
||||
s.textContent = `.doclib-card:hover .doclib-card-icon-btn{opacity:.4}.doclib-card-icon-btn:hover{opacity:1!important}.doclib-card-text-btn{background:none;border:1px solid var(--border);color:var(--fg-muted);font-size:10px;padding:3px 8px;border-radius:4px;cursor:pointer;transition:border-color .15s,color .15s}.doclib-card-text-btn:hover{border-color:var(--accent,var(--red));color:var(--accent,var(--red))}.doclib-card-text-btn-danger{border-color:var(--color-danger,#e06c75)!important;color:var(--color-danger,#e06c75)!important}.doclib-card-text-btn-danger:hover{border-color:#ff4d4d!important;color:#ff4d4d!important}.doclib-card-chevron{display:none;align-items:center;justify-content:center;align-self:center;opacity:0.6;transition:transform .15s ease;flex-shrink:0;height:14px;line-height:0}.doclib-card-expanded .doclib-card-chevron{display:inline-flex;transform:rotate(180deg)}.doclib-card-chevron svg{display:block}`;
|
||||
s.textContent = `.doclib-card:hover .doclib-card-icon-btn{opacity:.4}.doclib-card-icon-btn:hover{opacity:1!important}.doclib-card-text-btn{background:none;border:1px solid var(--border);color:var(--fg-muted);font-size:10px;padding:3px 8px;border-radius:4px;cursor:pointer;transition:border-color .15s,color .15s}.doclib-card-text-btn:hover{border-color:var(--accent,var(--red));color:var(--accent,var(--red))}.doclib-card-text-btn-danger{border-color:var(--color-danger,#e06c75)!important;color:var(--color-danger,#e06c75)!important}.doclib-card-text-btn-danger:hover{border-color:#ff4d4d!important;color:#ff4d4d!important}.doclib-card-chevron{display:inline-flex;align-items:center;justify-content:center;align-self:center;opacity:0.6;transition:transform .15s ease;flex-shrink:0;height:14px;line-height:0;transform:rotate(0deg)}.doclib-card-expanded .doclib-card-chevron{transform:rotate(180deg)}.doclib-card-chevron svg{display:block}`;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
@@ -798,33 +948,54 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
expandedActions.className = 'doclib-card-expanded-actions';
|
||||
|
||||
const openBtn = document.createElement('button');
|
||||
openBtn.className = 'doclib-card-text-btn doclib-card-action-btn';
|
||||
openBtn.className = 'doclib-card-text-btn doclib-card-action-btn doclib-expanded-open-btn';
|
||||
openBtn.innerHTML = '<svg 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:-1px;margin-right:3px;"><path d="M5 12h14M13 5l7 7-7 7"/></svg>Open';
|
||||
if (doc.session_id) {
|
||||
openBtn.title = 'Open in original session';
|
||||
openBtn.addEventListener('click', (e) => { e.stopPropagation(); libraryOpenInSession(doc); });
|
||||
} else {
|
||||
// Orphaned doc (closed / session detached) is still openable in the editor
|
||||
// by id — libraryOpenDocument handles the no-session case (#1602).
|
||||
openBtn.title = 'Open in the editor';
|
||||
openBtn.addEventListener('click', (e) => { e.stopPropagation(); libraryOpenDocument(doc); });
|
||||
}
|
||||
openBtn.title = 'Open document';
|
||||
openBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (window.innerWidth <= 768) {
|
||||
_showLibDropdown(openBtn, [
|
||||
{
|
||||
label: doc.session_id ? 'Open in original' : 'Open document',
|
||||
icon: 'open',
|
||||
action: () => doc.session_id ? libraryOpenInSession(doc) : libraryOpenDocument(doc),
|
||||
},
|
||||
{ label: 'Open in new chat', icon: 'copy', action: () => libraryImportDocument(doc, { newSession: true }) },
|
||||
]);
|
||||
} else if (doc.session_id) {
|
||||
libraryOpenInSession(doc);
|
||||
} else {
|
||||
// Orphaned docs remain directly openable by id.
|
||||
libraryOpenDocument(doc);
|
||||
}
|
||||
});
|
||||
|
||||
const cloneBtn = document.createElement('button');
|
||||
cloneBtn.className = 'doclib-card-text-btn doclib-card-action-btn';
|
||||
cloneBtn.innerHTML = '<svg 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:-1px;margin-right:3px;"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>Clone';
|
||||
cloneBtn.title = 'Clone — copy to active session';
|
||||
cloneBtn.className = 'doclib-card-text-btn doclib-card-action-btn doclib-expanded-clone-btn';
|
||||
cloneBtn.innerHTML = '<svg 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:-1px;margin-right:3px;"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>Copy to session';
|
||||
cloneBtn.title = 'Copy to current session';
|
||||
cloneBtn.addEventListener('click', (e) => { e.stopPropagation(); libraryImportDocument(doc); });
|
||||
|
||||
const expandedExportBtn = document.createElement('button');
|
||||
expandedExportBtn.type = 'button';
|
||||
expandedExportBtn.className = 'doclib-card-text-btn doclib-card-action-btn doclib-expanded-export-btn';
|
||||
expandedExportBtn.innerHTML = '<svg 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:-1px;margin-right:3px;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>Export';
|
||||
expandedExportBtn.title = 'Export — save to this computer';
|
||||
expandedExportBtn.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
await exportDocumentFile();
|
||||
});
|
||||
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'doclib-card-text-btn doclib-card-action-btn doclib-card-text-btn-danger';
|
||||
deleteBtn.className = 'doclib-card-text-btn doclib-card-action-btn doclib-card-text-btn-danger doclib-expanded-delete-btn';
|
||||
deleteBtn.innerHTML = '<svg 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:-1px;margin-right:3px;"><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 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>Delete';
|
||||
deleteBtn.addEventListener('click', (e) => { e.stopPropagation(); libraryDeleteSingle(doc.id, card); });
|
||||
|
||||
// Archive sits next to Delete on the LEFT — same lineup as the chat
|
||||
// and research footers. Label flips to Restore inside the Archive view.
|
||||
const archiveBtn = document.createElement('button');
|
||||
archiveBtn.className = 'doclib-card-text-btn doclib-card-action-btn';
|
||||
archiveBtn.className = 'doclib-card-text-btn doclib-card-action-btn doclib-expanded-archive-btn';
|
||||
archiveBtn.innerHTML = '<svg 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:-1px;margin-right:3px;"><polyline points="21 8 21 21 3 21 3 8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/></svg>' + (_libraryArchivedView ? 'Restore' : 'Archive');
|
||||
archiveBtn.title = _libraryArchivedView ? 'Restore to active documents' : 'Archive (hide from the main list)';
|
||||
archiveBtn.addEventListener('click', async (e) => {
|
||||
@@ -843,7 +1014,7 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
leftGroup.className = 'doclib-action-group';
|
||||
const btnRow = document.createElement('div');
|
||||
btnRow.className = 'doclib-action-btn-row';
|
||||
// Export lives in the ⋮ menu — keep the footer uncrowded with Clone + Open.
|
||||
btnRow.appendChild(expandedExportBtn);
|
||||
btnRow.appendChild(cloneBtn);
|
||||
btnRow.appendChild(openBtn);
|
||||
leftGroup.appendChild(btnRow);
|
||||
@@ -855,14 +1026,37 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
expandedActions.appendChild(archiveBtn);
|
||||
expandedActions.appendChild(leftGroup);
|
||||
|
||||
const mobileMoreBtn = document.createElement('button');
|
||||
mobileMoreBtn.type = 'button';
|
||||
mobileMoreBtn.className = 'doclib-card-text-btn doclib-card-action-btn doclib-expanded-mobile-more';
|
||||
mobileMoreBtn.title = 'Document actions';
|
||||
mobileMoreBtn.setAttribute('aria-label', 'Document actions');
|
||||
mobileMoreBtn.innerHTML = '<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/></svg>';
|
||||
mobileMoreBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
_showLibDropdown(mobileMoreBtn, [
|
||||
{ label: 'Export file', icon: 'export', action: () => expandedExportBtn.click() },
|
||||
{
|
||||
label: _libraryArchivedView ? 'Restore document' : 'Archive document',
|
||||
icon: _libraryArchivedView ? 'restore' : 'archive',
|
||||
action: () => archiveBtn.click(),
|
||||
},
|
||||
{ label: 'Delete document', icon: 'delete', danger: true, action: () => deleteBtn.click() },
|
||||
]);
|
||||
});
|
||||
expandedActions.appendChild(mobileMoreBtn);
|
||||
|
||||
preview.appendChild(expandedActions);
|
||||
card.appendChild(preview);
|
||||
|
||||
card.addEventListener('click', () => {
|
||||
card.addEventListener('click', (e) => {
|
||||
if (card._suppressNextClick) { card._suppressNextClick = false; return; }
|
||||
if (_librarySelectMode) {
|
||||
const cb = card.querySelector('.memory-select-cb');
|
||||
if (cb) { cb.checked = !cb.checked; cb.dispatchEvent(new Event('change')); }
|
||||
// Checkbox and menu clicks have their own handlers. Other clicks on
|
||||
// the card, including code previews, select the whole document.
|
||||
if (e.target.closest('.memory-select-cb, .doclib-card-menu-wrap')) return;
|
||||
libraryToggleCardSelection(card, doc.id);
|
||||
} else {
|
||||
libraryExpandCard(card, doc);
|
||||
}
|
||||
@@ -911,6 +1105,7 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
if (!instant) await new Promise(r => setTimeout(r, 120));
|
||||
|
||||
card.classList.add('doclib-card-expanded');
|
||||
bindExpandedCardDismiss(card, () => _collapseExpandedCard(card));
|
||||
if (grid) grid.scrollTop = 0;
|
||||
|
||||
// Clean up sibling inline styles (CSS display:none takes over now)
|
||||
@@ -996,65 +1191,61 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
}
|
||||
|
||||
async function libraryOpenDocument(doc) {
|
||||
_prepareDocumentOpen?.(doc.session_id);
|
||||
closeLibrary();
|
||||
// Orphaned doc (session deleted) — just open in editor without switching session
|
||||
if (!doc.session_id) {
|
||||
_loadDocument(doc.id);
|
||||
await _loadDocument(doc.id);
|
||||
return;
|
||||
}
|
||||
const currentSessionId = sessionModule && sessionModule.getCurrentSessionId();
|
||||
if (doc.session_id !== currentSessionId) {
|
||||
await sessionModule.selectSession(doc.session_id);
|
||||
}
|
||||
_loadDocument(doc.id);
|
||||
await _loadDocument(doc.id);
|
||||
}
|
||||
|
||||
/** Open a document in its linked session */
|
||||
async function libraryOpenInSession(doc) {
|
||||
if (!doc.session_id) return;
|
||||
_prepareDocumentOpen?.(doc.session_id);
|
||||
closeLibrary();
|
||||
|
||||
// Step 1: switch session if needed and wait for it to load
|
||||
const currentSessionId = sessionModule && sessionModule.getCurrentSessionId();
|
||||
if (doc.session_id !== currentSessionId) {
|
||||
await sessionModule.selectSession(doc.session_id);
|
||||
// Give the session UI a moment to settle
|
||||
await new Promise(r => setTimeout(r, 150));
|
||||
}
|
||||
|
||||
// Step 2: ensure doc is in tabs
|
||||
const docs = _getDocs();
|
||||
if (!docs.has(doc.id)) {
|
||||
const res = await fetch(`${API_BASE}/api/document/${doc.id}`);
|
||||
if (res.ok) {
|
||||
const full = await res.json();
|
||||
_addDocToTabs(full, doc.session_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: open panel (slide-in is handled by openPanel)
|
||||
if (!_isOpenFn()) _openPanel();
|
||||
|
||||
_switchToDoc(doc.id);
|
||||
// Explicit loads also restore a minimized editor. Checking only isOpen here
|
||||
// left mobile documents switched behind their bottom dock chip because a
|
||||
// minimized panel intentionally keeps the document module alive.
|
||||
await _loadDocument(doc.id);
|
||||
_syncDocIndicator();
|
||||
}
|
||||
|
||||
/** Copy a document from the library into the current session */
|
||||
async function libraryImportDocument(doc) {
|
||||
let sessionId = sessionModule && sessionModule.getCurrentSessionId();
|
||||
/** Copy a document from the library into the current or a fresh session. */
|
||||
async function libraryImportDocument(doc, { newSession = false } = {}) {
|
||||
let sessionId = newSession ? null : (sessionModule && sessionModule.getCurrentSessionId());
|
||||
if (!sessionId) {
|
||||
// Create a new session if none exists
|
||||
if (sessionModule && sessionModule.hasPendingChat && sessionModule.hasPendingChat()) {
|
||||
// "Open here" may target an already prepared new chat. "Open in new
|
||||
// chat" always creates a distinct session, even when a chat is active.
|
||||
if (!newSession && sessionModule && sessionModule.hasPendingChat && sessionModule.hasPendingChat()) {
|
||||
const ok = await sessionModule.materializePendingSession();
|
||||
if (ok) sessionId = sessionModule.getCurrentSessionId();
|
||||
}
|
||||
if (!sessionId) {
|
||||
// No pending chat either — trigger new session, preserving the current model
|
||||
// Trigger a session using the current chat model when possible.
|
||||
const curModel = sessionModule.getCurrentModel ? sessionModule.getCurrentModel() : null;
|
||||
const sessions = sessionModule ? sessionModule.getSessions() : [];
|
||||
// Prefer the session matching the current model, otherwise fall back to first with a model
|
||||
const currentId = sessionModule.getCurrentSessionId ? sessionModule.getCurrentSessionId() : null;
|
||||
const current = sessions.find(s => s.id === currentId);
|
||||
// Prefer the current endpoint, then a session matching the current
|
||||
// model, otherwise fall back to the first configured chat endpoint.
|
||||
const withModel = sessions.filter(s => s.endpoint_url && s.model);
|
||||
const match = (curModel && withModel.find(s => s.model === curModel)) || withModel[0];
|
||||
const match = (current?.endpoint_url && current?.model ? current : null)
|
||||
|| (curModel && withModel.find(s => s.model === curModel))
|
||||
|| withModel[0];
|
||||
if (match) {
|
||||
sessionModule.createDirectChat(match.endpoint_url, match.model, match.endpoint_id);
|
||||
const ok = await sessionModule.materializePendingSession();
|
||||
@@ -1108,7 +1299,7 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
|
||||
_switchToDoc(created.id);
|
||||
_syncDocIndicator();
|
||||
if (uiModule) uiModule.showToast('Document cloned to session');
|
||||
if (uiModule) uiModule.showToast(newSession ? 'Document opened in new chat' : 'Document opened here');
|
||||
} catch (e) {
|
||||
console.error('Failed to import document:', e);
|
||||
if (uiModule) uiModule.showError('Failed to import document');
|
||||
@@ -1149,6 +1340,14 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
libraryUpdateBulkCount();
|
||||
}
|
||||
|
||||
function libraryToggleCardSelection(card, id) {
|
||||
const cb = card?.querySelector('.memory-select-cb');
|
||||
if (!cb) return;
|
||||
cb.checked = !cb.checked;
|
||||
cb.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
card.classList.toggle('selected', _librarySelectedIds.has(id));
|
||||
}
|
||||
|
||||
function libraryToggleSelectAll() {
|
||||
const selectAllEl = document.getElementById('doclib-select-all');
|
||||
if (!selectAllEl) return;
|
||||
@@ -1326,7 +1525,7 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
return;
|
||||
}
|
||||
const extMap = {
|
||||
javascript: '.js', python: '.py', html: '.html', css: '.css',
|
||||
javascript: '.js', python: '.py', html: '.html', svg: '.svg', css: '.css',
|
||||
markdown: '.md', json: '.json', yaml: '.yml', bash: '.sh',
|
||||
sql: '.sql', rust: '.rs', go: '.go', java: '.java', c: '.c', cpp: '.cpp',
|
||||
typescript: '.ts', ruby: '.rb', php: '.php', text: '.txt',
|
||||
@@ -1345,9 +1544,9 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
}));
|
||||
for (const doc of docs) {
|
||||
if (!doc) continue;
|
||||
const ext = extMap[doc.language] || '.txt';
|
||||
const filename = (doc.title || 'document') + (doc.title && doc.title.includes('.') ? '' : ext);
|
||||
const blob = new Blob([doc.current_content || ''], { type: 'text/plain' });
|
||||
const exported = _documentExport(doc, extMap);
|
||||
const filename = exported.filename;
|
||||
const blob = new Blob([exported.content], { type: exported.type });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
@@ -1616,7 +1815,7 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
<div class="modal-body" style="display:flex;flex-direction:column;gap:10px;overflow:hidden;">
|
||||
<div id="doclib-panel-chats" data-doclib-panel="chats" class="admin-card" style="display:none;flex:1;flex-direction:column;overflow:hidden;">
|
||||
<div style="display:flex;align-items:baseline;gap:8px;margin-bottom:2px;">
|
||||
<h2 style="margin:0;padding:0;line-height:1;">Chats <span id="doclib-chats-stats" class="memory-count" style="font-size:0.6em;opacity:0.6;font-weight:normal"></span></h2>
|
||||
<h2 style="margin:0;padding:0;line-height:1;"><svg class="doclib-section-title-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>Chats <span id="doclib-chats-stats" class="memory-count" style="font-size:0.6em;opacity:0.6;font-weight:normal"></span></h2>
|
||||
</div>
|
||||
<p class="memory-desc doclib-desc">All active chat sessions. Click to open.</p>
|
||||
<div class="memory-toolbar">
|
||||
@@ -1627,8 +1826,8 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
<option value="most-messages">Most messages</option>
|
||||
<option value="alpha">A\u2013Z</option>
|
||||
</select>
|
||||
<button class="memory-toolbar-btn" id="doclib-chats-tidy-btn" title="AI tidy: delete junk sessions and organize into folders"><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</button>
|
||||
<button class="memory-toolbar-btn" id="doclib-chats-select-btn">Select</button>
|
||||
<button class="memory-toolbar-btn" id="doclib-chats-tidy-btn" title="AI tidy: delete junk sessions and organize into folders"><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</button>
|
||||
</div>
|
||||
<input type="text" id="doclib-chats-search" placeholder="Search chats\u2026" class="memory-search-input" />
|
||||
<div id="doclib-chats-chips" class="doclib-lang-chips"></div>
|
||||
@@ -1644,7 +1843,7 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
</div>
|
||||
<div id="doclib-panel-archive" data-doclib-panel="archive" class="admin-card" style="display:none;flex:1;flex-direction:column;overflow:hidden;">
|
||||
<div style="display:flex;align-items:baseline;gap:8px;margin-bottom:2px;">
|
||||
<h2 style="margin:0;padding:0;line-height:1;position:relative;top:2px;">Archive <span id="doclib-arc-stats" class="memory-count" style="font-size:0.6em;opacity:0.6;font-weight:normal"></span></h2>
|
||||
<h2 style="margin:0;padding:0;line-height:1;position:relative;top:2px;"><svg class="doclib-section-title-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="21 8 21 21 3 21 3 8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/></svg>Archive <span id="doclib-arc-stats" class="memory-count" style="font-size:0.6em;opacity:0.6;font-weight:normal"></span></h2>
|
||||
</div>
|
||||
<p class="memory-desc doclib-desc" style="position:relative;top:0.5px;">Archived sessions. Restore to make active again.</p>
|
||||
<div class="memory-toolbar">
|
||||
@@ -1671,7 +1870,7 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
</div>
|
||||
<div id="doclib-panel-research" data-doclib-panel="research" class="admin-card" style="display:none;flex:1;flex-direction:column;overflow:hidden;">
|
||||
<div style="display:flex;align-items:baseline;gap:8px;margin-bottom:2px;margin-top:10px;">
|
||||
<h2 style="margin:0;padding:0;line-height:1;">Research <span id="doclib-research-stats" class="memory-count" style="font-size:0.6em;opacity:0.6;font-weight:normal"></span></h2>
|
||||
<h2 style="margin:0;padding:0;line-height:1;"><svg class="doclib-section-title-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>Research <span id="doclib-research-stats" class="memory-count" style="font-size:0.6em;opacity:0.6;font-weight:normal"></span></h2>
|
||||
</div>
|
||||
<p class="memory-desc doclib-desc" style="position:relative;top:-1px;">Completed deep research reports. Click to view.</p>
|
||||
<div class="memory-toolbar">
|
||||
@@ -1682,8 +1881,8 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
<option value="most-sources">Most sources</option>
|
||||
<option value="alpha">A\u2013Z</option>
|
||||
</select>
|
||||
<button class="memory-toolbar-btn" id="doclib-research-select-btn">Select</button>
|
||||
<button class="memory-toolbar-btn" id="doclib-research-tidy-btn" title="Tidy: delete research with no sources or empty reports">Tidy</button>
|
||||
<button class="memory-toolbar-btn" id="doclib-research-select-btn">Select</button>
|
||||
</div>
|
||||
<input type="text" id="doclib-research-search" placeholder="Search research\u2026" class="memory-search-input" />
|
||||
</div>
|
||||
@@ -1698,9 +1897,9 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
</div>
|
||||
<div data-doclib-panel="documents" class="admin-card" style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
|
||||
<div style="display:flex;align-items:baseline;gap:8px;margin-bottom:2px;">
|
||||
<h2 style="margin:0;padding:0;line-height:1;">Documents <span id="doclib-stats" class="memory-count" style="font-size:0.6em;opacity:0.6;font-weight:normal"></span></h2>
|
||||
<button class="memory-toolbar-btn" id="doclib-import-file-btn" title="Import files from disk" style="margin-left:auto;"><svg 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:-1px;margin-right:2px;"><polyline points="7 10 12 5 17 10"/><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="21" x2="19" y2="21"/></svg> Import</button>
|
||||
<button class="memory-toolbar-btn" id="doclib-create-btn" title="Create new blank document"><svg 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:-1px;margin-right:3px;"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg> Create</button>
|
||||
<h2 style="margin:0;padding:0;line-height:1;"><svg class="doclib-section-title-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="8" y1="13" x2="16" y2="13"/><line x1="8" y1="17" x2="13" y2="17"/></svg>Documents <span id="doclib-stats" class="memory-count" style="font-size:0.6em;opacity:0.6;font-weight:normal"></span></h2>
|
||||
<button class="memory-toolbar-btn" id="doclib-import-file-btn" title="Import files from disk" style="margin-left:auto;"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="var(--accent, var(--red))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:2px;"><polyline points="7 10 12 5 17 10"/><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="21" x2="19" y2="21"/></svg> Import</button>
|
||||
<button class="memory-toolbar-btn" id="doclib-create-btn" title="Create new blank document"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="var(--accent, var(--red))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg> Create</button>
|
||||
</div>
|
||||
<p class="memory-desc doclib-desc">Open documents in a session, clone to a new or import new files.</p>
|
||||
<div class="memory-toolbar">
|
||||
@@ -1711,8 +1910,8 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
<option value="edits">Most edits</option>
|
||||
<option value="alpha">A\u2013Z</option>
|
||||
</select>
|
||||
<button class="memory-toolbar-btn" id="doclib-select-btn" title="Select documents">Select</button>
|
||||
<button class="memory-toolbar-btn" id="doclib-tidy-btn" title="Tidy: remove empty / junk / duplicate documents">Tidy</button>
|
||||
<button class="memory-toolbar-btn" id="doclib-select-btn" title="Select documents">Select</button>
|
||||
</div>
|
||||
<input type="text" id="doclib-search" placeholder="Search titles & content\u2026" class="memory-search-input" />
|
||||
<div id="doclib-chips" class="doclib-lang-chips"></div>
|
||||
@@ -1722,6 +1921,7 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
<label class="memory-bulk-check-all" style="position:relative;top:0px;left:1px;"><input type="checkbox" id="doclib-select-all" /> All</label>
|
||||
<span id="doclib-selected-count">0 Selected</span>
|
||||
<button id="doclib-bulk-actions" class="memory-toolbar-btn" style="position:relative;top:-2px;margin-left:auto;"><svg 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:-1px;margin-right:3px;"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>Actions <span style="opacity:0.55;font-size:9px;">▼</span></button>
|
||||
<button id="doclib-bulk-delete" class="memory-toolbar-btn danger" title="Delete selected documents" disabled style="position:relative;top:-2px;"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>Delete</button>
|
||||
<button id="doclib-bulk-cancel" class="memory-toolbar-btn" title="Cancel (Esc)" style="margin-left:4px;margin-right:4px;padding:3px 6px;position:relative;top:-2px;"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
|
||||
</div>
|
||||
<div class="doclib-grid" id="doclib-grid"></div>
|
||||
@@ -1731,6 +1931,13 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
modal.querySelectorAll('#doclib-chats-sort, #doclib-arc-sort, #doclib-research-sort, #doclib-sort')
|
||||
.forEach(_enhanceLibrarySortSelect);
|
||||
modal.addEventListener('click', (event) => {
|
||||
if (event.target.closest('.library-sort-picker')) return;
|
||||
modal.querySelectorAll('.library-sort-menu:not([hidden])').forEach(menu => { menu.hidden = true; });
|
||||
modal.querySelectorAll('.library-sort-btn[aria-expanded="true"]').forEach(button => { button.setAttribute('aria-expanded', 'false'); });
|
||||
});
|
||||
|
||||
// Make modal draggable (same logic as other modals)
|
||||
{
|
||||
@@ -1918,6 +2125,7 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
if (grid) {
|
||||
grid.querySelectorAll('.doclib-card-expanded').forEach(c => {
|
||||
if (c !== card) {
|
||||
unbindExpandedCardDismiss(c);
|
||||
c.classList.remove('doclib-card-expanded');
|
||||
const p = c.querySelector('.doclib-chat-preview');
|
||||
if (p) { p.style.display = 'none'; p.innerHTML = ''; }
|
||||
@@ -1925,12 +2133,19 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
});
|
||||
}
|
||||
if (isOpen) {
|
||||
unbindExpandedCardDismiss(card);
|
||||
card.classList.remove('doclib-card-expanded');
|
||||
preview.style.display = 'none';
|
||||
preview.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
card.classList.add('doclib-card-expanded');
|
||||
bindExpandedCardDismiss(card, () => {
|
||||
unbindExpandedCardDismiss(card);
|
||||
card.classList.remove('doclib-card-expanded');
|
||||
preview.style.display = 'none';
|
||||
preview.innerHTML = '';
|
||||
});
|
||||
preview.style.display = 'block';
|
||||
preview.innerHTML = '<div style="opacity:0.4;font-size:11px;padding:8px 4px;">Loading…</div>';
|
||||
try {
|
||||
@@ -2101,7 +2316,7 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
// brand-new "New Chat" rows don't show "\u00b7 0 msgs".
|
||||
const _chatMsgs = s.message_count || 0;
|
||||
const msgCountHtml = _chatMsgs > 0
|
||||
? '<span style="opacity:0.45;font-weight:normal;font-size:0.9em;margin-left:6px;">\u00b7 ' + _chatMsgs + ' msg' + (_chatMsgs === 1 ? '' : 's') + '</span>'
|
||||
? '<span style="color:var(--accent,var(--red));opacity:0.8;font-weight:normal;font-size:0.9em;margin-left:6px;">\u00b7 ' + _chatMsgs + ' msg' + (_chatMsgs === 1 ? '' : 's') + '</span>'
|
||||
: '';
|
||||
card.innerHTML =
|
||||
'<div class="doclib-chat-header" style="display:flex;align-items:center;width:100%;gap:6px;">' +
|
||||
@@ -2157,8 +2372,8 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
const folders = Object.keys(counts).sort();
|
||||
if (folders.length < 1) { el.innerHTML = ''; return; }
|
||||
el.innerHTML = '';
|
||||
const mk = (label, val, count) => { const c = document.createElement('button'); c.className = 'memory-cat-chip' + (_chatsModelFilter === val ? ' active' : ''); c.textContent = label + ' (' + count + ')'; c.addEventListener('click', () => { _chatsModelFilter = _chatsModelFilter === val ? '' : val; _renderChatsGrid(); _renderChatsChips(); }); el.appendChild(c); };
|
||||
mk('all', '', _chatsSessions.length);
|
||||
const mk = (label, val, count) => { const c = document.createElement('button'); c.className = 'skills-summary-chip' + (_chatsModelFilter === val ? ' active' : ''); _setLibraryCountChipContent(c, label, count); c.addEventListener('click', () => { _chatsModelFilter = _chatsModelFilter === val ? '' : val; _renderChatsGrid(); _renderChatsChips(); }); el.appendChild(c); };
|
||||
mk('All', '', _chatsSessions.length);
|
||||
folders.forEach(f => mk(f, f, counts[f]));
|
||||
}
|
||||
|
||||
@@ -2350,6 +2565,7 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
if (grid) {
|
||||
grid.querySelectorAll('.doclib-card-expanded').forEach(c => {
|
||||
if (c !== card) {
|
||||
unbindExpandedCardDismiss(c);
|
||||
c.classList.remove('doclib-card-expanded');
|
||||
const p = c.querySelector('.doclib-chat-preview');
|
||||
if (p) { p.style.display = 'none'; p.innerHTML = ''; }
|
||||
@@ -2357,11 +2573,18 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
});
|
||||
}
|
||||
if (card.classList.contains('doclib-card-expanded')) {
|
||||
unbindExpandedCardDismiss(card);
|
||||
card.classList.remove('doclib-card-expanded');
|
||||
preview.style.display = 'none'; preview.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
card.classList.add('doclib-card-expanded');
|
||||
bindExpandedCardDismiss(card, () => {
|
||||
unbindExpandedCardDismiss(card);
|
||||
card.classList.remove('doclib-card-expanded');
|
||||
preview.style.display = 'none';
|
||||
preview.innerHTML = '';
|
||||
});
|
||||
preview.style.display = 'block';
|
||||
preview.innerHTML = '<div style="opacity:0.4;font-size:11px;padding:8px 4px;">Loading…</div>';
|
||||
try {
|
||||
@@ -2466,6 +2689,7 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
'<div class="memory-item-title">' + arcIconSvg + _esc(s.name || 'Untitled') + '</div>' +
|
||||
'<div class="memory-item-meta" style="font-size:10px;opacity:0.4;margin-top:2px;">' + [model, _relTime(s.updated_at)].filter(Boolean).join(' \u00b7 ') + '</div>' +
|
||||
'</div>' +
|
||||
'<span class="doclib-card-chevron" aria-hidden="true"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>' +
|
||||
'<div class="memory-item-actions"><button class="memory-item-btn _arc-menu" title="Actions"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/></svg></button></div>' +
|
||||
'</div>' +
|
||||
'<div class="doclib-chat-preview" style="display:none;"></div>';
|
||||
@@ -2510,6 +2734,7 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
'<div class="memory-item-title">' + _arcDocIco + _esc(d.title || 'Untitled') + '</div>' +
|
||||
'<div class="memory-item-meta" style="font-size:10px;opacity:0.4;margin-top:2px;">' + ['Document', (d.language || 'text'), _relTime(d.updated_at)].filter(Boolean).join(' · ') + '</div>' +
|
||||
'</div>' +
|
||||
'<span class="doclib-card-chevron" aria-hidden="true"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>' +
|
||||
'<div class="memory-item-actions"><button class="memory-item-btn _arc-doc-menu" title="Actions"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/></svg></button></div>' +
|
||||
'</div>' +
|
||||
'<div class="doclib-chat-preview" style="display:none;"></div>';
|
||||
@@ -2547,6 +2772,7 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
'<div class="memory-item-title">' + _arcResIco + _esc(r.query || 'Research') + '</div>' +
|
||||
'<div class="memory-item-meta" style="font-size:10px;opacity:0.4;margin-top:2px;">' + ['Research', (r.source_count ? r.source_count + ' sources' : ''), _relTime(r.completed_at ? new Date(r.completed_at * 1000).toISOString() : '')].filter(Boolean).join(' · ') + '</div>' +
|
||||
'</div>' +
|
||||
'<span class="doclib-card-chevron" aria-hidden="true"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>' +
|
||||
'<div class="memory-item-actions"><button class="memory-item-btn _arc-res-menu" title="Actions"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/></svg></button></div>' +
|
||||
'</div>' +
|
||||
'<div class="doclib-chat-preview" style="display:none;"></div>';
|
||||
@@ -2583,8 +2809,8 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
el.innerHTML = '';
|
||||
const mk = (label, val, count) => {
|
||||
const c = document.createElement('button');
|
||||
c.className = 'memory-cat-chip' + (_arcTypeFilter === val ? ' active' : '');
|
||||
c.textContent = label + ' (' + count + ')';
|
||||
c.className = 'skills-summary-chip' + (_arcTypeFilter === val ? ' active' : '');
|
||||
_setLibraryCountChipContent(c, label, count);
|
||||
c.addEventListener('click', () => { _arcTypeFilter = _arcTypeFilter === val ? '' : val; _renderArcGrid(); _renderArcChips(); });
|
||||
el.appendChild(c);
|
||||
};
|
||||
@@ -2710,6 +2936,7 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
if (grid) {
|
||||
grid.querySelectorAll('.doclib-card-expanded').forEach(c => {
|
||||
if (c !== card) {
|
||||
unbindExpandedCardDismiss(c);
|
||||
c.classList.remove('doclib-card-expanded');
|
||||
const p = c.querySelector('.doclib-chat-preview');
|
||||
if (p) { p.style.display = 'none'; p.innerHTML = ''; }
|
||||
@@ -2717,12 +2944,19 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
});
|
||||
}
|
||||
if (isOpen) {
|
||||
unbindExpandedCardDismiss(card);
|
||||
card.classList.remove('doclib-card-expanded');
|
||||
preview.style.display = 'none';
|
||||
preview.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
card.classList.add('doclib-card-expanded');
|
||||
bindExpandedCardDismiss(card, () => {
|
||||
unbindExpandedCardDismiss(card);
|
||||
card.classList.remove('doclib-card-expanded');
|
||||
preview.style.display = 'none';
|
||||
preview.innerHTML = '';
|
||||
});
|
||||
preview.style.display = 'block';
|
||||
preview.innerHTML = '<div style="opacity:0.4;font-size:11px;padding:8px 4px;">Loading…</div>';
|
||||
let detail = item;
|
||||
@@ -2904,6 +3138,7 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
html += `<div class="memory-item-title"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;opacity:0.4;flex-shrink:0;"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>${_esc(r.query || 'Untitled Research')}</div>`;
|
||||
html += `<div class="memory-item-meta" style="font-size:10px;opacity:0.4;margin-top:2px;">${metaText}</div>`;
|
||||
html += `</div>`;
|
||||
html += `<span class="doclib-card-chevron" aria-hidden="true"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>`;
|
||||
if (!_researchSelectMode) html += `<div class="memory-item-actions"><button class="memory-item-btn doclib-research-delete" data-rid="${r.id}" title="Delete"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/></svg></button></div>`;
|
||||
html += `</div>`;
|
||||
html += `<div class="doclib-chat-preview" style="display:none;"></div>`;
|
||||
@@ -3353,10 +3588,11 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
{ label: _libraryArchivedView ? 'Restore' : 'Archive', icon: _libraryArchivedView ? 'restore' : 'archive', action: libraryBulkArchive },
|
||||
{ label: 'Clone', icon: 'clone', action: libraryBulkClone },
|
||||
{ label: 'Export', icon: 'open', action: libraryBulkExport },
|
||||
{ label: 'Delete', icon: 'delete', danger: true, action: libraryBulkDelete },
|
||||
], { onCancel: libraryExitSelectMode });
|
||||
});
|
||||
|
||||
document.getElementById('doclib-bulk-delete')?.addEventListener('click', libraryBulkDelete);
|
||||
|
||||
const bulkCancelBtn = document.getElementById('doclib-bulk-cancel');
|
||||
if (bulkCancelBtn) bulkCancelBtn.addEventListener('click', libraryExitSelectMode);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user