Changeset 0.28.3.1 (#188)
This commit is contained in:
@@ -785,8 +785,8 @@ function editAdminExtension(id) {
|
||||
${scriptEditor}
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end">
|
||||
<button class="btn-small" onclick="_closeExtEditForm('${id}')">Cancel</button>
|
||||
<button class="btn-small btn-primary" onclick="saveAdminExtension('${id}')">Save Changes</button>
|
||||
<button class="btn-small" data-action="_closeExtEditForm" data-args='${JSON.stringify([id])}'">Cancel</button>
|
||||
<button class="btn-small btn-primary" data-action="saveAdminExtension" data-args='${JSON.stringify([id])}'">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -888,34 +888,34 @@ async function saveAdminExtension(id) {
|
||||
}
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window._adminPersonaForm = _adminPersonaForm;
|
||||
window._initAdminListeners = _initAdminListeners;
|
||||
window.adminResetUserPassword = adminResetUserPassword;
|
||||
window.bulkSetUserModelVisibility = bulkSetUserModelVisibility;
|
||||
window.cycleModelVisibility = cycleModelVisibility;
|
||||
window.deleteAdminExtension = deleteAdminExtension;
|
||||
window.deleteAdminPersona = deleteAdminPersona;
|
||||
window.deleteGroup = deleteGroup;
|
||||
window.deleteTeam = deleteTeam;
|
||||
window.deleteUser = deleteUser;
|
||||
window.deleteUserPersona = deleteUserPersona;
|
||||
window.editAdminExtension = editAdminExtension;
|
||||
window.editAdminPersona = editAdminPersona;
|
||||
window.ensureAdminPersonaForm = ensureAdminPersonaForm;
|
||||
window.removeGroupMember = removeGroupMember;
|
||||
window.removeTeamMember = removeTeamMember;
|
||||
window.saveGrant = saveGrant;
|
||||
window.settingsDeleteTeamPersona = settingsDeleteTeamPersona;
|
||||
window.settingsRemoveTeamMember = settingsRemoveTeamMember;
|
||||
window.settingsUpdateTeamMember = settingsUpdateTeamMember;
|
||||
window.showApproveForm = showApproveForm;
|
||||
window.toggleAdminExtension = toggleAdminExtension;
|
||||
window.toggleAdminPersona = toggleAdminPersona;
|
||||
window.toggleTeamActive = toggleTeamActive;
|
||||
window.toggleUserActive = toggleUserActive;
|
||||
window.toggleUserModelVisibility = toggleUserModelVisibility;
|
||||
window.toggleUserRole = toggleUserRole;
|
||||
window.updateTeamMember = updateTeamMember;
|
||||
window._closeExtEditForm = _closeExtEditForm;
|
||||
window.saveAdminExtension = saveAdminExtension;
|
||||
sb.register('_adminPersonaForm', _adminPersonaForm);
|
||||
sb.register('_initAdminListeners', _initAdminListeners);
|
||||
sb.register('adminResetUserPassword', adminResetUserPassword);
|
||||
sb.register('bulkSetUserModelVisibility', bulkSetUserModelVisibility);
|
||||
sb.register('cycleModelVisibility', cycleModelVisibility);
|
||||
sb.register('deleteAdminExtension', deleteAdminExtension);
|
||||
sb.register('deleteAdminPersona', deleteAdminPersona);
|
||||
sb.register('deleteGroup', deleteGroup);
|
||||
sb.register('deleteTeam', deleteTeam);
|
||||
sb.register('deleteUser', deleteUser);
|
||||
sb.register('deleteUserPersona', deleteUserPersona);
|
||||
sb.register('editAdminExtension', editAdminExtension);
|
||||
sb.register('editAdminPersona', editAdminPersona);
|
||||
sb.register('ensureAdminPersonaForm', ensureAdminPersonaForm);
|
||||
sb.register('removeGroupMember', removeGroupMember);
|
||||
sb.register('removeTeamMember', removeTeamMember);
|
||||
sb.register('saveGrant', saveGrant);
|
||||
sb.register('settingsDeleteTeamPersona', settingsDeleteTeamPersona);
|
||||
sb.register('settingsRemoveTeamMember', settingsRemoveTeamMember);
|
||||
sb.register('settingsUpdateTeamMember', settingsUpdateTeamMember);
|
||||
sb.register('showApproveForm', showApproveForm);
|
||||
sb.register('toggleAdminExtension', toggleAdminExtension);
|
||||
sb.register('toggleAdminPersona', toggleAdminPersona);
|
||||
sb.register('toggleTeamActive', toggleTeamActive);
|
||||
sb.register('toggleUserActive', toggleUserActive);
|
||||
sb.register('toggleUserModelVisibility', toggleUserModelVisibility);
|
||||
sb.register('toggleUserRole', toggleUserRole);
|
||||
sb.register('updateTeamMember', updateTeamMember);
|
||||
sb.register('_closeExtEditForm', _closeExtEditForm);
|
||||
sb.register('saveAdminExtension', saveAdminExtension);
|
||||
})();
|
||||
|
||||
@@ -198,5 +198,5 @@ async function _loadSurfaceList() {
|
||||
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window._loadAdminSurfaces = _loadAdminSurfaces;
|
||||
sb.register('_loadAdminSurfaces', _loadAdminSurfaces);
|
||||
})();
|
||||
|
||||
@@ -993,5 +993,5 @@ const API = {
|
||||
}
|
||||
};
|
||||
|
||||
window.API = API;
|
||||
sb.ns('API', API);
|
||||
})();
|
||||
|
||||
@@ -137,6 +137,6 @@ async function fetchModels() {
|
||||
} catch (e) { console.warn('Model fetch failed:', e.message); }
|
||||
}
|
||||
|
||||
window.App = App;
|
||||
window.fetchModels = fetchModels;
|
||||
sb.ns('App', App);
|
||||
sb.register('fetchModels', fetchModels);
|
||||
})();
|
||||
|
||||
@@ -30,17 +30,17 @@ async function init() {
|
||||
`<strong>Network proxy blocked this request</strong><br>` +
|
||||
`Proxy response: "${API._esc(e.proxyTitle)}"<br>` +
|
||||
`<span class="splash-error-hint">Ask your network admin to whitelist this domain. ` +
|
||||
`<a href="#" onclick="openDebugModal();return false">Run diagnostics</a></span>`;
|
||||
`<a href="#" data-action="openDebugModal" ">Run diagnostics</a></span>`;
|
||||
} else if (e.name === 'TimeoutError' || e.name === 'AbortError') {
|
||||
splashErr.innerHTML =
|
||||
`<strong>Connection timed out</strong><br>` +
|
||||
`<span class="splash-error-hint">Server may be starting up, or a proxy is blocking the connection. ` +
|
||||
`<a href="#" onclick="openDebugModal();return false">Run diagnostics</a></span>`;
|
||||
`<a href="#" data-action="openDebugModal" ">Run diagnostics</a></span>`;
|
||||
} else {
|
||||
splashErr.innerHTML =
|
||||
`<strong>Cannot reach server</strong><br>` +
|
||||
`<span class="splash-error-hint">${API._esc(e.message)}. ` +
|
||||
`<a href="#" onclick="openDebugModal();return false">Run diagnostics</a></span>`;
|
||||
`<a href="#" data-action="openDebugModal" ">Run diagnostics</a></span>`;
|
||||
}
|
||||
showSplash(null);
|
||||
return;
|
||||
@@ -582,10 +582,10 @@ async function initBanners() {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.handleLogin = handleLogin;
|
||||
window.handleRegister = handleRegister;
|
||||
window.handleLogout = handleLogout;
|
||||
window.switchAuthTab = switchAuthTab;
|
||||
window.startApp = startApp;
|
||||
window.initBanners = initBanners;
|
||||
sb.register('handleLogin', handleLogin);
|
||||
sb.register('handleRegister', handleRegister);
|
||||
sb.register('handleLogout', handleLogout);
|
||||
sb.register('switchAuthTab', switchAuthTab);
|
||||
sb.register('startApp', startApp);
|
||||
sb.register('initBanners', initBanners);
|
||||
})();
|
||||
|
||||
@@ -71,11 +71,11 @@ const ChannelModels = {
|
||||
const html = this._roster.map(m => `
|
||||
<span class="ch-model-pill${m.is_default ? ' ch-model-default' : ''}"
|
||||
data-model-id="${esc(m.id)}" title="${esc(m.model_id)}">
|
||||
<span class="ch-model-pill-name" onclick="ChannelModels.setDefault('${esc(m.id)}')">${esc(m.display_name)}</span>
|
||||
<button class="ch-model-pill-remove" onclick="ChannelModels.remove('${esc(m.id)}')" title="Remove model">✕</button>
|
||||
<span class="ch-model-pill-name" data-action="ChannelModels.setDefault" data-args='${JSON.stringify([esc(m.id)])}'">${esc(m.display_name)}</span>
|
||||
<button class="ch-model-pill-remove" data-action="ChannelModels.remove" data-args='${JSON.stringify([esc(m.id)])}'" title="Remove model">✕</button>
|
||||
</span>
|
||||
`).join('') + `
|
||||
<button class="ch-model-add-btn" onclick="ChannelModels.showAddDialog()" title="Add model to channel">+ Add</button>
|
||||
<button class="ch-model-add-btn" data-action="ChannelModels.showAddDialog" " title="Add model to channel">+ Add</button>
|
||||
`;
|
||||
container.innerHTML = html;
|
||||
},
|
||||
@@ -132,8 +132,8 @@ const ChannelModels = {
|
||||
<input type="text" id="chModelAddName" placeholder="e.g. Claude-3-Opus" maxlength="50">
|
||||
</label>
|
||||
<div class="ch-model-add-actions">
|
||||
<button class="btn-secondary" onclick="ChannelModels.closeAddDialog()">Cancel</button>
|
||||
<button class="btn-primary" onclick="ChannelModels.submitAdd()">Add</button>
|
||||
<button class="btn-secondary" data-action="ChannelModels.closeAddDialog" ">Cancel</button>
|
||||
<button class="btn-primary" data-action="ChannelModels.submitAdd" ">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -436,5 +436,5 @@ function _shortName(fullName) {
|
||||
}
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.ChannelModels = ChannelModels;
|
||||
sb.ns('ChannelModels', ChannelModels);
|
||||
})();
|
||||
|
||||
@@ -152,5 +152,5 @@ const ChatPane = {
|
||||
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.ChatPane = ChatPane;
|
||||
sb.ns('ChatPane', ChatPane);
|
||||
})();
|
||||
|
||||
@@ -1258,25 +1258,25 @@ async function rejectWorkflowStage() {
|
||||
}
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.ChatInput = ChatInput;
|
||||
window.loadChats = loadChats;
|
||||
window.selectChat = selectChat;
|
||||
window.newChat = newChat;
|
||||
window.newGroupChat = newGroupChat;
|
||||
window.deleteChat = deleteChat;
|
||||
window.startRenameChat = startRenameChat;
|
||||
window.sendMessage = sendMessage;
|
||||
window.stopGeneration = stopGeneration;
|
||||
window.reloadActivePath = reloadActivePath;
|
||||
window.regenerateMessage = regenerateMessage;
|
||||
window.regenerate = regenerate;
|
||||
window.editMessage = editMessage;
|
||||
window.submitEdit = submitEdit;
|
||||
window.cancelEdit = cancelEdit;
|
||||
window.switchSibling = switchSibling;
|
||||
window.advanceWorkflowStage = advanceWorkflowStage;
|
||||
window.rejectWorkflowStage = rejectWorkflowStage;
|
||||
window._initChatListeners = _initChatListeners;
|
||||
window.summarizeAndContinue = summarizeAndContinue;
|
||||
window.toggleChannelAutoCompact = toggleChannelAutoCompact;
|
||||
sb.ns('ChatInput', ChatInput);
|
||||
sb.register('loadChats', loadChats);
|
||||
sb.register('selectChat', selectChat);
|
||||
sb.register('newChat', newChat);
|
||||
sb.register('newGroupChat', newGroupChat);
|
||||
sb.register('deleteChat', deleteChat);
|
||||
sb.register('startRenameChat', startRenameChat);
|
||||
sb.register('sendMessage', sendMessage);
|
||||
sb.register('stopGeneration', stopGeneration);
|
||||
sb.register('reloadActivePath', reloadActivePath);
|
||||
sb.register('regenerateMessage', regenerateMessage);
|
||||
sb.register('regenerate', regenerate);
|
||||
sb.register('editMessage', editMessage);
|
||||
sb.register('submitEdit', submitEdit);
|
||||
sb.register('cancelEdit', cancelEdit);
|
||||
sb.register('switchSibling', switchSibling);
|
||||
sb.register('advanceWorkflowStage', advanceWorkflowStage);
|
||||
sb.register('rejectWorkflowStage', rejectWorkflowStage);
|
||||
sb.register('_initChatListeners', _initChatListeners);
|
||||
sb.register('summarizeAndContinue', summarizeAndContinue);
|
||||
sb.register('toggleChannelAutoCompact', toggleChannelAutoCompact);
|
||||
})();
|
||||
|
||||
@@ -331,5 +331,5 @@ function _ceDetectLanguage(path) {
|
||||
}
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.CodeEditor = CodeEditor;
|
||||
sb.ns('CodeEditor', CodeEditor);
|
||||
})();
|
||||
|
||||
@@ -668,12 +668,12 @@ async function purgeCache() {
|
||||
DebugLog.init();
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.DebugLog = DebugLog;
|
||||
window.openDebugModal = openDebugModal;
|
||||
window.switchDebugTab = switchDebugTab;
|
||||
window.clearDebugLog = clearDebugLog;
|
||||
window.copyDebugLog = copyDebugLog;
|
||||
window.exportDebugLog = exportDebugLog;
|
||||
window.runDebugDiagnostics = runDebugDiagnostics;
|
||||
window.purgeCache = purgeCache;
|
||||
sb.ns('DebugLog', DebugLog);
|
||||
sb.register('openDebugModal', openDebugModal);
|
||||
sb.register('switchDebugTab', switchDebugTab);
|
||||
sb.register('clearDebugLog', clearDebugLog);
|
||||
sb.register('copyDebugLog', copyDebugLog);
|
||||
sb.register('exportDebugLog', exportDebugLog);
|
||||
sb.register('runDebugDiagnostics', runDebugDiagnostics);
|
||||
sb.register('purgeCache', purgeCache);
|
||||
})();
|
||||
|
||||
@@ -99,5 +99,5 @@ function _clientPos(e, isHoriz) {
|
||||
}
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.initDragResize = initDragResize;
|
||||
sb.register('initDragResize', initDragResize);
|
||||
})();
|
||||
|
||||
@@ -331,5 +331,5 @@ const Events = {
|
||||
}
|
||||
};
|
||||
|
||||
window.Events = Events;
|
||||
sb.ns('Events', Events);
|
||||
})();
|
||||
|
||||
@@ -488,5 +488,5 @@ const Extensions = {
|
||||
};
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.Extensions = Extensions;
|
||||
sb.ns('Extensions', Extensions);
|
||||
})();
|
||||
|
||||
@@ -268,5 +268,5 @@ function _ftFileIcon(name) {
|
||||
}
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.FileTree = FileTree;
|
||||
sb.ns('FileTree', FileTree);
|
||||
})();
|
||||
|
||||
@@ -350,7 +350,7 @@ function _renderStrip() {
|
||||
<span class="att-name" title="${esc(a.filename)}">${esc(name)}</span>
|
||||
<span class="att-meta">${esc(size)}${label ? ' · ' + label : ''}</span>
|
||||
</span>
|
||||
<button class="att-remove" onclick="unstageFile('${a.localId}')" title="Remove">✕</button>
|
||||
<button class="att-remove" data-action="unstageFile" data-args='${JSON.stringify([a.localId])}'" title="Remove">✕</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
@@ -423,7 +423,7 @@ function renderMessageFiles(msgId) {
|
||||
const visionHint = !hasVision
|
||||
? '<span class="att-vision-hint" title="Current model does not support images">👁️🗨️</span>'
|
||||
: '';
|
||||
return `<div class="msg-att msg-att-image" onclick="openLightbox('${esc(att.id)}')">
|
||||
return `<div class="msg-att msg-att-image" data-action="openLightbox" data-args='${JSON.stringify([esc(att.id)])}'">
|
||||
<img class="msg-att-img" data-att-id="${esc(att.id)}" alt="${esc(att.filename)}"
|
||||
style="max-width:${w}px" loading="lazy">
|
||||
${visionHint}
|
||||
@@ -433,7 +433,7 @@ function renderMessageFiles(msgId) {
|
||||
// Document chip
|
||||
const icon = _docIcon(att.content_type);
|
||||
const size = formatFileSize(att.size_bytes);
|
||||
return `<a class="msg-att msg-att-doc" href="#" onclick="downloadFile('${esc(att.id)}','${esc(att.filename)}');return false">
|
||||
return `<a class="msg-att msg-att-doc" href="#" data-action="downloadFile" data-args='${JSON.stringify([esc(att.id), esc(att.filename)])}' data-prevent-default">
|
||||
<span class="att-doc-icon">${icon}</span>
|
||||
<span class="att-doc-info">
|
||||
<span class="att-doc-name">${esc(att.filename)}</span>
|
||||
@@ -839,22 +839,22 @@ function _hasDragFiles(e) {
|
||||
}
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window._initFileListeners = _initFileListeners;
|
||||
window._pollTimers = _pollTimers;
|
||||
window._startPolling = _startPolling;
|
||||
window._stopPolling = _stopPolling;
|
||||
window.clearStaged = clearStaged;
|
||||
window.closeLightbox = closeLightbox;
|
||||
window.consumeStaged = consumeStaged;
|
||||
window.getStagedFileIds = getStagedFileIds;
|
||||
window.hasStagedFiles = hasStagedFiles;
|
||||
window.initFiles = initFiles;
|
||||
window.isSendBlocked = isSendBlocked;
|
||||
window.loadAdminStorage = loadAdminStorage;
|
||||
window.loadAuthImages = loadAuthImages;
|
||||
window.loadChannelFiles = loadChannelFiles;
|
||||
window.renderMessageFiles = renderMessageFiles;
|
||||
window.unstageFile = unstageFile;
|
||||
window.openLightbox = openLightbox;
|
||||
window.downloadFile = downloadFile;
|
||||
sb.register('_initFileListeners', _initFileListeners);
|
||||
sb.register('_pollTimers', _pollTimers);
|
||||
sb.register('_startPolling', _startPolling);
|
||||
sb.register('_stopPolling', _stopPolling);
|
||||
sb.register('clearStaged', clearStaged);
|
||||
sb.register('closeLightbox', closeLightbox);
|
||||
sb.register('consumeStaged', consumeStaged);
|
||||
sb.register('getStagedFileIds', getStagedFileIds);
|
||||
sb.register('hasStagedFiles', hasStagedFiles);
|
||||
sb.register('initFiles', initFiles);
|
||||
sb.register('isSendBlocked', isSendBlocked);
|
||||
sb.register('loadAdminStorage', loadAdminStorage);
|
||||
sb.register('loadAuthImages', loadAuthImages);
|
||||
sb.register('loadChannelFiles', loadChannelFiles);
|
||||
sb.register('renderMessageFiles', renderMessageFiles);
|
||||
sb.register('unstageFile', unstageFile);
|
||||
sb.register('openLightbox', openLightbox);
|
||||
sb.register('downloadFile', downloadFile);
|
||||
})();
|
||||
|
||||
@@ -369,5 +369,5 @@ function _debounce(fn, ms) {
|
||||
}
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.MemoryUI = MemoryUI;
|
||||
sb.ns('MemoryUI', MemoryUI);
|
||||
})();
|
||||
|
||||
@@ -174,5 +174,5 @@ const ModelSelector = {
|
||||
};
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.ModelSelector = ModelSelector;
|
||||
sb.ns('ModelSelector', ModelSelector);
|
||||
})();
|
||||
|
||||
@@ -440,5 +440,5 @@ const NoteEditor = {
|
||||
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.NoteEditor = NoteEditor;
|
||||
sb.ns('NoteEditor', NoteEditor);
|
||||
})();
|
||||
|
||||
@@ -495,10 +495,10 @@ function invalidateNoteGraph() {
|
||||
}
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.openNoteGraph = openNoteGraph;
|
||||
window.closeNoteGraph = closeNoteGraph;
|
||||
window._render = _render;
|
||||
window._graphResetZoom = _graphResetZoom;
|
||||
window._graphToggleOrphans = _graphToggleOrphans;
|
||||
window.invalidateNoteGraph = invalidateNoteGraph;
|
||||
sb.register('openNoteGraph', openNoteGraph);
|
||||
sb.register('closeNoteGraph', closeNoteGraph);
|
||||
sb.register('_render', _render);
|
||||
sb.register('_graphResetZoom', _graphResetZoom);
|
||||
sb.register('_graphToggleOrphans', _graphToggleOrphans);
|
||||
sb.register('invalidateNoteGraph', invalidateNoteGraph);
|
||||
})();
|
||||
|
||||
@@ -66,9 +66,9 @@ function _noteListItem(note, isSearch) {
|
||||
<div class="note-item ${_selectedNoteIds.has(note.id) ? 'selected' : ''}" data-note-id="${note.id}">
|
||||
<div class="note-select-col" style="display:${_notesSelectMode ? '' : 'none'}">
|
||||
<input type="checkbox" class="note-checkbox" data-id="${note.id}" ${checked}
|
||||
onclick="event.stopPropagation(); _toggleNoteSelect('${note.id}', this.checked)">
|
||||
data-action="_toggleNoteSelectCb" data-args='${JSON.stringify([note.id])}' data-pass-el">
|
||||
</div>
|
||||
<div class="note-content-col" onclick="${_notesSelectMode ? `_toggleNoteSelect('${note.id}')` : `openNoteEditor('${note.id}')`}">
|
||||
<div class="note-content-col" data-action="_noteItemClick" data-args='${JSON.stringify([note.id])}'">
|
||||
<div class="note-item-header">
|
||||
<span class="note-item-title">${esc(note.title)}</span>
|
||||
<span class="note-item-time">${time}</span>
|
||||
@@ -422,7 +422,7 @@ async function _loadBacklinks(noteId) {
|
||||
countEl.textContent = links.length;
|
||||
container.style.display = links.length > 0 ? '' : 'none';
|
||||
listEl.innerHTML = links.map(n => `
|
||||
<div class="note-backlink-item" onclick="openNoteEditor('${n.id}')">
|
||||
<div class="note-backlink-item" data-action="openNoteEditor" data-args='${JSON.stringify([n.id])}'">
|
||||
<span class="note-backlink-title">${esc(n.title)}</span>
|
||||
<span class="note-backlink-folder text-muted">${esc(n.folder_path)}</span>
|
||||
</div>
|
||||
@@ -454,12 +454,12 @@ function _renderWikilinksInReadMode(containerEl) {
|
||||
const tid = `transclusion-${++transclusionId}`;
|
||||
return `<div class="transclusion-embed" id="${tid}" data-title="${esc(titleTrimmed)}">
|
||||
<div class="transclusion-header">
|
||||
<span class="wikilink-chip wikilink-transclusion" onclick="_navigateToLinkedNote('${esc(titleTrimmed)}')" title="Open: ${esc(titleTrimmed)}">↗ ${esc(displayText)}</span>
|
||||
<span class="wikilink-chip wikilink-transclusion" data-action="_navigateToLinkedNote" data-args='${JSON.stringify([esc(titleTrimmed)])}'" title="Open: ${esc(titleTrimmed)}">↗ ${esc(displayText)}</span>
|
||||
</div>
|
||||
<div class="transclusion-content"><span class="text-muted">Loading…</span></div>
|
||||
</div>`;
|
||||
}
|
||||
return `<span class="wikilink-chip" onclick="_navigateToLinkedNote('${esc(titleTrimmed)}')" title="Link: ${esc(titleTrimmed)}">${esc(displayText)}</span>`;
|
||||
return `<span class="wikilink-chip" data-action="_navigateToLinkedNote" data-args='${JSON.stringify([esc(titleTrimmed)])}'" title="Link: ${esc(titleTrimmed)}">${esc(displayText)}</span>`;
|
||||
}
|
||||
);
|
||||
containerEl.innerHTML = rendered;
|
||||
@@ -818,7 +818,7 @@ function _registerNotesPanel() {
|
||||
</div>
|
||||
<!-- Backlinks -->
|
||||
<div id="noteBacklinks" class="note-backlinks" style="display:none;">
|
||||
<div class="note-backlinks-header" onclick="toggleBacklinks()">
|
||||
<div class="note-backlinks-header" data-action="toggleBacklinks">
|
||||
Backlinks <span id="noteBacklinksCount" class="badge">0</span>
|
||||
</div>
|
||||
<div id="noteBacklinksList" class="note-backlinks-list"></div>
|
||||
@@ -832,8 +832,8 @@ function _registerNotesPanel() {
|
||||
<span id="graphNodeCount">0</span> notes · <span id="graphEdgeCount">0</span> links
|
||||
</span>
|
||||
<div style="flex:1"></div>
|
||||
<button class="btn-small" onclick="_graphToggleOrphans()" title="Toggle orphan nodes">Orphans</button>
|
||||
<button class="btn-small" onclick="_graphResetZoom()" title="Reset zoom">Reset</button>
|
||||
<button class="btn-small" data-action="_graphToggleOrphans" title="Toggle orphan nodes">Orphans</button>
|
||||
<button class="btn-small" data-action="_graphResetZoom" title="Reset zoom">Reset</button>
|
||||
</div>
|
||||
<canvas id="noteGraphCanvas" style="width:100%;display:block;"></canvas>
|
||||
</div>`;
|
||||
@@ -944,17 +944,33 @@ function _initNotesListeners() {
|
||||
// All wiring moved into _registerNotesPanel().
|
||||
}
|
||||
|
||||
// ── Wrappers for compound onclick expressions ──
|
||||
// Checkbox toggle: reads .checked from the clicked element
|
||||
function _toggleNoteSelectCb(el, noteId) {
|
||||
_toggleNoteSelect(noteId, el.checked);
|
||||
}
|
||||
// Note list item click: mode-dependent dispatch
|
||||
function _noteItemClick(noteId) {
|
||||
if (_notesSelectMode) {
|
||||
_toggleNoteSelect(noteId);
|
||||
} else {
|
||||
openNoteEditor(noteId);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window._currentNote = _currentNote;
|
||||
window._editingNoteId = _editingNoteId;
|
||||
window._initNotesListeners = _initNotesListeners;
|
||||
window._registerNotesPanel = _registerNotesPanel;
|
||||
window.deleteNote = deleteNote;
|
||||
window.openNoteEditor = openNoteEditor;
|
||||
window.openNotes = openNotes;
|
||||
window.saveMessageToNote = saveMessageToNote;
|
||||
window.saveNote = saveNote;
|
||||
window._toggleNoteSelect = _toggleNoteSelect;
|
||||
window._navigateToLinkedNote = _navigateToLinkedNote;
|
||||
window.toggleBacklinks = toggleBacklinks;
|
||||
sb.register('_currentNote', _currentNote);
|
||||
sb.register('_editingNoteId', _editingNoteId);
|
||||
sb.register('_initNotesListeners', _initNotesListeners);
|
||||
sb.register('_registerNotesPanel', _registerNotesPanel);
|
||||
sb.register('deleteNote', deleteNote);
|
||||
sb.register('openNoteEditor', openNoteEditor);
|
||||
sb.register('openNotes', openNotes);
|
||||
sb.register('saveMessageToNote', saveMessageToNote);
|
||||
sb.register('saveNote', saveNote);
|
||||
sb.register('_toggleNoteSelect', _toggleNoteSelect);
|
||||
sb.register('_navigateToLinkedNote', _navigateToLinkedNote);
|
||||
sb.register('toggleBacklinks', toggleBacklinks);
|
||||
sb.register('_toggleNoteSelectCb', _toggleNoteSelectCb);
|
||||
sb.register('_noteItemClick', _noteItemClick);
|
||||
})();
|
||||
|
||||
@@ -168,5 +168,5 @@ NotifPrefs._getAdminSmtp = function() {
|
||||
};
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.NotifPrefs = NotifPrefs;
|
||||
sb.ns('NotifPrefs', NotifPrefs);
|
||||
})();
|
||||
|
||||
@@ -120,7 +120,7 @@ const Notifications = {
|
||||
|
||||
let html = `<div class="notif-dd-header">
|
||||
<span>Notifications</span>
|
||||
<button class="notif-mark-all" onclick="Notifications.markAllRead()">Mark all ✓</button>
|
||||
<button class="notif-mark-all" data-action="Notifications.markAllRead">Mark all ✓</button>
|
||||
</div><div class="notif-dd-list">`;
|
||||
|
||||
for (const n of items.slice(0, 10)) {
|
||||
@@ -128,7 +128,7 @@ const Notifications = {
|
||||
const dot = unread ? '●' : '○';
|
||||
const cls = unread ? 'notif-item notif-unread' : 'notif-item';
|
||||
const ago = _timeAgo(n.created_at);
|
||||
html += `<div class="${cls}" data-id="${n.id}" data-resource-type="${n.resource_type || ''}" data-resource-id="${n.resource_id || ''}" onclick="Notifications._onItemClick(this)">
|
||||
html += `<div class="${cls}" data-id="${n.id}" data-resource-type="${n.resource_type || ''}" data-resource-id="${n.resource_id || ''}" data-action="Notifications._onItemClick" data-pass-el>
|
||||
<span class="notif-dot">${dot}</span>
|
||||
<div class="notif-body">
|
||||
<div class="notif-title">${esc(n.title)}</div>
|
||||
@@ -140,7 +140,7 @@ const Notifications = {
|
||||
|
||||
html += `</div>`;
|
||||
html += `<div class="notif-dd-footer">
|
||||
<button class="notif-view-all" onclick="Notifications.openPanel()">View all →</button>
|
||||
<button class="notif-view-all" data-action="Notifications.openPanel">View all →</button>
|
||||
</div>`;
|
||||
|
||||
return html;
|
||||
@@ -227,12 +227,12 @@ const Notifications = {
|
||||
el.style.display = 'none';
|
||||
el.innerHTML = `<div class="notif-panel" id="notifPanelInner">
|
||||
<div class="notif-panel-toolbar">
|
||||
<button class="btn-small" onclick="Notifications.markAllRead()">Mark all read</button>
|
||||
<button class="btn-small btn-danger" onclick="Notifications.clearAll()">Clear all</button>
|
||||
<button class="btn-small" data-action="Notifications.markAllRead">Mark all read</button>
|
||||
<button class="btn-small btn-danger" data-action="Notifications.clearAll">Clear all</button>
|
||||
</div>
|
||||
<div class="notif-panel-list" id="notifPanelList"></div>
|
||||
<div class="notif-panel-more" id="notifPanelMore" style="display:none">
|
||||
<button class="btn-small" onclick="Notifications.loadMore()">Load more</button>
|
||||
<button class="btn-small" data-action="Notifications.loadMore">Load more</button>
|
||||
</div>
|
||||
</div>`;
|
||||
body.appendChild(el);
|
||||
@@ -278,7 +278,7 @@ const Notifications = {
|
||||
const cls = unread ? 'notif-panel-item notif-unread' : 'notif-panel-item';
|
||||
const dot = unread ? '●' : '○';
|
||||
const ago = _timeAgo(n.created_at);
|
||||
html += `<div class="${cls}" data-id="${n.id}" data-resource-type="${n.resource_type || ''}" data-resource-id="${n.resource_id || ''}" onclick="Notifications._onPanelItemClick(this)">
|
||||
html += `<div class="${cls}" data-id="${n.id}" data-resource-type="${n.resource_type || ''}" data-resource-id="${n.resource_id || ''}" data-action="Notifications._onPanelItemClick" data-pass-el>
|
||||
<div class="notif-panel-row">
|
||||
<span class="notif-dot">${dot}</span>
|
||||
<div class="notif-body">
|
||||
@@ -286,7 +286,7 @@ const Notifications = {
|
||||
${n.body ? `<div class="notif-detail">${esc(n.body)}</div>` : ''}
|
||||
<div class="notif-time">${ago}</div>
|
||||
</div>
|
||||
<button class="notif-delete" onclick="event.stopPropagation(); Notifications._deleteItem('${n.id}')" title="Delete">✕</button>
|
||||
<button class="notif-delete" data-action="Notifications._deleteItem" data-args='${JSON.stringify([n.id])}'" title="Delete">✕</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
@@ -430,5 +430,5 @@ function _timeAgo(isoStr) {
|
||||
}
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.Notifications = Notifications;
|
||||
sb.ns('Notifications', Notifications);
|
||||
})();
|
||||
|
||||
@@ -347,6 +347,6 @@ async function _api(method, path, body) {
|
||||
}
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.Pages = Pages;
|
||||
window._val = _val; // used by pages-splash.js
|
||||
sb.ns('Pages', Pages);
|
||||
sb.register('_val', _val); // used by pages-splash.js
|
||||
})();
|
||||
|
||||
@@ -588,5 +588,5 @@ PaneContainer.registerPreset('split', {
|
||||
});
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.PaneContainer = PaneContainer;
|
||||
sb.ns('PaneContainer', PaneContainer);
|
||||
})();
|
||||
|
||||
@@ -448,10 +448,10 @@ function switchSidePanelTab(tab) {
|
||||
}
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.PanelRegistry = PanelRegistry;
|
||||
window.toggleSidePanelFullscreen = toggleSidePanelFullscreen;
|
||||
window._initWorkspaceResize = _initWorkspaceResize;
|
||||
window._initPanelSwipe = _initPanelSwipe;
|
||||
window._initPanelResponsive = _initPanelResponsive;
|
||||
window._initPanelOverlay = _initPanelOverlay;
|
||||
sb.ns('PanelRegistry', PanelRegistry);
|
||||
sb.register('toggleSidePanelFullscreen', toggleSidePanelFullscreen);
|
||||
sb.register('_initWorkspaceResize', _initWorkspaceResize);
|
||||
sb.register('_initPanelSwipe', _initPanelSwipe);
|
||||
sb.register('_initPanelResponsive', _initPanelResponsive);
|
||||
sb.register('_initPanelOverlay', _initPanelOverlay);
|
||||
})();
|
||||
|
||||
@@ -182,7 +182,7 @@ async function savePersonaKBs(personaId, kbPicker, apiPrefix) {
|
||||
}
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.renderPersonaKBPicker = renderPersonaKBPicker;
|
||||
window.loadPersonaKBs = loadPersonaKBs;
|
||||
window.savePersonaKBs = savePersonaKBs;
|
||||
sb.register('renderPersonaKBPicker', renderPersonaKBPicker);
|
||||
sb.register('loadPersonaKBs', loadPersonaKBs);
|
||||
sb.register('savePersonaKBs', savePersonaKBs);
|
||||
})();
|
||||
|
||||
@@ -267,18 +267,18 @@ function showChatContextMenu(e, chatId) {
|
||||
const order = getProjectChannelOrder(chat.projectId);
|
||||
const idx = order.indexOf(chatId);
|
||||
if (idx > 0) {
|
||||
items += `<button class="ctx-item" onclick="_moveChatInProject('${chat.projectId}','${chatId}',-1);dismissChatContextMenu()">
|
||||
items += `<button class="ctx-item" data-action="_moveChatInProject" data-args='${JSON.stringify([chat.projectId, chatId, -1])}'>
|
||||
<span class="ctx-icon">↑</span> Move up
|
||||
</button>`;
|
||||
}
|
||||
if (idx >= 0 && idx < order.length - 1) {
|
||||
items += `<button class="ctx-item" onclick="_moveChatInProject('${chat.projectId}','${chatId}',1);dismissChatContextMenu()">
|
||||
items += `<button class="ctx-item" data-action="_moveChatInProject" data-args='${JSON.stringify([chat.projectId, chatId, 1])}'>
|
||||
<span class="ctx-icon">↓</span> Move down
|
||||
</button>`;
|
||||
}
|
||||
if (idx >= 0) items += '<div class="ctx-divider"></div>';
|
||||
|
||||
items += `<button class="ctx-item" onclick="moveChatToProject('${chatId}', null);dismissChatContextMenu()">
|
||||
items += `<button class="ctx-item" data-action="moveChatToProject" data-args='${JSON.stringify([chatId, null])}'>
|
||||
<span class="ctx-icon">↩</span> Remove from project
|
||||
</button>`;
|
||||
items += '<div class="ctx-divider"></div>';
|
||||
@@ -287,7 +287,7 @@ function showChatContextMenu(e, chatId) {
|
||||
App.projects.forEach(p => {
|
||||
if (p.id === chat.projectId) return; // skip current
|
||||
const dot = p.color ? `<span class="project-dot" style="background:${esc(p.color)}"></span>` : '';
|
||||
items += `<button class="ctx-item" onclick="moveChatToProject('${chatId}','${p.id}');dismissChatContextMenu()">
|
||||
items += `<button class="ctx-item" data-action="moveChatToProject" data-args='${JSON.stringify([chatId, p.id])}'>
|
||||
${dot}<span>${esc(p.name)}</span>
|
||||
</button>`;
|
||||
});
|
||||
@@ -297,13 +297,13 @@ function showChatContextMenu(e, chatId) {
|
||||
}
|
||||
|
||||
items += '<div class="ctx-divider"></div>';
|
||||
items += `<button class="ctx-item" onclick="createProjectAndMove('${chatId}');dismissChatContextMenu()">
|
||||
items += `<button class="ctx-item" data-action="createProjectAndMove" data-args='${JSON.stringify([chatId])}'>
|
||||
<span class="ctx-icon">+</span> New project…
|
||||
</button>`;
|
||||
|
||||
// Workspace override (v0.21.5)
|
||||
items += '<div class="ctx-divider"></div>';
|
||||
items += `<button class="ctx-item" onclick="showWorkspacePicker('${chatId}');dismissChatContextMenu()">
|
||||
items += `<button class="ctx-item" data-action="showWorkspacePicker" data-args='${JSON.stringify([chatId])}'>
|
||||
<span class="ctx-icon">📁</span> Set workspace…
|
||||
</button>`;
|
||||
|
||||
@@ -312,13 +312,13 @@ function showChatContextMenu(e, chatId) {
|
||||
if (folders.length > 0 || chat.folderId) {
|
||||
items += '<div class="ctx-divider"></div>';
|
||||
if (chat.folderId) {
|
||||
items += `<button class="ctx-item" onclick="moveChatToFolder('${chatId}',null);dismissChatContextMenu()">
|
||||
items += `<button class="ctx-item" data-action="moveChatToFolder" data-args='${JSON.stringify([chatId, null])}'>
|
||||
<span class="ctx-icon">↩</span> Remove from folder
|
||||
</button>`;
|
||||
}
|
||||
folders.forEach(f => {
|
||||
if (f.id === chat.folderId) return; // skip current
|
||||
items += `<button class="ctx-item" onclick="moveChatToFolder('${chatId}','${f.id}');dismissChatContextMenu()">
|
||||
items += `<button class="ctx-item" data-action="moveChatToFolder" data-args='${JSON.stringify([chatId, f.id])}'>
|
||||
<span class="ctx-icon">📂</span> ${esc(f.name)}
|
||||
</button>`;
|
||||
});
|
||||
@@ -331,6 +331,7 @@ function showChatContextMenu(e, chatId) {
|
||||
menu.style.top = Math.min(e.clientY, window.innerHeight - 200) + 'px';
|
||||
|
||||
document.body.appendChild(menu);
|
||||
_ctxDispatch(menu);
|
||||
_ctxMenu = menu;
|
||||
|
||||
// Dismiss on click outside
|
||||
@@ -341,6 +342,31 @@ function dismissChatContextMenu() {
|
||||
if (_ctxMenu) { _ctxMenu.remove(); _ctxMenu = null; }
|
||||
}
|
||||
|
||||
// ── Delegated action dispatcher for context menus and panel buttons ──
|
||||
// Buttons use data-action="fnName" data-args='[...]' instead of onclick.
|
||||
// One listener per menu/panel handles all clicks.
|
||||
function _ctxDispatch(container) {
|
||||
if (container._ctxBound) return; // idempotent — don't stack listeners
|
||||
container._ctxBound = true;
|
||||
container.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-action]');
|
||||
if (!btn) return;
|
||||
e.stopPropagation();
|
||||
const fn = sb.resolve(btn.dataset.action);
|
||||
if (typeof fn !== 'function') {
|
||||
console.warn('[ctx] Unknown action:', btn.dataset.action);
|
||||
return;
|
||||
}
|
||||
const args = JSON.parse(btn.dataset.args || '[]');
|
||||
fn(...args);
|
||||
if (btn.dataset.closeOverlay === 'true') {
|
||||
btn.closest('.modal-overlay')?.remove();
|
||||
} else {
|
||||
dismissChatContextMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function createProjectAndMove(chatId) {
|
||||
const name = prompt('New project name:');
|
||||
if (!name || !name.trim()) return;
|
||||
@@ -467,21 +493,21 @@ function showProjectMenu(e, projectId) {
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'project-ctx-menu';
|
||||
menu.innerHTML = `
|
||||
<button class="ctx-item" onclick="toggleActiveProject('${projectId}');dismissChatContextMenu()">
|
||||
<button class="ctx-item" data-action="toggleActiveProject" data-args='${JSON.stringify([projectId])}'>
|
||||
<span class="ctx-icon">${isActive ? '📍' : '📌'}</span> ${activeLabel}
|
||||
</button>
|
||||
<button class="ctx-item" onclick="openProjectPanel('${projectId}');dismissChatContextMenu()">
|
||||
<button class="ctx-item" data-action="openProjectPanel" data-args='${JSON.stringify([projectId])}'>
|
||||
<span class="ctx-icon">⚙</span> Project settings
|
||||
</button>
|
||||
<div class="ctx-divider"></div>
|
||||
<button class="ctx-item" onclick="renameProject('${projectId}');dismissChatContextMenu()">
|
||||
<button class="ctx-item" data-action="renameProject" data-args='${JSON.stringify([projectId])}'>
|
||||
<span class="ctx-icon">✏</span> Rename
|
||||
</button>
|
||||
<button class="ctx-item" onclick="setProjectColor('${projectId}');dismissChatContextMenu()">
|
||||
<button class="ctx-item" data-action="setProjectColor" data-args='${JSON.stringify([projectId])}'>
|
||||
<span class="ctx-icon">🎨</span> Color
|
||||
</button>
|
||||
<div class="ctx-divider"></div>
|
||||
<button class="ctx-item ctx-danger" onclick="deleteProject('${projectId}');dismissChatContextMenu()">
|
||||
<button class="ctx-item ctx-danger" data-action="deleteProject" data-args='${JSON.stringify([projectId])}'>
|
||||
<span class="ctx-icon">🗑</span> Delete project
|
||||
</button>`;
|
||||
|
||||
@@ -489,6 +515,7 @@ function showProjectMenu(e, projectId) {
|
||||
menu.style.top = Math.min(e.clientY, window.innerHeight - 150) + 'px';
|
||||
|
||||
document.body.appendChild(menu);
|
||||
_ctxDispatch(menu);
|
||||
_ctxMenu = menu;
|
||||
setTimeout(() => document.addEventListener('click', dismissChatContextMenu, { once: true }), 0);
|
||||
}
|
||||
@@ -684,8 +711,9 @@ async function _renderProjectKBs(projectId) {
|
||||
list.innerHTML = kbs.map(kb => `
|
||||
<div class="project-panel-item">
|
||||
<span class="project-panel-item-name">${esc(kb.name || kb.kb_id)}</span>
|
||||
<button class="project-panel-item-remove" onclick="_removeProjectKB('${projectId}','${kb.kb_id}')" title="Remove">✕</button>
|
||||
<button class="project-panel-item-remove" data-action="_removeProjectKB" data-args='${JSON.stringify([projectId, kb.kb_id])}' title="Remove">✕</button>
|
||||
</div>`).join('');
|
||||
_ctxDispatch(list);
|
||||
} catch (_) {
|
||||
list.innerHTML = '<div class="project-panel-empty">Failed to load KBs</div>';
|
||||
}
|
||||
@@ -733,12 +761,13 @@ async function _showKBPicker() {
|
||||
menu.style.left = Math.min(rect.left, window.innerWidth - 200) + 'px';
|
||||
menu.style.top = (rect.bottom + 4) + 'px';
|
||||
menu.innerHTML = available.map(kb => `
|
||||
<button class="ctx-item" onclick="_addProjectKB('${_projectPanelId}','${kb.id}');dismissChatContextMenu()">
|
||||
<button class="ctx-item" data-action="_addProjectKB" data-args='${JSON.stringify([_projectPanelId, kb.id])}'>
|
||||
${esc(kb.name)}
|
||||
</button>`).join('');
|
||||
|
||||
document.body.appendChild(menu);
|
||||
_ctxMenu = menu;
|
||||
_ctxDispatch(menu);
|
||||
_ctxMenu = menu;
|
||||
setTimeout(() => document.addEventListener('click', dismissChatContextMenu, { once: true }), 0);
|
||||
} catch (e) {
|
||||
UI.toast('Failed to load KBs', 'error');
|
||||
@@ -768,8 +797,9 @@ async function _renderProjectNotes(projectId) {
|
||||
list.innerHTML = notes.map(n => `
|
||||
<div class="project-panel-item">
|
||||
<span class="project-panel-item-name">${esc(n.title || n.note_id)}</span>
|
||||
<button class="project-panel-item-remove" onclick="_removeProjectNote('${projectId}','${n.note_id}')" title="Remove">✕</button>
|
||||
<button class="project-panel-item-remove" data-action="_removeProjectNote" data-args='${JSON.stringify([projectId, n.note_id])}' title="Remove">✕</button>
|
||||
</div>`).join('');
|
||||
_ctxDispatch(list);
|
||||
} catch (_) {
|
||||
list.innerHTML = '<div class="project-panel-empty">Failed to load notes</div>';
|
||||
}
|
||||
@@ -1575,28 +1605,28 @@ function showChannelContextMenu(e, channelId) {
|
||||
menu.className = 'project-ctx-menu';
|
||||
|
||||
let items = '';
|
||||
items += `<button class="ctx-item" onclick="openParticipantsPanel('${channelId}');dismissChatContextMenu()">
|
||||
items += `<button class="ctx-item" data-action="openParticipantsPanel" data-args='${JSON.stringify([channelId])}'>
|
||||
<span class="ctx-icon">👥</span> Members
|
||||
</button>`;
|
||||
items += `<button class="ctx-item" onclick="renameChannel('${channelId}');dismissChatContextMenu()">
|
||||
items += `<button class="ctx-item" data-action="renameChannel" data-args='${JSON.stringify([channelId])}'>
|
||||
<span class="ctx-icon">✏</span> Rename
|
||||
</button>`;
|
||||
if (ch.type === 'channel' || ch.type === 'group') {
|
||||
items += `<button class="ctx-item" onclick="editChannelTopic('${channelId}');dismissChatContextMenu()">
|
||||
items += `<button class="ctx-item" data-action="editChannelTopic" data-args='${JSON.stringify([channelId])}'>
|
||||
<span class="ctx-icon">💬</span> Set topic
|
||||
</button>`;
|
||||
items += `<button class="ctx-item" onclick="cycleChannelAiMode('${channelId}');dismissChatContextMenu()">
|
||||
items += `<button class="ctx-item" data-action="cycleChannelAiMode" data-args='${JSON.stringify([channelId])}'>
|
||||
<span class="ctx-icon">🤖</span> AI mode: ${esc(ch.aiMode || 'auto')}
|
||||
</button>`;
|
||||
}
|
||||
items += '<div class="ctx-divider"></div>';
|
||||
items += `<button class="ctx-item" onclick="archiveChannel('${channelId}');dismissChatContextMenu()">
|
||||
items += `<button class="ctx-item" data-action="archiveChannel" data-args='${JSON.stringify([channelId])}'>
|
||||
<span class="ctx-icon">📦</span> Archive
|
||||
</button>`;
|
||||
// Delete gated by retention mode (default=flexible allows delete)
|
||||
const retentionMode = App.policies?.channel_retention_mode || 'flexible';
|
||||
if (retentionMode === 'flexible') {
|
||||
items += `<button class="ctx-item ctx-danger" onclick="deleteChannel('${channelId}');dismissChatContextMenu()">
|
||||
items += `<button class="ctx-item ctx-danger" data-action="deleteChannel" data-args='${JSON.stringify([channelId])}'>
|
||||
<span class="ctx-icon">🗑</span> Delete
|
||||
</button>`;
|
||||
}
|
||||
@@ -1606,6 +1636,7 @@ function showChannelContextMenu(e, channelId) {
|
||||
menu.style.top = Math.min(e.clientY, window.innerHeight - 150) + 'px';
|
||||
|
||||
document.body.appendChild(menu);
|
||||
_ctxDispatch(menu);
|
||||
_ctxMenu = menu;
|
||||
setTimeout(() => document.addEventListener('click', dismissChatContextMenu, { once: true }), 0);
|
||||
}
|
||||
@@ -1725,17 +1756,18 @@ function showFolderMenu(e, folderId) {
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'project-ctx-menu';
|
||||
menu.innerHTML = `
|
||||
<button class="ctx-item" onclick="renameFolder('${folderId}');dismissChatContextMenu()">
|
||||
<button class="ctx-item" data-action="renameFolder" data-args='${JSON.stringify([folderId])}'>
|
||||
<span class="ctx-icon">✏</span> Rename
|
||||
</button>
|
||||
<div class="ctx-divider"></div>
|
||||
<button class="ctx-item ctx-danger" onclick="deleteFolder('${folderId}');dismissChatContextMenu()">
|
||||
<button class="ctx-item ctx-danger" data-action="deleteFolder" data-args='${JSON.stringify([folderId])}'>
|
||||
<span class="ctx-icon">🗑</span> Delete folder
|
||||
</button>`;
|
||||
|
||||
menu.style.left = Math.min(e.clientX, window.innerWidth - 160) + 'px';
|
||||
menu.style.top = Math.min(e.clientY, window.innerHeight - 100) + 'px';
|
||||
document.body.appendChild(menu);
|
||||
_ctxDispatch(menu);
|
||||
_ctxMenu = menu;
|
||||
setTimeout(() => document.addEventListener('click', dismissChatContextMenu, { once: true }), 0);
|
||||
}
|
||||
@@ -1920,9 +1952,10 @@ async function _refreshParticipantsList(channelId) {
|
||||
<span style="flex:1;font-size:13px;">${esc(name)}</span>
|
||||
<span style="font-size:11px;color:var(--text-3);">${esc(p.participant_type)} · ${esc(role)}</span>
|
||||
${role !== 'owner' ? `<button class="btn-small" style="color:var(--danger);"
|
||||
onclick="_removeParticipant('${channelId}','${p.id}')">✕</button>` : ''}
|
||||
data-action="_removeParticipant" data-args='${JSON.stringify([channelId, p.id])}'>✕</button>` : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
_ctxDispatch(el);
|
||||
} catch (e) {
|
||||
el.innerHTML = `<div class="empty-hint">Failed to load: ${esc(e.message)}</div>`;
|
||||
}
|
||||
@@ -1946,6 +1979,7 @@ async function _showAddParticipantSearch(channelId, pType) {
|
||||
|
||||
const input = overlay.querySelector('#_addPartSearch');
|
||||
const results = overlay.querySelector('#_addPartResults');
|
||||
_ctxDispatch(results); // delegate data-action clicks on search results
|
||||
input.focus();
|
||||
|
||||
let debounce = null;
|
||||
@@ -1972,7 +2006,7 @@ async function _showAddParticipantSearch(channelId, pType) {
|
||||
results.innerHTML = items.slice(0, 10).map(item =>
|
||||
`<div style="padding:6px 8px;cursor:pointer;border-radius:4px;font-size:13px;"
|
||||
class="hover-bg"
|
||||
onclick="_addParticipant('${channelId}','${pType}','${item.id}');this.closest('.modal-overlay').remove();">
|
||||
data-action="_addParticipant" data-args='${JSON.stringify([channelId, pType, item.id])}' data-close-overlay="true">
|
||||
${pType === 'persona' ? '⚡' : '👤'} ${esc(item.name)}
|
||||
</div>`
|
||||
).join('');
|
||||
@@ -2008,52 +2042,52 @@ async function _removeParticipant(channelId, recordId) {
|
||||
}
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window._registerProjectPanel = _registerProjectPanel;
|
||||
window.createProject = createProject;
|
||||
window.cycleChannelAiMode = cycleChannelAiMode;
|
||||
window.deleteChannel = deleteChannel;
|
||||
window.deleteFolder = deleteFolder;
|
||||
window.deleteProject = deleteProject;
|
||||
window.loadChannels = loadChannels;
|
||||
window.loadFolders = loadFolders;
|
||||
window.loadProjectChannelPositions = loadProjectChannelPositions;
|
||||
window.loadProjects = loadProjects;
|
||||
window.loadUsers = loadUsers;
|
||||
window.newChannelOrDM = newChannelOrDM;
|
||||
window.newFolder = newFolder;
|
||||
window.onChatDragEnd = onChatDragEnd;
|
||||
window.onChatDragStart = onChatDragStart;
|
||||
window.onChatSectionDrop = onChatSectionDrop;
|
||||
window.onFolderDrop = onFolderDrop;
|
||||
window.onProjectDragLeave = onProjectDragLeave;
|
||||
window.onProjectDragOver = onProjectDragOver;
|
||||
window.onProjectDrop = onProjectDrop;
|
||||
window.onUnfiledDrop = onUnfiledDrop;
|
||||
window.openParticipantsPanel = openParticipantsPanel;
|
||||
window.openProjectPanel = openProjectPanel;
|
||||
window.selectChannel = selectChannel;
|
||||
window.showChannelContextMenu = showChannelContextMenu;
|
||||
window.showChatContextMenu = showChatContextMenu;
|
||||
window.showFolderMenu = showFolderMenu;
|
||||
window.showProjectMenu = showProjectMenu;
|
||||
window.toggleProjectCollapse = toggleProjectCollapse;
|
||||
window.renameProject = renameProject;
|
||||
window.setProjectColor = setProjectColor;
|
||||
window.toggleActiveProject = toggleActiveProject;
|
||||
window.moveChatToProject = moveChatToProject;
|
||||
window.dismissChatContextMenu = dismissChatContextMenu;
|
||||
window.createProjectAndMove = createProjectAndMove;
|
||||
window.moveChatToFolder = moveChatToFolder;
|
||||
window._removeProjectKB = _removeProjectKB;
|
||||
window._addProjectKB = _addProjectKB;
|
||||
window._removeProjectNote = _removeProjectNote;
|
||||
window.showWorkspacePicker = showWorkspacePicker;
|
||||
window._moveChatInProject = _moveChatInProject;
|
||||
window.renameChannel = renameChannel;
|
||||
window.editChannelTopic = editChannelTopic;
|
||||
window.archiveChannel = archiveChannel;
|
||||
window.renameFolder = renameFolder;
|
||||
window._showAddParticipantSearch = _showAddParticipantSearch;
|
||||
window._addParticipant = _addParticipant;
|
||||
window._removeParticipant = _removeParticipant;
|
||||
sb.register('_registerProjectPanel', _registerProjectPanel);
|
||||
sb.register('createProject', createProject);
|
||||
sb.register('cycleChannelAiMode', cycleChannelAiMode);
|
||||
sb.register('deleteChannel', deleteChannel);
|
||||
sb.register('deleteFolder', deleteFolder);
|
||||
sb.register('deleteProject', deleteProject);
|
||||
sb.register('loadChannels', loadChannels);
|
||||
sb.register('loadFolders', loadFolders);
|
||||
sb.register('loadProjectChannelPositions', loadProjectChannelPositions);
|
||||
sb.register('loadProjects', loadProjects);
|
||||
sb.register('loadUsers', loadUsers);
|
||||
sb.register('newChannelOrDM', newChannelOrDM);
|
||||
sb.register('newFolder', newFolder);
|
||||
sb.register('onChatDragEnd', onChatDragEnd);
|
||||
sb.register('onChatDragStart', onChatDragStart);
|
||||
sb.register('onChatSectionDrop', onChatSectionDrop);
|
||||
sb.register('onFolderDrop', onFolderDrop);
|
||||
sb.register('onProjectDragLeave', onProjectDragLeave);
|
||||
sb.register('onProjectDragOver', onProjectDragOver);
|
||||
sb.register('onProjectDrop', onProjectDrop);
|
||||
sb.register('onUnfiledDrop', onUnfiledDrop);
|
||||
sb.register('openParticipantsPanel', openParticipantsPanel);
|
||||
sb.register('openProjectPanel', openProjectPanel);
|
||||
sb.register('selectChannel', selectChannel);
|
||||
sb.register('showChannelContextMenu', showChannelContextMenu);
|
||||
sb.register('showChatContextMenu', showChatContextMenu);
|
||||
sb.register('showFolderMenu', showFolderMenu);
|
||||
sb.register('showProjectMenu', showProjectMenu);
|
||||
sb.register('toggleProjectCollapse', toggleProjectCollapse);
|
||||
sb.register('renameProject', renameProject);
|
||||
sb.register('setProjectColor', setProjectColor);
|
||||
sb.register('toggleActiveProject', toggleActiveProject);
|
||||
sb.register('moveChatToProject', moveChatToProject);
|
||||
sb.register('dismissChatContextMenu', dismissChatContextMenu);
|
||||
sb.register('createProjectAndMove', createProjectAndMove);
|
||||
sb.register('moveChatToFolder', moveChatToFolder);
|
||||
sb.register('_removeProjectKB', _removeProjectKB);
|
||||
sb.register('_addProjectKB', _addProjectKB);
|
||||
sb.register('_removeProjectNote', _removeProjectNote);
|
||||
sb.register('showWorkspacePicker', showWorkspacePicker);
|
||||
sb.register('_moveChatInProject', _moveChatInProject);
|
||||
sb.register('renameChannel', renameChannel);
|
||||
sb.register('editChannelTopic', editChannelTopic);
|
||||
sb.register('archiveChannel', archiveChannel);
|
||||
sb.register('renameFolder', renameFolder);
|
||||
sb.register('_showAddParticipantSearch', _showAddParticipantSearch);
|
||||
sb.register('_addParticipant', _addParticipant);
|
||||
sb.register('_removeParticipant', _removeParticipant);
|
||||
})();
|
||||
|
||||
@@ -556,5 +556,5 @@ const REPL = {
|
||||
// ensuring API.user.role is available for admin gate check.
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.REPL = REPL;
|
||||
sb.ns('REPL', REPL);
|
||||
})();
|
||||
|
||||
133
src/js/sb.js
Normal file
133
src/js/sb.js
Normal file
@@ -0,0 +1,133 @@
|
||||
// ==========================================
|
||||
// Chat Switchboard – Action Registry (sb.js)
|
||||
// ==========================================
|
||||
// Central registry for all dispatchable actions. Loaded first
|
||||
// in base.html, before all other JS files.
|
||||
//
|
||||
// Files register their exports:
|
||||
// sb.ns('UI', UI); // namespace object
|
||||
// sb.register('deleteChat', fn); // standalone action
|
||||
//
|
||||
// Dispatch (from data-action attributes):
|
||||
// sb.resolve('deleteChat') → fn
|
||||
// sb.resolve('UI.copyMessage') → UI.copyMessage.bind(UI)
|
||||
//
|
||||
// Template bridge (Go onclick handlers):
|
||||
// sb.call('deleteChat', id) → fn(id)
|
||||
// sb.call('UI.toggleFolder', id) → UI.toggleFolder(id)
|
||||
//
|
||||
// Introspection:
|
||||
// sb.list() → { actions: [...], namespaces: [...] }
|
||||
//
|
||||
// Extension hook (future):
|
||||
// Extensions register actions into the same registry.
|
||||
// Platform and extension actions are dispatched identically.
|
||||
//
|
||||
// Exports: window.sb
|
||||
|
||||
'use strict';
|
||||
|
||||
const sb = {
|
||||
_actions: new Map(),
|
||||
_namespaces: new Map(),
|
||||
|
||||
/**
|
||||
* Register a standalone action function.
|
||||
* Also sets window[name] for backward compatibility with cross-file
|
||||
* direct calls. This side-effect is removed when files convert to
|
||||
* ES module imports (Phase 4).
|
||||
*/
|
||||
register(name, fn) {
|
||||
if (typeof fn !== 'function') {
|
||||
console.warn('[sb] register: not a function:', name);
|
||||
return;
|
||||
}
|
||||
this._actions.set(name, fn);
|
||||
window[name] = fn; // backward compat — remove when ES modules land
|
||||
},
|
||||
|
||||
/**
|
||||
* Register a namespace object (API, UI, App, etc.).
|
||||
* Methods are accessible via dot notation: sb.resolve('UI.toast').
|
||||
* Also sets window[name] for backward compatibility.
|
||||
*/
|
||||
ns(name, obj) {
|
||||
if (obj == null || typeof obj !== 'object') {
|
||||
console.warn('[sb] ns: not an object:', name);
|
||||
return;
|
||||
}
|
||||
this._namespaces.set(name, obj);
|
||||
window[name] = obj; // backward compat
|
||||
},
|
||||
|
||||
/**
|
||||
* Resolve an action by name. Returns the function or null.
|
||||
* Supports flat names ('deleteChat') and dot notation ('UI.copyMessage').
|
||||
*/
|
||||
resolve(name) {
|
||||
// Flat action
|
||||
const flat = this._actions.get(name);
|
||||
if (flat) return flat;
|
||||
|
||||
// Dot notation: namespace.method
|
||||
const dot = name.indexOf('.');
|
||||
if (dot > 0) {
|
||||
const nsName = name.slice(0, dot);
|
||||
const method = name.slice(dot + 1);
|
||||
const obj = this._namespaces.get(nsName);
|
||||
if (obj && typeof obj[method] === 'function') {
|
||||
return obj[method].bind(obj);
|
||||
}
|
||||
// Try nested: UI._deleteRoutingPolicy → UI['_deleteRoutingPolicy']
|
||||
if (obj && typeof obj[method] !== 'undefined') {
|
||||
return typeof obj[method] === 'function' ? obj[method].bind(obj) : null;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: check window (catches un-migrated globals)
|
||||
if (typeof window[name] === 'function') return window[name];
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Call an action by name. Template bridge.
|
||||
* Go templates use: onclick="sb.call('action', arg1, arg2)"
|
||||
*/
|
||||
call(name, ...args) {
|
||||
const fn = this.resolve(name);
|
||||
if (fn) return fn(...args);
|
||||
console.warn('[sb] Unknown action:', name);
|
||||
},
|
||||
|
||||
/**
|
||||
* Call with event passthrough (for context menu positioning).
|
||||
* Go templates use: onclick="sb.callEvent(event, 'showProjectMenu', id)"
|
||||
*/
|
||||
callEvent(event, name, ...args) {
|
||||
event?.stopPropagation?.();
|
||||
const fn = this.resolve(name);
|
||||
if (fn) return fn(event, ...args);
|
||||
console.warn('[sb] Unknown action:', name);
|
||||
},
|
||||
|
||||
/**
|
||||
* Introspection: list all registered actions and namespaces.
|
||||
*/
|
||||
list() {
|
||||
return {
|
||||
actions: [...this._actions.keys()].sort(),
|
||||
namespaces: [...this._namespaces.keys()].sort(),
|
||||
total: this._actions.size + this._namespaces.size,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if an action is registered.
|
||||
*/
|
||||
has(name) {
|
||||
return this.resolve(name) !== null;
|
||||
},
|
||||
};
|
||||
|
||||
window.sb = sb;
|
||||
@@ -3,10 +3,6 @@
|
||||
// ==========================================
|
||||
// Settings save, provider CRUD, avatar, command palette,
|
||||
// user model roles.
|
||||
//
|
||||
// Exports: window.loadSettings,window.saveSettings,window.updateAvatarPreview,window._userProvForm,window._userProvList,window.handleSaveAdminSettings,window._userRoleConfig,window.toggleCmdPalette,window.closeCmdPalette,window._initSettingsListeners,window._initAdminSettingsToggles
|
||||
// (also: window.toggleUserModelVisibility, window.bulkSetUserModelVisibility,
|
||||
// window.deleteUserPersona, window.loadTeamWorkflows — self-assigned)
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
@@ -793,7 +789,7 @@ async function _importAdminSettings(file) {
|
||||
// chat and admin surfaces).
|
||||
|
||||
if (typeof toggleUserModelVisibility === 'undefined') {
|
||||
window.toggleUserModelVisibility = async function(modelId, providerConfigId, currentlyHidden) {
|
||||
sb.register('toggleUserModelVisibility', async function(modelId, providerConfigId, currentlyHidden) {
|
||||
const compositeKey = (providerConfigId || '') + ':' + modelId;
|
||||
try {
|
||||
await API.setModelPreference(modelId, providerConfigId || null, !currentlyHidden);
|
||||
@@ -802,11 +798,11 @@ if (typeof toggleUserModelVisibility === 'undefined') {
|
||||
await UI.loadUserModels();
|
||||
if (typeof fetchModels === 'function') await fetchModels();
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof bulkSetUserModelVisibility === 'undefined') {
|
||||
window.bulkSetUserModelVisibility = async function(show) {
|
||||
sb.register('bulkSetUserModelVisibility', async function(show) {
|
||||
const models = (App.models || []).filter(m => !m.isPersona);
|
||||
if (!models.length) return;
|
||||
const shouldHide = !show;
|
||||
@@ -827,11 +823,11 @@ if (typeof bulkSetUserModelVisibility === 'undefined') {
|
||||
if (typeof fetchModels === 'function') await fetchModels();
|
||||
UI.toast(`${toUpdate.length} model${toUpdate.length !== 1 ? 's' : ''} ${show ? 'shown' : 'hidden'}`);
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof deleteUserPersona === 'undefined') {
|
||||
window.deleteUserPersona = async function(id, name) {
|
||||
sb.register('deleteUserPersona', async function(id, name) {
|
||||
if (!await showConfirm(`Delete persona "${name}"?`)) return;
|
||||
try {
|
||||
await API.deleteUserPersona(id);
|
||||
@@ -839,12 +835,12 @@ if (typeof deleteUserPersona === 'undefined') {
|
||||
await UI.loadUserPersonas();
|
||||
if (typeof fetchModels === 'function') await fetchModels();
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// v0.27.0: Team Workflows settings section — lists workflows for user's teams.
|
||||
// Loaded as a dynamic section in the settings surface.
|
||||
window.loadTeamWorkflows = async function() {
|
||||
sb.register('loadTeamWorkflows', async function() {
|
||||
var el = document.getElementById('settingsDynamic');
|
||||
if (!el) return;
|
||||
|
||||
@@ -898,18 +894,18 @@ window.loadTeamWorkflows = async function() {
|
||||
} catch (e) {
|
||||
el.innerHTML = '<div class="settings-placeholder">Failed to load workflows: ' + esc(e.message) + '</div>';
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.loadSettings = loadSettings;
|
||||
window.saveSettings = saveSettings;
|
||||
window.updateAvatarPreview = updateAvatarPreview;
|
||||
window._userProvForm = _userProvForm;
|
||||
window._userProvList = _userProvList;
|
||||
window.handleSaveAdminSettings = handleSaveAdminSettings;
|
||||
window._userRoleConfig = _userRoleConfig;
|
||||
window.toggleCmdPalette = toggleCmdPalette;
|
||||
window.closeCmdPalette = closeCmdPalette;
|
||||
window._initSettingsListeners = _initSettingsListeners;
|
||||
window._initAdminSettingsToggles = _initAdminSettingsToggles;
|
||||
sb.register('loadSettings', loadSettings);
|
||||
sb.register('saveSettings', saveSettings);
|
||||
sb.register('updateAvatarPreview', updateAvatarPreview);
|
||||
sb.register('_userProvForm', _userProvForm);
|
||||
sb.register('_userProvList', _userProvList);
|
||||
sb.register('handleSaveAdminSettings', handleSaveAdminSettings);
|
||||
sb.register('_userRoleConfig', _userRoleConfig);
|
||||
sb.register('toggleCmdPalette', toggleCmdPalette);
|
||||
sb.register('closeCmdPalette', closeCmdPalette);
|
||||
sb.register('_initSettingsListeners', _initSettingsListeners);
|
||||
sb.register('_initAdminSettingsToggles', _initAdminSettingsToggles);
|
||||
})();
|
||||
|
||||
@@ -239,7 +239,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
window._loadAdminTasks = async function() {
|
||||
sb.register('_loadAdminTasks', async function() {
|
||||
var container = document.getElementById('adminDynamic');
|
||||
if (!container) return;
|
||||
container.innerHTML = SCAFFOLD_HTML;
|
||||
@@ -251,5 +251,5 @@
|
||||
document.getElementById('taskCfgSaveBtn').addEventListener('click', saveConfig);
|
||||
|
||||
await loadTasks();
|
||||
};
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
'<p style="color:var(--text-2);font-size:12px;margin:0 0 12px">Quick-start with a pre-configured task. You can customize after creation.</p>' +
|
||||
'<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px">';
|
||||
TEMPLATES.forEach(function(tmpl) {
|
||||
html += '<div class="settings-section" style="padding:10px 12px;cursor:pointer" onclick="window._createFromTemplate(\'' + esc(tmpl.name) + '\')">' +
|
||||
html += '<div class="settings-section" style="padding:10px 12px;cursor:pointer" data-action="_createFromTemplate" data-args=\'' + JSON.stringify([esc(tmpl.name)]) + '\'>' +
|
||||
'<div style="font-weight:600;font-size:13px">' + esc(tmpl.name) + '</div>' +
|
||||
'<div style="font-size:11px;color:var(--text-2);margin-top:4px">' + esc(tmpl.schedule) + '</div>' +
|
||||
'</div>';
|
||||
@@ -269,13 +269,13 @@
|
||||
}
|
||||
|
||||
// Template launcher
|
||||
window._createFromTemplate = function(name) {
|
||||
sb.register('_createFromTemplate', function(name) {
|
||||
var tmpl = TEMPLATES.find(function(t) { return t.name === name; });
|
||||
if (tmpl) showCreateForm({ name: tmpl.name, prompt: tmpl.prompt, schedule: tmpl.schedule });
|
||||
};
|
||||
});
|
||||
|
||||
// ── Entry point (called from settings loader) ──
|
||||
window._loadSettingsTasks = function() {
|
||||
sb.register('_loadSettingsTasks', function() {
|
||||
loadTaskList();
|
||||
};
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.TaskSidebar = {
|
||||
const TaskSidebar = {
|
||||
|
||||
_loaded: false,
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
section.className = 'sb-section';
|
||||
section.id = 'sbSectionTasks';
|
||||
section.innerHTML =
|
||||
'<div class="sb-section-header" onclick="UI.toggleSidebarSection(\'tasks\')">' +
|
||||
'<div class="sb-section-header" data-action="UI.toggleSidebarSection" data-args=\'["tasks"]\'>' +
|
||||
'<span class="sb-section-arrow" id="sbArrowTasks">\u25be</span>' +
|
||||
'<span class="sb-section-label">Tasks</span>' +
|
||||
'<span class="sb-queue-badge" id="sbTaskBadge" style="display:none">0</span>' +
|
||||
@@ -107,7 +107,7 @@
|
||||
'<span class="sb-queue-icon">' + icon + '</span>' +
|
||||
'<span class="sb-queue-label">' + label + '</span>' +
|
||||
'<button class="btn-small" style="padding:0 4px;font-size:10px" ' +
|
||||
'onclick="event.stopPropagation();TaskSidebar.runNow(\'' + t.id + '\')" title="Run Now">\u25b6</button>' +
|
||||
'data-action="TaskSidebar.runNow" data-args=\'' + JSON.stringify([t.id]) + '\' title="Run Now">\u25b6</button>' +
|
||||
'</div>';
|
||||
});
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
}
|
||||
}
|
||||
};
|
||||
sb.ns('TaskSidebar', TaskSidebar);
|
||||
|
||||
function _esc(s) { return s ? s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"') : ''; }
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ function dismissContextWarning() {
|
||||
}
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.Tokens = Tokens;
|
||||
window.updateInputTokens = updateInputTokens;
|
||||
window.updateContextWarning = updateContextWarning;
|
||||
sb.ns('Tokens', Tokens);
|
||||
sb.register('updateInputTokens', updateInputTokens);
|
||||
sb.register('updateContextWarning', updateContextWarning);
|
||||
})();
|
||||
|
||||
@@ -146,7 +146,7 @@ Object.assign(UI, {
|
||||
document.getElementById('teamAdminPicker').style.display = '';
|
||||
document.getElementById('teamAdminContent').style.display = 'none';
|
||||
document.getElementById('teamAdminPickerList').innerHTML = adminTeams.map(t => `
|
||||
<div class="team-card" style="cursor:pointer" onclick="UI.openTeamManage('${t.id}', '${esc(t.name)}')">
|
||||
<div class="team-card" style="cursor:pointer" data-action="UI.openTeamManage" data-args='${JSON.stringify([t.id, esc(t.name)])}'>
|
||||
<div class="team-card-info">
|
||||
<strong>${esc(t.name)}</strong>
|
||||
<span class="badge-admin">admin</span>
|
||||
@@ -220,12 +220,12 @@ Object.assign(UI, {
|
||||
<td class="admin-user-time">${lastSeen}</td>
|
||||
<td class="admin-actions-cell">
|
||||
${isPending
|
||||
? `<button class="icon-btn" onclick="showApproveForm('${u.id}','${esc(u.username)}')" title="Approve">✓</button>`
|
||||
: `<button class="icon-btn" onclick="toggleUserActive('${u.id}',false)" title="Disable">⏸</button>`
|
||||
? `<button class="icon-btn" data-action="showApproveForm" data-args='${JSON.stringify([u.id, esc(u.username)])}' title="Approve">✓</button>`
|
||||
: `<button class="icon-btn" data-action="toggleUserActive" data-args='${JSON.stringify([u.id, false])}' title="Disable">⏸</button>`
|
||||
}
|
||||
<button class="icon-btn" onclick="toggleUserRole('${u.id}','${u.role === 'admin' ? 'user' : 'admin'}')" title="${u.role === 'admin' ? 'Demote' : 'Promote'}">⇅</button>
|
||||
<button class="icon-btn" onclick="adminResetUserPassword('${u.id}','${esc(u.username)}')" title="Reset PW">🔑</button>
|
||||
<button class="icon-btn icon-btn-danger" onclick="deleteUser('${u.id}','${esc(u.username)}')" title="Delete">🗑</button>
|
||||
<button class="icon-btn" data-action="toggleUserRole" data-args='${JSON.stringify([u.id, u.role === "admin" ? "user" : "admin"])}' title="${u.role === 'admin' ? 'Demote' : 'Promote'}">⇅</button>
|
||||
<button class="icon-btn" data-action="adminResetUserPassword" data-args='${JSON.stringify([u.id, esc(u.username)])}' title="Reset PW">🔑</button>
|
||||
<button class="icon-btn icon-btn-danger" data-action="deleteUser" data-args='${JSON.stringify([u.id, esc(u.username)])}' title="Delete">🗑</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
@@ -426,9 +426,9 @@ Object.assign(UI, {
|
||||
<td><span class="badge-provider-type">${esc(ext.tier)}</span></td>
|
||||
<td style="font-size:12px;color:var(--text-3);font-family:var(--font-mono,monospace)">v${esc(ext.version)}</td>
|
||||
<td class="admin-actions-cell">
|
||||
<button class="icon-btn" onclick="editAdminExtension('${ext.id}')" title="Edit">✎</button>
|
||||
<button class="icon-btn" onclick="toggleAdminExtension('${ext.id}',${!ext.is_enabled})" title="${ext.is_enabled ? 'Disable' : 'Enable'}">${ext.is_enabled ? '⏸' : '▶'}</button>
|
||||
<button class="icon-btn icon-btn-danger" onclick="deleteAdminExtension('${ext.id}','${esc(ext.name)}')" title="Uninstall">🗑</button>
|
||||
<button class="icon-btn" data-action="editAdminExtension" data-args='${JSON.stringify([ext.id])}' title="Edit">✎</button>
|
||||
<button class="icon-btn" data-action="toggleAdminExtension" data-args='${JSON.stringify([ext.id, !ext.is_enabled])}' title="${ext.is_enabled ? 'Disable' : 'Enable'}">${ext.is_enabled ? '⏸' : '▶'}</button>
|
||||
<button class="icon-btn icon-btn-danger" data-action="deleteAdminExtension" data-args='${JSON.stringify([ext.id, esc(ext.name)])}' title="Uninstall">🗑</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('')}</tbody>
|
||||
@@ -652,7 +652,7 @@ Object.assign(UI, {
|
||||
<td style="font-weight:500;font-family:var(--font-mono,monospace);font-size:12px">${esc(m.model_id || m.id)}</td>
|
||||
<td style="font-size:12px;color:var(--text-3)">${esc(m.provider_name || '')}</td>
|
||||
<td>${badges}</td>
|
||||
<td><button class="admin-model-toggle ${visClass}" onclick="cycleModelVisibility('${m.id}','${vis}')">${visLabel}</button></td>
|
||||
<td><button class="admin-model-toggle ${visClass}" data-action="cycleModelVisibility" data-args='${JSON.stringify([m.id, vis])}'>${visLabel}</button></td>
|
||||
</tr>`;
|
||||
}).join('')}</tbody>
|
||||
</table>
|
||||
@@ -733,10 +733,10 @@ Object.assign(UI, {
|
||||
<td style="font-size:12px;color:var(--text-3);font-family:var(--font-mono,monospace)">${esc(p.base_model_id)}</td>
|
||||
<td>${p.is_active ? '<span class="badge-active">active</span>' : '<span class="badge-disabled">inactive</span>'}</td>
|
||||
<td class="admin-actions-cell">
|
||||
<button class="icon-btn" onclick="UI.openGrantPicker('persona','${p.id}','${esc(p.name)}','[data-grant-persona="${p.id}"]')" title="Access">🔒</button>
|
||||
<button class="icon-btn" onclick="editAdminPersona('${p.id}')" title="Edit">✎</button>
|
||||
<button class="icon-btn" onclick="toggleAdminPersona('${p.id}',${!p.is_active})" title="${p.is_active ? 'Disable' : 'Enable'}">${p.is_active ? '⏸' : '▶'}</button>
|
||||
<button class="icon-btn icon-btn-danger" onclick="deleteAdminPersona('${p.id}','${esc(p.name)}')" title="Delete">🗑</button>
|
||||
<button class="icon-btn" data-action="UI.openGrantPicker" data-args='${JSON.stringify(["persona", p.id, esc(p.name), `[data-grant-persona="${p.id}"]`])}' title="Access">🔒</button>
|
||||
<button class="icon-btn" data-action="editAdminPersona" data-args='${JSON.stringify([p.id])}' title="Edit">✎</button>
|
||||
<button class="icon-btn" data-action="toggleAdminPersona" data-args='${JSON.stringify([p.id, !p.is_active])}' title="${p.is_active ? 'Disable' : 'Enable'}">${p.is_active ? '⏸' : '▶'}</button>
|
||||
<button class="icon-btn icon-btn-danger" data-action="deleteAdminPersona" data-args='${JSON.stringify([p.id, esc(p.name)])}' title="Delete">🗑</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('')}</tbody>
|
||||
@@ -775,9 +775,9 @@ Object.assign(UI, {
|
||||
</td>
|
||||
<td style="font-size:12px;color:var(--text-2)">${t.member_count} member${t.member_count !== 1 ? 's' : ''}</td>
|
||||
<td class="admin-actions-cell">
|
||||
<button class="icon-btn" onclick="UI.openTeamDetail('${t.id}','${esc(t.name)}')" title="Members">👥</button>
|
||||
<button class="icon-btn" onclick="toggleTeamActive('${t.id}',${!t.is_active})" title="${t.is_active ? 'Disable' : 'Enable'}">${t.is_active ? '⏸' : '▶'}</button>
|
||||
<button class="icon-btn icon-btn-danger" onclick="deleteTeam('${t.id}','${esc(t.name)}')" title="Delete">🗑</button>
|
||||
<button class="icon-btn" data-action="UI.openTeamDetail" data-args='${JSON.stringify([t.id, esc(t.name)])}' title="Members">👥</button>
|
||||
<button class="icon-btn" data-action="toggleTeamActive" data-args='${JSON.stringify([t.id, !t.is_active])}' title="${t.is_active ? 'Disable' : 'Enable'}">${t.is_active ? '⏸' : '▶'}</button>
|
||||
<button class="icon-btn icon-btn-danger" data-action="deleteTeam" data-args='${JSON.stringify([t.id, esc(t.name)])}' title="Delete">🗑</button>
|
||||
</td>
|
||||
</tr>`).join('')}</tbody>
|
||||
</table>
|
||||
@@ -830,7 +830,7 @@ Object.assign(UI, {
|
||||
${m.user_role === 'admin' ? '<span class="badge-admin" style="margin-left:4px;font-size:9px">sys-admin</span>' : ''}
|
||||
</td>
|
||||
<td class="admin-actions-cell">
|
||||
<button class="icon-btn icon-btn-danger" onclick="removeTeamMember('${teamId}','${m.id}','${esc(m.email)}')" title="Remove">🗑</button>
|
||||
<button class="icon-btn icon-btn-danger" data-action="removeTeamMember" data-args='${JSON.stringify([teamId, m.id, esc(m.email)])}' title="Remove">🗑</button>
|
||||
</td>
|
||||
</tr>`).join('')}</tbody>
|
||||
</table>
|
||||
@@ -885,8 +885,8 @@ Object.assign(UI, {
|
||||
<td>${scopeBadge}</td>
|
||||
<td style="font-size:12px;color:var(--text-2)">${g.member_count} member${g.member_count !== 1 ? 's' : ''}</td>
|
||||
<td class="admin-actions-cell">
|
||||
<button class="icon-btn" onclick="UI.openGroupDetail('${g.id}','${esc(g.name)}')" title="Members">👥</button>
|
||||
<button class="icon-btn icon-btn-danger" onclick="deleteGroup('${g.id}','${esc(g.name)}')" title="Delete">🗑</button>
|
||||
<button class="icon-btn" data-action="UI.openGroupDetail" data-args='${JSON.stringify([g.id, esc(g.name)])}' title="Members">👥</button>
|
||||
<button class="icon-btn icon-btn-danger" data-action="deleteGroup" data-args='${JSON.stringify([g.id, esc(g.name)])}' title="Delete">🗑</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('')}</tbody>
|
||||
@@ -1011,7 +1011,7 @@ Object.assign(UI, {
|
||||
</td>
|
||||
<td class="admin-user-time">${new Date(m.added_at).toLocaleDateString()}</td>
|
||||
<td class="admin-actions-cell">
|
||||
<button class="icon-btn icon-btn-danger" onclick="removeGroupMember('${groupId}','${m.user_id}','${esc(m.username || m.email)}')" title="Remove">🗑</button>
|
||||
<button class="icon-btn icon-btn-danger" data-action="removeGroupMember" data-args='${JSON.stringify([groupId, m.user_id, esc(m.username || m.email)])}' title="Remove">🗑</button>
|
||||
</td>
|
||||
</tr>`).join('')}</tbody>
|
||||
</table>
|
||||
@@ -1064,7 +1064,7 @@ Object.assign(UI, {
|
||||
</label>`).join('') : '<span class="text-muted" style="font-size:12px">No groups exist yet — create one in People → Groups</span>'}
|
||||
</div>
|
||||
<div class="form-row" style="margin-top:8px">
|
||||
<button class="btn-small btn-primary" onclick="saveGrant('${resourceType}', '${resourceId}')">Save</button>
|
||||
<button class="btn-small btn-primary" data-action="saveGrant" data-args='${JSON.stringify([resourceType, resourceId])}'>Save</button>
|
||||
<button class="btn-small" onclick="this.closest('.admin-inline-form').remove()">Cancel</button>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -1185,9 +1185,9 @@ Object.assign(UI, {
|
||||
<td class="admin-user-time">${p.priority}</td>
|
||||
<td>${p.is_active ? '<span class="badge-active">active</span>' : '<span class="badge-disabled">inactive</span>'}</td>
|
||||
<td class="admin-actions-cell">
|
||||
<button class="icon-btn" onclick="UI._showRoutingForm('${esc(p.id)}')" title="Edit">✎</button>
|
||||
<button class="icon-btn" onclick="UI._toggleRoutingPolicy('${esc(p.id)}',${!p.is_active})" title="${p.is_active ? 'Disable' : 'Enable'}">${p.is_active ? '⏸' : '▶'}</button>
|
||||
<button class="icon-btn icon-btn-danger" onclick="UI._deleteRoutingPolicy('${esc(p.id)}','${esc(p.name)}')" title="Delete">🗑</button>
|
||||
<button class="icon-btn" data-action="UI._showRoutingForm" data-args='${JSON.stringify([esc(p.id)])}' title="Edit">✎</button>
|
||||
<button class="icon-btn" data-action="UI._toggleRoutingPolicy" data-args='${JSON.stringify([esc(p.id), !p.is_active])}' title="${p.is_active ? 'Disable' : 'Enable'}">${p.is_active ? '⏸' : '▶'}</button>
|
||||
<button class="icon-btn icon-btn-danger" data-action="UI._deleteRoutingPolicy" data-args='${JSON.stringify([esc(p.id), esc(p.name)])}' title="Delete">🗑</button>
|
||||
</td>
|
||||
</tr>`).join('')}</tbody>
|
||||
</table>
|
||||
@@ -1341,7 +1341,7 @@ Object.assign(UI, {
|
||||
<td>${esc(o.provider_config_id?.slice(0, 8) || 'any')}</td>
|
||||
<td>${esc(o.field)}</td>
|
||||
<td><strong>${esc(String(o.value))}</strong></td>
|
||||
<td><button class="btn-small btn-danger" onclick="UI._deleteCapOverride('${esc(o.model_id)}', '${esc(o.id)}')">×</button></td>
|
||||
<td><button class="btn-small btn-danger" data-action="UI._deleteCapOverride" data-args='${JSON.stringify([esc(o.model_id), esc(o.id)])}'>×</button></td>
|
||||
</tr>`).join('')}</tbody>
|
||||
</table>`;
|
||||
} catch (e) {
|
||||
@@ -1546,7 +1546,7 @@ Object.assign(UI, {
|
||||
<td>${esc(ch.owner_name)}</td>
|
||||
<td>${ch.message_count}</td>
|
||||
<td>${age}</td>
|
||||
<td><button class="btn-small btn-danger" onclick="UI._purgeChannel('${ch.id}')">Purge</button></td>
|
||||
<td><button class="btn-small btn-danger" data-action="UI._purgeChannel" data-args='${JSON.stringify([ch.id])}'>Purge</button></td>
|
||||
</tr>`;
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
@@ -1570,7 +1570,7 @@ Object.assign(UI, {
|
||||
});
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.ADMIN_SECTIONS = ADMIN_SECTIONS;
|
||||
window.ADMIN_LABELS = ADMIN_LABELS;
|
||||
window.ADMIN_LOADERS = ADMIN_LOADERS;
|
||||
sb.ns('ADMIN_SECTIONS', ADMIN_SECTIONS);
|
||||
sb.ns('ADMIN_LABELS', ADMIN_LABELS);
|
||||
sb.ns('ADMIN_LOADERS', ADMIN_LOADERS);
|
||||
})();
|
||||
|
||||
@@ -13,6 +13,46 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ── Delegated Action Dispatcher ────────────────────
|
||||
// Buttons use data-action="fnName" data-args='[...]' instead of onclick.
|
||||
// data-pass-event: prepends the click event as first arg (for positioning).
|
||||
// data-stop-prop: calls event.stopPropagation() (handled by default).
|
||||
function _uiDispatch(container) {
|
||||
if (container._uiBound) return;
|
||||
container._uiBound = true;
|
||||
container.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-action]');
|
||||
if (!btn) return;
|
||||
e.stopPropagation();
|
||||
const action = btn.dataset.action;
|
||||
const fn = sb.resolve(action);
|
||||
if (typeof fn !== 'function') {
|
||||
console.warn('[ui] Unknown action:', action);
|
||||
return;
|
||||
}
|
||||
const args = JSON.parse(btn.dataset.args || '[]');
|
||||
if (btn.hasAttribute('data-pass-event')) {
|
||||
fn(e, ...args);
|
||||
} else if (btn.hasAttribute('data-pass-el')) {
|
||||
fn(btn, ...args);
|
||||
} else {
|
||||
fn(...args);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Wrapper: toggle archived projects visibility + re-render
|
||||
function _toggleArchivedProjects() {
|
||||
App.showArchivedProjects = !App.showArchivedProjects;
|
||||
UI.renderProjectsSection();
|
||||
}
|
||||
|
||||
// Wrapper: read edit input value at click time, then call submitEdit
|
||||
function _submitEditFromDOM(messageId) {
|
||||
const val = document.getElementById('editInput_' + messageId)?.value || '';
|
||||
submitEdit(messageId, val);
|
||||
}
|
||||
|
||||
// ── Avatar Helper ────────────────────────────
|
||||
// Returns HTML for a message avatar: <img> if data URI available, emoji fallback.
|
||||
function avatarHTML(role, avatarDataURI) {
|
||||
@@ -445,10 +485,10 @@ const UI = {
|
||||
ondragover="onProjectDragOver(event)"
|
||||
ondragleave="onProjectDragLeave(event)"
|
||||
ondrop="onProjectDrop(event,'${proj.id}')">
|
||||
<div class="sb-proj-header" onclick="toggleProjectCollapse('${proj.id}')">
|
||||
<div class="sb-proj-header" data-action="toggleProjectCollapse" data-args='${JSON.stringify([proj.id])}' >
|
||||
${collapsed ? dot : `<span class="sb-proj-arrow">${arrow}</span>${dot}<span class="sb-proj-name">${esc(proj.name)}</span>${pinIcon}`}
|
||||
${!collapsed ? `<span class="sb-proj-count">${chats.length}</span>
|
||||
<button class="sb-proj-menu" onclick="event.stopPropagation();showProjectMenu(event,'${proj.id}')" title="Options">⋯</button>` : ''}
|
||||
<button class="sb-proj-menu" data-action="showProjectMenu" data-args='${JSON.stringify([proj.id])}' data-pass-event title="Options">⋯</button>` : ''}
|
||||
</div>`;
|
||||
|
||||
if (!isCollapsed && !collapsed) {
|
||||
@@ -462,7 +502,7 @@ const UI = {
|
||||
});
|
||||
|
||||
if (hasArchived && !collapsed) {
|
||||
html += `<button class="sb-show-archived" onclick="App.showArchivedProjects=!App.showArchivedProjects;UI.renderProjectsSection()">
|
||||
html += `<button class="sb-show-archived" data-action="_toggleArchivedProjects"">
|
||||
${showArchived ? '▾ Hide archived' : `▸ Show archived (${projects.filter(p => p.isArchived).length})`}
|
||||
</button>`;
|
||||
}
|
||||
@@ -506,12 +546,11 @@ const UI = {
|
||||
: '';
|
||||
|
||||
html += `<div class="sb-channel-item${isActive ? ' active' : ''}"
|
||||
onclick="selectChannel('${ch.id}')"
|
||||
oncontextmenu="showChannelContextMenu(event,'${ch.id}')"
|
||||
data-action="selectChannel" data-args='${JSON.stringify([ch.id])}' oncontextmenu="showChannelContextMenu(event,'${ch.id}')"
|
||||
title="${esc(ch.name)}">
|
||||
<span class="sb-ch-icon">${icon}</span>
|
||||
${!collapsed ? `<span class="sb-ch-name">${esc(ch.name)}</span>${onlineDot}${unreadBadge}
|
||||
<button class="sb-ch-menu" onclick="event.stopPropagation();showChannelContextMenu(event,'${ch.id}')" title="Options">⋯</button>` : unreadBadge}
|
||||
<button class="sb-ch-menu" data-action="showChannelContextMenu" data-args='${JSON.stringify([ch.id])}' data-pass-event title="Options">⋯</button>` : unreadBadge}
|
||||
</div>`;
|
||||
});
|
||||
|
||||
@@ -576,12 +615,12 @@ const UI = {
|
||||
ondragover="event.preventDefault();event.dataTransfer.dropEffect='move';this.classList.add('drag-over')"
|
||||
ondragleave="this.classList.remove('drag-over')"
|
||||
ondrop="onFolderDrop(event,'${f.id}')">
|
||||
<div class="sb-folder-header" onclick="UI.toggleFolder('${f.id}')">
|
||||
<div class="sb-folder-header" data-action="UI.toggleFolder" data-args='${JSON.stringify([f.id])}'">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/>
|
||||
</svg>
|
||||
<span class="sb-folder-name">${esc(f.name)}</span>
|
||||
<button class="sb-folder-menu" onclick="event.stopPropagation();showFolderMenu(event,'${f.id}')" title="Folder options">⋯</button>
|
||||
<button class="sb-folder-menu" data-action="showFolderMenu" data-args='${JSON.stringify([f.id])}' data-pass-event title="Folder options">⋯</button>
|
||||
<span class="sb-folder-arrow">${isOpen ? '▾' : '▸'}</span>
|
||||
</div>
|
||||
${isOpen
|
||||
@@ -656,15 +695,14 @@ const UI = {
|
||||
? '<span class="chat-type-icon" title="Group Chat">👥</span> '
|
||||
: c.type === 'workflow' ? '<span class="chat-type-icon" title="Workflow">⚙</span> ' : '';
|
||||
return `<div class="chat-item${active}${indent}"
|
||||
onclick="selectChat('${c.id}')"
|
||||
oncontextmenu="showChatContextMenu(event,'${c.id}')"
|
||||
data-action="selectChat" data-args='${JSON.stringify([c.id])}' oncontextmenu="showChatContextMenu(event,'${c.id}')"
|
||||
draggable="true"
|
||||
ondragstart="onChatDragStart(event,'${c.id}')"
|
||||
ondragend="onChatDragEnd(event)">
|
||||
<span class="chat-item-title" ondblclick="startRenameChat('${c.id}');event.stopPropagation();"
|
||||
title="Double-click to rename">${typeIcon}${esc(c.title)}</span>
|
||||
<span class="chat-item-time">${time}</span>
|
||||
<button class="chat-item-delete" onclick="deleteChat('${c.id}');event.stopPropagation();" title="Delete">✕</button>
|
||||
<button class="chat-item-delete" data-action="deleteChat" data-args='${JSON.stringify([c.id])}' title="Delete">✕</button>
|
||||
</div>`;
|
||||
},
|
||||
|
||||
@@ -689,7 +727,7 @@ const UI = {
|
||||
const hiddenCount = messages.filter((m, i) => i < summaryIdx && m.role !== 'system').length;
|
||||
if (hiddenCount > 0) {
|
||||
html += `<div class="message-summary-divider" id="summaryDivider">
|
||||
<button class="summary-toggle" onclick="toggleSummarizedHistory()">
|
||||
<button class="summary-toggle" data-action="toggleSummarizedHistory"">
|
||||
▸ ${hiddenCount} earlier messages summarized
|
||||
</button>
|
||||
</div>
|
||||
@@ -779,17 +817,15 @@ const UI = {
|
||||
const sibTotal = msg.siblingCount || 1;
|
||||
const branchNav = hasSiblings ? `
|
||||
<div class="branch-nav">
|
||||
<button class="branch-arrow" onclick="switchSibling('${msg.id}', -1)"
|
||||
${sibPos <= 1 ? 'disabled' : ''} title="Previous version">‹</button>
|
||||
<button class="branch-arrow" data-action="switchSibling" data-args='${JSON.stringify([msg.id, -1])}' ${sibPos <= 1 ? 'disabled' : ''} title="Previous version">‹</button>
|
||||
<span class="branch-pos">${sibPos}/${sibTotal}</span>
|
||||
<button class="branch-arrow" onclick="switchSibling('${msg.id}', 1)"
|
||||
${sibPos >= sibTotal ? 'disabled' : ''} title="Next version">›</button>
|
||||
<button class="branch-arrow" data-action="switchSibling" data-args='${JSON.stringify([msg.id, 1])}' ${sibPos >= sibTotal ? 'disabled' : ''} title="Next version">›</button>
|
||||
</div>` : '';
|
||||
|
||||
// Action buttons
|
||||
const msgId = msg.id ? esc(msg.id) : '';
|
||||
const editBtn = isSelf && msgId ? `<button class="msg-action-btn" onclick="editMessage('${msgId}')" title="Edit">Edit</button>` : '';
|
||||
const regenBtn = !isUser && msgId ? `<button class="msg-action-btn" onclick="regenerateMessage('${msgId}')" title="Regenerate">Regen</button>` : '';
|
||||
const editBtn = isSelf && msgId ? `<button class="msg-action-btn" data-action="editMessage" data-args='${JSON.stringify([msgId])}' title="Edit">Edit</button>` : '';
|
||||
const regenBtn = !isUser && msgId ? `<button class="msg-action-btn" data-action="regenerateMessage" data-args='${JSON.stringify([msgId])}' title="Regenerate">Regen</button>` : '';
|
||||
|
||||
return `
|
||||
<div class="message ${roleClass}${dimmed ? ' msg-dimmed' : ''}" data-msg-id="${msgId}">
|
||||
@@ -803,8 +839,8 @@ const UI = {
|
||||
<div class="msg-actions">
|
||||
${editBtn}
|
||||
${regenBtn}
|
||||
<button class="msg-action-btn" onclick="UI.copyMessage(${index})" title="Copy">Copy</button>
|
||||
<button class="msg-action-btn" onclick="saveMessageToNote(${index})" title="Save to note">Note</button>
|
||||
<button class="msg-action-btn" data-action="UI.copyMessage" data-args='${JSON.stringify([index])}'" title="Copy">Copy</button>
|
||||
<button class="msg-action-btn" data-action="saveMessageToNote" data-args='${JSON.stringify([index])}'" title="Save to note">Note</button>
|
||||
</div>
|
||||
</div>
|
||||
${!isUser ? _renderToolCallsHTML(msg.tool_calls) : ''}
|
||||
@@ -845,21 +881,21 @@ const UI = {
|
||||
`<span>with <b>${esc(name)}</b></span>` +
|
||||
`<span class="ctx-hint">@mention a persona to bring in AI</span>` +
|
||||
`<span style="flex:1;"></span>` +
|
||||
`<button class="ctx-participants-btn" onclick="openParticipantsPanel('${ac.id}')" title="Manage participants">👥 Members</button>`;
|
||||
`<button class="ctx-participants-btn" data-action="openParticipantsPanel" data-args='${JSON.stringify([ac.id])}' title="Manage participants">👥 Members</button>`;
|
||||
} else if (ac.type === 'group') {
|
||||
html = `<span class="ctx-mode">Group</span>` +
|
||||
`<span class="ctx-hint">No @mention = leader responds · @mention to route · @all for everyone</span>` +
|
||||
`<span style="flex:1;"></span>` +
|
||||
`<button class="ctx-participants-btn" onclick="openParticipantsPanel('${ac.id}')" title="Manage participants">👥 Members</button>`;
|
||||
`<button class="ctx-participants-btn" data-action="openParticipantsPanel" data-args='${JSON.stringify([ac.id])}' title="Manage participants">👥 Members</button>`;
|
||||
} else if (ac.type === 'channel') {
|
||||
const ch = (App.channels || []).find(c => c.id === ac.id);
|
||||
const mode = ch?.aiMode || 'auto';
|
||||
const topic = ch?.topic || '';
|
||||
const modeLabel = mode === 'auto' ? 'AI: auto' : mode === 'mention_only' ? 'AI: @mention only' : 'AI: off';
|
||||
html = `<button class="ctx-mode ctx-mode-btn" onclick="cycleChannelAiMode('${ac.id}')" title="Click to change AI mode">${esc(modeLabel)}</button>`;
|
||||
html = `<button class="ctx-mode ctx-mode-btn" data-action="cycleChannelAiMode" data-args='${JSON.stringify([ac.id])}' title="Click to change AI mode">${esc(modeLabel)}</button>`;
|
||||
if (topic) html += `<span class="ctx-topic">${esc(topic)}</span>`;
|
||||
html += `<span style="flex:1;"></span>`;
|
||||
html += `<button class="ctx-participants-btn" onclick="openParticipantsPanel('${ac.id}')" title="Manage participants">👥 Members</button>`;
|
||||
html += `<button class="ctx-participants-btn" data-action="openParticipantsPanel" data-args='${JSON.stringify([ac.id])}' title="Manage participants">👥 Members</button>`;
|
||||
}
|
||||
el.innerHTML = html;
|
||||
el.style.display = '';
|
||||
@@ -910,8 +946,8 @@ const UI = {
|
||||
let actions = '';
|
||||
if (status === 'active' && API.isAdmin) {
|
||||
actions =
|
||||
'<button class="btn-small btn-primary" onclick="advanceWorkflowStage()">Advance ▸</button>' +
|
||||
(current > 0 ? '<button class="btn-small btn-danger" onclick="rejectWorkflowStage()">◂ Reject</button>' : '');
|
||||
'<button class="btn-small btn-primary" data-action="advanceWorkflowStage"">Advance ▸</button>' +
|
||||
(current > 0 ? '<button class="btn-small btn-danger" data-action="rejectWorkflowStage"">◂ Reject</button>' : '');
|
||||
}
|
||||
|
||||
el.innerHTML =
|
||||
@@ -942,8 +978,8 @@ const UI = {
|
||||
textEl.innerHTML = `
|
||||
<textarea class="msg-edit-input" id="editInput_${messageId}">${escaped}</textarea>
|
||||
<div class="msg-edit-actions">
|
||||
<button class="msg-edit-cancel" onclick="cancelEdit()">Cancel</button>
|
||||
<button class="msg-edit-submit" onclick="submitEdit('${messageId}', document.getElementById('editInput_${messageId}').value)">Submit & Regenerate</button>
|
||||
<button class="msg-edit-cancel" data-action="cancelEdit">Cancel</button>
|
||||
<button class="msg-edit-submit" data-action="_submitEditFromDOM" data-args='${JSON.stringify([messageId])}'">Submit & Regenerate</button>
|
||||
</div>`;
|
||||
|
||||
// Focus and select all
|
||||
@@ -1604,9 +1640,15 @@ const UI = {
|
||||
}
|
||||
};
|
||||
|
||||
// ── Body-level delegation for data-action buttons ──
|
||||
// Wired once, handles all data-action clicks across sidebar, messages, etc.
|
||||
_uiDispatch(document.body);
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.UI = UI;
|
||||
window.renderPersonaForm = renderPersonaForm;
|
||||
// onclick handler (called from innerHTML)
|
||||
window.toggleSummarizedHistory = toggleSummarizedHistory;
|
||||
sb.ns('UI', UI);
|
||||
sb.register('renderPersonaForm', renderPersonaForm);
|
||||
sb.register('_uiDispatch', _uiDispatch);
|
||||
sb.register('_toggleArchivedProjects', _toggleArchivedProjects);
|
||||
sb.register('_submitEditFromDOM', _submitEditFromDOM);
|
||||
sb.register('toggleSummarizedHistory', toggleSummarizedHistory);
|
||||
})();
|
||||
|
||||
@@ -179,7 +179,7 @@ function _formatMarked(text) {
|
||||
const decoded = _decodeHTML(code);
|
||||
const handled = Extensions.runBlockRenderers(lang, decoded, fakeContainer);
|
||||
if (handled && fakeContainer.innerHTML) {
|
||||
const popBtn = `<button class="ext-popout-btn" onclick="popOutExtBlock('${codeId}')" title="Open in side panel">⧉</button>`;
|
||||
const popBtn = `<button class="ext-popout-btn" data-action="popOutExtBlock" data-args='${JSON.stringify([codeId])}'" title="Open in side panel">⧉</button>`;
|
||||
return `<div class="ext-rendered" data-ext-block="${codeId}" data-ext-lang="${lang}">${popBtn}${fakeContainer.innerHTML}</div>`;
|
||||
}
|
||||
if (handled && !fakeContainer.innerHTML) {
|
||||
@@ -197,16 +197,16 @@ function _formatMarked(text) {
|
||||
|
||||
// Build toolbar buttons
|
||||
const collapseBtn = lineCount > 5
|
||||
? `<button class="code-collapse-btn" onclick="toggleCodeCollapse('${codeId}')" title="${shouldCollapse ? 'Expand' : 'Collapse'}">${shouldCollapse ? '▸' : '▾'} ${lineCount} lines</button>`
|
||||
? `<button class="code-collapse-btn" data-action="toggleCodeCollapse" data-args='${JSON.stringify([codeId])}'" title="${shouldCollapse ? 'Expand' : 'Collapse'}">${shouldCollapse ? '▸' : '▾'} ${lineCount} lines</button>`
|
||||
: '';
|
||||
|
||||
let toolbar = collapseBtn;
|
||||
toolbar += `<button class="copy-code-btn" onclick="navigator.clipboard.writeText(document.getElementById('${codeId}').textContent).then(()=>UI.toast('Copied','success'))">Copy</button>`;
|
||||
toolbar += `<button class="copy-code-btn" onclick="downloadCode('${codeId}','${lang}')">Download</button>`;
|
||||
toolbar += `<button class="copy-code-btn" data-action="_copyCodeBlock" data-args='${JSON.stringify([codeId])}'">Copy</button>`;
|
||||
toolbar += `<button class="copy-code-btn" data-action="downloadCode" data-args='${JSON.stringify([codeId, lang])}'">Download</button>`;
|
||||
|
||||
// HTML preview button (only for HTML-like content)
|
||||
if (lang === 'html' || lang === 'htm' || (!lang && _looksLikeHTML(code))) {
|
||||
toolbar += `<button class="copy-code-btn preview-code-btn" onclick="toggleHTMLPreview('${codeId}')">Preview</button>`;
|
||||
toolbar += `<button class="copy-code-btn preview-code-btn" data-action="toggleHTMLPreview" data-args='${JSON.stringify([codeId])}'">Preview</button>`;
|
||||
}
|
||||
|
||||
const langLabel = lang ? `<span class="code-lang">${lang}</span>` : '';
|
||||
@@ -226,15 +226,15 @@ function _formatBasic(text) {
|
||||
const shouldCollapse = lineCount > COLLAPSE_THRESHOLD;
|
||||
|
||||
const collapseBtn = lineCount > 5
|
||||
? `<button class="code-collapse-btn" onclick="toggleCodeCollapse('${id}')" title="${shouldCollapse ? 'Expand' : 'Collapse'}">${shouldCollapse ? '▸' : '▾'} ${lineCount} lines</button>`
|
||||
? `<button class="code-collapse-btn" data-action="toggleCodeCollapse" data-args='${JSON.stringify([id])}'" title="${shouldCollapse ? 'Expand' : 'Collapse'}">${shouldCollapse ? '▸' : '▾'} ${lineCount} lines</button>`
|
||||
: '';
|
||||
|
||||
let toolbar = collapseBtn;
|
||||
toolbar += `<button class="copy-code-btn" onclick="navigator.clipboard.writeText(document.getElementById('${id}').textContent).then(()=>UI.toast('Copied','success'))">Copy</button>`;
|
||||
toolbar += `<button class="copy-code-btn" onclick="downloadCode('${id}','${lang}')">Download</button>`;
|
||||
toolbar += `<button class="copy-code-btn" data-action="_copyCodeBlock" data-args='${JSON.stringify([id])}'">Copy</button>`;
|
||||
toolbar += `<button class="copy-code-btn" data-action="downloadCode" data-args='${JSON.stringify([id, lang])}'">Download</button>`;
|
||||
|
||||
if (lang === 'html' || lang === 'htm' || (!lang && _looksLikeHTML(code))) {
|
||||
toolbar += `<button class="copy-code-btn preview-code-btn" onclick="toggleHTMLPreview('${id}')">Preview</button>`;
|
||||
toolbar += `<button class="copy-code-btn preview-code-btn" data-action="toggleHTMLPreview" data-args='${JSON.stringify([id])}'">Preview</button>`;
|
||||
}
|
||||
|
||||
const langLabel = lang ? `<span class="code-lang">${lang}</span>` : '';
|
||||
@@ -531,7 +531,7 @@ function _renderToolCallsHTML(toolCalls) {
|
||||
try {
|
||||
const parsed = typeof result === 'string' ? JSON.parse(result) : result;
|
||||
if (parsed.id) {
|
||||
noteLink = `<button class="tool-note-link" onclick="openNotes(); setTimeout(() => openNoteEditor('${esc(parsed.id)}'), 300)">📝 View</button>`;
|
||||
noteLink = `<button class="tool-note-link" data-action="_openNoteFromTool" data-args='${JSON.stringify([esc(parsed.id)])}'">📝 View</button>`;
|
||||
}
|
||||
} catch (e) { /* not JSON */ }
|
||||
}
|
||||
@@ -584,18 +584,30 @@ function runExtensionPostRender(container) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Wrappers for compound onclick expressions ──
|
||||
function _copyCodeBlock(codeId) {
|
||||
const el = document.getElementById(codeId);
|
||||
if (el) navigator.clipboard.writeText(el.textContent).then(() => UI.toast('Copied', 'success'));
|
||||
}
|
||||
function _openNoteFromTool(noteId) {
|
||||
openNotes();
|
||||
setTimeout(() => openNoteEditor(noteId), 300);
|
||||
}
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
// Cross-file function calls
|
||||
window.formatMessage = formatMessage;
|
||||
window.clearPreview = clearPreview;
|
||||
window.runExtensionPostRender = runExtensionPostRender;
|
||||
window._livePreviewUpdate = _livePreviewUpdate;
|
||||
window._relativeTime = _relativeTime;
|
||||
window._renderToolCallsHTML = _renderToolCallsHTML;
|
||||
window._registerPreviewPanel = _registerPreviewPanel;
|
||||
// onclick handlers (called from innerHTML)
|
||||
window.toggleCodeCollapse = toggleCodeCollapse;
|
||||
window.toggleHTMLPreview = toggleHTMLPreview;
|
||||
window.downloadCode = downloadCode;
|
||||
window.popOutExtBlock = popOutExtBlock;
|
||||
sb.register('formatMessage', formatMessage);
|
||||
sb.register('clearPreview', clearPreview);
|
||||
sb.register('runExtensionPostRender', runExtensionPostRender);
|
||||
sb.register('_livePreviewUpdate', _livePreviewUpdate);
|
||||
sb.register('_relativeTime', _relativeTime);
|
||||
sb.register('_renderToolCallsHTML', _renderToolCallsHTML);
|
||||
sb.register('_registerPreviewPanel', _registerPreviewPanel);
|
||||
// data-action handlers
|
||||
sb.register('toggleCodeCollapse', toggleCodeCollapse);
|
||||
sb.register('toggleHTMLPreview', toggleHTMLPreview);
|
||||
sb.register('downloadCode', downloadCode);
|
||||
sb.register('popOutExtBlock', popOutExtBlock);
|
||||
sb.register('_copyCodeBlock', _copyCodeBlock);
|
||||
sb.register('_openNoteFromTool', _openNoteFromTool);
|
||||
})();
|
||||
|
||||
@@ -1116,21 +1116,21 @@ function mountAvatarUpload(containerEl, opts = {}) {
|
||||
}
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.esc = esc;
|
||||
window.createComponentRegistry = createComponentRegistry;
|
||||
window.componentMixin = componentMixin;
|
||||
window.Providers = Providers;
|
||||
window.Roles = Roles;
|
||||
window.renderCapBadges = renderCapBadges;
|
||||
window.renderProviderForm = renderProviderForm;
|
||||
window.renderProviderList = renderProviderList;
|
||||
window.renderRoleConfig = renderRoleConfig;
|
||||
window.renderUsageDashboard = renderUsageDashboard;
|
||||
window.showConfirm = showConfirm;
|
||||
window.showPrompt = showPrompt;
|
||||
window.openModal = openModal;
|
||||
window.closeModal = closeModal;
|
||||
window.checkTabsOverflow = checkTabsOverflow;
|
||||
window.Theme = Theme;
|
||||
window.mountAvatarUpload = mountAvatarUpload;
|
||||
sb.register('esc', esc);
|
||||
sb.register('createComponentRegistry', createComponentRegistry);
|
||||
sb.register('componentMixin', componentMixin);
|
||||
sb.ns('Providers', Providers);
|
||||
sb.ns('Roles', Roles);
|
||||
sb.register('renderCapBadges', renderCapBadges);
|
||||
sb.register('renderProviderForm', renderProviderForm);
|
||||
sb.register('renderProviderList', renderProviderList);
|
||||
sb.register('renderRoleConfig', renderRoleConfig);
|
||||
sb.register('renderUsageDashboard', renderUsageDashboard);
|
||||
sb.register('showConfirm', showConfirm);
|
||||
sb.register('showPrompt', showPrompt);
|
||||
sb.register('openModal', openModal);
|
||||
sb.register('closeModal', closeModal);
|
||||
sb.register('checkTabsOverflow', checkTabsOverflow);
|
||||
sb.ns('Theme', Theme);
|
||||
sb.register('mountAvatarUpload', mountAvatarUpload);
|
||||
})();
|
||||
|
||||
@@ -329,7 +329,7 @@ Object.assign(UI, {
|
||||
}
|
||||
section.style.display = '';
|
||||
el.innerHTML = teams.map(t => `
|
||||
<div class="team-card"${t.my_role === 'admin' ? ` style="cursor:pointer" onclick="UI.openTeamManage('${t.id}', '${esc(t.name)}')"` : ''}>
|
||||
<div class="team-card"${t.my_role === 'admin' ? ` style="cursor:pointer" data-action="UI.openTeamManage" data-args='${JSON.stringify([t.id, esc(t.name)])}'"` : ''}>
|
||||
<div class="team-card-info">
|
||||
<strong>${esc(t.name)}</strong>
|
||||
<span class="badge-${t.my_role === 'admin' ? 'admin' : 'user'}">${t.my_role}</span>
|
||||
@@ -362,7 +362,7 @@ Object.assign(UI, {
|
||||
const adminTeams = (UI._myTeams || []).filter(t => t.my_role === 'admin');
|
||||
const titleEl = document.getElementById('teamAdminTitle');
|
||||
if (adminTeams.length > 1) {
|
||||
titleEl.innerHTML = `<span class="team-admin-back" onclick="UI.showTeamPicker()" title="Back to teams">←</span> ${esc(teamName)}`;
|
||||
titleEl.innerHTML = `<span class="team-admin-back" data-action="UI.showTeamPicker" " title="Back to teams">←</span> ${esc(teamName)}`;
|
||||
} else {
|
||||
titleEl.textContent = teamName;
|
||||
}
|
||||
@@ -409,7 +409,7 @@ Object.assign(UI, {
|
||||
</select>
|
||||
</td>
|
||||
<td class="admin-actions-cell">
|
||||
<button class="icon-btn icon-btn-danger" onclick="settingsRemoveTeamMember('${teamId}','${m.id}','${esc(m.email)}')" title="Remove">🗑</button>
|
||||
<button class="icon-btn icon-btn-danger" data-action="settingsRemoveTeamMember" data-args='${JSON.stringify([teamId, m.id, esc(m.email)])}'" title="Remove">🗑</button>
|
||||
</td>
|
||||
</tr>`).join('')}</tbody>
|
||||
</table>
|
||||
@@ -521,7 +521,7 @@ Object.assign(UI, {
|
||||
<strong>${icon ? icon + ' ' : ''}${esc(p.name)}</strong>
|
||||
<div class="persona-meta" style="font-size:11px">${esc(p.base_model_id)}</div>
|
||||
</div>
|
||||
<button class="btn-delete" onclick="settingsDeleteTeamPersona('${teamId}','${p.id}','${esc(p.name)}')" title="Delete">✕</button>
|
||||
<button class="btn-delete" data-action="settingsDeleteTeamPersona" data-args='${JSON.stringify([teamId, p.id, esc(p.name)])}'" title="Delete">✕</button>
|
||||
</div>`;
|
||||
}).join('') || '<div class="empty-hint">No team personas — create one to give your team preconfigured models</div>';
|
||||
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
|
||||
@@ -812,8 +812,8 @@ Object.assign(UI, {
|
||||
let html = `<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
||||
<span class="text-muted" style="font-size:12px">${visibleCount} visible, ${hiddenCount} hidden of ${models.length} total</span>
|
||||
<div style="flex:1"></div>
|
||||
<button class="btn-small" onclick="bulkSetUserModelVisibility(true)">Show All</button>
|
||||
<button class="btn-small" onclick="bulkSetUserModelVisibility(false)">Hide All</button>
|
||||
<button class="btn-small" data-action="bulkSetUserModelVisibility" data-args='[true]'">Show All</button>
|
||||
<button class="btn-small" data-action="bulkSetUserModelVisibility" data-args='[false]'">Hide All</button>
|
||||
</div>`;
|
||||
|
||||
html += models.map(m => {
|
||||
@@ -832,7 +832,7 @@ Object.assign(UI, {
|
||||
<span class="model-caps-inline">${badges}</span>
|
||||
<span class="model-provider">${esc(m.provider_name || m.provider || '')}</span>
|
||||
${src}
|
||||
<button class="admin-model-toggle ${toggleCls}" onclick="toggleUserModelVisibility('${esc(mid)}', '${esc(cfgId)}', ${hidden})" title="${hidden ? 'Show in selector' : 'Hide from selector'}">${toggleLabel}</button>
|
||||
<button class="admin-model-toggle ${toggleCls}" data-action="toggleUserModelVisibility" data-args='${JSON.stringify([esc(mid), esc(cfgId), hidden])}'" title="${hidden ? 'Show in selector' : 'Hide from selector'}">${toggleLabel}</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
el.innerHTML = html;
|
||||
@@ -858,7 +858,7 @@ Object.assign(UI, {
|
||||
<div class="persona-meta">${esc(p.base_model_id)} · ${esc(p.provider_name || 'auto')}</div>
|
||||
</div>
|
||||
<div class="persona-actions">
|
||||
<button class="btn-delete" onclick="deleteUserPersona('${p.id}', '${esc(p.name)}')" title="Delete">✕</button>
|
||||
<button class="btn-delete" data-action="deleteUserPersona" data-args='${JSON.stringify([p.id, esc(p.name)])}'" title="Delete">✕</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
@@ -138,5 +138,5 @@ const UserMenu = {
|
||||
};
|
||||
|
||||
// ── Exports ─────────────────────────────────
|
||||
window.UserMenu = UserMenu;
|
||||
sb.ns('UserMenu', UserMenu);
|
||||
})();
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
|
||||
// ── Loader (called from ADMIN_LOADERS.workflows) ──
|
||||
|
||||
window._loadAdminWorkflows = async function() {
|
||||
sb.register('_loadAdminWorkflows', async function() {
|
||||
// Self-scaffold: inject our HTML into adminDynamic
|
||||
var target = document.getElementById('adminDynamic');
|
||||
if (target && !document.getElementById('wfAdminList')) {
|
||||
@@ -148,18 +148,18 @@
|
||||
list.innerHTML =
|
||||
'<div class="empty-hint">No workflows yet</div>' +
|
||||
'<button class="btn-small btn-primary" style="margin-top:8px" ' +
|
||||
'onclick="_wfCreate()">+ Create Workflow</button>';
|
||||
'data-action="_wfCreate">+ Create Workflow</button>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">' +
|
||||
'<span class="text-muted">' + wfs.length + ' workflow(s)</span>' +
|
||||
'<button class="btn-small btn-primary" onclick="_wfCreate()">+ New</button></div>';
|
||||
'<button class="btn-small btn-primary" data-action="_wfCreate">+ New</button></div>';
|
||||
html += '<table class="admin-table"><thead><tr>' +
|
||||
'<th>Name</th><th>Slug</th><th>Entry</th><th>Active</th><th>Version</th>' +
|
||||
'</tr></thead><tbody>';
|
||||
for (const wf of wfs) {
|
||||
html += '<tr style="cursor:pointer" onclick="_wfOpen(\'' + wf.id + '\')">' +
|
||||
html += '<tr style="cursor:pointer" data-action="_wfOpen" data-args=\'' + JSON.stringify([wf.id]) + '\'>' +
|
||||
'<td>' + esc(wf.name) + '</td>' +
|
||||
'<td><code>' + esc(wf.slug) + '</code></td>' +
|
||||
'<td>' + esc(wf.entry_mode || 'authenticated') + '</td>' +
|
||||
@@ -172,11 +172,11 @@
|
||||
} catch (e) {
|
||||
list.innerHTML = '<div class="empty-hint">Failed to load: ' + esc(e.message) + '</div>';
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// ── Create ──────────────────────────────
|
||||
|
||||
window._wfCreate = async function() {
|
||||
sb.register('_wfCreate', async function() {
|
||||
const name = prompt('Workflow name:');
|
||||
if (!name) return;
|
||||
try {
|
||||
@@ -186,11 +186,11 @@
|
||||
} catch (e) {
|
||||
UI.toast('Failed: ' + e.message, 'error');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// ── Open Detail ─────────────────────────
|
||||
|
||||
window._wfOpen = async function(id) {
|
||||
sb.register('_wfOpen', async function(id) {
|
||||
const list = document.getElementById('wfAdminList');
|
||||
const detail = document.getElementById('wfAdminDetail');
|
||||
try {
|
||||
@@ -227,7 +227,7 @@
|
||||
} catch (e) {
|
||||
UI.toast('Failed to load workflow: ' + e.message, 'error');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// ── Stage List ──────────────────────────
|
||||
|
||||
@@ -255,8 +255,8 @@
|
||||
'<span class="wf-stage-name">' + esc(s.name) + '</span>' +
|
||||
personaLabel +
|
||||
'<span class="badge">' + (s.history_mode || 'full') + '</span>' +
|
||||
'<button class="btn-small" onclick="_wfEditStage(\'' + s.id + '\')">Edit</button>' +
|
||||
'<button class="btn-small btn-danger" onclick="_wfDeleteStage(\'' + s.id + '\')">×</button>' +
|
||||
'<button class="btn-small" data-action="_wfEditStage" data-args=\'' + JSON.stringify([s.id]) + '\'>Edit</button>' +
|
||||
'<button class="btn-small btn-danger" data-action="_wfDeleteStage" data-args=\'' + JSON.stringify([s.id]) + '\'>×</button>' +
|
||||
'</div>';
|
||||
});
|
||||
el.innerHTML = html;
|
||||
@@ -437,7 +437,7 @@
|
||||
|
||||
// ── Edit / Delete Stage ─────────────────
|
||||
|
||||
window._wfEditStage = async function(stageId) {
|
||||
sb.register('_wfEditStage', async function(stageId) {
|
||||
if (!_currentWf) return;
|
||||
try {
|
||||
const resp = await API.listStages(_currentWf.id);
|
||||
@@ -456,9 +456,9 @@
|
||||
} catch (e) {
|
||||
UI.toast('Failed: ' + e.message, 'error');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
window._wfDeleteStage = async function(stageId) {
|
||||
sb.register('_wfDeleteStage', async function(stageId) {
|
||||
if (!_currentWf) return;
|
||||
if (!confirm('Delete this stage?')) return;
|
||||
try {
|
||||
@@ -468,6 +468,6 @@
|
||||
} catch (e) {
|
||||
UI.toast('Failed: ' + e.message, 'error');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.WorkflowQueue = {
|
||||
const WorkflowQueue = {
|
||||
|
||||
_loaded: false,
|
||||
_myAssignments: [],
|
||||
@@ -28,7 +28,7 @@
|
||||
section.className = 'sb-section';
|
||||
section.id = 'sbSectionQueue';
|
||||
section.innerHTML =
|
||||
'<div class="sb-section-header" onclick="UI.toggleSidebarSection(\'queue\')">' +
|
||||
'<div class="sb-section-header" data-action="UI.toggleSidebarSection" data-args=\'["queue"]\'>' +
|
||||
'<span class="sb-section-arrow" id="sbArrowQueue">▾</span>' +
|
||||
'<span class="sb-section-label">Workflows</span>' +
|
||||
'<span class="sb-queue-badge" id="sbQueueBadge" style="display:none">0</span>' +
|
||||
@@ -79,7 +79,7 @@
|
||||
workflows.forEach(function(wf) {
|
||||
var isActive = App.activeId === wf.id;
|
||||
html += '<div class="sb-queue-item' + (isActive ? ' active' : '') + '" ' +
|
||||
'onclick="selectChannel(\'' + wf.id + '\')" ' +
|
||||
'data-action="selectChannel" data-args=\'' + JSON.stringify([wf.id]) + '\' ' +
|
||||
'title="' + esc(wf.title) + '">' +
|
||||
'<span class="sb-queue-icon">⚡</span>' +
|
||||
'<span class="sb-queue-label">' + esc(wf.title || 'Workflow') + '</span>' +
|
||||
@@ -93,10 +93,10 @@
|
||||
if (alreadyShown) return;
|
||||
|
||||
html += '<div class="sb-queue-item" ' +
|
||||
'onclick="WorkflowQueue.openAssignment(\'' + a.id + '\',\'' + a.channel_id + '\')">' +
|
||||
'data-action="WorkflowQueue.openAssignment" data-args=\'' + JSON.stringify([a.id, a.channel_id]) + '\'>' +
|
||||
'<span class="sb-queue-icon">📋</span>' +
|
||||
'<span class="sb-queue-label">Assignment: Stage ' + a.stage + '</span>' +
|
||||
'<button class="btn-small" onclick="event.stopPropagation();WorkflowQueue.complete(\'' + a.id + '\')">✓</button>' +
|
||||
'<button class="btn-small" data-action="WorkflowQueue.complete" data-args=\'' + JSON.stringify([a.id]) + '\'>✓</button>' +
|
||||
'</div>';
|
||||
});
|
||||
}
|
||||
@@ -158,7 +158,7 @@
|
||||
uItems.forEach(function(a) {
|
||||
html += '<tr><td>Stage ' + a.stage + '</td>' +
|
||||
'<td>' + new Date(a.created_at).toLocaleDateString() + '</td>' +
|
||||
'<td><button class="btn-small btn-primary" onclick="WorkflowQueue.claim(\'' + a.id + '\')">Claim</button></td></tr>';
|
||||
'<td><button class="btn-small btn-primary" data-action="WorkflowQueue.claim" data-args=\'' + JSON.stringify([a.id]) + '\'>Claim</button></td></tr>';
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
}
|
||||
@@ -169,7 +169,7 @@
|
||||
cItems.forEach(function(a) {
|
||||
html += '<tr><td>Stage ' + a.stage + '</td>' +
|
||||
'<td>' + (a.assigned_to || '—') + '</td>' +
|
||||
'<td><button class="btn-small" onclick="WorkflowQueue.complete(\'' + a.id + '\')">Complete</button></td></tr>';
|
||||
'<td><button class="btn-small" data-action="WorkflowQueue.complete" data-args=\'' + JSON.stringify([a.id]) + '\'>Complete</button></td></tr>';
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
}
|
||||
@@ -187,6 +187,7 @@
|
||||
}
|
||||
},
|
||||
};
|
||||
sb.ns('WorkflowQueue', WorkflowQueue);
|
||||
|
||||
// Auto-init when chat surface loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
Reference in New Issue
Block a user