Upload 2 new, 10 modified files from chat-switchboard-v0.7.4.zip
This commit is contained in:
181
src/js/app.js
181
src/js/app.js
@@ -858,6 +858,13 @@ function initListeners() {
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Sidebar chat search
|
||||
const _chatSearchInput = document.getElementById('chatSearchInput');
|
||||
_chatSearchInput?.addEventListener('input', () => UI.renderChatList());
|
||||
document.getElementById('chatSearchClear')?.addEventListener('click', () => {
|
||||
if (_chatSearchInput) { _chatSearchInput.value = ''; UI.renderChatList(); _chatSearchInput.focus(); }
|
||||
});
|
||||
|
||||
// Chat actions
|
||||
document.getElementById('sendBtn').addEventListener('click', sendMessage);
|
||||
document.getElementById('stopBtn').addEventListener('click', stopGeneration);
|
||||
@@ -1097,11 +1104,38 @@ function initListeners() {
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Escape: stop generation → close command palette → close topmost modal
|
||||
if (e.key === 'Escape') {
|
||||
if (App.isGenerating) { stopGeneration(); return; }
|
||||
// Close topmost open modal
|
||||
if (document.getElementById('cmdPalette')?.classList.contains('active')) {
|
||||
closeCmdPalette(); return;
|
||||
}
|
||||
const open = [...document.querySelectorAll('.modal-overlay.active')];
|
||||
if (open.length) closeModal(open[open.length - 1].id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl/Cmd+K: command palette
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
toggleCmdPalette();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl/Cmd+Shift+N: new chat
|
||||
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'N') {
|
||||
e.preventDefault();
|
||||
newChat();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl/Cmd+Shift+S: focus sidebar search
|
||||
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'S') {
|
||||
e.preventDefault();
|
||||
const sb = document.getElementById('sidebar');
|
||||
if (sb.classList.contains('collapsed')) UI.toggleSidebar();
|
||||
document.getElementById('chatSearchInput')?.focus();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1111,6 +1145,151 @@ function initListeners() {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Command Palette ──────────────────────────
|
||||
|
||||
const _cmdCommands = [
|
||||
// Navigation
|
||||
{ id: 'new-chat', label: 'New Chat', group: 'Navigation', hint: '⌘⇧N', icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>', action: () => newChat() },
|
||||
{ id: 'search-chats', label: 'Search Chats', group: 'Navigation', hint: '⌘⇧S', icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>', action: () => { const sb = document.getElementById('sidebar'); if (sb.classList.contains('collapsed')) UI.toggleSidebar(); document.getElementById('chatSearchInput')?.focus(); } },
|
||||
{ id: 'toggle-sidebar', label: 'Toggle Sidebar', group: 'Navigation', hint: '', icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>', action: () => UI.toggleSidebar() },
|
||||
|
||||
// Tools
|
||||
{ id: 'notes', label: 'Open Notes', group: 'Tools', hint: '', icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>', action: () => openNotes() },
|
||||
{ id: 'export-md', label: 'Export Chat (Markdown)', group: 'Tools', hint: '', icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>', action: () => UI.exportChat('md') },
|
||||
{ id: 'export-json', label: 'Export Chat (JSON)', group: 'Tools', hint: '', icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>', action: () => UI.exportChat('json') },
|
||||
|
||||
// Settings
|
||||
{ id: 'settings', label: 'Settings', group: 'Settings', hint: '', icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>', action: () => UI.openSettings() },
|
||||
{ id: 'admin', label: 'Admin Panel', group: 'Settings', hint: '', icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>', action: () => UI.openAdmin(), visible: () => API.user?.role === 'admin' },
|
||||
{ id: 'debug', label: 'Debug Log', group: 'Settings', hint: '⌘⇧D', icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 11V6a2 2 0 0 0-2-2h-1a2 2 0 0 0-2 2"/><path d="M9 6a2 2 0 0 0-2 2v3"/><circle cx="12" cy="14" r="6"/></svg>', action: () => openDebugModal() },
|
||||
|
||||
// Account
|
||||
{ id: 'sign-out', label: 'Sign Out', group: 'Account', hint: '', icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>', action: () => handleLogout() },
|
||||
];
|
||||
|
||||
let _cmdActiveIndex = 0;
|
||||
|
||||
function toggleCmdPalette() {
|
||||
const el = document.getElementById('cmdPalette');
|
||||
if (el.classList.contains('active')) { closeCmdPalette(); return; }
|
||||
openCmdPalette();
|
||||
}
|
||||
|
||||
function openCmdPalette() {
|
||||
const el = document.getElementById('cmdPalette');
|
||||
const input = document.getElementById('cmdInput');
|
||||
el.classList.add('active');
|
||||
input.value = '';
|
||||
_cmdActiveIndex = 0;
|
||||
_renderCmdResults('');
|
||||
input.focus();
|
||||
|
||||
// Wire up input events (use named handler to avoid duplicates)
|
||||
input.oninput = () => { _cmdActiveIndex = 0; _renderCmdResults(input.value); };
|
||||
input.onkeydown = _handleCmdKey;
|
||||
|
||||
// Close on overlay click
|
||||
el.onclick = (e) => { if (e.target === el) closeCmdPalette(); };
|
||||
}
|
||||
|
||||
function closeCmdPalette() {
|
||||
const el = document.getElementById('cmdPalette');
|
||||
el.classList.remove('active');
|
||||
document.getElementById('cmdInput').oninput = null;
|
||||
document.getElementById('cmdInput').onkeydown = null;
|
||||
}
|
||||
|
||||
function _handleCmdKey(e) {
|
||||
const results = document.getElementById('cmdResults');
|
||||
const items = results.querySelectorAll('.cmd-item');
|
||||
if (!items.length) return;
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
_cmdActiveIndex = (_cmdActiveIndex + 1) % items.length;
|
||||
_highlightCmdItem(items);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
_cmdActiveIndex = (_cmdActiveIndex - 1 + items.length) % items.length;
|
||||
_highlightCmdItem(items);
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
items[_cmdActiveIndex]?.click();
|
||||
}
|
||||
}
|
||||
|
||||
function _highlightCmdItem(items) {
|
||||
items.forEach((it, i) => it.classList.toggle('active', i === _cmdActiveIndex));
|
||||
items[_cmdActiveIndex]?.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
|
||||
function _getVisibleCommands() {
|
||||
return _cmdCommands.filter(c => !c.visible || c.visible());
|
||||
}
|
||||
|
||||
function _renderCmdResults(query) {
|
||||
const el = document.getElementById('cmdResults');
|
||||
const q = query.trim().toLowerCase();
|
||||
let commands = _getVisibleCommands();
|
||||
|
||||
// Add recent chats as commands when searching
|
||||
if (q) {
|
||||
const chatMatches = App.chats
|
||||
.filter(c => (c.title || '').toLowerCase().includes(q))
|
||||
.slice(0, 5)
|
||||
.map(c => ({
|
||||
id: 'chat-' + c.id,
|
||||
label: c.title || 'Untitled',
|
||||
group: 'Chats',
|
||||
hint: _relativeTime(c.updatedAt),
|
||||
icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
|
||||
action: () => selectChat(c.id),
|
||||
}));
|
||||
commands = [...chatMatches, ...commands];
|
||||
}
|
||||
|
||||
// Filter by query
|
||||
if (q) {
|
||||
commands = commands.filter(c => c.label.toLowerCase().includes(q));
|
||||
}
|
||||
|
||||
if (commands.length === 0) {
|
||||
el.innerHTML = '<div class="cmd-group-label" style="padding:16px 12px;text-transform:none;letter-spacing:0">No matching commands</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Clamp active index
|
||||
_cmdActiveIndex = Math.min(_cmdActiveIndex, commands.length - 1);
|
||||
|
||||
// Render grouped
|
||||
let html = '';
|
||||
let lastGroup = '';
|
||||
commands.forEach((c, i) => {
|
||||
if (c.group !== lastGroup) {
|
||||
lastGroup = c.group;
|
||||
html += `<div class="cmd-group-label">${c.group}</div>`;
|
||||
}
|
||||
html += `<div class="cmd-item${i === _cmdActiveIndex ? ' active' : ''}" data-cmd="${c.id}">
|
||||
${c.icon}
|
||||
<span class="cmd-item-label">${c.label}</span>
|
||||
${c.hint ? `<span class="cmd-item-hint">${c.hint}</span>` : ''}
|
||||
</div>`;
|
||||
});
|
||||
el.innerHTML = html;
|
||||
|
||||
// Attach click handlers
|
||||
el.querySelectorAll('.cmd-item').forEach((item, i) => {
|
||||
item.addEventListener('click', () => {
|
||||
closeCmdPalette();
|
||||
commands[i].action();
|
||||
});
|
||||
item.addEventListener('mouseenter', () => {
|
||||
_cmdActiveIndex = i;
|
||||
_highlightCmdItem(el.querySelectorAll('.cmd-item'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Settings Handlers ────────────────────────
|
||||
|
||||
function handleSaveSettings() {
|
||||
|
||||
Reference in New Issue
Block a user