Changeset 0.23.0 (#153)
This commit is contained in:
178
src/js/chat.js
178
src/js/chat.js
@@ -185,7 +185,7 @@ async function toggleChannelAutoCompact(enabled) {
|
||||
|
||||
async function loadChats() {
|
||||
try {
|
||||
const resp = await API.listChannels(1, 100, 'direct');
|
||||
const resp = await API.listChannels(1, 100);
|
||||
App.chats = (resp.data || []).map(c => ({
|
||||
id: c.id,
|
||||
title: c.title,
|
||||
@@ -404,6 +404,123 @@ async function newChat() {
|
||||
window.history.replaceState(null, '', `${window.__BASE__ || ''}/`);
|
||||
}
|
||||
|
||||
// ── New Group Chat (v0.23.0) ─────────────────
|
||||
// Creates a group channel and adds persona participants.
|
||||
// Personas can @mention each other → AI-to-AI chaining.
|
||||
|
||||
async function newGroupChat() {
|
||||
const personas = (App.models || []).filter(m => m.isPersona);
|
||||
if (personas.length === 0) {
|
||||
UI.toast('No personas available. Ask your admin to create one.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'confirm-overlay';
|
||||
|
||||
const personaCheckboxes = personas.map(p => {
|
||||
const scope = p.personaScope || 'global';
|
||||
const badge = scope === 'personal' ? ' <span class="dd-hint">personal</span>'
|
||||
: scope === 'team' ? ` <span class="dd-hint">${esc(p.personaTeamName || 'team')}</span>` : '';
|
||||
const handle = p.personaHandle ? ` <span class="group-persona-handle">@${esc(p.personaHandle)}</span>` : '';
|
||||
return `<label class="group-persona-item">
|
||||
<input type="checkbox" value="${esc(p.personaId)}" data-name="${esc(p.name)}" data-handle="${esc(p.personaHandle || '')}">
|
||||
<span class="group-persona-name">${esc(p.name)}${handle}${badge}</span>
|
||||
<span class="group-persona-model">${esc(p.baseModelId || '')}</span>
|
||||
</label>`;
|
||||
}).join('');
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div class="confirm-dialog" style="max-width:480px">
|
||||
<div class="confirm-header">New Group Chat</div>
|
||||
<div class="confirm-body">
|
||||
<div style="margin-bottom:12px">
|
||||
<label style="font-size:12px;color:var(--text-secondary);display:block;margin-bottom:4px">Channel Name</label>
|
||||
<input id="groupChatTitle" type="text" class="settings-input" placeholder="e.g. Code Review Team" style="width:100%;box-sizing:border-box" autofocus>
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:12px;color:var(--text-secondary);display:block;margin-bottom:6px">Add Personas (select at least one)</label>
|
||||
<div class="group-persona-list" style="max-height:200px;overflow-y:auto;display:flex;flex-direction:column;gap:4px">
|
||||
${personaCheckboxes}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="confirm-footer">
|
||||
<button class="btn-small" data-action="cancel">Cancel</button>
|
||||
<button class="btn-small btn-primary" data-action="ok">Create</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
function close(result) {
|
||||
overlay.remove();
|
||||
document.removeEventListener('keydown', onKey);
|
||||
resolve(result);
|
||||
}
|
||||
|
||||
function onKey(e) {
|
||||
if (e.key === 'Escape') close(false);
|
||||
}
|
||||
|
||||
overlay.querySelector('[data-action="cancel"]').addEventListener('click', () => close(false));
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(false); });
|
||||
document.addEventListener('keydown', onKey);
|
||||
|
||||
overlay.querySelector('[data-action="ok"]').addEventListener('click', async () => {
|
||||
const title = overlay.querySelector('#groupChatTitle').value.trim() || 'Group Chat';
|
||||
const checked = [...overlay.querySelectorAll('.group-persona-list input:checked')];
|
||||
if (checked.length === 0) {
|
||||
UI.toast('Select at least one persona', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create group channel
|
||||
const resp = await API.createChannel(title, null, null, 'group');
|
||||
const chat = {
|
||||
id: resp.id, title: resp.title, type: 'group', model: null,
|
||||
messages: [], messageCount: 0, projectId: resp.project_id || null,
|
||||
workspace_id: resp.workspace_id || null, updatedAt: resp.updated_at,
|
||||
settings: resp.settings || {}
|
||||
};
|
||||
|
||||
// Add persona participants
|
||||
for (const cb of checked) {
|
||||
try {
|
||||
await API.addParticipant(chat.id, {
|
||||
participant_type: 'persona',
|
||||
participant_id: cb.value,
|
||||
role: 'member'
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Failed to add persona:', cb.dataset.name, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
App.chats.unshift(chat);
|
||||
App.currentChatId = chat.id;
|
||||
UI.renderChatList();
|
||||
Events.emit('chat.created', { chatId: chat.id, projectId: null }, { localOnly: true });
|
||||
window.history.replaceState(null, '', `${window.__BASE__ || ''}/chat/${chat.id}`);
|
||||
|
||||
// Load the channel model roster (populated by participant auto-add)
|
||||
if (typeof ChannelModels !== 'undefined') ChannelModels.load(chat.id);
|
||||
|
||||
UI.showEmptyState();
|
||||
const names = checked.map(cb => cb.dataset.name).join(', ');
|
||||
UI.toast(`Group created with ${checked.length} persona${checked.length > 1 ? 's' : ''}: ${names}`, 'success');
|
||||
ChatInput.focus();
|
||||
} catch (e) {
|
||||
UI.toast('Failed to create group: ' + e.message, 'error');
|
||||
}
|
||||
close(true);
|
||||
});
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
overlay.querySelector('#groupChatTitle').focus();
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteChat(chatId) {
|
||||
if (!await showConfirm('Delete this chat?')) return;
|
||||
try {
|
||||
@@ -908,6 +1025,65 @@ function _initChatListeners() {
|
||||
// Multi-model channel roster (v0.20.0)
|
||||
if (typeof ChannelModels !== 'undefined') ChannelModels.init();
|
||||
|
||||
// AI-to-AI chain: handle messages arriving via WebSocket (v0.23.0)
|
||||
// When a persona @mentions another persona, the follow-up completion
|
||||
// runs server-side and delivers the result via WS, not SSE.
|
||||
Events.on('message.created', (payload) => {
|
||||
if (!payload || !payload.channel_id) return;
|
||||
if (payload.channel_id !== App.currentChatId) return;
|
||||
|
||||
const chat = App.chats.find(c => c.id === payload.channel_id);
|
||||
if (!chat) return;
|
||||
|
||||
// Append the chained message
|
||||
chat.messages.push({
|
||||
id: payload.id,
|
||||
parent_id: null,
|
||||
role: payload.role || 'assistant',
|
||||
content: payload.content || '',
|
||||
model: payload.model || '',
|
||||
modelName: payload.display_name || payload.model || '',
|
||||
timestamp: new Date().toISOString(),
|
||||
tool_calls: null,
|
||||
metadata: payload.chain_depth ? { chain_depth: payload.chain_depth } : null,
|
||||
siblingCount: 1,
|
||||
siblingIndex: 0,
|
||||
participant_type: payload.participant_type,
|
||||
participant_id: payload.participant_id,
|
||||
});
|
||||
|
||||
// Remove typing indicator and re-render
|
||||
document.getElementById('typingIndicator')?.remove();
|
||||
UI.renderMessages(chat.messages);
|
||||
});
|
||||
|
||||
// Typing indicators from persona chain activity
|
||||
Events.on('typing.start', (payload) => {
|
||||
if (!payload || payload.channel_id !== App.currentChatId) return;
|
||||
if (payload.participant_type !== 'persona') return;
|
||||
|
||||
// Show typing indicator with persona name
|
||||
document.getElementById('typingIndicator')?.remove();
|
||||
const container = document.getElementById('chatMessages');
|
||||
if (!container) return;
|
||||
const typing = document.createElement('div');
|
||||
typing.id = 'typingIndicator';
|
||||
typing.className = 'message assistant';
|
||||
const name = payload.display_name || 'AI';
|
||||
typing.innerHTML = `
|
||||
<div class="msg-row">
|
||||
<div class="msg-avatar"><span class="avatar-emoji">⚡</span></div>
|
||||
<div class="msg-body"><span class="chain-typing-name">${name}</span> <div class="typing-dots"><span></span><span></span><span></span></div></div>
|
||||
</div>`;
|
||||
container.appendChild(typing);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
});
|
||||
|
||||
Events.on('typing.stop', (payload) => {
|
||||
if (!payload || payload.channel_id !== App.currentChatId) return;
|
||||
document.getElementById('typingIndicator')?.remove();
|
||||
});
|
||||
|
||||
// Close modals on overlay click
|
||||
document.querySelectorAll('.modal-overlay').forEach(overlay => {
|
||||
overlay.addEventListener('click', (e) => {
|
||||
|
||||
Reference in New Issue
Block a user