diff --git a/cortex/static/app.js b/cortex/static/app.js index dc134db..4d63623 100644 --- a/cortex/static/app.js +++ b/cortex/static/app.js @@ -6,13 +6,16 @@ const backendToggle = document.getElementById('backend-toggle'); const sessionsBtn = document.getElementById('sessions-btn'); const sessionsPanel = document.getElementById('sessions-panel'); - const heightRow = document.getElementById('height-row'); const heightSel = document.getElementById('height-sel'); const enterToggle = document.getElementById('enter-toggle'); - const noteTypeBtnEl = document.getElementById('note-type-btn'); - const noteBtnEl = document.getElementById('note-btn'); - const agentModeBtnEl = document.getElementById('agent-mode-btn'); - const stopBtn = document.getElementById('stop'); + const stopBtn = document.getElementById('stop'); + const mode_select_btn_el = document.getElementById('mode-select-btn'); + const mode_dropdown_el = document.getElementById('mode-dropdown'); + const mode_icon_el = document.getElementById('mode-icon'); + const mode_label_el = document.getElementById('mode-label'); + const note_vis_btn_el = document.getElementById('note-vis-btn'); + const settings_btn_el = document.getElementById('settings-btn'); + const settings_dd_el = document.getElementById('settings-dropdown'); // User/persona injected by the server at /{user}/{persona} const CORTEX_USER = (window.CORTEX_CONFIG || {}).user || 'scott'; @@ -55,10 +58,6 @@ inputEl.style.maxHeight = maxHeight + 'px'; const sh = inputEl.scrollHeight; inputEl.style.height = Math.min(sh, maxHeight) + 'px'; - // Show semi-hidden controls when content exceeds ~3 lines or a larger max is set - const showExtras = sh > 80 || maxHeight > 120; - heightRow.style.display = showExtras ? 'flex' : 'none'; - enterToggle.style.display = showExtras ? 'block' : 'none'; } heightSel.value = String(maxHeight); @@ -68,88 +67,106 @@ syncHeight(); }); - // ── Agent mode ─────────────────────────────────────────────── - let agentMode = localStorage.getItem('agentMode') === 'true'; + // ── Input mode — dropdown select with MRU ordering ────────── + const MODES = { + chat: { icon: '💬', label: 'Chat' }, + note: { icon: '📝', label: 'Note' }, + otr: { icon: '🔒', label: 'OTR' }, + agent: { icon: '⚡', label: 'Agent' }, + }; + const send_labels = { chat: 'Send', note: 'Add Note', otr: 'Send', agent: 'Run' }; - function updateAgentModeUI() { - agentModeBtnEl.classList.toggle('active', agentMode); - updateInputPlaceholder(); - if (agentMode) sendBtn.textContent = 'Run'; - else if (!noteMode) sendBtn.textContent = 'Send'; + let current_mode = localStorage.getItem('current_mode') || 'chat'; + let note_public = false; + // MRU list — most recent first; used to sort dropdown options + let mode_mru = JSON.parse(localStorage.getItem('mode_mru') || '["chat","note","otr","agent"]'); + + function push_mru(mode) { + mode_mru = [mode, ...mode_mru.filter(m => m !== mode)]; + localStorage.setItem('mode_mru', JSON.stringify(mode_mru)); } - agentModeBtnEl.addEventListener('click', () => { - agentMode = !agentMode; - if (agentMode) { otr_mode = false; update_otr_ui(); } - localStorage.setItem('agentMode', agentMode); - updateAgentModeUI(); + function set_mode(mode) { + push_mru(mode); + current_mode = mode; + localStorage.setItem('current_mode', current_mode); + close_mode_dropdown(); + update_mode_ui(); inputEl.focus(); - }); - - // ── Off the record mode ────────────────────────────────────── - let otr_mode = false; - const otr_btn_el = document.getElementById('otr-btn'); - - function update_otr_ui() { - otr_btn_el.classList.toggle('active', otr_mode); - inputEl.classList.toggle('otr-mode', otr_mode); - updateInputPlaceholder(); } - otr_btn_el.addEventListener('click', () => { - otr_mode = !otr_mode; - if (otr_mode) { - agentMode = false; - noteMode = false; - localStorage.setItem('agentMode', false); - updateAgentModeUI(); - updateInputMode(); - } - update_otr_ui(); - inputEl.focus(); + function open_mode_dropdown() { + // Build options in MRU order (least recent at top, most recent at bottom) + // — bottom is visually closest to the button since dropdown opens upward + const ordered = [...mode_mru].reverse(); + mode_dropdown_el.innerHTML = ''; + ordered.forEach(mode => { + const m = MODES[mode]; + const btn = document.createElement('button'); + btn.className = 'mode-option' + (mode === current_mode ? ' current' : ''); + btn.innerHTML = + `${m.label}` + + (mode === current_mode ? '✓' : ''); + btn.addEventListener('click', () => set_mode(mode)); + mode_dropdown_el.appendChild(btn); + }); + mode_dropdown_el.classList.add('open'); + } + + function close_mode_dropdown() { + mode_dropdown_el.classList.remove('open'); + } + + mode_select_btn_el.addEventListener('click', (e) => { + e.stopPropagation(); + if (mode_dropdown_el.classList.contains('open')) close_mode_dropdown(); + else open_mode_dropdown(); }); - // ── Note mode ──────────────────────────────────────────────── - let noteMode = false; - let notePublic = false; - - function updateInputMode() { - if (noteMode) { - noteBtnEl.classList.add('active'); - noteTypeBtnEl.style.display = 'block'; - sendBtn.textContent = 'Add Note'; - inputEl.classList.add('note-mode'); - if (notePublic) { - inputEl.classList.add('public'); - noteBtnEl.classList.add('public'); - noteTypeBtnEl.textContent = 'public'; - noteTypeBtnEl.classList.add('public'); - } else { - inputEl.classList.remove('public'); - noteBtnEl.classList.remove('public'); - noteTypeBtnEl.textContent = 'private'; - noteTypeBtnEl.classList.remove('public'); - } - } else { - noteBtnEl.classList.remove('active', 'public'); - noteTypeBtnEl.style.display = 'none'; - sendBtn.textContent = agentMode ? 'Run' : 'Send'; - inputEl.classList.remove('note-mode', 'public'); + document.addEventListener('click', (e) => { + if (!mode_dropdown_el.contains(e.target) && e.target !== mode_select_btn_el) { + close_mode_dropdown(); } + }); + + function update_mode_ui() { + const m = MODES[current_mode]; + + // Update trigger button + mode_icon_el.textContent = m.icon; + mode_label_el.textContent = m.label; + mode_select_btn_el.className = current_mode === 'chat' + ? '' : `mode-${current_mode}`; + + // Note visibility sub-button + const is_note = current_mode === 'note'; + note_vis_btn_el.style.display = is_note ? '' : 'none'; + note_vis_btn_el.textContent = note_public ? 'pub' : 'prv'; + note_vis_btn_el.classList.toggle('pub', note_public); + + // Textarea mode classes + inputEl.classList.toggle('mode-note', current_mode === 'note'); + inputEl.classList.toggle('public', current_mode === 'note' && note_public); + inputEl.classList.toggle('mode-otr', current_mode === 'otr'); + inputEl.classList.toggle('mode-agent', current_mode === 'agent'); + + // Send button label + sendBtn.textContent = send_labels[current_mode] || 'Send'; + updateInputPlaceholder(); } function updateInputPlaceholder() { - if (noteMode) { - inputEl.placeholder = notePublic + if (current_mode === 'note') { + inputEl.placeholder = note_public ? 'Public note — LLM sees this next turn…' : 'Private note — only you see this…'; - } else if (agentMode) { + } else if (current_mode === 'agent') { inputEl.placeholder = ctrlEnterMode ? `Task for ${personaLabel}… (Gemini tool loop — Ctrl+Enter to run)` : `Task for ${personaLabel}… (Gemini tool loop)`; - } else if (otr_mode) { - inputEl.placeholder = `Off the record — not logged or distilled…`; + } else if (current_mode === 'otr') { + inputEl.placeholder = 'Off the record — not logged or distilled…'; } else { inputEl.placeholder = ctrlEnterMode ? `Message ${personaLabel}… (Ctrl+Enter to send)` @@ -157,16 +174,22 @@ } } - noteBtnEl.addEventListener('click', () => { - noteMode = !noteMode; - if (noteMode) { otr_mode = false; update_otr_ui(); } - updateInputMode(); - inputEl.focus(); + // Note private/public sub-toggle + note_vis_btn_el.addEventListener('click', (e) => { + e.stopPropagation(); + note_public = !note_public; + update_mode_ui(); }); - noteTypeBtnEl.addEventListener('click', () => { - notePublic = !notePublic; - updateInputMode(); + // ── Settings dropdown ───────────────────────────────────────── + settings_btn_el.addEventListener('click', (e) => { + e.stopPropagation(); + settings_dd_el.classList.toggle('open'); + }); + document.addEventListener('click', (e) => { + if (!settings_dd_el.contains(e.target) && e.target !== settings_btn_el) { + settings_dd_el.classList.remove('open'); + } }); // ── Persona name + switcher ────────────────────────────────── @@ -657,7 +680,7 @@ inputEl.value = ''; syncHeight(); - if (!notePublic) { + if (!note_public) { // Private: UI only, never sent to backend addMessage('note-private', text); return; @@ -717,7 +740,7 @@ include_long: memLong, include_mid: memMid, include_short: memShort, - off_record: otr_mode, + off_record: current_mode === 'otr', user: CORTEX_USER, persona: CORTEX_PERSONA, }), @@ -882,8 +905,8 @@ } function dispatchSend() { - if (noteMode) addNote(); - else if (agentMode) sendOrchestrate(); + if (current_mode === 'note') addNote(); + else if (current_mode === 'agent') sendOrchestrate(); else sendMessage(); } @@ -960,7 +983,10 @@ await loadFile(fileSelect.value); } - filesBtn.addEventListener('click', openFileModal); + filesBtn.addEventListener('click', () => { + settings_dd_el.classList.remove('open'); + openFileModal(); + }); fileSelect.addEventListener('change', () => loadFile(fileSelect.value)); @@ -1203,7 +1229,7 @@ updateTierUI(); updateMemUI(); - updateAgentModeUI(); + update_mode_ui(); // ── Init ───────────────────────────────────────────────────── updateEnterToggleUI(); diff --git a/cortex/static/index.html b/cortex/static/index.html index b707335..8a60ca2 100644 --- a/cortex/static/index.html +++ b/cortex/static/index.html @@ -27,14 +27,23 @@