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.icon}${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 @@
Cortex · Local
- - - - ? - 👤 -
- -
+ +
@@ -79,6 +88,12 @@
+ +
@@ -116,23 +131,20 @@
+ +
+ + +
+ + +
-
- -
- - -
- - - - - - +
diff --git a/cortex/static/style.css b/cortex/static/style.css index ed7f801..de77d8e 100644 --- a/cortex/static/style.css +++ b/cortex/static/style.css @@ -110,6 +110,7 @@ align-items: center; gap: 12px; position: relative; + flex-wrap: wrap; } .header-emoji { @@ -188,11 +189,57 @@ padding: 5px 10px; cursor: pointer; transition: border-color 0.15s, color 0.15s; + white-space: nowrap; } .hdr-btn:hover { border-color: var(--muted); color: var(--text); } - #sessions-btn { margin-left: auto; } + /* Right-side nav group — pushed to the end */ + #hdr-nav { + display: flex; + align-items: center; + gap: 8px; + margin-left: auto; + } + + /* Settings dropdown */ + .hdr-dropdown-wrap { position: relative; } + + .hdr-dropdown { + display: none; + position: absolute; + top: calc(100% + 6px); + right: 0; + min-width: 170px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0,0,0,0.4); + z-index: 200; + overflow: hidden; + } + .hdr-dropdown.open { display: block; } + + .hdr-dd-item { + display: block; + width: 100%; + text-align: left; + padding: 0.55rem 0.85rem; + color: var(--text); + text-decoration: none; + font-size: 0.85rem; + background: none; + border: none; + cursor: pointer; + transition: background 0.1s; + box-sizing: border-box; + } + .hdr-dd-item:hover { background: var(--border); } + + .hdr-dd-divider { + border-top: 1px solid var(--border); + margin: 0.25rem 0; + } /* Sessions panel */ #sessions-panel { @@ -448,16 +495,103 @@ .message.note-private .note-content { color: #c9a84c; white-space: pre-wrap; } .message.note-public .note-content { color: #4abfb0; white-space: pre-wrap; } - /* ── Input area ────────────────────────────────────────────── */ + /* ── Input area — 3-col: [mode-toggle] [textarea] [send-col] ── */ #input-area { - padding: 14px 20px; + padding: 12px 20px; background: var(--surface); border-top: 1px solid var(--border); display: flex; + flex-direction: row; gap: 10px; align-items: flex-end; } + /* ── Mode select — compact dropdown ─────────────────────────── */ + #mode-select { + position: relative; + flex-shrink: 0; + display: flex; + flex-direction: column; + align-items: stretch; + gap: 4px; + } + + #mode-select-btn { + display: flex; + align-items: center; + gap: 7px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--muted); + padding: 8px 11px; + cursor: pointer; + font-size: 0.85rem; + white-space: nowrap; + transition: border-color 0.15s, color 0.15s; + } + #mode-select-btn:hover { border-color: var(--muted); color: var(--text); } + #mode-select-btn.mode-note { border-color: rgba(180,130,40,0.6); color: #c9a84c; } + #mode-select-btn.mode-otr { border-color: rgba(120,80,160,0.6); color: #a87fd4; } + #mode-select-btn.mode-agent { border-color: rgba(80,140,200,0.6); color: #7cb9e8; } + + #mode-icon { font-size: 1rem; line-height: 1; } + .mode-arrow { font-size: 0.55rem; color: var(--muted); margin-left: 2px; opacity: 0.5; } + + /* Dropdown — opens upward; MRU at bottom = closest to button */ + #mode-dropdown { + display: none; + position: absolute; + bottom: calc(100% + 4px); + left: 0; + min-width: 100%; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + box-shadow: 0 -4px 16px var(--shadow); + z-index: 100; + overflow: hidden; + } + #mode-dropdown.open { display: block; } + + .mode-option { + display: flex; + align-items: center; + gap: 8px; + padding: 0.5rem 0.85rem; + cursor: pointer; + font-size: 0.85rem; + color: var(--muted); + border: none; + background: none; + width: 100%; + text-align: left; + transition: background 0.1s, color 0.1s; + } + .mode-option:hover { background: var(--border); color: var(--text); } + .mode-option.current { color: var(--text); font-weight: 500; } + .mode-option .opt-icon { font-size: 1rem; line-height: 1; } + .mode-option .opt-check { margin-left: auto; font-size: 0.7rem; opacity: 0.7; } + + /* Note visibility sub-button — shown below mode-select when note is active */ + #note-vis-btn { + display: none; + background: var(--bg); + border: 1px solid rgba(180,130,40,0.35); + border-radius: 6px; + color: rgba(180,130,40,0.75); + font-size: 0.7rem; + padding: 4px 8px; + cursor: pointer; + text-align: center; + transition: opacity 0.15s; + } + #note-vis-btn:hover { opacity: 0.75; } + #note-vis-btn.pub { + border-color: rgba(40,170,150,0.35); + color: rgba(40,170,150,0.75); + } + #input { flex: 1; background: var(--bg); @@ -475,121 +609,21 @@ #input:focus { outline: none; border-color: var(--muted); } - #input.note-mode { border-color: rgba(180, 130, 40, 0.55); } - #input.note-mode:focus { border-color: rgba(180, 130, 40, 0.85); } - #input.note-mode.public { border-color: rgba(40, 170, 150, 0.55); } - #input.note-mode.public:focus { border-color: rgba(40, 170, 150, 0.85); } + #input.mode-note { border-color: rgba(180,130,40,0.55); } + #input.mode-note:focus { border-color: rgba(180,130,40,0.85); } + #input.mode-note.public { border-color: rgba(40,170,150,0.55); } + #input.mode-note.public:focus { border-color: rgba(40,170,150,0.85); } + #input.mode-otr { border-color: rgba(120,80,160,0.4); background: rgba(120,80,160,0.04); } + #input.mode-agent { border-color: rgba(80,140,200,0.4); } - /* Right column — all controls stacked, fixed width */ - #right-col { + /* Send column — right side, stacked */ + #send-col { display: flex; flex-direction: column; align-items: stretch; gap: 4px; flex-shrink: 0; - width: 88px; - } - - /* Semi-hidden controls: height selector row */ - #height-row { - display: none; /* shown by JS when content > 3 lines */ - align-items: center; - gap: 4px; - } - - #height-row span { - font-size: 0.65rem; - color: var(--muted); - flex-shrink: 0; - } - - #height-sel { - flex: 1; - background: var(--bg); - border: 1px solid var(--border); - border-radius: 5px; - color: var(--muted); - font-size: 0.65rem; - padding: 2px 4px; - cursor: pointer; - min-width: 0; - } - - #height-sel:focus { outline: none; border-color: var(--muted); } - - /* Semi-hidden: enter-mode toggle */ - #enter-toggle { - display: none; /* shown by JS when content > 3 lines */ - background: var(--bg); - border: 1px solid var(--border); - border-radius: 5px; - color: var(--muted); - font-size: 0.68rem; - padding: 3px 6px; - cursor: pointer; - text-align: center; - transition: border-color 0.15s, color 0.15s; - } - - #enter-toggle:hover { border-color: var(--muted); color: var(--text); } - - /* Note type toggle — only visible in note mode */ - #note-type-btn { - display: none; - background: var(--bg); - border: 1px solid rgba(180, 130, 40, 0.4); - border-radius: 5px; - color: rgba(180, 130, 40, 0.85); - font-size: 0.68rem; - padding: 3px 6px; - cursor: pointer; - text-align: center; - transition: opacity 0.15s; - } - - #note-type-btn.public { - border-color: rgba(40, 170, 150, 0.4); - color: rgba(40, 170, 150, 0.85); - } - - #note-type-btn:hover { opacity: 0.75; } - - /* Note button */ - #note-btn { - background: var(--bg); - border: 1px solid var(--border); - color: var(--muted); - border-radius: 8px; - padding: 8px 0; - cursor: pointer; - font-size: 0.85rem; - text-align: center; - transition: border-color 0.15s, color 0.15s; - } - - #note-btn:hover { border-color: var(--muted); color: var(--text); } - #note-btn.active { border-color: rgba(180, 130, 40, 0.6); color: #c9a84c; } - #note-btn.active.public { border-color: rgba(40, 170, 150, 0.6); color: #4abfb0; } - - /* OTR button */ - #otr-btn { - background: var(--bg); - border: 1px solid var(--border); - color: var(--muted); - border-radius: 8px; - padding: 8px 0; - cursor: pointer; - font-size: 0.85rem; - text-align: center; - transition: border-color 0.15s, color 0.15s; - } - #otr-btn:hover { border-color: var(--muted); color: var(--text); } - #otr-btn.active { border-color: rgba(120, 80, 160, 0.6); color: #a87fd4; } - - /* OTR textarea styling */ - #input.otr-mode { - border-color: rgba(120, 80, 160, 0.4); - background: rgba(120, 80, 160, 0.04); + width: 80px; } /* Send button */ @@ -1116,12 +1150,11 @@ @media (max-width: 520px) { header { padding: 8px 12px; gap: 8px; } header .subtitle { display: none; } + .btn-label { display: none; } - /* Persona dropdown: avoid clipping off left edge on narrow screens */ + /* Persona dropdown: avoid clipping off left edge */ .persona-dropdown { left: 0; right: auto; min-width: 140px; } - /* Logout button: keep visible but compact */ - #logout-btn { padding: 5px 8px; font-size: 1rem; } #messages { padding: 12px; } /* dvh adjusts as soft keyboard opens/closes */ @@ -1130,32 +1163,49 @@ /* Hide session ID — saves vertical space */ #session-id { display: none; } - /* Input area: stack textarea above button row */ + /* + * Footer on mobile: textarea on top (full width), then + * mode-toggle + send-col side by side below. + * flex-wrap + order:-1 achieves this without a wrapper div. + */ #input-area { - flex-direction: column; - align-items: stretch; + flex-wrap: wrap; + align-items: center; padding: 8px 12px; gap: 6px; } - /* 16px minimum prevents iOS Safari auto-zoom on focus */ - #input { font-size: 16px; } - - /* Right col goes horizontal, full width */ - #right-col { - flex-direction: row; + /* Textarea floats to top row, full width */ + #input { + order: -1; width: 100%; + flex-basis: 100%; + font-size: 16px; /* prevent iOS Safari auto-zoom */ + } + + /* Mode select: row layout (btn left, note-vis right) */ + #mode-select { + flex-direction: row; + flex: 1; + align-items: center; + } + #mode-select-btn { flex: 1; justify-content: center; } + + /* Note vis button sits to the right of the mode btn on mobile */ + #note-vis-btn { margin-top: 0; } + + /* Dropdown still opens upward on mobile */ + #mode-dropdown { min-width: 140px; } + + /* Send col: horizontal in bottom row */ + #send-col { + flex-direction: row; + width: auto; gap: 6px; } - /* Desktop-only controls — hide on mobile */ - #height-row, - #enter-toggle { display: none !important; } - /* Larger touch targets */ - #send, #stop { padding: 12px 0; flex: 1; font-size: 1rem; } - #note-btn { padding: 12px 14px; } - #note-type-btn { padding: 6px 10px; } + #send, #stop { padding: 12px 14px; font-size: 1rem; } } /* ── Touch devices — no hover capability ─────────────────── */