Changeset 0.28.3.1 (#188)
This commit is contained in:
@@ -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);
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user