Changeset 0.23.2 (#155)
This commit is contained in:
@@ -12,7 +12,7 @@ function avatarHTML(role, avatarDataURI) {
|
||||
if (avatarDataURI) {
|
||||
return `<img src="${avatarDataURI}" alt="" class="msg-avatar-img">`;
|
||||
}
|
||||
return role === 'user' ? '👤' : '🤖';
|
||||
return role === 'user' ? '👤' : role === 'other-user' ? '👤' : '🤖';
|
||||
}
|
||||
|
||||
// Look up the current model/persona avatar for assistant messages.
|
||||
@@ -394,7 +394,7 @@ const UI = {
|
||||
|
||||
let html = '';
|
||||
channels.forEach(ch => {
|
||||
const isActive = App.currentChannelId === ch.id;
|
||||
const isActive = App.activeId === ch.id;
|
||||
const isDM = ch.type === 'dm';
|
||||
const isChannel = ch.type === 'channel';
|
||||
const online = App.presence?.[ch.dmPartnerId] === 'online';
|
||||
@@ -413,9 +413,11 @@ const UI = {
|
||||
|
||||
html += `<div class="sb-channel-item${isActive ? ' active' : ''}"
|
||||
onclick="selectChannel('${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}` : unreadBadge}
|
||||
${!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}
|
||||
</div>`;
|
||||
});
|
||||
|
||||
@@ -470,18 +472,21 @@ const UI = {
|
||||
|
||||
let html = '';
|
||||
|
||||
// Folder groups — always render, even when empty
|
||||
// Folder groups — always render, even when empty (with drag handlers)
|
||||
folders.forEach(f => {
|
||||
const items = folderMap[f.id] || [];
|
||||
const isOpen = !App.collapsedFolders?.[f.id];
|
||||
html += `<div class="sb-folder-group">
|
||||
html += `<div class="sb-folder-group"
|
||||
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}')">
|
||||
<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>
|
||||
<span class="sb-folder-arrow">${isOpen ? '▾' : '▸'}</span>
|
||||
<button class="sb-folder-menu" onclick="event.stopPropagation();showFolderMenu(event,'${f.id}')" title="Folder options">⋯</button>
|
||||
<span class="sb-folder-arrow">${isOpen ? '▾' : '▸'}</span>
|
||||
</div>
|
||||
${isOpen
|
||||
? (items.length > 0
|
||||
@@ -503,6 +508,17 @@ const UI = {
|
||||
else groups.older.push(c);
|
||||
});
|
||||
|
||||
// When folders exist, wrap unfiled chats in a drop zone
|
||||
if (folders.length > 0) {
|
||||
html += `<div class="sb-unfiled-zone"
|
||||
ondragover="event.preventDefault();event.dataTransfer.dropEffect='move';this.classList.add('drag-over')"
|
||||
ondragleave="this.classList.remove('drag-over')"
|
||||
ondrop="onUnfiledDrop(event)">`;
|
||||
if (unfiled.length === 0) {
|
||||
html += '<div class="sb-unfiled-hint">Drop here to unfile</div>';
|
||||
}
|
||||
}
|
||||
|
||||
const renderTimeGroup = (label, items) => {
|
||||
if (!items.length) return;
|
||||
html += `<div class="chat-group-label">${label}</div>`;
|
||||
@@ -514,6 +530,10 @@ const UI = {
|
||||
renderTimeGroup('Previous 7 days', groups.week);
|
||||
renderTimeGroup('Older', groups.older);
|
||||
|
||||
if (folders.length > 0) {
|
||||
html += '</div>'; // close .sb-unfiled-zone
|
||||
}
|
||||
|
||||
el.innerHTML = html;
|
||||
|
||||
// Always keep projects section in sync
|
||||
@@ -528,7 +548,7 @@ const UI = {
|
||||
|
||||
_chatItemHTML(c, indented = false) {
|
||||
const time = _relativeTime(c.updatedAt);
|
||||
const active = c.id === App.currentChatId ? ' active' : '';
|
||||
const active = c.id === App.activeId ? ' active' : '';
|
||||
const indent = indented ? ' indented' : '';
|
||||
const typeIcon = c.type === 'group'
|
||||
? '<span class="chat-type-icon" title="Group Chat">👥</span> '
|
||||
@@ -617,19 +637,40 @@ const UI = {
|
||||
// Skip summary messages in normal rendering — they get _summaryHTML
|
||||
if (_isSummaryMessage(msg)) return '';
|
||||
const time = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
|
||||
let assistantLabel = 'Assistant';
|
||||
if (!isUser) {
|
||||
// Use stored modelName, or resolve from channel roster, or model list, or fall back
|
||||
if (msg.modelName) {
|
||||
assistantLabel = msg.modelName;
|
||||
|
||||
// v0.23.2: Sender attribution — distinguish self from other humans
|
||||
const isSelf = !isUser ? false
|
||||
: (!msg.participant_id || msg.participant_id === API.user?.id);
|
||||
const isOtherUser = isUser && !isSelf;
|
||||
|
||||
let senderLabel = 'You';
|
||||
let senderAvatarURI = API.user?.avatar || null;
|
||||
|
||||
if (isOtherUser) {
|
||||
senderLabel = msg.sender_name || 'User';
|
||||
senderAvatarURI = msg.sender_avatar || null;
|
||||
} else if (!isUser) {
|
||||
// Assistant message — use model/persona label
|
||||
senderLabel = 'Assistant';
|
||||
senderAvatarURI = assistantAvatarURI(msg);
|
||||
if (msg.sender_name) {
|
||||
senderLabel = msg.sender_name;
|
||||
} else if (msg.modelName) {
|
||||
senderLabel = msg.modelName;
|
||||
} else if (typeof ChannelModels !== 'undefined' && msg.model) {
|
||||
assistantLabel = ChannelModels.resolveDisplayName(msg.model);
|
||||
senderLabel = ChannelModels.resolveDisplayName(msg.model);
|
||||
} else if (msg.model) {
|
||||
const m = App.models.find(x => x.id === msg.model || x.baseModelId === msg.model);
|
||||
assistantLabel = m?.name || msg.model;
|
||||
senderLabel = m?.name || msg.model;
|
||||
}
|
||||
if (msg.sender_avatar) {
|
||||
senderAvatarURI = msg.sender_avatar;
|
||||
}
|
||||
}
|
||||
|
||||
// CSS class: self = right-aligned "user", other humans = left-aligned "other-user", AI = "assistant"
|
||||
const roleClass = isSelf ? 'user' : isOtherUser ? 'other-user' : msg.role;
|
||||
|
||||
// Branch indicator: show ‹ 2/3 › when siblings exist
|
||||
const hasSiblings = (msg.siblingCount || 1) > 1;
|
||||
const sibPos = (msg.siblingIndex || 0) + 1; // display as 1-indexed
|
||||
@@ -645,16 +686,16 @@ const UI = {
|
||||
|
||||
// Action buttons
|
||||
const msgId = msg.id ? esc(msg.id) : '';
|
||||
const editBtn = isUser && msgId ? `<button class="msg-action-btn" onclick="editMessage('${msgId}')" title="Edit">Edit</button>` : '';
|
||||
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>` : '';
|
||||
|
||||
return `
|
||||
<div class="message ${msg.role}${dimmed ? ' msg-dimmed' : ''}" data-msg-id="${msgId}">
|
||||
<div class="message ${roleClass}${dimmed ? ' msg-dimmed' : ''}" data-msg-id="${msgId}">
|
||||
<div class="msg-inner">
|
||||
<div class="msg-avatar">${avatarHTML(msg.role, isUser ? API.user?.avatar : assistantAvatarURI(msg))}</div>
|
||||
<div class="msg-avatar">${avatarHTML(isOtherUser ? 'other-user' : msg.role, senderAvatarURI)}</div>
|
||||
<div class="msg-body">
|
||||
<div class="msg-head">
|
||||
<span class="msg-role">${isUser ? 'You' : esc(assistantLabel)}</span>
|
||||
<span class="msg-role">${esc(senderLabel)}</span>
|
||||
${branchNav}
|
||||
${time ? `<span class="msg-time">${time}</span>` : ''}
|
||||
<div class="msg-actions">
|
||||
@@ -680,6 +721,45 @@ const UI = {
|
||||
<h2>Chat Switchboard</h2>
|
||||
<p>Select a model and start chatting</p>
|
||||
</div>`;
|
||||
this.updateContextBanner();
|
||||
},
|
||||
|
||||
// v0.23.2: Show/hide context banner based on active conversation type
|
||||
updateContextBanner() {
|
||||
const el = document.getElementById('channelContextBanner');
|
||||
if (!el) return;
|
||||
const ac = App.activeConversation;
|
||||
if (!ac || ac.type === 'direct') {
|
||||
el.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
if (ac.type === 'dm') {
|
||||
const ch = (App.channels || []).find(c => c.id === ac.id);
|
||||
const name = ch?.name || 'someone';
|
||||
html = `<span class="ctx-mode">DM</span>` +
|
||||
`<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>`;
|
||||
} 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>`;
|
||||
} 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>`;
|
||||
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>`;
|
||||
}
|
||||
el.innerHTML = html;
|
||||
el.style.display = '';
|
||||
},
|
||||
|
||||
showEditInline(messageId, currentContent) {
|
||||
@@ -1278,7 +1358,7 @@ const UI = {
|
||||
// ── Export ────────────────────────────────
|
||||
|
||||
exportChat(format) {
|
||||
const chat = App.chats.find(c => c.id === App.currentChatId);
|
||||
const chat = App.getActiveChat();
|
||||
if (!chat) return UI.toast('No chat to export', 'warning');
|
||||
|
||||
let content, ext, mime;
|
||||
@@ -1302,7 +1382,7 @@ const UI = {
|
||||
// ── Message Actions ──────────────────────
|
||||
|
||||
copyMessage(index) {
|
||||
const chat = App.chats.find(c => c.id === App.currentChatId);
|
||||
const chat = App.getActiveChat();
|
||||
if (chat?.messages[index]) {
|
||||
navigator.clipboard.writeText(chat.messages[index].content)
|
||||
.then(() => UI.toast('Copied', 'success'))
|
||||
|
||||
Reference in New Issue
Block a user