|
|
|
|
|
|
|
|
| import Storage from './js/storage.js';
|
| import uiModule from './js/ui.js';
|
| import workspaceModule from './js/workspace.js';
|
| import fileHandlerModule from './js/fileHandler.js';
|
| import modelsModule from './js/models.js';
|
| import ragModule from './js/rag.js';
|
| import presetsModule from './js/presets.js';
|
| import searchModule from './js/search.js';
|
| import chatModule from './js/chat.js';
|
| import compareModule from './js/compare/index.js';
|
| import documentModule from './js/document.js';
|
| import searchChatModule from './js/search-chat.js';
|
| import { makeWindowDraggable } from './js/windowDrag.js';
|
| import markdownModule from './js/markdown.js';
|
| import chatRenderer from './js/chatRenderer.js';
|
| import sessionModule from './js/sessions.js';
|
| import memoryModule from './js/memory.js';
|
| import voiceRecorderModule from './js/voiceRecorder.js';
|
| import censorModule from './js/censor.js';
|
| import galleryModule from './js/gallery.js';
|
| import tasksModule from './js/tasks.js';
|
| import calendarModule from './js/calendar.js';
|
| import notesModule from './js/notes.js';
|
| import adminModule from './js/admin.js';
|
| import settingsModule from './js/settings.js';
|
|
|
| import './js/modalManager.js';
|
|
|
| import './js/tileManager.js';
|
| import themeModule from './js/theme.js';
|
|
|
|
|
|
|
|
|
|
|
| import cookbookModule from './js/cookbook.js';
|
| import groupModule from './js/group.js';
|
| import * as researchPanelModule from './js/research/panel.js';
|
| import ttsModule from './js/tts-ai.js';
|
| import spinnerModule from './js/spinner.js';
|
| import { initKeyboardShortcuts } from './js/keyboard-shortcuts.js';
|
| import { initSidebarLayout, syncRailSide } from './js/sidebar-layout.js';
|
| import { initSectionCollapse, initSectionDrag } from './js/section-management.js';
|
|
|
| const API_BASE = window.location.origin;
|
| window.themeModule = themeModule;
|
| window.sessionModule = sessionModule;
|
| window.uiModule = uiModule;
|
| window.adminModule = adminModule;
|
| window.cookbookModule = cookbookModule;
|
|
|
|
|
| const _origFetch = window.fetch;
|
| window.fetch = async function(...args) {
|
| const res = await _origFetch.apply(this, args);
|
| if (res.status === 401 && !String(args[0]).includes('/api/auth/')) {
|
| window.location.href = '/login';
|
| }
|
| return res;
|
| };
|
|
|
|
|
|
|
|
|
| const el = uiModule.el;
|
|
|
|
|
|
|
|
|
| let _defaultChat = null;
|
| async function _refreshDefaultChat() {
|
| try {
|
| const d = await (await fetch('/api/default-chat')).json();
|
| if (d && d.endpoint_url && d.model) {
|
| _defaultChat = d;
|
| try { window.__odysseusDefaultChat = d; } catch (_) {}
|
| return d;
|
| }
|
| } catch (_) {}
|
| return null;
|
| }
|
|
|
|
|
| _refreshDefaultChat();
|
|
|
| async function _createDirectChatFromPreferredModel() {
|
| if (!sessionModule) return false;
|
|
|
| const pending = sessionModule.getPendingChat && sessionModule.getPendingChat();
|
| if (pending && pending.url && pending.modelId) {
|
| sessionModule.createDirectChat(pending.url, pending.modelId, pending.endpointId);
|
| return true;
|
| }
|
|
|
| const sessions = sessionModule.getSessions();
|
| const currentId = sessionModule.getCurrentSessionId();
|
| const current = sessions.find(s => s.id === currentId);
|
| if (current && current.endpoint_url && current.model) {
|
| sessionModule.createDirectChat(current.endpoint_url, current.model, current.endpoint_id);
|
| return true;
|
| }
|
|
|
| const dc = await _refreshDefaultChat();
|
| if (dc) {
|
| sessionModule.createDirectChat(dc.endpoint_url, dc.model, dc.endpoint_id);
|
| return true;
|
| }
|
|
|
| const withModel = sessions.filter(s => s.endpoint_url && s.model);
|
| if (withModel.length > 0) {
|
| const last = withModel[0];
|
| sessionModule.createDirectChat(last.endpoint_url, last.model, last.endpoint_id);
|
| return true;
|
| }
|
|
|
| return false;
|
| }
|
|
|
|
|
|
|
|
|
| function initializeEventListeners() {
|
|
|
|
|
|
|
|
|
| const _overflowAttach = el('overflow-attach-btn');
|
| if (_overflowAttach) _overflowAttach.addEventListener('click', fileHandlerModule.openPicker);
|
| el('file-input').addEventListener('change', (e)=>{
|
| for (const f of e.target.files) fileHandlerModule.addFiles([f]);
|
| fileHandlerModule.renderAttachStrip();
|
|
|
| const ta = el('message');
|
| if (ta) setTimeout(() => ta.focus(), 100);
|
| });
|
|
|
|
|
| window.addEventListener('paste', async (e)=>{
|
| if (!e.clipboardData) return;
|
| let changed = false;
|
| for (const item of e.clipboardData.items){
|
| if (item.kind === 'file'){
|
| const f = item.getAsFile();
|
| if (f) {
|
| fileHandlerModule.addFiles([f]);
|
| changed = true;
|
| }
|
| }
|
| }
|
| if (changed) fileHandlerModule.renderAttachStrip();
|
| });
|
|
|
|
|
|
|
|
|
|
|
| const _metaCountEl = el('current-meta-count');
|
| const _chatHistEl = el('chat-history');
|
| if (_metaCountEl && _chatHistEl) {
|
| let _countScheduled = false;
|
| const _updateMsgCount = () => {
|
| _countScheduled = false;
|
| const n = _chatHistEl.querySelectorAll(':scope > .msg').length;
|
| _metaCountEl.textContent = n ? `· ${n} msg${n === 1 ? '' : 's'}` : '';
|
| };
|
| const _scheduleCount = () => {
|
| if (_countScheduled) return;
|
| _countScheduled = true;
|
| requestAnimationFrame(_updateMsgCount);
|
| };
|
| new MutationObserver(_scheduleCount).observe(_chatHistEl, { childList: true });
|
| _updateMsgCount();
|
| }
|
|
|
|
|
| el('chat-history').addEventListener('scroll', uiModule.debounce(() => {
|
| const box = el('chat-history');
|
| const atBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 80;
|
| uiModule.setAutoScroll(atBottom);
|
| }, 100));
|
|
|
| el('chat-history').addEventListener('scroll', () => {
|
| document.querySelectorAll('.ctx-popup, .memory-used-detail, .msg-overflow-menu').forEach(p => p.remove());
|
| document.querySelectorAll('.memory-used-pill').forEach(p => { p._openDetail = null; });
|
| }, { passive: true });
|
|
|
| el('chat-history').addEventListener('wheel', (e) => {
|
|
|
| if (e.deltaY < 0) uiModule.setAutoScroll(false);
|
| });
|
| let _touchThrottled = false;
|
| el('chat-history').addEventListener('touchmove', () => {
|
| if (_touchThrottled) return;
|
| _touchThrottled = true;
|
| uiModule.setAutoScroll(false);
|
| requestAnimationFrame(() => { _touchThrottled = false; });
|
| }, { passive: true });
|
|
|
|
|
| el('chat-history').addEventListener('click', (e) => {
|
| const link = e.target.closest('a.chat-link');
|
| if (!link) return;
|
| const href = link.getAttribute('href');
|
| if (href && href.startsWith('#') && sessionModule) {
|
| e.preventDefault();
|
| sessionModule.selectSession(href.slice(1));
|
| }
|
| });
|
|
|
|
|
| const exportDlBtn = el('export-dl-btn');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| window.closeAllPopups = function closeAllPopups(except) {
|
| document.querySelectorAll(
|
| '.export-dropdown-menu.open, .overflow-menu.open, .model-picker-menu.open, .doc-overflow-menu.open'
|
| ).forEach(m => { if (m !== except) m.classList.remove('open'); });
|
| document.querySelectorAll(
|
| '.skill-kebab-menu, .note-reminder-menu, .task-dropdown, .doclib-card-dropdown, .email-card-dropdown, .msg-overflow-menu'
|
| ).forEach(m => { if (m !== except) m.remove(); });
|
| };
|
|
|
|
|
|
|
|
|
|
|
| document.addEventListener('click', (e) => {
|
| if (e.target.closest('.icon-rail-btn, #sidebar .list-item, .section-header-flex')) {
|
| window.closeAllPopups();
|
| }
|
| });
|
|
|
| const exportMenu = el('export-dropdown-menu');
|
| if (exportDlBtn && exportMenu) {
|
| exportDlBtn.addEventListener('click', (e) => {
|
| e.stopPropagation();
|
| if (exportMenu.classList.contains('open')) {
|
| exportMenu.classList.remove('open');
|
| } else {
|
|
|
| if (exportMenu.parentElement !== document.body) document.body.appendChild(exportMenu);
|
| const rect = exportDlBtn.getBoundingClientRect();
|
| exportMenu.style.top = (rect.bottom + 4) + 'px';
|
| exportMenu.style.left = 'auto';
|
| exportMenu.style.right = (window.innerWidth - rect.right) + 'px';
|
| exportMenu.classList.add('open');
|
| }
|
| });
|
| document.addEventListener('click', () => exportMenu.classList.remove('open'));
|
| document.addEventListener('keydown', (e) => {
|
| if (e.key === 'Escape' && exportMenu.classList.contains('open')) {
|
| exportMenu.classList.remove('open');
|
| }
|
| });
|
|
|
|
|
|
|
| const _sidebarEl = el('sidebar');
|
| if (_sidebarEl) {
|
| let _wasHidden = _sidebarEl.classList.contains('hidden');
|
| new MutationObserver(() => {
|
| const nowHidden = _sidebarEl.classList.contains('hidden');
|
| if (_wasHidden && !nowHidden) window.closeAllPopups();
|
| _wasHidden = nowHidden;
|
| }).observe(_sidebarEl, { attributes: true, attributeFilter: ['class'] });
|
| }
|
|
|
| const currentMeta = el('current-meta');
|
| if (currentMeta) {
|
| currentMeta.style.cursor = 'pointer';
|
| currentMeta.addEventListener('click', (e) => {
|
| e.stopPropagation();
|
| exportDlBtn.click();
|
| });
|
| }
|
| }
|
|
|
|
|
|
|
| function _serializeChatTranscript() {
|
| const box = document.getElementById('chat-history');
|
| if (!box) return '';
|
| const parts = [];
|
| for (const child of box.children) {
|
| if (child.classList?.contains('msg')) {
|
| const isUser = child.classList.contains('msg-user');
|
| let label;
|
| if (isUser) {
|
| label = 'User';
|
| } else {
|
| const roleEl = child.querySelector('.role');
|
| const ts = roleEl?.querySelector('.role-timestamp');
|
| let raw = roleEl ? roleEl.textContent : '';
|
| if (ts) raw = raw.replace(ts.textContent, '');
|
| label = (raw || '').trim() || 'Assistant';
|
| }
|
| const body = child.querySelector('.body');
|
|
|
|
|
| const text = body ? (body.dataset.raw || body.innerText || body.textContent || '').trim() : '';
|
| if (text) parts.push(`${label}: ${text}`);
|
| } else if (child.classList?.contains('agent-thread')) {
|
| const lines = ['[Tool calls]'];
|
| for (const n of child.querySelectorAll('.agent-thread-node')) {
|
| const tool = n.querySelector('.agent-thread-tool')?.textContent?.trim() || 'tool';
|
| const cmd = n.querySelector('.agent-thread-cmd')?.textContent?.trim() || '';
|
| const output = n.querySelector('.agent-tool-output pre')?.textContent?.trim() || '';
|
| const status = n.classList.contains('error') ? 'failed' : 'done';
|
| let line = `- ${tool} [${status}]`;
|
| if (cmd) line += `\n cmd: ${cmd}`;
|
| if (output) {
|
| const truncated = output.length > 2000 ? output.slice(0, 2000) + '…' : output;
|
| line += `\n out: ${truncated}`;
|
| }
|
| lines.push(line);
|
| }
|
| parts.push(lines.join('\n'));
|
| }
|
| }
|
| return parts.join('\n\n');
|
| }
|
|
|
|
|
| const exportCopyBtn = el('export-copy-btn');
|
| if (exportCopyBtn) {
|
| exportCopyBtn.addEventListener('click', async (e) => {
|
| e.stopPropagation();
|
| exportMenu.classList.remove('open');
|
| const transcript = _serializeChatTranscript();
|
|
|
|
|
| if (!transcript.trim()) { uiModule.showToast('Nothing to copy yet'); return; }
|
| await uiModule.copyToClipboard(transcript);
|
| });
|
| }
|
|
|
|
|
| const exportPdfBtn = el('export-pdf-btn');
|
| if (exportPdfBtn) {
|
| exportPdfBtn.addEventListener('click', (e) => {
|
| e.stopPropagation();
|
| exportMenu.classList.remove('open');
|
| const meta = sessionModule.getSessions().find(s => s.id === sessionModule.getCurrentSessionId());
|
| const sessionName = meta ? meta.name : 'Odysseus Chat';
|
| const originalTitle = document.title;
|
| document.title = sessionName;
|
| const chatHistory = document.getElementById('chat-history');
|
| if (chatHistory) chatHistory.dataset.printTitle = sessionName;
|
| document.querySelectorAll('#chat-history details:not([open])').forEach(d => {
|
| d.setAttribute('open', '');
|
| d.dataset.printOpened = '1';
|
| });
|
| window.print();
|
| document.title = originalTitle;
|
| document.querySelectorAll('#chat-history details[data-print-opened]').forEach(d => {
|
| d.removeAttribute('open');
|
| d.removeAttribute('data-print-opened');
|
| });
|
| });
|
| }
|
|
|
|
|
| const exportDocBtn = el('export-doc-btn');
|
| if (exportDocBtn) {
|
| exportDocBtn.addEventListener('click', async (e) => {
|
| e.stopPropagation();
|
| exportMenu.classList.remove('open');
|
| try {
|
| const sessionId = sessionModule.getCurrentSessionId();
|
| const texts = _serializeChatTranscript();
|
| const meta = sessionModule.getSessions().find(s => s.id === sessionId);
|
| const title = meta?.name || 'Untitled';
|
| const res = await fetch(`${API_BASE}/api/document`, {
|
| method: 'POST',
|
| headers: { 'Content-Type': 'application/json' },
|
| body: JSON.stringify({ session_id: sessionId, title, content: texts }),
|
| });
|
| if (!res.ok) throw new Error('Failed');
|
| const doc = await res.json();
|
| if (documentModule) documentModule.loadDocument(doc.id);
|
| uiModule.showToast('Saved to documents');
|
| } catch (err) {
|
| console.error('Save to docs failed:', err);
|
| uiModule.showError('Failed to save to documents');
|
| }
|
| });
|
| }
|
|
|
|
|
| const exportRenameBtn = el('export-rename-btn');
|
| if (exportRenameBtn) {
|
| exportRenameBtn.addEventListener('click', (e) => {
|
| e.stopPropagation();
|
| exportMenu.classList.remove('open');
|
| let sid = sessionModule.getCurrentSessionId();
|
|
|
|
|
| const hasPending = sessionModule.hasPendingChat && sessionModule.hasPendingChat();
|
| if (!sid && !hasPending) return;
|
| const meta = sid ? sessionModule.getSessions().find(s => s.id === sid) : null;
|
| const currentName = meta?.name || '';
|
| const metaEl = el('current-meta');
|
| if (!metaEl) return;
|
|
|
|
|
| const input = document.createElement('input');
|
| input.type = 'text';
|
| input.value = currentName;
|
| input.className = 'session-rename-input';
|
| input.style.cssText = 'font-size:inherit;background:transparent;border:none;border-bottom:1px solid var(--accent, var(--red));color:var(--fg);outline:none;width:100%;padding:0;';
|
| const origText = metaEl.textContent;
|
| metaEl.textContent = '';
|
| metaEl.appendChild(input);
|
| input.focus();
|
| input.select();
|
|
|
| const commit = async () => {
|
| const newName = input.value.trim();
|
| if (newName && newName !== currentName) {
|
|
|
| if (!sid && sessionModule.materializePendingSession) {
|
| try { await sessionModule.materializePendingSession(); sid = sessionModule.getCurrentSessionId(); } catch (_) {}
|
| }
|
| if (!sid) { metaEl.textContent = newName; return; }
|
| const fd = new FormData();
|
| fd.append('name', newName);
|
| await fetch(`${API_BASE}/api/session/${sid}`, { method: 'PATCH', body: fd });
|
| const _m = sessionModule.getSessions().find(s => s.id === sid);
|
| if (_m) _m.name = newName;
|
| metaEl.textContent = newName;
|
| uiModule.showToast('Renamed');
|
| sessionModule.loadSessions();
|
| } else {
|
| metaEl.textContent = origText;
|
| }
|
| };
|
| input.addEventListener('blur', commit);
|
| input.addEventListener('keydown', (ev) => {
|
| if (ev.key === 'Enter') { ev.preventDefault(); input.blur(); }
|
| if (ev.key === 'Escape') { input.removeEventListener('blur', commit); metaEl.textContent = origText; }
|
| });
|
| });
|
| }
|
|
|
|
|
| const closeCustomPreset = el('close-custom-preset');
|
| const cancelCustomPreset = el('cancel-custom-preset');
|
| const saveCustomPreset = el('save-custom-preset');
|
|
|
| if (closeCustomPreset) {
|
| closeCustomPreset.addEventListener('click', () => {
|
| el('custom-preset-modal').classList.add('hidden');
|
| });
|
| }
|
|
|
| if (cancelCustomPreset) {
|
| cancelCustomPreset.addEventListener('click', () => {
|
| el('custom-preset-modal').classList.add('hidden');
|
| });
|
| }
|
|
|
| if (saveCustomPreset) {
|
| saveCustomPreset.addEventListener('click', async () => {
|
|
|
| const activeTab = document.querySelector('.preset-tab.active');
|
| if (activeTab && activeTab.dataset.chartab === 'group') return;
|
| await presetsModule.saveCustomPreset(uiModule.showToast, uiModule.showError);
|
| });
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| document.addEventListener('keydown', (e) => {
|
| if (e.key === 'Escape') {
|
|
|
| const confirmOverlay = document.getElementById('styled-confirm-overlay');
|
| if (confirmOverlay && !confirmOverlay.classList.contains('hidden')) return;
|
|
|
|
|
| const editingMemory = document.querySelector('.memory-item-editing');
|
| if (editingMemory) {
|
| if (window.memoryModule) window.memoryModule.renderMemoryList();
|
| return;
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
| const scoreboardOverlay = document.getElementById('scoreboard-overlay');
|
| if (scoreboardOverlay) {
|
| scoreboardOverlay.remove();
|
| return;
|
| }
|
|
|
| if (searchChatModule && searchChatModule.isOpen()) {
|
| searchChatModule.closeSearch();
|
| return;
|
| }
|
|
|
|
|
| const cmpOverlay = document.getElementById('compare-model-overlay');
|
| if (cmpOverlay) {
|
| cmpOverlay.remove();
|
| return;
|
| }
|
|
|
|
|
| const themeModal = document.getElementById('theme-modal');
|
| if (themeModal && !themeModal.classList.contains('hidden')) {
|
| themeModule.closePopup();
|
| return;
|
| }
|
|
|
|
|
|
|
|
|
|
|
| const calendarModal = document.getElementById('calendar-modal');
|
| if (calendarModal && !calendarModal.classList.contains('hidden') && getComputedStyle(calendarModal).display !== 'none') {
|
| return;
|
| }
|
|
|
|
|
| const modelPickerMenu = document.getElementById('model-picker-menu');
|
| if (modelPickerMenu && modelPickerMenu.classList.contains('open')) {
|
| modelPickerMenu.classList.remove('open');
|
| return;
|
| }
|
|
|
|
|
|
|
| const modalItemMap = {
|
| 'cookbook-modal': null,
|
| 'rename-session-modal': null,
|
| 'rename-ai-modal': null,
|
| 'custom-preset-modal': null,
|
| 'memory-modal': null,
|
| };
|
|
|
|
|
| const dynamicModals = ['library-modal', 'archive-modal', 'doclib-modal', 'gallery-modal', 'tasks-modal', 'email-lib-modal'];
|
| for (const id of dynamicModals) {
|
| const m = document.getElementById(id);
|
| if (id === 'gallery-modal') {
|
| const editor = document.getElementById('gallery-editor-container');
|
| const editing = !!window.__galleryEditLive || !!(
|
| editor &&
|
| getComputedStyle(editor).display !== 'none' &&
|
| editor.querySelector('.gallery-editor')
|
| );
|
| if (editing) {
|
| e.preventDefault();
|
| e.stopImmediatePropagation();
|
| return;
|
| }
|
| }
|
| if (m) { dismissModal(m); return; }
|
| }
|
|
|
| for (const modalId of Object.keys(modalItemMap)) {
|
| const modal = el(modalId);
|
| if (modal && !modal.classList.contains('hidden')) {
|
| dismissModal(modal);
|
| return;
|
| }
|
| }
|
|
|
|
|
|
|
|
|
|
|
| if (documentModule && documentModule.isPanelOpen()) {
|
|
|
| const docTextarea = document.getElementById('doc-editor-textarea');
|
| if (docTextarea && docTextarea.selectionStart !== docTextarea.selectionEnd) {
|
| return;
|
| }
|
| documentModule.closePanel('down');
|
| return;
|
| }
|
| }
|
| });
|
|
|
|
|
| const _modalSidebarMap = {
|
| 'memory-modal': null,
|
| 'theme-modal': null,
|
| };
|
| const _dynamicModalIds = ['library-modal', 'archive-modal', 'doclib-modal', 'gallery-modal', 'tasks-modal'];
|
| function dismissModal(modal) {
|
| if (!modal || modal.classList.contains('hidden')) return;
|
| if (modal.id === 'gallery-modal') {
|
| const editor = document.getElementById('gallery-editor-container');
|
| const editing = !!window.__galleryEditLive || !!(
|
| editor &&
|
| getComputedStyle(editor).display !== 'none' &&
|
| editor.querySelector('.gallery-editor')
|
| );
|
| if (editing) return;
|
| }
|
| const content = modal.querySelector('.modal-content') || modal.querySelector('#theme-popup');
|
| if (content && !content.classList.contains('modal-closing')) {
|
| content.classList.remove('sheet-ready');
|
| content.style.transform = '';
|
| content.style.transition = '';
|
| content.classList.add('modal-closing');
|
| content.addEventListener('animationend', () => {
|
| if (_dynamicModalIds.includes(modal.id)) {
|
| modal.remove();
|
| } else {
|
| modal.classList.add('hidden');
|
| content.classList.remove('modal-closing');
|
| }
|
| }, { once: true });
|
|
|
| setTimeout(() => {
|
| if (modal.parentElement && !modal.classList.contains('hidden')) {
|
| if (_dynamicModalIds.includes(modal.id)) modal.remove();
|
| else { modal.classList.add('hidden'); content.classList.remove('modal-closing'); }
|
| }
|
| }, 250);
|
| } else {
|
| if (content) content.classList.remove('sheet-ready');
|
| if (_dynamicModalIds.includes(modal.id)) modal.remove();
|
| else modal.classList.add('hidden');
|
| }
|
| }
|
|
|
|
|
| document.addEventListener('click', (e) => {
|
| if (uiModule.isTouchInsideModal()) return;
|
| const modal = e.target.closest('.modal');
|
| if (!modal || modal.classList.contains('hidden')) return;
|
| if (e.target.closest('.modal-content')) return;
|
| dismissModal(modal);
|
| });
|
|
|
|
|
|
|
|
|
| function _startFreshChat() {
|
| try {
|
| const prevId = sessionModule && sessionModule.getCurrentSessionId ? sessionModule.getCurrentSessionId() : null;
|
| if (chatModule && chatModule.detachCurrentStream) chatModule.detachCurrentStream(prevId);
|
| else if (chatModule && chatModule.abortCurrentRequest) chatModule.abortCurrentRequest();
|
| } catch (e) {
|
| console.warn('fresh chat stream detach failed:', e);
|
| }
|
| if (sessionModule) sessionModule.setCurrentSessionId(null);
|
| const box = el('chat-history');
|
| if (box) box.innerHTML = '';
|
| if (chatModule && chatModule.showWelcomeScreen) {
|
| chatModule.showWelcomeScreen();
|
| }
|
|
|
| if (documentModule && documentModule.closePanel) documentModule.closePanel();
|
| if (researchPanelModule && researchPanelModule.isOpen()) researchPanelModule.closePanel();
|
|
|
| const _overflowRes = el('overflow-research-btn');
|
| if (_overflowRes) _overflowRes.classList.remove('active');
|
| if (typeof updatePlusDot === 'function') updatePlusDot();
|
|
|
| const modeToggle = el('agent-mode-toggle');
|
| if (modeToggle && modeToggle.checked) { modeToggle.checked = false; modeToggle.dispatchEvent(new Event('change')); }
|
|
|
| if (presetsModule && presetsModule.deactivateCharacter) presetsModule.deactivateCharacter();
|
| }
|
|
|
|
|
| function _syncResearchIndicator(active) {
|
| const btn = el('research-toggle-btn');
|
| const overflow = el('overflow-research-btn');
|
| const toolBtn = el('tool-research-btn');
|
| const chk = el('research-toggle');
|
| if (btn) {
|
| btn.style.display = active ? '' : 'none';
|
| btn.classList.toggle('active', active);
|
| }
|
|
|
| if (overflow) {
|
| overflow.classList.toggle('active', active);
|
| overflow.style.display = active ? 'none' : '';
|
| }
|
| if (toolBtn) toolBtn.classList.toggle('active', active);
|
| if (chk) chk.checked = active;
|
|
|
| const bashChk = el('bash-toggle');
|
| const bashBtn = el('bash-toggle-btn');
|
| if (active) {
|
| if (bashChk && bashChk.checked) {
|
| bashChk.checked = false;
|
| if (bashBtn) bashBtn.classList.remove('active');
|
| saveToolPref('bash', (loadToggleState().mode || 'chat'), false);
|
| }
|
| }
|
| const s = loadToggleState(); s.research = active; saveToggleState(s);
|
| updatePlusDot();
|
| document.dispatchEvent(new CustomEvent('overflow-state-change'));
|
| }
|
|
|
|
|
| function _syncGroupIndicator(active) {
|
| const btn = el('group-toggle-btn');
|
| const overflow = el('overflow-group-btn');
|
| const chk = el('group-toggle');
|
| if (btn) {
|
| btn.style.display = active ? '' : 'none';
|
| btn.classList.toggle('active', active);
|
| }
|
| if (overflow) {
|
| overflow.classList.toggle('active', active);
|
| overflow.style.display = active ? 'none' : '';
|
| }
|
| if (chk) chk.checked = active;
|
|
|
| const _mpw = el('model-picker-wrap');
|
| if (_mpw) _mpw.style.display = active ? 'none' : '';
|
|
|
| if (active) {
|
| _syncResearchIndicator(false);
|
| const _webChk = el('web-toggle');
|
| if (_webChk && _webChk.checked) {
|
| _webChk.checked = false;
|
| saveToolPref('web', (loadToggleState().mode || 'chat'), false);
|
| }
|
| }
|
| const s = loadToggleState(); s.group = active; saveToggleState(s);
|
| updatePlusDot();
|
| document.dispatchEvent(new CustomEvent('overflow-state-change'));
|
|
|
|
|
| const ws = el('welcome-screen');
|
| const welcomeName = document.querySelector('.welcome-name');
|
| const welcomeSub = el('welcome-sub');
|
| const tipEl = el('welcome-tip');
|
| const _resIco = '<svg class="welcome-boat" style="position:relative;top:0.5px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>';
|
| if (active) {
|
| if (welcomeName) {
|
| if (!welcomeName.dataset.researchOrigHtml) welcomeName.dataset.researchOrigHtml = welcomeName.innerHTML;
|
| welcomeName.innerHTML = _resIco + 'Deep Research';
|
| }
|
| if (welcomeSub) {
|
| if (!welcomeSub.dataset.researchOrigText) welcomeSub.dataset.researchOrigText = welcomeSub.textContent;
|
| welcomeSub.textContent = 'Deep multi-step research with source gathering and synthesis.';
|
| }
|
| if (tipEl) {
|
| if (!tipEl.dataset.researchOrigTip) tipEl.dataset.researchOrigTip = tipEl.textContent;
|
| tipEl.textContent = '';
|
| tipEl.style.display = 'none';
|
| }
|
|
|
| const _incBtn = el('incognito-btn');
|
| if (_incBtn) { _incBtn.dataset.researchOrigDisplay = _incBtn.style.display; _incBtn.style.display = 'none'; }
|
|
|
| if (window.documentModule && window.documentModule.isPanelOpen()) {
|
| window.documentModule.closePanel();
|
| }
|
| } else {
|
| if (welcomeName && welcomeName.dataset.researchOrigHtml) {
|
| welcomeName.innerHTML = welcomeName.dataset.researchOrigHtml;
|
| delete welcomeName.dataset.researchOrigHtml;
|
| }
|
| if (welcomeSub && welcomeSub.dataset.researchOrigText) {
|
| welcomeSub.textContent = welcomeSub.dataset.researchOrigText;
|
| delete welcomeSub.dataset.researchOrigText;
|
| }
|
| if (tipEl && tipEl.dataset.researchOrigTip) {
|
| tipEl.textContent = tipEl.dataset.researchOrigTip;
|
| tipEl.style.opacity = '';
|
| tipEl.style.display = '';
|
| delete tipEl.dataset.researchOrigTip;
|
| }
|
|
|
| const _incBtn2 = el('incognito-btn');
|
| if (_incBtn2 && _incBtn2.dataset.researchOrigDisplay !== undefined) {
|
| _incBtn2.style.display = _incBtn2.dataset.researchOrigDisplay;
|
| delete _incBtn2.dataset.researchOrigDisplay;
|
| }
|
| }
|
| if (ws) { ws.style.animation = 'none'; ws.offsetHeight; ws.style.animation = 'welcome-enter 0.3s ease-out both'; }
|
| }
|
|
|
|
|
|
|
| function _closeCompareIfActive() {
|
| if (compareModule && compareModule.isActive()) {
|
| compareModule.deactivate(true);
|
| return true;
|
| }
|
| return false;
|
| }
|
|
|
|
|
| const toolCompareBtn = el('tool-compare-btn');
|
| if (toolCompareBtn) {
|
| toolCompareBtn.addEventListener('click', () => {
|
| if (compareModule) {
|
| if (compareModule.isActive()) {
|
|
|
| compareModule.toggleMode();
|
| return;
|
| }
|
|
|
| const resChk = el('research-toggle');
|
| if (resChk && resChk.checked) {
|
| _syncResearchIndicator(false);
|
| }
|
| _startFreshChat();
|
| compareModule.toggleMode();
|
| }
|
| });
|
| }
|
|
|
| const toolResearchBtn = el('tool-research-btn');
|
| if (toolResearchBtn) {
|
| toolResearchBtn.addEventListener('click', () => {
|
| researchPanelModule.toggle();
|
| });
|
| }
|
|
|
|
|
| const toolCookbookBtn = el('tool-cookbook-btn');
|
| if (toolCookbookBtn) {
|
| toolCookbookBtn.addEventListener('click', async () => {
|
| if (!cookbookModule) return;
|
|
|
| const Modals = await import('./js/modalManager.js');
|
| if (!Modals.toggle('cookbook-modal')) {
|
|
|
| cookbookModule.open();
|
| }
|
| });
|
| }
|
|
|
|
|
| const toolDoclibBtn = el('tool-doclib-btn');
|
| if (toolDoclibBtn) {
|
| toolDoclibBtn.addEventListener('click', () => {
|
| if (_closeCompareIfActive()) return;
|
| if (documentModule) {
|
| if (documentModule.isLibraryOpen()) {
|
| documentModule.closeLibrary();
|
| } else {
|
| documentModule.openLibrary();
|
| }
|
| }
|
| });
|
| }
|
|
|
|
|
| const toolGalleryBtn = el('tool-gallery-btn');
|
| if (toolGalleryBtn) {
|
| toolGalleryBtn.addEventListener('click', async () => {
|
| if (!galleryModule) return;
|
| const Modals = await import('./js/modalManager.js');
|
| if (!Modals.toggle('gallery-modal')) {
|
| if (galleryModule.isGalleryOpen()) galleryModule.closeGallery();
|
| else galleryModule.openGallery();
|
| }
|
| });
|
| }
|
|
|
|
|
| const toolTasksBtn = el('tool-tasks-btn');
|
| if (toolTasksBtn) {
|
|
|
| const agentsBtns = [el("rail-agents"), el("tool-agents-btn")].filter(Boolean);
|
| agentsBtns.forEach(btn => {
|
| btn.addEventListener("click", () => {
|
| });
|
| });
|
| toolTasksBtn.addEventListener('click', () => {
|
| if (tasksModule) {
|
| tasksModule.isTasksOpen() ? tasksModule.closeTasks() : tasksModule.openTasks();
|
| }
|
| });
|
| }
|
|
|
|
|
| const toolCalendarBtn = el('tool-calendar-btn');
|
| if (toolCalendarBtn) {
|
| toolCalendarBtn.addEventListener('click', async () => {
|
| if (!calendarModule) return;
|
| const Modals = await import('./js/modalManager.js');
|
|
|
|
|
| if (!Modals.toggle('calendar-modal')) {
|
| if (calendarModule.isCalendarOpen()) calendarModule.closeCalendar();
|
| else calendarModule.openCalendar();
|
| }
|
| });
|
| }
|
|
|
|
|
| const toolNotesBtn = el('tool-notes-btn');
|
| if (toolNotesBtn) {
|
| toolNotesBtn.addEventListener('click', () => {
|
| if (notesModule) {
|
| notesModule.togglePanel();
|
| }
|
| });
|
| }
|
|
|
| if (notesModule && notesModule.refreshDueBadge) {
|
| notesModule.refreshDueBadge();
|
| setInterval(() => notesModule.refreshDueBadge(), 5 * 60 * 1000);
|
| }
|
|
|
|
|
|
|
| const urlPath = window.location.pathname;
|
|
|
|
|
|
|
| const _iconRailWidth = () => {
|
| const r = document.getElementById('icon-rail');
|
| if (!r) return 0;
|
| const cs = window.getComputedStyle(r);
|
| if (cs.display === 'none' || cs.visibility === 'hidden') return 0;
|
| return Math.round(r.getBoundingClientRect().width);
|
| };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| const _collapseSidebarToRail = () => {
|
| const sb = document.getElementById('sidebar');
|
| const rail = document.getElementById('icon-rail');
|
| if (!sb || !rail) return;
|
| const wasVisible = !sb.classList.contains('hidden');
|
| if (wasVisible) {
|
| document.body.dataset.routeCollapsedSidebar = '1';
|
| }
|
| sb.classList.add('hidden');
|
| rail.classList.remove('rail-hidden');
|
|
|
|
|
| try { window.syncRailSide && window.syncRailSide(); } catch (_) {}
|
| };
|
|
|
|
|
|
|
|
|
| const _restoreSidebarIfRouteCollapsed = () => {
|
| if (document.body.dataset.routeCollapsedSidebar !== '1') return;
|
| delete document.body.dataset.routeCollapsedSidebar;
|
| const sb = document.getElementById('sidebar');
|
| if (!sb) return;
|
| sb.classList.remove('hidden');
|
| try { window.syncRailSide && window.syncRailSide(); } catch (_) {}
|
| };
|
|
|
|
|
| window._restoreSidebarIfRouteCollapsed = _restoreSidebarIfRouteCollapsed;
|
|
|
|
|
|
|
| {
|
| const sb = document.getElementById('sidebar');
|
| if (sb && typeof MutationObserver !== 'undefined') {
|
| new MutationObserver(() => {
|
| if (!sb.classList.contains('hidden')) {
|
| delete document.body.dataset.routeCollapsedSidebar;
|
| }
|
| }).observe(sb, { attributes: true, attributeFilter: ['class'] });
|
| }
|
| }
|
| const _routeOpen = {
|
| '/notes': () => {
|
| if (!notesModule) return;
|
| _collapseSidebarToRail();
|
| notesModule.openPanel();
|
|
|
|
|
|
|
|
|
| const _go = () => {
|
| const btn = document.getElementById('notes-fullscreen-toggle');
|
| const pane = document.querySelector('.notes-pane');
|
| if (!pane) return false;
|
| if (!pane.classList.contains('notes-pane-fullscreen') && btn) btn.click();
|
| return true;
|
| };
|
| if (!_go()) {
|
| requestAnimationFrame(_go);
|
| setTimeout(_go, 50);
|
| setTimeout(_go, 200);
|
| }
|
| },
|
| '/calendar': () => calendarModule && calendarModule.openCalendar(),
|
| '/cookbook': () => document.getElementById('tool-cookbook-btn')?.click(),
|
| '/email': () => {
|
|
|
|
|
| _collapseSidebarToRail();
|
|
|
|
|
|
|
|
|
|
|
| try { document.getElementById('rail-new-session')?.click(); } catch (_) {}
|
|
|
|
|
|
|
| const hdr = document.querySelector('#email-section .section-header-flex');
|
| if (hdr) hdr.click();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| const _goFullscreen = () => {
|
| const modal = document.getElementById('email-lib-modal');
|
| if (!modal) return false;
|
| modal.classList.add('email-lib-fullscreen');
|
| return true;
|
| };
|
| _goFullscreen();
|
| requestAnimationFrame(_goFullscreen);
|
| setTimeout(_goFullscreen, 50);
|
| setTimeout(_goFullscreen, 200);
|
| },
|
| '/memory': () => document.getElementById('tool-memory-btn')?.click(),
|
| '/gallery': () => document.getElementById('tool-gallery-btn')?.click(),
|
| '/tasks': () => document.getElementById('tool-tasks-btn')?.click(),
|
| '/library': () => sessionModule && sessionModule.openLibrary && sessionModule.openLibrary(),
|
| };
|
| const _opener = _routeOpen[urlPath];
|
|
|
|
|
|
|
|
|
|
|
| if (_opener) window._odysseusRouteOpener = _opener;
|
|
|
|
|
| const toolLibraryBtn = el('tool-library-btn');
|
| if (toolLibraryBtn) {
|
| toolLibraryBtn.addEventListener('click', () => {
|
| if (sessionModule) sessionModule.openLibrary();
|
| });
|
| }
|
|
|
|
|
|
|
|
|
| const libraryNewDocBtn = el('library-new-doc-btn');
|
| if (libraryNewDocBtn) {
|
| libraryNewDocBtn.addEventListener('click', async (e) => {
|
| e.stopPropagation();
|
| try {
|
| if (documentModule && documentModule.newDocument) await documentModule.newDocument();
|
| } catch (err) {
|
| console.error('New document from Library failed:', err);
|
| if (uiModule && uiModule.showError) uiModule.showError('Could not create document');
|
| }
|
| });
|
| }
|
|
|
|
|
| const chatsLibraryBtn = el('chats-library-btn');
|
| if (chatsLibraryBtn) {
|
| chatsLibraryBtn.addEventListener('click', (e) => {
|
| e.stopPropagation();
|
| if (sessionModule) sessionModule.openLibrary('chats');
|
| });
|
| }
|
|
|
| const toolArchiveBtn = el('tool-archive-btn');
|
| if (toolArchiveBtn) {
|
| toolArchiveBtn.addEventListener('click', () => {
|
| if (sessionModule) sessionModule.openLibrary('archive');
|
| });
|
| }
|
|
|
| const toolThemeBtn = el('tool-theme-btn');
|
| if (toolThemeBtn) {
|
| toolThemeBtn.addEventListener('click', () => {
|
| const tm = document.getElementById('theme-modal');
|
| if (tm) tm.classList.remove('hidden');
|
| });
|
| }
|
|
|
|
|
| const toggleSidebarOption = el('toggle-sidebar-option');
|
| if (toggleSidebarOption) {
|
| toggleSidebarOption.addEventListener('click', () => {
|
| const sidebar = el('sidebar');
|
| sidebar.classList.toggle('hidden');
|
| });
|
| }
|
|
|
|
|
| const userBarSettings = el('user-bar-settings');
|
| const userBarProfile = el('user-bar-profile');
|
| const userBarAdmin = el('user-bar-admin');
|
|
|
| if (userBarSettings) {
|
| userBarSettings.addEventListener('click', () => settingsModule.open());
|
| }
|
| if (userBarProfile) {
|
|
|
|
|
| userBarProfile.addEventListener('click', () => settingsModule.open('account'));
|
| }
|
| if (userBarAdmin) {
|
| userBarAdmin.addEventListener('click', () => adminModule.open());
|
| }
|
|
|
|
|
| fetch(`${API_BASE}/api/auth/status`, { credentials: 'same-origin' })
|
| .then(r => r.json())
|
| .then(d => {
|
| window._isAdmin = !!d.is_admin;
|
| if (d.is_admin && userBarAdmin) userBarAdmin.style.display = '';
|
| const userBarName = el('user-bar-name');
|
| const userBarAvatar = el('user-bar-avatar');
|
| if (userBarName && d.username) {
|
| let displayName = d.username;
|
|
|
| if (displayName.includes('@')) {
|
| const [local, domain] = displayName.split('@');
|
| const ext = domain.includes('.') ? domain.slice(domain.lastIndexOf('.')) : '';
|
| displayName = local.charAt(0) + '•••@••••' + ext;
|
| }
|
| userBarName.textContent = displayName;
|
| if (userBarAvatar) userBarAvatar.textContent = d.username.charAt(0).toUpperCase();
|
| }
|
|
|
| if (d.privileges) {
|
| window._userPrivileges = d.privileges;
|
| const p = d.privileges;
|
|
|
| if (!p.can_use_agent) {
|
| const modeToggle = document.getElementById('mode-toggle');
|
| if (modeToggle) modeToggle.closest('.chat-input-toggle')?.style.setProperty('display', 'none');
|
| }
|
|
|
| if (!p.can_use_bash) {
|
| const bashToggle = document.getElementById('bash-toggle');
|
| if (bashToggle) bashToggle.closest('.chat-input-toggle')?.style.setProperty('display', 'none');
|
| const bashBtn = document.getElementById('bash-toggle-btn');
|
| if (bashBtn) bashBtn.style.display = 'none';
|
| }
|
|
|
| if (!p.can_use_documents) {
|
| const docBtn = document.getElementById('overflow-doc-btn');
|
| if (docBtn) docBtn.style.display = 'none';
|
| const docInd = document.getElementById('doc-indicator-btn');
|
| if (docInd) docInd.style.display = 'none';
|
| }
|
|
|
| if (!p.can_use_research) {
|
| const resBtn = document.getElementById('research-toggle-btn');
|
| if (resBtn) resBtn.style.display = 'none';
|
| const resOverflow = document.getElementById('overflow-research-btn');
|
| if (resOverflow) resOverflow.style.display = 'none';
|
| }
|
|
|
| }
|
| })
|
| .catch(() => {});
|
|
|
|
|
| const sortBtn = el('session-sort-btn');
|
| const sortDropdown = el('session-sort-dropdown');
|
| if (sortBtn && sortDropdown) {
|
| sortBtn.addEventListener('click', (e) => {
|
| e.stopPropagation();
|
| sortDropdown.style.display = sortDropdown.style.display === 'block' ? 'none' : 'block';
|
| });
|
| document.addEventListener('click', () => { sortDropdown.style.display = 'none'; });
|
| sortDropdown.addEventListener('click', (e) => e.stopPropagation());
|
|
|
|
|
| sortDropdown.querySelectorAll('.sort-option').forEach(opt => {
|
| opt.addEventListener('click', () => {
|
| const mode = opt.dataset.sort;
|
| const current = sessionModule.getSortMode();
|
|
|
| if (current === mode) {
|
| sessionModule.setSortMode(null);
|
| sortDropdown.style.display = 'none';
|
| uiModule.showToast('Manual order');
|
| } else {
|
| sessionModule.setSortMode(mode);
|
| sortDropdown.style.display = 'none';
|
| uiModule.showToast(`Sorted: ${opt.textContent.trim().toLowerCase()}`);
|
| }
|
| _syncSortChecks();
|
| });
|
| });
|
|
|
|
|
| function _syncSortChecks() {
|
| const current = sessionModule.getSortMode();
|
| sortDropdown.querySelectorAll('.sort-option').forEach(o => {
|
| const check = o.querySelector('.sort-check') || document.createElement('span');
|
| check.className = 'sort-check';
|
| check.style.cssText = 'float:right;font-size:20px;line-height:1;position:relative;top:3px;color:var(--accent, var(--red));opacity:' + (o.dataset.sort === current ? '1' : '0');
|
| check.textContent = '\u2022';
|
| if (!o.querySelector('.sort-check')) o.appendChild(check);
|
| });
|
|
|
| if (sortBtn) sortBtn.classList.toggle('active', !!current);
|
| }
|
|
|
| sortBtn.addEventListener('click', _syncSortChecks);
|
| _syncSortChecks();
|
|
|
|
|
|
|
|
|
| async function _runTidy(skipLlm) {
|
| const btnIcon = sortBtn.querySelector('.sort-icon');
|
| if (btnIcon) btnIcon.style.display = 'none';
|
| const wp = spinnerModule.create('', 'clean', 'whirlpool');
|
| const wpEl = wp.createElement();
|
| wpEl.style.cssText = 'width:13px;height:13px;display:inline-block;vertical-align:middle;margin-top:-5px;';
|
| sortBtn.appendChild(wpEl);
|
| wp.start();
|
| sortDropdown.style.display = 'none';
|
| try {
|
| const url = `${API_BASE}/api/sessions/auto-sort${skipLlm ? '?skip_llm=true' : ''}`;
|
| const res = await fetch(url, { method: 'POST' });
|
| const data = await res.json();
|
| if (!res.ok) throw new Error(data.detail || 'Auto-sort failed');
|
| if (data.status === 'ok') {
|
| sessionModule.setSortMode(null);
|
| _syncSortChecks();
|
| if (skipLlm) {
|
|
|
|
|
| const cleaned = (data.deleted_empty || 0) + (data.deleted_throwaway || 0);
|
| uiModule.showToast(cleaned ? `Cleaned ${cleaned} empty/throwaway chat${cleaned === 1 ? '' : 's'}` : 'Already clean');
|
| } else {
|
|
|
|
|
|
|
| const remaining = data.unfiled_remaining || 0;
|
| let msg;
|
| if (data.updated > 0) {
|
| msg = `Sorted ${data.updated} into ${data.folders.length} folder${data.folders.length === 1 ? '' : 's'}`;
|
| if (remaining > 0) msg += ` — ${remaining} unfiled left, hit Tidy again`;
|
| } else if (remaining > 0) {
|
| msg = `${remaining} unfiled chats — hit Tidy again`;
|
| } else {
|
| msg = 'All sorted';
|
| }
|
| uiModule.showToast(msg);
|
| }
|
| if (sessionModule) await sessionModule.loadSessions();
|
| } else {
|
| uiModule.showToast(data.reason || 'Nothing to sort');
|
| }
|
| } catch (e) {
|
| uiModule.showError('Auto-sort: ' + e.message);
|
| } finally {
|
| wp.destroy();
|
| if (wpEl.parentNode) wpEl.parentNode.removeChild(wpEl);
|
| if (btnIcon) btnIcon.style.display = '';
|
| }
|
| }
|
|
|
| const autoSortBtn = el('auto-sort-sessions-btn');
|
| if (autoSortBtn) autoSortBtn.addEventListener('click', () => _runTidy(false));
|
|
|
|
|
| const autoSortMoreBtn = el('auto-sort-sessions-more');
|
| const autoSortNoaiBtn = el('auto-sort-sessions-noai-btn');
|
| if (autoSortMoreBtn && autoSortNoaiBtn) {
|
| autoSortMoreBtn.addEventListener('click', (e) => {
|
| e.stopPropagation();
|
| autoSortNoaiBtn.style.display = autoSortNoaiBtn.style.display === 'none' ? 'block' : 'none';
|
| });
|
| autoSortNoaiBtn.addEventListener('click', () => _runTidy(true));
|
| }
|
| }
|
|
|
|
|
| const modelSortBtn = el('model-sort-btn');
|
| const modelSortDropdown = el('model-sort-dropdown');
|
| if (modelSortBtn && modelSortDropdown) {
|
| modelSortBtn.addEventListener('click', (e) => {
|
| e.stopPropagation();
|
| modelSortDropdown.style.display = modelSortDropdown.style.display === 'block' ? 'none' : 'block';
|
| });
|
| document.addEventListener('click', () => { modelSortDropdown.style.display = 'none'; });
|
| modelSortDropdown.addEventListener('click', (e) => e.stopPropagation());
|
| modelSortDropdown.querySelectorAll('.sort-option').forEach(opt => {
|
| opt.addEventListener('click', () => {
|
| const mode = opt.dataset.sort;
|
| Storage.set('odysseus-model-sort', mode);
|
| if (modelsModule) modelsModule.refreshModels();
|
| modelSortDropdown.style.display = 'none';
|
| uiModule.showToast('Models sorted: ' + opt.textContent.trim().toLowerCase());
|
| });
|
| });
|
| }
|
|
|
|
|
|
|
|
|
|
|
| const _prefetchedFeatures = sessionStorage.getItem('ody-prefetch-features');
|
| sessionStorage.removeItem('ody-prefetch-features');
|
| window._initFeaturesReady = (_prefetchedFeatures
|
| ? Promise.resolve(JSON.parse(_prefetchedFeatures))
|
| : fetch(`${API_BASE}/api/auth/features`, { credentials: 'same-origin' }).then(r => r.json())
|
| ).then(features => {
|
| const map = {
|
| web_search: ['web-toggle-btn'],
|
| deep_research: ['research-toggle-btn', 'tool-research-btn', 'overflow-research-btn', 'rail-research'],
|
| document_editor: ['overflow-doc-btn', 'rail-documents'],
|
| gallery: ['tool-gallery-btn', 'rail-gallery'],
|
| };
|
| Object.entries(map).forEach(([key, ids]) => {
|
| if (features[key] === false) {
|
| ids.forEach(id => { const e = el(id); if (e) e.style.display = 'none'; });
|
| }
|
| });
|
|
|
|
|
|
|
|
|
|
|
|
|
| try { if (window.applyUIVis && window.loadUIVis) window.applyUIVis(window.loadUIVis()); } catch (_) {}
|
| })
|
| .catch(() => {});
|
|
|
|
|
| const _prefetchedSettings = sessionStorage.getItem('ody-prefetch-settings');
|
| sessionStorage.removeItem('ody-prefetch-settings');
|
| window._initSettingsReady = (_prefetchedSettings
|
| ? Promise.resolve(JSON.parse(_prefetchedSettings))
|
| : fetch(`${API_BASE}/api/auth/settings`, { credentials: 'same-origin' }).then(r => r.json())
|
| ).then(settings => {
|
|
|
|
|
|
|
|
|
|
|
| const ttsOff = settings.tts_enabled === false || !settings.tts_provider || settings.tts_provider === 'disabled';
|
| const overflowTts = el('overflow-tts-btn');
|
| if (overflowTts) {
|
| overflowTts.style.display = ttsOff ? 'none' : '';
|
| }
|
| })
|
| .catch(() => {});
|
|
|
|
|
|
|
|
|
| const renameAiOption = el('rename-ai-option');
|
| const renameAiModal = el('rename-ai-modal');
|
| const closeRenameAi = el('close-rename-ai');
|
| const cancelRenameAi = el('cancel-rename-ai');
|
| const saveAiName = el('save-ai-name');
|
| const aiNameInput = el('ai-name-input');
|
|
|
| if (renameAiOption) {
|
| renameAiOption.addEventListener('click', () => {
|
| const currentName = aiNameInput.value;
|
| renameAiModal.classList.remove('hidden');
|
| });
|
| }
|
|
|
| if (closeRenameAi) {
|
| closeRenameAi.addEventListener('click', () => {
|
| renameAiModal.classList.add('hidden');
|
| });
|
| }
|
|
|
| if (cancelRenameAi) {
|
| cancelRenameAi.addEventListener('click', () => {
|
| renameAiModal.classList.add('hidden');
|
| });
|
| }
|
|
|
| if (saveAiName) {
|
| saveAiName.addEventListener('click', async () => {
|
| const newName = aiNameInput.value.trim();
|
|
|
| if (!newName) {
|
| uiModule.showError('Please enter a name for the AI');
|
| return;
|
| }
|
|
|
| try {
|
| const response = await fetch(`${API_BASE}/api/ai/name`, {
|
| method: 'POST',
|
| headers: {'Content-Type': 'application/json'},
|
| body: JSON.stringify({ name: newName })
|
| });
|
|
|
| const result = await response.json();
|
| if (result.success) {
|
| uiModule.showToast(`AI renamed to ${newName}`);
|
| renameAiModal.classList.add('hidden');
|
| aiNameInput.value = '';
|
| }
|
| } catch (e) {
|
| uiModule.showError('Failed to rename AI: ' + e.message);
|
| }
|
| });
|
| }
|
|
|
|
|
| const memoryModal = el('memory-modal');
|
| const closeMemoryBtn = el('close-memory-modal');
|
|
|
|
|
| const closeThemeBtn = el('close-theme-popup');
|
| if (closeThemeBtn && themeModule) {
|
| closeThemeBtn.addEventListener('click', () => {
|
| themeModule.closePopup();
|
| });
|
| }
|
|
|
|
|
| const renameSessionModal = el('rename-session-modal');
|
| const closeRenameSession = el('close-rename-session');
|
| const cancelRenameSession = el('cancel-rename-session');
|
| const saveSessionName = el('save-session-name');
|
| const sessionNameInput = el('session-name-input');
|
|
|
|
|
| if (closeRenameSession) {
|
| closeRenameSession.addEventListener('click', () => {
|
| renameSessionModal.classList.add('hidden');
|
| });
|
| }
|
|
|
| if (cancelRenameSession) {
|
| cancelRenameSession.addEventListener('click', () => {
|
| renameSessionModal.classList.add('hidden');
|
| });
|
| }
|
|
|
| if (saveSessionName) {
|
| saveSessionName.addEventListener('click', async () => {
|
| const newName = sessionNameInput.value.trim();
|
|
|
| if (!newName) {
|
| uiModule.showError('Please enter a name for the session');
|
| return;
|
| }
|
|
|
| try {
|
| const response = await fetch(`${API_BASE}/api/session/${sessionModule.getCurrentSessionId()}`, {
|
| method: 'PATCH',
|
| headers: {'Content-Type': 'application/json'},
|
| body: JSON.stringify({ name: newName })
|
| });
|
|
|
| const result = await response.json();
|
| if (response.ok) {
|
| uiModule.showToast(`Session renamed to ${newName}`);
|
| renameSessionModal.classList.add('hidden');
|
| sessionNameInput.value = '';
|
|
|
| const meta = sessionModule.getSessions().find(s => s.id === sessionModule.getCurrentSessionId());
|
| if (meta) {
|
| meta.name = newName;
|
| const ver = window._appVersion ? ` v${window._appVersion}` : '';
|
| el('current-meta').textContent = `Session: ${meta.name}${meta.model ? ' ' + meta.model.split('/').pop() : ''}${meta.rag ? ' [RAG]' : ''}${ver}`;
|
| }
|
|
|
| await sessionModule.loadSessions();
|
| } else {
|
| throw new Error(result.detail || 'Failed to rename session');
|
| }
|
| } catch (e) {
|
| uiModule.showError('Failed to rename session: ' + e.message);
|
| }
|
| });
|
| }
|
|
|
| if (closeMemoryBtn) {
|
| closeMemoryBtn.addEventListener('click', () => {
|
| dismissModal(memoryModal);
|
| });
|
| }
|
|
|
|
|
| const toolMemoryBtn = el('tool-memory-btn');
|
| if (toolMemoryBtn && memoryModal) {
|
| toolMemoryBtn.addEventListener('click', () => {
|
| memoryModal.classList.remove('hidden');
|
| if (memoryModule && memoryModule.renderMemoryList) memoryModule.renderMemoryList();
|
| if (memoryModule && memoryModule.updateMemoryCount) memoryModule.updateMemoryCount();
|
| });
|
| }
|
|
|
| const addMemBtn = el('add-memory-btn');
|
| if (addMemBtn) {
|
| addMemBtn.addEventListener('click', memoryModule.addNewMemory);
|
| }
|
|
|
| const memorySearchInput = el('memory-search');
|
| if (memorySearchInput) {
|
| memorySearchInput.addEventListener('input', () => {
|
| memoryModule.renderMemoryList();
|
| memoryModule.updateMemoryCount();
|
| });
|
| }
|
|
|
| const newMemoryInput = el('new-memory-input');
|
| if (newMemoryInput) {
|
| newMemoryInput.addEventListener('keypress', (e) => {
|
| if (e.key === 'Enter') {
|
| memoryModule.addNewMemory();
|
| }
|
| });
|
| }
|
|
|
|
|
|
|
|
|
| function loadToggleState() {
|
| return Storage.loadToggleState();
|
| }
|
| function saveToggleState(state) {
|
| Storage.saveToggleState(state);
|
| }
|
|
|
|
|
|
|
| const MODE_TOOLS = [
|
| { btnId: 'web-toggle-btn', checkboxId: 'web-toggle', stateKey: 'web' },
|
| { btnId: 'bash-toggle-btn', checkboxId: 'bash-toggle', stateKey: 'bash' },
|
| ];
|
|
|
| function _modeKey(stateKey, mode) { return `${stateKey}_${mode}`; }
|
|
|
| function loadToolPref(stateKey, mode) {
|
| const state = loadToggleState();
|
| const key = _modeKey(stateKey, mode);
|
| if (Object.prototype.hasOwnProperty.call(state, key)) return !!state[key];
|
| return mode === 'agent';
|
| }
|
|
|
| function saveToolPref(stateKey, mode, value) {
|
| const state = loadToggleState();
|
| state[_modeKey(stateKey, mode)] = value;
|
| saveToggleState(state);
|
| }
|
|
|
| const TOOL_TOGGLE_TOAST_LABELS = {
|
| web: 'Web search',
|
| bash: 'Shell',
|
| };
|
|
|
| function showToolToggleToast(stateKey, active) {
|
| const label = TOOL_TOGGLE_TOAST_LABELS[stateKey];
|
| if (!label || !uiModule?.showToast) return;
|
| uiModule.showToast(`${label} ${active ? 'on' : 'off'}`, 1800);
|
| }
|
|
|
| function applyModeToToggles(mode) {
|
| MODE_TOOLS.forEach(({ btnId, checkboxId, stateKey }) => {
|
| const btn = el(btnId);
|
| if (!btn) return;
|
|
|
| if (mode === 'chat' && stateKey === 'bash') {
|
| btn.style.display = 'none';
|
| return;
|
| }
|
|
|
| btn.style.display = '';
|
| if (btn.style.display === 'none') return;
|
| const on = loadToolPref(stateKey, mode);
|
| btn.classList.toggle('active', on);
|
| if (checkboxId) { const chk = el(checkboxId); if (chk) chk.checked = on; }
|
| });
|
| }
|
|
|
|
|
| (function initModeToggle() {
|
| const agentBtn = el('mode-agent-btn');
|
| const chatBtn = el('mode-chat-btn');
|
| if (!agentBtn || !chatBtn) return;
|
| const state = loadToggleState();
|
| let currentMode = state.mode || 'chat';
|
|
|
|
|
| if (currentMode === 'chat') {
|
| const bashBtn = el('bash-toggle-btn');
|
| if (bashBtn) bashBtn.style.display = 'none';
|
| }
|
|
|
| function setMode(mode) {
|
| currentMode = mode;
|
| const st = loadToggleState();
|
| st.mode = mode;
|
| saveToggleState(st);
|
| agentBtn.classList.toggle('active', mode === 'agent');
|
| chatBtn.classList.toggle('active', mode === 'chat');
|
| agentBtn.setAttribute('aria-pressed', String(mode === 'agent'));
|
| chatBtn.setAttribute('aria-pressed', String(mode === 'chat'));
|
|
|
| const toggle = agentBtn.closest('.mode-toggle');
|
| if (toggle) toggle.classList.toggle('mode-chat', mode === 'chat');
|
|
|
| try { workspaceModule.applyMode(mode); } catch (_) {}
|
|
|
| setTimeout(() => applyModeToToggles(mode), 500);
|
| }
|
| agentBtn.addEventListener('click', () => {
|
|
|
| const resChk = el('research-toggle');
|
| if (resChk && resChk.checked) _syncResearchIndicator(false);
|
| setMode('agent');
|
| });
|
| chatBtn.addEventListener('click', () => setMode('chat'));
|
| setMode(currentMode);
|
| })();
|
|
|
|
|
| const SPLASH_COUNT_KEY = 'odysseus-tool-splash-counts';
|
| const SPLASH_MAX = 2;
|
| const _toolSplashes = {
|
| web: { role: 'Web Search', text: 'Searches the web for relevant information to include in the response. Results are fetched and summarized before the AI answers.' },
|
| bash: { role: 'Shell Access', text: 'Gives the AI access to a sandboxed shell for running commands, installing packages, and executing scripts. Use with caution.' },
|
| builder: { role: 'Tool Builder', text: 'Create custom mini-apps and tools the AI can use. Describe what you need and the AI will build a tool you can reuse across conversations.' },
|
| research: { role: 'Deep Research', text: 'Multi-round web search with source analysis. Takes longer but produces comprehensive, well-sourced answers. Your next message will trigger a deep research cycle.' },
|
| };
|
| function _showToolSplash(key) {
|
| const splash = _toolSplashes[key];
|
| if (!splash) return;
|
|
|
| const counts = Storage.getJSON(SPLASH_COUNT_KEY, {});
|
| const seen = counts[key] || 0;
|
| if (seen >= SPLASH_MAX) return;
|
| counts[key] = seen + 1;
|
| Storage.setJSON(SPLASH_COUNT_KEY, counts);
|
|
|
| if (chatModule && chatModule.hideWelcomeScreen) {
|
| chatModule.hideWelcomeScreen();
|
| }
|
| const chatBox = document.getElementById('chat-history');
|
| if (!chatBox) return;
|
| const div = document.createElement('div');
|
| div.className = 'msg msg-ai tool-splash';
|
| div.innerHTML = '<div class="role">' + splash.role + '</div><div class="body" style="opacity:0.7;font-size:0.92em">' + splash.text + '</div>';
|
| chatBox.appendChild(div);
|
| if (uiModule) uiModule.scrollHistory();
|
| }
|
|
|
|
|
| function setupToggle(btnId, checkboxId, stateKey) {
|
| const btn = el(btnId);
|
| if (!btn) return;
|
|
|
| const mode = (loadToggleState().mode) || 'chat';
|
| const saved = loadToolPref(stateKey, mode);
|
| const chk = el(checkboxId);
|
| if (chk) chk.checked = saved;
|
| btn.classList.toggle('active', saved);
|
| btn.setAttribute('aria-pressed', String(saved));
|
| btn.addEventListener('click', () => {
|
| const curMode = (loadToggleState().mode) || 'chat';
|
| const chk = el(checkboxId);
|
| chk.checked = !chk.checked;
|
| btn.classList.toggle('active', chk.checked);
|
| btn.setAttribute('aria-pressed', String(chk.checked));
|
| saveToolPref(stateKey, curMode, chk.checked);
|
| showToolToggleToast(stateKey, chk.checked);
|
| if (chk.checked) _showToolSplash(stateKey);
|
|
|
| if (stateKey === 'web' && chk.checked) {
|
| const resChk = el('research-toggle');
|
| if (resChk && resChk.checked) {
|
| _syncResearchIndicator(false);
|
| }
|
| }
|
| });
|
| }
|
| setupToggle('web-toggle-btn', 'web-toggle', 'web');
|
| setupToggle('bash-toggle-btn', 'bash-toggle', 'bash');
|
| try { workspaceModule.initWorkspace(); } catch (_) {}
|
|
|
|
|
| const overflowDocBtn = el('overflow-doc-btn');
|
| if (overflowDocBtn) {
|
| overflowDocBtn.addEventListener('click', async () => {
|
| if (!documentModule) return;
|
| if (documentModule.isPanelOpen()) {
|
| documentModule.closePanel();
|
| overflowDocBtn.classList.remove('active');
|
| const st = loadToggleState(); st.doc = false; saveToggleState(st);
|
| } else {
|
| let sessionId = sessionModule.getCurrentSessionId();
|
|
|
| if (!sessionId && sessionModule.hasPendingChat && sessionModule.hasPendingChat()) {
|
| await sessionModule.materializePendingSession();
|
| sessionId = sessionModule.getCurrentSessionId();
|
| }
|
| if (sessionId) {
|
| documentModule.loadSessionDocs(sessionId, { forceOpen: true });
|
| } else {
|
| documentModule.ensureDocPanel();
|
| }
|
| overflowDocBtn.classList.add('active');
|
| const st = loadToggleState(); st.doc = true; saveToggleState(st);
|
| }
|
| });
|
| }
|
|
|
|
|
| const docIndicatorBtn = el('doc-indicator-btn');
|
| if (docIndicatorBtn) {
|
| docIndicatorBtn.addEventListener('click', () => {
|
| const ob = el('overflow-doc-btn');
|
| if (ob) ob.click();
|
| });
|
| }
|
|
|
|
|
| function _syncRagIndicator(active) {
|
| const indicator = el('rag-indicator-btn');
|
| const overflow = el('overflow-rag-btn');
|
| const chk = el('rag-toggle');
|
| if (chk) chk.checked = active;
|
| if (indicator) {
|
| indicator.style.display = active ? '' : 'none';
|
| indicator.classList.toggle('active', active);
|
| }
|
| if (overflow) overflow.classList.toggle('active', active);
|
| const s = loadToggleState(); s.rag = active; saveToggleState(s);
|
| updatePlusDot();
|
| }
|
| window._syncRagIndicator = _syncRagIndicator;
|
| window._syncResearchIndicator = _syncResearchIndicator;
|
|
|
|
|
|
|
|
|
| window._syncGroupIndicator = _syncGroupIndicator;
|
|
|
| {
|
| const st = loadToggleState();
|
| const ragState = st.rag || false;
|
| _syncRagIndicator(ragState);
|
| }
|
|
|
|
|
| function updatePlusDot() {
|
| const plusBtn = el('overflow-plus-btn');
|
| if (!plusBtn) return;
|
| const menu = el('overflow-menu');
|
| const anyActive = menu ? Array.from(menu.querySelectorAll('.overflow-menu-item.active')).some(item => item.style.display !== 'none') : false;
|
| plusBtn.classList.toggle('has-active', anyActive);
|
| }
|
|
|
| document.addEventListener('overflow-state-change', () => updatePlusDot());
|
|
|
|
|
| const chatInputBar = document.querySelector('.chat-input-bar');
|
|
|
| const _msgTextarea = el('message');
|
| if (chatInputBar && _msgTextarea) {
|
| let _refocusOnBlur = false;
|
| function _flagRefocus(e) {
|
| if (e.target.closest('textarea, input')) return;
|
|
|
| if (e.target.closest('#overflow-attach-btn')) return;
|
|
|
| if (e.target.closest('.model-picker-btn')) return;
|
|
|
|
|
|
|
|
|
| if (e.target.closest('#overflow-plus-btn')) return;
|
| if (document.activeElement === _msgTextarea) _refocusOnBlur = true;
|
| }
|
| chatInputBar.addEventListener('touchstart', _flagRefocus, { passive: true });
|
|
|
| const _overflowMenu = el('overflow-menu');
|
| if (_overflowMenu) _overflowMenu.addEventListener('touchstart', _flagRefocus, { passive: true });
|
|
|
| const _pickerMenu = document.getElementById('model-picker-menu');
|
| if (_pickerMenu) _pickerMenu.addEventListener('touchstart', _flagRefocus, { passive: true });
|
|
|
| const _attachStrip = el('attach-strip');
|
| if (_attachStrip) _attachStrip.addEventListener('touchstart', _flagRefocus, { passive: true });
|
| _msgTextarea.addEventListener('blur', () => {
|
| if (_refocusOnBlur) {
|
| _refocusOnBlur = false;
|
| setTimeout(() => _msgTextarea.focus(), 0);
|
| }
|
| });
|
|
|
| document.addEventListener('touchend', () => { setTimeout(() => { _refocusOnBlur = false; }, 50); }, { passive: true });
|
| }
|
|
|
| (function initOverflowMenu() {
|
| const plusBtn = el('overflow-plus-btn');
|
| const menu = el('overflow-menu');
|
| if (!plusBtn || !menu) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| const ownerWrap = menu.parentElement;
|
| const pickerWrap = el('model-picker-wrap');
|
| let _vvReposition = null;
|
|
|
|
|
|
|
| function positionMenu() {
|
| const r = plusBtn.getBoundingClientRect();
|
| menu.style.left = r.left + 'px';
|
| menu.style.right = 'auto';
|
| menu.style.bottom = 'auto';
|
| menu.style.maxHeight = '';
|
| menu.style.overflowY = '';
|
| const avail = r.top - 16;
|
| const natural = menu.scrollHeight;
|
| const h = Math.min(natural, avail);
|
| if (natural > avail) {
|
| menu.style.maxHeight = avail + 'px';
|
| menu.style.overflowY = 'auto';
|
| }
|
| menu.style.top = (r.top - 8 - h) + 'px';
|
| }
|
|
|
|
|
|
|
| plusBtn.addEventListener('pointerdown', (e) => { e.preventDefault(); });
|
| plusBtn.addEventListener('click', (e) => {
|
| e.stopPropagation();
|
|
|
|
|
|
|
| const isOpen = !menu.classList.contains('hidden') && !menu.classList.contains('closing');
|
| if (isOpen) {
|
| closeOverflowMenu();
|
| return;
|
| }
|
|
|
| menu.classList.remove('closing');
|
| menu.classList.remove('hidden');
|
| plusBtn.classList.add('expanded');
|
| document.body.appendChild(menu);
|
|
|
| if (pickerWrap) pickerWrap.style.visibility = 'hidden';
|
|
|
|
|
|
|
| positionMenu();
|
| if (window.visualViewport && !_vvReposition) {
|
| _vvReposition = () => positionMenu();
|
| window.visualViewport.addEventListener('resize', _vvReposition);
|
| window.visualViewport.addEventListener('scroll', _vvReposition);
|
| }
|
| });
|
| function closeOverflowMenu() {
|
| if (menu.classList.contains('hidden')) return;
|
| if (menu.classList.contains('closing')) return;
|
| if (_vvReposition && window.visualViewport) {
|
| window.visualViewport.removeEventListener('resize', _vvReposition);
|
| window.visualViewport.removeEventListener('scroll', _vvReposition);
|
| _vvReposition = null;
|
| }
|
|
|
|
|
| menu.classList.add('closing');
|
| plusBtn.classList.remove('expanded');
|
| if (pickerWrap) pickerWrap.style.visibility = '';
|
|
|
|
|
| setTimeout(() => {
|
| menu.classList.add('hidden');
|
| menu.classList.remove('closing');
|
| if (ownerWrap) ownerWrap.appendChild(menu);
|
| }, 400);
|
| }
|
|
|
|
|
|
|
| menu.querySelectorAll('.overflow-menu-item').forEach(item => {
|
| item.addEventListener('pointerdown', (e) => { e.preventDefault(); });
|
| item.addEventListener('click', () => closeOverflowMenu());
|
| });
|
| document.addEventListener('click', (e) => {
|
| if (!menu.contains(e.target) && e.target !== plusBtn) closeOverflowMenu();
|
| });
|
| document.addEventListener('keydown', (e) => {
|
| if (e.key === 'Escape' && !menu.classList.contains('hidden')) closeOverflowMenu();
|
| });
|
|
|
|
|
| const researchBtn = el('research-toggle-btn');
|
| if (researchBtn) {
|
| const st = loadToggleState();
|
| const resState = st.research || false;
|
| el('research-toggle').checked = resState;
|
| researchBtn.classList.toggle('active', resState);
|
| researchBtn.style.display = resState ? '' : 'none';
|
|
|
| const overflowRes = el('overflow-research-btn');
|
| if (overflowRes) overflowRes.classList.toggle('active', resState);
|
| const toolRes = el('tool-research-btn');
|
| if (toolRes) toolRes.classList.toggle('active', resState);
|
|
|
| if (resState) {
|
| const webChk = el('web-toggle');
|
| const webBtn = el('web-toggle-btn');
|
| if (webChk && webChk.checked) {
|
| webChk.checked = false;
|
| if (webBtn) webBtn.classList.remove('active');
|
| saveToolPref('web', (st.mode || 'chat'), false);
|
| }
|
| }
|
|
|
| researchBtn.addEventListener('click', () => {
|
| const chk = el('research-toggle');
|
| const turningOn = chk ? !chk.checked : false;
|
| _syncResearchIndicator(turningOn);
|
| if (turningOn) {
|
| _showToolSplash('research');
|
|
|
| if (presetsModule && presetsModule.deactivateCharacter) presetsModule.deactivateCharacter();
|
|
|
| const webChk = el('web-toggle');
|
| const webBtn = el('web-toggle-btn');
|
| if (webChk && webChk.checked) {
|
| webChk.checked = false;
|
| if (webBtn) webBtn.classList.remove('active');
|
| saveToolPref('web', (loadToggleState().mode || 'chat'), false);
|
| }
|
|
|
| const rs = loadToggleState();
|
| if (rs.mode === 'agent') {
|
| rs.mode = 'chat';
|
| saveToggleState(rs);
|
| const ab = el('mode-agent-btn'), cb = el('mode-chat-btn');
|
| if (ab) ab.classList.remove('active');
|
| if (cb) cb.classList.add('active');
|
| applyModeToToggles('chat');
|
| }
|
| }
|
| });
|
| }
|
|
|
| updatePlusDot();
|
| })();
|
|
|
|
|
| (function initToolbarOverflow() {
|
| const inputLeft = document.querySelector('.chat-input-left');
|
| const overflowMenu = el('overflow-menu');
|
| const overflowWrapper = document.querySelector('.overflow-wrapper');
|
| if (!inputLeft || !overflowMenu || !overflowWrapper) return;
|
|
|
|
|
| const collapsibleIds = ['bash-toggle-btn', 'web-toggle-btn'];
|
| const collapsibleBtns = collapsibleIds.map(id => el(id)).filter(Boolean);
|
|
|
| const overflowMirrors = new Map();
|
|
|
|
|
| collapsibleBtns.forEach(btn => {
|
| const mirror = document.createElement('button');
|
| mirror.type = 'button';
|
| mirror.className = 'overflow-menu-item toolbar-overflow-mirror';
|
| mirror.dataset.mirrorOf = btn.id;
|
| const title = btn.title || btn.id.replace(/-/g, ' ');
|
| mirror.innerHTML = btn.querySelector('svg').outerHTML + '<span>' + title + '</span>' +
|
| '<span class="overflow-active-dot"></span>';
|
| mirror.style.display = 'none';
|
| mirror.addEventListener('click', () => btn.click());
|
|
|
| overflowMenu.insertBefore(mirror, overflowMenu.firstChild);
|
| overflowMirrors.set(btn.id, mirror);
|
| });
|
|
|
| function syncMirrorStates() {
|
| overflowMirrors.forEach((mirror, btnId) => {
|
| const btn = el(btnId);
|
| if (btn) mirror.classList.toggle('active', btn.classList.contains('active'));
|
| });
|
| updatePlusDot();
|
| }
|
|
|
| function checkToolbarOverflow() {
|
| const inputBottom = inputLeft.parentElement;
|
| if (!inputBottom) return;
|
| const rightEl = document.querySelector('.chat-input-right');
|
| const available = inputBottom.clientWidth -
|
| (rightEl ? rightEl.offsetWidth : 0) - 16;
|
|
|
|
|
| collapsibleBtns.forEach(btn => btn.classList.remove('toolbar-collapsed'));
|
| overflowMirrors.forEach(m => m.style.display = 'none');
|
|
|
|
|
| const prevOverflow = inputLeft.style.overflow;
|
| inputLeft.style.overflow = 'visible';
|
| inputLeft.style.flexWrap = 'nowrap';
|
|
|
|
|
| void inputLeft.offsetWidth;
|
|
|
|
|
| const wrapperWidth = overflowWrapper.offsetWidth + 4;
|
|
|
|
|
| const btnWidths = collapsibleBtns.map(btn => btn.offsetWidth + 4);
|
|
|
|
|
| let otherWidth = 0;
|
| Array.from(inputLeft.children).forEach(c => {
|
| if (c === overflowWrapper) return;
|
| if (collapsibleBtns.includes(c)) return;
|
| if (c.offsetWidth) otherWidth += c.offsetWidth + 4;
|
| });
|
|
|
| let totalWidth = wrapperWidth + otherWidth + btnWidths.reduce((a, b) => a + b, 0);
|
|
|
|
|
| const _resChk = el('research-toggle');
|
| const _researchOn = _resChk && _resChk.checked;
|
| const _docViewOn = document.body.classList.contains('doc-view');
|
| if (_researchOn && _docViewOn) {
|
| collapsibleBtns.forEach(btn => {
|
| btn.classList.add('toolbar-collapsed');
|
| const mirror = overflowMirrors.get(btn.id);
|
| if (mirror) mirror.style.display = '';
|
| });
|
| inputLeft.style.overflow = prevOverflow;
|
| inputLeft.style.flexWrap = '';
|
| syncMirrorStates();
|
| return;
|
| }
|
|
|
|
|
| if (totalWidth > available) {
|
| for (let i = 0; i < collapsibleBtns.length; i++) {
|
| collapsibleBtns[i].classList.add('toolbar-collapsed');
|
| const mirror = overflowMirrors.get(collapsibleBtns[i].id);
|
| if (mirror) mirror.style.display = '';
|
| totalWidth -= btnWidths[i];
|
| if (totalWidth <= available) break;
|
| }
|
| }
|
|
|
|
|
| inputLeft.style.overflow = prevOverflow;
|
| inputLeft.style.flexWrap = '';
|
| syncMirrorStates();
|
| }
|
|
|
|
|
| const observer = new MutationObserver(() => syncMirrorStates());
|
| collapsibleBtns.forEach(btn => {
|
| observer.observe(btn, { attributes: true, attributeFilter: ['class'] });
|
| });
|
|
|
|
|
| window.addEventListener('resize', () => requestAnimationFrame(checkToolbarOverflow));
|
|
|
| checkToolbarOverflow();
|
|
|
| document.addEventListener('overflow-state-change', () =>
|
| requestAnimationFrame(checkToolbarOverflow));
|
|
|
| const sidebarEl = el('sidebar');
|
| if (sidebarEl) {
|
| new MutationObserver(() => requestAnimationFrame(checkToolbarOverflow))
|
| .observe(sidebarEl, { attributes: true, attributeFilter: ['class'] });
|
| }
|
|
|
| new MutationObserver(() => requestAnimationFrame(checkToolbarOverflow))
|
| .observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
|
|
| const inputBottom = inputLeft.parentElement;
|
| if (inputBottom) {
|
| new ResizeObserver(() => requestAnimationFrame(checkToolbarOverflow)).observe(inputBottom);
|
| }
|
| })();
|
|
|
|
|
| (function initModelPickerResponsive() {
|
| const inputTop = document.querySelector('.chat-input-top');
|
| const pickerWrap = el('model-picker-wrap');
|
| if (!inputTop || !pickerWrap) return;
|
|
|
| const PLACEHOLDER_HIDE_WIDTH = 400;
|
| const PICKER_HIDE_WIDTH = 220;
|
| const TOOLBAR_HIDE_WIDTH = 160;
|
| const textarea = el('message');
|
| const inputBottom = document.querySelector('.chat-input-bottom');
|
| const _isMobile = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
|
|
| function checkPickerOverflow() {
|
|
|
| if (_isMobile) return;
|
| const w = inputTop.clientWidth;
|
|
|
| pickerWrap.classList.toggle('picker-auto-hidden', w < PICKER_HIDE_WIDTH);
|
|
|
| if (textarea) {
|
| textarea.setAttribute('placeholder', w < PLACEHOLDER_HIDE_WIDTH ? '' : 'Message Odysseus...');
|
| }
|
|
|
| if (inputBottom) {
|
| inputBottom.classList.toggle('toolbar-auto-hidden', w < TOOLBAR_HIDE_WIDTH);
|
| }
|
| }
|
|
|
| const ro = new ResizeObserver(() => requestAnimationFrame(checkPickerOverflow));
|
| ro.observe(inputTop);
|
| checkPickerOverflow();
|
| })();
|
|
|
|
|
| (function initTTSToggle() {
|
| const ttsBtn = document.getElementById('overflow-tts-btn');
|
| if (!ttsBtn) return;
|
| try {
|
| const st = loadToggleState();
|
| if (st.ttsMode) {
|
| ttsBtn.classList.add('active');
|
| if (window.aiTTSManager) window.aiTTSManager.autoPlay = true;
|
| }
|
| } catch(e) {}
|
|
|
| ttsBtn.addEventListener('click', () => {
|
| const isActive = !ttsBtn.classList.contains('active');
|
| ttsBtn.classList.toggle('active', isActive);
|
| if (window.aiTTSManager) window.aiTTSManager.autoPlay = isActive;
|
| const s = loadToggleState(); s.ttsMode = isActive; saveToggleState(s);
|
| updatePlusDot();
|
| });
|
| })();
|
|
|
|
|
|
|
| const compareIndicatorBtn = el('compare-indicator-btn');
|
| if (compareIndicatorBtn) {
|
| compareIndicatorBtn.addEventListener('click', () => {
|
| if (compareModule && compareModule.isActive()) {
|
| compareModule.closeCompare();
|
| }
|
| });
|
| }
|
|
|
|
|
| const overflowRagBtn = el('overflow-rag-btn');
|
| const ragIndicatorBtn = el('rag-indicator-btn');
|
| if (overflowRagBtn) {
|
| overflowRagBtn.addEventListener('click', () => {
|
| const chk = el('rag-toggle');
|
| const isActive = chk ? !chk.checked : true;
|
| _syncRagIndicator(isActive);
|
| });
|
| }
|
| if (ragIndicatorBtn) {
|
| ragIndicatorBtn.addEventListener('click', () => {
|
| _syncRagIndicator(false);
|
| });
|
| }
|
|
|
|
|
| const overflowResearchBtn = el('overflow-research-btn');
|
| if (overflowResearchBtn) {
|
| overflowResearchBtn.addEventListener('click', () => {
|
| const chk = el('research-toggle');
|
| const turningOn = chk ? !chk.checked : false;
|
| _syncResearchIndicator(turningOn);
|
| if (turningOn) {
|
| _showToolSplash('research');
|
|
|
| if (presetsModule && presetsModule.deactivateCharacter) presetsModule.deactivateCharacter();
|
|
|
| const webChk = el('web-toggle');
|
| const webBtn = el('web-toggle-btn');
|
| if (webChk && webChk.checked) {
|
| webChk.checked = false;
|
| if (webBtn) webBtn.classList.remove('active');
|
| saveToolPref('web', (loadToggleState().mode || 'chat'), false);
|
| }
|
|
|
| const rs2 = loadToggleState();
|
| if (rs2.mode === 'agent') {
|
| rs2.mode = 'chat';
|
| saveToggleState(rs2);
|
| const ab2 = el('mode-agent-btn'), cb2 = el('mode-chat-btn');
|
| if (ab2) ab2.classList.remove('active');
|
| if (cb2) cb2.classList.add('active');
|
| applyModeToToggles('chat');
|
| }
|
| }
|
| });
|
| }
|
|
|
|
|
| const overflowGroupBtn = el('overflow-group-btn');
|
| if (overflowGroupBtn) {
|
| overflowGroupBtn.addEventListener('click', async () => {
|
| const chk = el('group-toggle');
|
| const turningOn = chk ? !chk.checked : false;
|
| if (turningOn) {
|
| const picked = await groupModule.showModelPicker();
|
| if (!picked || picked.length < 2) return;
|
| groupModule.setActive(true);
|
| _syncGroupIndicator(true);
|
| _startFreshChat();
|
|
|
| const _chatBox = document.getElementById('chat-history');
|
| if (_chatBox) {
|
| _chatBox.querySelectorAll('.tool-splash').forEach(s => s.remove());
|
|
|
| if (chatModule && chatModule.hideWelcomeScreen) chatModule.hideWelcomeScreen();
|
| }
|
|
|
| const sid = sessionModule.getCurrentSessionId() || 'group-' + Date.now();
|
| await groupModule.startGroup(picked, sid);
|
|
|
| const _mpw = el('model-picker-wrap');
|
| if (_mpw) _mpw.style.display = 'none';
|
| uiModule.showToast(`Group chat ready — ${picked.length} models`);
|
| } else {
|
| _syncGroupIndicator(false);
|
| groupModule.stopGroup();
|
|
|
| const _mpWrap2 = el('model-picker-wrap');
|
| if (_mpWrap2) _mpWrap2.style.display = '';
|
| }
|
| });
|
| }
|
|
|
|
|
| const groupToggleBtn = el('group-toggle-btn');
|
| if (groupToggleBtn) {
|
| groupToggleBtn.addEventListener('click', () => {
|
| _syncGroupIndicator(false);
|
| groupModule.stopGroup();
|
| });
|
| }
|
|
|
|
|
| const incognitoBtn = el('incognito-btn');
|
| const INCOGNITO_EYE_OPEN = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>';
|
| const INCOGNITO_EYE_CLOSED = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><line x1="8" y1="16" x2="16" y2="8"/><line x1="8" y1="8" x2="16" y2="16"/></svg>';
|
| const SESSION_ICON_CHAT = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>';
|
| const SESSION_ICON_INCOGNITO = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/><path d="M14.12 14.12a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>';
|
|
|
| function _syncSessionIncognitoIcon(active) {
|
| const activeSession = document.querySelector('.list-item.active-session .session-icon');
|
| if (activeSession) {
|
| activeSession.innerHTML = active ? SESSION_ICON_INCOGNITO : SESSION_ICON_CHAT;
|
| activeSession.style.color = active ? 'var(--accent)' : '';
|
| }
|
| }
|
|
|
| if (incognitoBtn) {
|
| incognitoBtn.addEventListener('mousedown', (e) => e.preventDefault());
|
| incognitoBtn.addEventListener('click', () => {
|
|
|
| const ws = el('welcome-screen');
|
| if (ws && ws.classList.contains('hidden')) return;
|
| const chk = el('incognito-toggle');
|
| chk.checked = !chk.checked;
|
| incognitoBtn.classList.toggle('active', chk.checked);
|
| const tipEl = el('welcome-tip');
|
| incognitoBtn.title = chk.checked ? 'Disable Nobody mode' : 'Enable Nobody mode — no memory, no history saved';
|
| const welcomeName = document.querySelector('.welcome-name');
|
| if (chk.checked) {
|
| incognitoBtn.innerHTML = INCOGNITO_EYE_CLOSED + '<span class="incognito-label">Nobody</span>';
|
| if (welcomeName) {
|
| welcomeName.dataset.originalHtml = welcomeName.innerHTML;
|
| welcomeName.innerHTML = '<svg class="welcome-boat" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><line x1="8" y1="16" x2="16" y2="8"/><line x1="8" y1="8" x2="16" y2="16"/></svg>Nobody';
|
|
|
| welcomeName.style.animation = 'none';
|
| welcomeName.offsetHeight;
|
| welcomeName.style.animation = '';
|
| }
|
| if (ws) { ws.style.animation = 'none'; ws.offsetHeight; ws.style.animation = 'welcome-enter 0.3s ease-out both'; }
|
| const welcomeSub = el('welcome-sub');
|
| if (welcomeSub) {
|
| if (!welcomeSub.dataset.originalText) welcomeSub.dataset.originalText = welcomeSub.textContent;
|
| welcomeSub.textContent = "Who am I? I'm nobody.";
|
| welcomeSub.style.display = '';
|
| }
|
| if (tipEl) { tipEl.dataset.originalTip = tipEl.textContent; tipEl.textContent = 'Temporary session \u2014 won\u2019t be saved and no memory activation.'; tipEl.style.opacity = '0.5'; tipEl.style.marginTop = '8px'; }
|
|
|
|
|
|
|
|
|
| const _offIds = ['web-toggle', 'bash-toggle', 'research-toggle'];
|
| _offIds.forEach(id => { const c = el(id); if (c) c.checked = false; });
|
| ['web-toggle-btn', 'bash-toggle-btn'].forEach(id => { const b = el(id); if (b) b.classList.remove('active'); });
|
| const _ab = el('mode-agent-btn'), _cb = el('mode-chat-btn');
|
| if (_ab) _ab.classList.remove('active');
|
| if (_cb) _cb.classList.add('active');
|
| const ts = Storage.getJSON(Storage.KEYS.TOGGLES, {});
|
| ts.research = false; ts.mode = 'chat';
|
| Storage.setJSON(Storage.KEYS.TOGGLES, ts);
|
| } else {
|
| incognitoBtn.innerHTML = INCOGNITO_EYE_OPEN + '<span class="incognito-label">Nobody</span>';
|
| if (welcomeName && welcomeName.dataset.originalHtml) {
|
| welcomeName.innerHTML = welcomeName.dataset.originalHtml;
|
|
|
| welcomeName.style.animation = 'none';
|
| welcomeName.offsetHeight;
|
| welcomeName.style.animation = '';
|
| }
|
| if (ws) { ws.style.animation = 'none'; ws.offsetHeight; ws.style.animation = 'welcome-enter 0.3s ease-out both'; }
|
| const welcomeSub2 = el('welcome-sub');
|
| if (welcomeSub2) {
|
| if (welcomeSub2.dataset.originalText) {
|
| welcomeSub2.textContent = welcomeSub2.dataset.originalText;
|
| delete welcomeSub2.dataset.originalText;
|
| }
|
| welcomeSub2.style.display = '';
|
| }
|
| if (tipEl && tipEl.dataset.originalTip) { tipEl.textContent = tipEl.dataset.originalTip; tipEl.style.opacity = ''; tipEl.style.marginTop = ''; }
|
|
|
|
|
| const _ts = Storage.getJSON(Storage.KEYS.TOGGLES, {});
|
| let _dirty = false;
|
| ['web_agent', 'bash_agent', 'web_chat', 'bash_chat'].forEach(k => {
|
| if (_ts[k] === false) { delete _ts[k]; _dirty = true; }
|
| });
|
| if (_dirty) Storage.setJSON(Storage.KEYS.TOGGLES, _ts);
|
|
|
| const _curMode = (Storage.getJSON(Storage.KEYS.TOGGLES, {}) || {}).mode || 'chat';
|
| try { applyModeToToggles(_curMode); } catch (_) {}
|
| }
|
|
|
| if (!chk.checked && ws && ws.classList.contains('hidden')) {
|
| incognitoBtn.style.display = 'none';
|
| }
|
|
|
| const _incInd = el('incognito-indicator');
|
| if (_incInd) _incInd.style.display = chk.checked ? '' : 'none';
|
|
|
| _syncSessionIncognitoIcon(chk.checked);
|
| });
|
| }
|
|
|
|
|
| const incognitoIndicator = el('incognito-indicator');
|
| if (incognitoIndicator) {
|
| incognitoIndicator.addEventListener('click', () => {
|
| if (incognitoBtn) incognitoBtn.click();
|
| else {
|
| const chk = el('incognito-toggle');
|
| if (chk) { chk.checked = false; }
|
| incognitoIndicator.style.display = 'none';
|
| }
|
| });
|
| }
|
|
|
|
|
| function _deactivateIncognito() {
|
| const chk = el('incognito-toggle');
|
| if (!chk || !chk.checked) return;
|
| if (incognitoBtn) incognitoBtn.click();
|
| }
|
|
|
|
|
| const UI_VIS_KEY = 'odysseus-ui-visibility';
|
|
|
|
|
| const UI_VIS_MAP = {
|
| 'sidebar-brand': '.sidebar-brand-title',
|
| 'sidebar-new-chat': '#sidebar-new-chat-btn',
|
| 'sidebar-search': '#sidebar-search-btn',
|
| 'sessions-section': '#sessions-section',
|
| 'email-section': '#email-section',
|
| 'models-section': '#models-section',
|
| 'tools-section': '#tools-section',
|
|
|
|
|
| 'tool-calendar': '#tool-calendar-btn',
|
| 'tool-compare': '#tool-compare-btn',
|
| 'tool-cookbook': '#tool-cookbook-btn',
|
| 'tool-research': '#tool-research-btn',
|
| 'tool-gallery': '#tool-gallery-btn',
|
| 'tool-library': '#tool-library-btn',
|
| 'tool-memory': '#tool-memory-btn',
|
| 'tool-notes': '#tool-notes-btn',
|
| 'tool-tasks': '#tool-tasks-btn',
|
| 'tool-theme': '#tool-theme-btn',
|
| 'user-bar': '#user-bar-profile',
|
| 'sidebar-settings-btn':'#user-bar-settings',
|
| 'chat-meta': '.chat-meta-overlay',
|
| 'welcome-text': '.welcome-name, .welcome-sub, #welcome-tip',
|
| 'incognito-btn': '.incognito-btn',
|
| 'web-toggle-btn': '#web-toggle-btn',
|
| 'doc-toggle-btn': '#overflow-doc-btn',
|
| 'rag-toggle-btn': '#overflow-rag-btn',
|
| 'bash-toggle-btn': '#bash-toggle-btn',
|
| 'overflow-plus-btn': '.overflow-wrapper',
|
| 'mode-toggle': '.mode-toggle',
|
| 'preset-mini-btn': '#overflow-preset-btn',
|
| 'attach-btn': '#overflow-attach-btn',
|
| 'research-btn': '#overflow-research-btn',
|
| 'rail-new-chat': '#rail-new-session',
|
| };
|
|
|
|
|
| const UI_VIS_DEFAULT_OFF = new Set(['models-section', 'rag-toggle-btn', 'text-emojis']);
|
|
|
|
|
| const UI_VIS_ADMIN_ONLY = new Set([]);
|
|
|
| function loadUIVis() {
|
| return Storage.getJSON(UI_VIS_KEY, {});
|
| }
|
|
|
| function saveUIVis(state) {
|
| Storage.setJSON(UI_VIS_KEY, state);
|
| }
|
|
|
| function applyUIVis(state) {
|
| Object.entries(UI_VIS_MAP).forEach(([key, selector]) => {
|
|
|
| if (key === 'section-drag-reorder') return;
|
| const visible = key in state ? state[key] !== false : !UI_VIS_DEFAULT_OFF.has(key);
|
| document.querySelectorAll(selector).forEach(el => {
|
| el.style.display = visible ? '' : 'none';
|
| });
|
| });
|
|
|
| const dragEnabled = state['section-drag-reorder'] === true;
|
| document.body.classList.toggle('rearrange-mode', dragEnabled);
|
| document.querySelectorAll('.section[draggable]').forEach(el => {
|
| el.setAttribute('draggable', dragEnabled ? 'true' : 'false');
|
| });
|
|
|
|
|
| applyTextEmojis(state['text-emojis'] === true);
|
|
|
| document.body.classList.toggle('hide-thinking', state['show-thinking'] === false);
|
| }
|
|
|
|
|
| function syncRearrangeChecks() {
|
| const on = loadUIVis()['section-drag-reorder'] === true;
|
| document.querySelectorAll('.rearrange-toggle .rearrange-check').forEach(ch => {
|
| ch.style.opacity = on ? '1' : '0';
|
| });
|
| }
|
| document.querySelectorAll('.rearrange-toggle').forEach(toggle => {
|
| toggle.addEventListener('click', () => {
|
| const state = loadUIVis();
|
| const wasOn = state['section-drag-reorder'] === true;
|
| state['section-drag-reorder'] = !wasOn;
|
| saveUIVis(state);
|
| applyUIVis(state);
|
| syncRearrangeChecks();
|
| uiModule.showToast(!wasOn ? 'Rearrange enabled' : 'Rearrange disabled');
|
|
|
|
|
| const dd = toggle.closest('[id$="-sort-dropdown"]');
|
| if (dd) dd.style.display = 'none';
|
| });
|
| });
|
|
|
|
|
|
|
|
|
| document.addEventListener('keydown', (e) => {
|
| if (e.key !== 'Escape') return;
|
| if (!document.body.classList.contains('rearrange-mode')) return;
|
| e.preventDefault();
|
| e.stopPropagation();
|
| const state = loadUIVis();
|
| state['section-drag-reorder'] = false;
|
| saveUIVis(state);
|
| applyUIVis(state);
|
| syncRearrangeChecks();
|
| uiModule.showToast('Rearrange disabled');
|
| }, true);
|
|
|
| const _sessionSortBtn = el('session-sort-btn');
|
| const _modelSortBtn = el('model-sort-btn');
|
| if (_sessionSortBtn) _sessionSortBtn.addEventListener('click', syncRearrangeChecks);
|
| if (_modelSortBtn) _modelSortBtn.addEventListener('click', syncRearrangeChecks);
|
| syncRearrangeChecks();
|
|
|
|
|
|
|
| const EMOJI_RE = /(?:\p{Emoji_Presentation}|\p{Extended_Pictographic})(?:\uFE0F|\u200D(?:\p{Emoji_Presentation}|\p{Extended_Pictographic}))*/gu;
|
|
|
|
|
| const EMOJI_MAP = {
|
| '😀':'grinning','😃':'smiley','😄':'smile','😁':'grin','😆':'laughing','😅':'sweat smile',
|
| '🤣':'rofl','😂':'joy','🙂':'slightly smiling','🙃':'upside down','😉':'wink',
|
| '😊':'blush','😇':'innocent','🥰':'smiling hearts','😍':'heart eyes','🤩':'star struck',
|
| '😘':'kissing heart','😗':'kissing','😚':'kissing closed eyes','😙':'kissing smiling eyes',
|
| '🥲':'smiling tear','😋':'yum','😛':'tongue','😜':'winking tongue','🤪':'zany',
|
| '😝':'squinting tongue','🤑':'money mouth','🤗':'hugging','🤭':'hand over mouth',
|
| '🤫':'shushing','🤔':'thinking','🫡':'saluting','🤐':'zipper mouth','🤨':'raised eyebrow',
|
| '😐':'neutral','😑':'expressionless','😶':'no mouth','🫥':'dotted line face',
|
| '😏':'smirk','😒':'unamused','🙄':'eye roll','😬':'grimacing','🤥':'lying',
|
| '😌':'relieved','😔':'pensive','😪':'sleepy','🤤':'drooling','😴':'sleeping',
|
| '😷':'mask','🤒':'thermometer','🤕':'head bandage','🤢':'nauseated','🤮':'vomiting',
|
| '🥵':'hot','🥶':'cold','🥴':'woozy','😵':'dizzy','🤯':'exploding head',
|
| '🤠':'cowboy','🥳':'party','🥸':'disguised','😎':'sunglasses','🤓':'nerd',
|
| '🧐':'monocle','😕':'confused','🫤':'diagonal mouth','😟':'worried','🙁':'slightly frowning',
|
| '😮':'open mouth','😯':'hushed','😲':'astonished','😳':'flushed','🥺':'pleading',
|
| '🥹':'holding back tears','😦':'frowning open mouth','😧':'anguished','😨':'fearful',
|
| '😰':'anxious sweat','😥':'sad relieved','😢':'crying','😭':'sobbing','😱':'screaming',
|
| '😖':'confounded','😣':'persevering','😞':'disappointed','😓':'downcast sweat',
|
| '😩':'weary','😫':'tired','🥱':'yawning','😤':'triumph','😡':'pouting',
|
| '😠':'angry','🤬':'swearing','😈':'smiling devil','👿':'angry devil',
|
| '💀':'skull','☠️':'skull crossbones','💩':'poop','🤡':'clown','👹':'ogre','👺':'goblin',
|
| '👻':'ghost','👽':'alien','👾':'space invader','🤖':'robot',
|
| '😺':'smiling cat','😸':'grinning cat','😹':'tears of joy cat','😻':'heart eyes cat',
|
| '😼':'wry cat','😽':'kissing cat','🙀':'weary cat','😿':'crying cat','😾':'pouting cat',
|
| '🙈':'see no evil','🙉':'hear no evil','🙊':'speak no evil',
|
| '👋':'wave','🤚':'raised back of hand','🖐️':'hand with fingers splayed','✋':'raised hand',
|
| '🖖':'vulcan salute','🫱':'rightward hand','🫲':'leftward hand',
|
| '👌':'ok hand','🤌':'pinched fingers','🤏':'pinching hand','✌️':'victory',
|
| '🤞':'crossed fingers','🫰':'hand with index finger and thumb crossed',
|
| '🤟':'love you','🤘':'rock on','🤙':'call me','👈':'point left','👉':'point right',
|
| '👆':'point up','🖕':'middle finger','👇':'point down','☝️':'index up',
|
| '🫵':'point at viewer','👍':'thumbs up','👎':'thumbs down','✊':'raised fist',
|
| '👊':'fist bump','🤛':'left fist','🤜':'right fist','👏':'clap','🙌':'raising hands',
|
| '🫶':'heart hands','👐':'open hands','🤲':'palms up','🤝':'handshake','🙏':'pray',
|
| '✍️':'writing','💅':'nail polish','🤳':'selfie','💪':'flexed biceps',
|
| '❤️':'red heart','🧡':'orange heart','💛':'yellow heart','💚':'green heart',
|
| '💙':'blue heart','💜':'purple heart','🖤':'black heart','🤍':'white heart',
|
| '🩷':'pink heart','🩵':'light blue heart','🩶':'grey heart','🤎':'brown heart',
|
| '💔':'broken heart','❤️🔥':'heart on fire','❤️🩹':'mending heart',
|
| '💕':'two hearts','💞':'revolving hearts','💓':'heartbeat','💗':'growing heart',
|
| '💖':'sparkling heart','💘':'heart with arrow','💝':'heart with ribbon',
|
| '💟':'heart decoration','🔥':'fire','💯':'100','✨':'sparkles','⭐':'star',
|
| '🌟':'glowing star','💫':'dizzy star','🎉':'party popper','🎊':'confetti ball',
|
| '🎈':'balloon','🎁':'gift','🏆':'trophy','🥇':'1st place','🥈':'2nd place','🥉':'3rd place',
|
| '⚡':'zap','💡':'light bulb','🔑':'key','🔒':'locked','🔓':'unlocked',
|
| '🔔':'bell','🔕':'bell off','📢':'loudspeaker','📣':'megaphone',
|
| '💬':'speech bubble','💭':'thought bubble','🗯️':'anger bubble',
|
| '✅':'check mark','❌':'cross mark','❓':'question','❗':'exclamation',
|
| '⚠️':'warning','🚫':'prohibited','⛔':'no entry','🔴':'red circle','🟢':'green circle',
|
| '🔵':'blue circle','🟡':'yellow circle','⚪':'white circle','⚫':'black circle',
|
| '🟠':'orange circle','🟣':'purple circle','🟤':'brown circle',
|
| '📁':'folder','📂':'open folder','📄':'document','📝':'memo','📎':'paperclip',
|
| '📌':'pin','📍':'round pin','🔗':'link','📊':'bar chart','📈':'chart up','📉':'chart down',
|
| '🔍':'magnifying glass left','🔎':'magnifying glass right',
|
| '🌐':'globe','🌍':'globe europe','🌎':'globe americas','🌏':'globe asia',
|
| '🕐':'clock 1','🕑':'clock 2','🕒':'clock 3','🕓':'clock 4',
|
| '⏰':'alarm clock','⏳':'hourglass flowing','⌛':'hourglass done',
|
| '🚀':'rocket','✈️':'airplane','🚗':'car','🚂':'train','🚢':'ship',
|
| '🏠':'house','🏢':'building','🏗️':'construction','🏭':'factory',
|
| '🎵':'musical note','🎶':'musical notes','🎤':'microphone','🎧':'headphones',
|
| '📷':'camera','📸':'camera flash','🎬':'clapperboard','📺':'television',
|
| '💻':'laptop','🖥️':'desktop','📱':'mobile phone','☎️':'telephone',
|
| '🔧':'wrench','🔨':'hammer','⚙️':'gear','🧲':'magnet','🧪':'test tube','🔬':'microscope',
|
| '📚':'books','📖':'open book','✏️':'pencil','🖊️':'pen','🖋️':'fountain pen',
|
| '🎯':'bullseye','♟️':'chess pawn','🎲':'game die','🧩':'puzzle piece',
|
| '🍕':'pizza','🍔':'burger','🍟':'fries','🌮':'taco','🍣':'sushi','🍩':'donut',
|
| '☕':'coffee','🍺':'beer','🍷':'wine','🥤':'cup with straw',
|
| '🐶':'dog','🐱':'cat','🐭':'mouse','🐹':'hamster','🐰':'rabbit','🦊':'fox',
|
| '🐻':'bear','🐼':'panda','🐨':'koala','🐯':'tiger','🦁':'lion','🐮':'cow',
|
| '🐷':'pig','🐸':'frog','🐵':'monkey','🐔':'chicken','🐧':'penguin','🐦':'bird',
|
| '🦅':'eagle','🦆':'duck','🦉':'owl','🐺':'wolf','🐗':'boar','🐴':'horse',
|
| '🦄':'unicorn','🐝':'bee','🐛':'bug','🦋':'butterfly','🐌':'snail','🐞':'ladybug',
|
| '🐍':'snake','🐢':'turtle','🐙':'octopus','🦀':'crab','🐠':'tropical fish',
|
| '🐳':'whale','🐋':'whale','🦈':'shark','🐊':'crocodile','🦕':'sauropod','🦖':'t-rex',
|
| '🌸':'cherry blossom','🌹':'rose','🌻':'sunflower','🌺':'hibiscus','🌷':'tulip',
|
| '🌱':'seedling','🌲':'evergreen tree','🌳':'deciduous tree','🍀':'four leaf clover',
|
| '🍎':'red apple','🍐':'pear','🍊':'tangerine','🍋':'lemon','🍌':'banana',
|
| '🍉':'watermelon','🍇':'grapes','🍓':'strawberry','🫐':'blueberries','🍑':'peach',
|
| '🌈':'rainbow','☀️':'sun','🌤️':'sun behind cloud','⛅':'sun behind cloud','☁️':'cloud',
|
| '🌧️':'rain','⛈️':'thunder','❄️':'snowflake','🌊':'wave',
|
| '👀':'eyes','👁️':'eye','👂':'ear','👃':'nose','👄':'mouth','👅':'tongue',
|
| '🧠':'brain','🦴':'bone','🦷':'tooth','👶':'baby','🧒':'child','👦':'boy','👧':'girl',
|
| '🧑':'person','👨':'man','👩':'woman','🧓':'older person',
|
| '👮':'police officer','🧑💻':'technologist','👨💻':'man technologist',
|
| '👩💻':'woman technologist',
|
| '🎓':'graduation cap','🧢':'billed cap','👑':'crown','💎':'gem','👓':'glasses','🕶️':'sunglasses',
|
| '🩸':'drop of blood','💊':'pill','🩹':'bandage','🧬':'dna','🦠':'microbe',
|
| '☢️':'radioactive','☣️':'biohazard','♻️':'recycling',
|
| '🏳️':'white flag','🏴':'black flag','🚩':'red flag','🏁':'checkered flag',
|
| '➡️':'right arrow','⬅️':'left arrow','⬆️':'up arrow','⬇️':'down arrow',
|
| '↗️':'upper right arrow','↘️':'lower right arrow','↙️':'lower left arrow','↖️':'upper left arrow',
|
| '↩️':'left curve','↪️':'right curve','🔄':'counterclockwise','🔃':'clockwise',
|
| '➕':'plus','➖':'minus','➗':'division','✖️':'multiply','♾️':'infinity',
|
| '‼️':'double exclamation','⁉️':'exclamation question',
|
| '©️':'copyright','®️':'registered','™️':'trademark',
|
| };
|
|
|
| function emojiToText(str) {
|
| return str.replace(EMOJI_RE, (match) => {
|
| const desc = EMOJI_MAP[match];
|
| if (desc) return ':' + desc + ':';
|
|
|
| return ':emoji:';
|
| });
|
| }
|
|
|
| const _DEOJ_SKIP = '.sources-section, .thinking-toggle, .memory-used-pill';
|
|
|
|
|
| function deEmojify(root) {
|
| if (!root || !root.querySelectorAll) return;
|
|
|
| root.querySelectorAll('.emoji[aria-label]').forEach((span) => {
|
| if (span.closest(_DEOJ_SKIP)) return;
|
| const label = span.getAttribute('aria-label') || '';
|
| span.replaceWith(document.createTextNode(emojiToText(label)));
|
| });
|
| const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
| const nodes = [];
|
| while (walker.nextNode()) nodes.push(walker.currentNode);
|
| for (const node of nodes) {
|
|
|
| if (node.parentElement && node.parentElement.closest(_DEOJ_SKIP)) continue;
|
| if (EMOJI_RE.test(node.textContent)) {
|
| EMOJI_RE.lastIndex = 0;
|
| node.textContent = emojiToText(node.textContent);
|
| }
|
| }
|
| }
|
|
|
|
|
| function applyTextEmojis(enabled) {
|
| document.body.classList.toggle('text-emojis', enabled);
|
| if (enabled) {
|
| document.querySelectorAll('.msg .body').forEach(deEmojify);
|
| }
|
| }
|
|
|
|
|
| let _deEmojifyTimer = null;
|
| const _chatObs = new MutationObserver(() => {
|
| if (!document.body.classList.contains('text-emojis')) return;
|
| clearTimeout(_deEmojifyTimer);
|
| _deEmojifyTimer = setTimeout(() => {
|
| document.querySelectorAll('.msg .body').forEach(deEmojify);
|
| }, 150);
|
| });
|
| const _chatBox = document.getElementById('chat-history');
|
| if (_chatBox) _chatObs.observe(_chatBox, { childList: true, subtree: true });
|
|
|
|
|
| (function migrateOldToolbarVis() {
|
| const OLD_KEY = 'odysseus-toolbar-visibility';
|
| try {
|
| const old = Storage.getJSON(OLD_KEY, null);
|
| if (old && typeof old === 'object') {
|
| const current = loadUIVis();
|
| let migrated = false;
|
| Object.entries(old).forEach(([btnId, val]) => {
|
| if (current[btnId] === undefined) {
|
| current[btnId] = val;
|
| migrated = true;
|
| }
|
| });
|
| if (migrated) saveUIVis(current);
|
| Storage.remove(OLD_KEY);
|
| }
|
| } catch {}
|
| })();
|
|
|
|
|
| window.loadUIVis = loadUIVis;
|
| window.saveUIVis = saveUIVis;
|
| window.applyUIVis = applyUIVis;
|
| window.UI_VIS_ADMIN_ONLY = UI_VIS_ADMIN_ONLY;
|
| window.UI_VIS_DEFAULT_OFF = UI_VIS_DEFAULT_OFF;
|
|
|
| (function initUIVisibility() {
|
|
|
| applyUIVis(loadUIVis());
|
|
|
|
|
|
|
| try {
|
| ['custom-preset-modal', 'rename-session-modal'].forEach((id) => {
|
| const m = document.getElementById(id);
|
| if (!m) return;
|
| const content = m.querySelector('.modal-content');
|
| const header = m.querySelector('.modal-header');
|
| if (!content || !header) return;
|
| makeWindowDraggable(m, {
|
| content, header,
|
| skipSelector: '.close-btn',
|
| enableDock: false,
|
| enableResize: false,
|
| });
|
|
|
|
|
| let wasHidden = m.classList.contains('hidden');
|
| new MutationObserver(() => {
|
| const isHidden = m.classList.contains('hidden');
|
| if (wasHidden && !isHidden) {
|
| content.style.position = '';
|
| content.style.left = '';
|
| content.style.top = '';
|
| content.style.right = '';
|
| content.style.bottom = '';
|
| content.style.margin = '';
|
| }
|
| wasHidden = isHidden;
|
| }).observe(m, { attributes: true, attributeFilter: ['class'] });
|
| });
|
| } catch (e) { console.error('Dialog drag init error:', e); }
|
| })();
|
|
|
|
|
|
|
|
|
|
|
|
|
| (function initModalMinimize() {
|
|
|
|
|
|
|
| const SKIP_IDS = new Set(['styled-confirm-overlay', 'custom-preset-modal']);
|
| const dockEntries = new Map();
|
|
|
| let dock = document.getElementById('modal-dock');
|
| if (!dock) {
|
| dock = document.createElement('div');
|
| dock.id = 'modal-dock';
|
| document.body.appendChild(dock);
|
| }
|
|
|
|
|
|
|
| function updateDockOffset() {
|
| const sidebar = document.getElementById('sidebar');
|
| const iconRail = document.getElementById('icon-rail');
|
| let leftPx = 0;
|
| let rightPx = 0;
|
| const sidebarRight = sidebar && sidebar.classList.contains('right-side');
|
| const sidebarVisible = sidebar &&
|
| !sidebar.classList.contains('hidden') &&
|
| sidebar.offsetWidth > 0;
|
| const railVisible = iconRail && iconRail.offsetWidth > 0;
|
| const sidebarW = sidebarVisible ? sidebar.offsetWidth : 0;
|
| const railW = railVisible ? iconRail.offsetWidth : 0;
|
| if (sidebarRight) {
|
| rightPx = sidebarW + railW;
|
| } else {
|
| leftPx = sidebarW + railW;
|
| }
|
| dock.style.left = leftPx + 'px';
|
| dock.style.right = rightPx + 'px';
|
| }
|
| updateDockOffset();
|
|
|
| if (window.ResizeObserver) {
|
| const ro = new ResizeObserver(updateDockOffset);
|
| const sb = document.getElementById('sidebar');
|
| const ir = document.getElementById('icon-rail');
|
| if (sb) ro.observe(sb);
|
| if (ir) ro.observe(ir);
|
| }
|
| window.addEventListener('resize', updateDockOffset);
|
|
|
| new MutationObserver(updateDockOffset).observe(document.body, {
|
| attributes: true, attributeFilter: ['class'],
|
| });
|
| const sbEl = document.getElementById('sidebar');
|
| if (sbEl) {
|
| new MutationObserver(updateDockOffset).observe(sbEl, {
|
| attributes: true, attributeFilter: ['class', 'style'],
|
| });
|
| }
|
|
|
| function modalTitle(modal) {
|
| const h = modal.querySelector('.modal-header h4, .modal-header h3, .modal-header h2');
|
| if (h && h.textContent.trim()) return h.textContent.trim();
|
| if (modal.id) return modal.id.replace(/-modal$|-overlay$|-popup$/, '').replace(/-/g, ' ');
|
| return 'Window';
|
| }
|
|
|
| function removeDockEntry(modal) {
|
| const entry = dockEntries.get(modal);
|
| if (entry) {
|
| entry.remove();
|
| dockEntries.delete(modal);
|
| }
|
| }
|
|
|
| function restoreModal(modal) {
|
| modal.classList.remove('minimized');
|
| modal.classList.remove('hidden');
|
| removeDockEntry(modal);
|
|
|
| modal.style.zIndex = '';
|
| }
|
|
|
| function minimizeModal(modal) {
|
| if (modal.classList.contains('hidden')) return;
|
| modal.classList.add('minimized');
|
| if (dockEntries.has(modal)) return;
|
|
|
| const entry = document.createElement('div');
|
| entry.className = 'modal-dock-item';
|
| entry.title = `Restore ${modalTitle(modal)}`;
|
|
|
| const label = document.createElement('span');
|
| label.className = 'modal-dock-label';
|
| label.textContent = modalTitle(modal);
|
|
|
| const closeX = document.createElement('button');
|
| closeX.className = 'modal-dock-close';
|
| closeX.textContent = '×';
|
| closeX.title = 'Close';
|
| closeX.addEventListener('click', (e) => {
|
| e.stopPropagation();
|
| modal.classList.remove('minimized');
|
| modal.classList.add('hidden');
|
| modal.style.display = '';
|
| removeDockEntry(modal);
|
| });
|
|
|
| entry.appendChild(label);
|
| entry.appendChild(closeX);
|
| entry.addEventListener('click', () => restoreModal(modal));
|
| dock.appendChild(entry);
|
| dockEntries.set(modal, entry);
|
| }
|
|
|
| function injectMinimizeButton(modal) {
|
| if (!modal || !modal.classList || !modal.classList.contains('modal')) return;
|
| if (modal.id && SKIP_IDS.has(modal.id)) return;
|
|
|
|
|
|
|
| if (modal.id && /^email-reader-/.test(modal.id)) return;
|
| if (modal.id && window.Modals && window.Modals.isRegistered && window.Modals.isRegistered(modal.id)) return;
|
| const header = modal.querySelector('.modal-header');
|
| if (!header) return;
|
| if (header.querySelector('.minimize-btn, .modal-minimize-btn')) return;
|
| const closeBtn = header.querySelector('.close-btn, .modal-close');
|
| if (!closeBtn) return;
|
|
|
| const minBtn = document.createElement('button');
|
| minBtn.className = 'minimize-btn';
|
| minBtn.type = 'button';
|
| minBtn.title = 'Minimize';
|
| minBtn.textContent = '_';
|
| minBtn.addEventListener('mousedown', (e) => e.stopPropagation());
|
| minBtn.addEventListener('click', (e) => {
|
| e.stopPropagation();
|
| minimizeModal(modal);
|
| });
|
| closeBtn.parentElement.insertBefore(minBtn, closeBtn);
|
|
|
|
|
| new MutationObserver(() => {
|
| if (modal.classList.contains('hidden') && !modal.classList.contains('minimized')) {
|
| removeDockEntry(modal);
|
| }
|
| }).observe(modal, { attributes: true, attributeFilter: ['class'] });
|
| }
|
|
|
|
|
| document.querySelectorAll('.modal').forEach(injectMinimizeButton);
|
|
|
|
|
| new MutationObserver((mutations) => {
|
| for (const m of mutations) {
|
| for (const n of m.addedNodes) {
|
| if (n.nodeType !== 1) continue;
|
| if (n.classList && n.classList.contains('modal')) {
|
| injectMinimizeButton(n);
|
| }
|
| if (n.querySelectorAll) {
|
| n.querySelectorAll('.modal').forEach(injectMinimizeButton);
|
| }
|
| }
|
| }
|
| }).observe(document.body, { childList: true, subtree: true });
|
| })();
|
|
|
|
|
| const overflowPresetBtn = el('overflow-preset-btn');
|
| if (overflowPresetBtn) {
|
| overflowPresetBtn.addEventListener('click', () => {
|
| if (presetsModule && presetsModule.openCustomPresetModal) {
|
| presetsModule.openCustomPresetModal();
|
| }
|
| });
|
| }
|
|
|
|
|
| const addDirBtn = el('add-directory-btn');
|
| if (addDirBtn) {
|
| addDirBtn.addEventListener('click', () => {
|
| ragModule.addRagDirectory(uiModule.showToast, uiModule.showError);
|
| });
|
| }
|
|
|
| const directoryInput = el('rag-directory');
|
| if (directoryInput) {
|
| directoryInput.addEventListener('keypress', (e) => {
|
| if (e.key === 'Enter') {
|
| ragModule.addRagDirectory(uiModule.showToast, uiModule.showError);
|
| }
|
| });
|
|
|
| }
|
|
|
|
|
| initSidebarLayout(Storage, {
|
| documentModule, _closeCompareIfActive, _deactivateIncognito,
|
| presetsModule, sessionModule, el, _defaultChat, _syncResearchIndicator
|
| });
|
|
|
|
|
|
|
|
|
|
|
|
|
| (function initTabSwipe() {
|
| if (window.innerWidth > 768) return;
|
|
|
| const SYSTEMS = [
|
| ['.preset-tabs', '.preset-tab'],
|
| ['.lib-tabs', '.lib-tab'],
|
| ['.memory-tabs', '.memory-tab'],
|
| ['.admin-tabs', '.admin-tab'],
|
| ];
|
| const _IGNORE = 'input, textarea, select, [contenteditable="true"], .preset-range, ' +
|
| '.note-cl-row, .minimized-dock-chip, canvas, .email-card-reader';
|
| let sx = 0, sy = 0, tracking = false;
|
|
|
| document.addEventListener('touchstart', (e) => {
|
| if (window.innerWidth > 768 || e.touches.length !== 1) { tracking = false; return; }
|
| if (e.target.closest && e.target.closest(_IGNORE)) { tracking = false; return; }
|
| sx = e.touches[0].clientX; sy = e.touches[0].clientY; tracking = true;
|
| }, { passive: true });
|
|
|
| document.addEventListener('touchend', (e) => {
|
| if (!tracking) return;
|
| tracking = false;
|
| const t = e.changedTouches[0];
|
| if (!t) return;
|
| const dx = t.clientX - sx, dy = t.clientY - sy;
|
|
|
| if (Math.abs(dx) < 60 || Math.abs(dx) < Math.abs(dy) * 1.5) return;
|
| for (const [barSel, tabSel] of SYSTEMS) {
|
| const bar = document.querySelector(barSel);
|
| if (!bar || bar.offsetParent === null) continue;
|
|
|
|
|
| const host = bar.closest('.modal, #notes-pane, .preset-modal-content, .admin-card') || bar.parentElement;
|
| const startEl = document.elementFromPoint(sx, sy);
|
| if (host && startEl && !host.contains(startEl)) continue;
|
| const tabs = [...bar.querySelectorAll(tabSel)];
|
| if (tabs.length < 2) continue;
|
| let idx = tabs.findIndex(tb => tb.classList.contains('active'));
|
| if (idx < 0) idx = 0;
|
|
|
| const nextIdx = dx < 0 ? idx + 1 : idx - 1;
|
| if (nextIdx < 0 || nextIdx >= tabs.length) return;
|
| tabs[nextIdx].click();
|
| return;
|
| }
|
| }, { passive: true });
|
| })();
|
|
|
|
|
| (function initElasticScroll() {
|
| const hist = el('chat-history');
|
| if (!hist) return;
|
| const SNAP_BACK = 'transform 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
|
|
|
| let wheelPull = 0;
|
| let wheelTimer = null;
|
| hist.addEventListener('wheel', (e) => {
|
| const atTop = hist.scrollTop <= 0 && e.deltaY < 0;
|
| const atBottom = hist.scrollTop + hist.clientHeight >= hist.scrollHeight - 1 && e.deltaY > 0;
|
| if (!atTop && !atBottom) { wheelPull = 0; return; }
|
|
|
| wheelPull += e.deltaY * -0.03;
|
| wheelPull = Math.max(-7, Math.min(7, wheelPull));
|
| hist.style.transition = 'none';
|
| hist.style.transform = `translateY(${wheelPull}px)`;
|
|
|
| clearTimeout(wheelTimer);
|
| wheelTimer = setTimeout(() => {
|
| wheelPull = 0;
|
| hist.style.transition = SNAP_BACK;
|
| hist.style.transform = '';
|
| }, 120);
|
| }, { passive: true });
|
| })();
|
|
|
|
|
| const railNewSession = el('rail-new-session');
|
| if (railNewSession) {
|
| railNewSession.addEventListener('click', async () => {
|
| if (!sessionModule) return;
|
| if (_closeCompareIfActive()) return;
|
| _deactivateIncognito();
|
|
|
| if (presetsModule && presetsModule.deactivateCharacter) presetsModule.deactivateCharacter();
|
|
|
| const _resChk = el('research-toggle');
|
| if (_resChk && _resChk.checked) _syncResearchIndicator(false);
|
| if (await _createDirectChatFromPreferredModel()) return;
|
|
|
| sessionModule.setCurrentSessionId(null);
|
| if (documentModule && documentModule.isPanelOpen && documentModule.isPanelOpen()) documentModule.closePanel();
|
| const docBtn3 = el('overflow-doc-btn');
|
| if (docBtn3) docBtn3.classList.remove('active', 'has-docs');
|
| const box = el('chat-history');
|
| if (box) box.innerHTML = '';
|
| if (chatModule && chatModule.showWelcomeScreen) {
|
| chatModule.showWelcomeScreen();
|
| }
|
| document.querySelectorAll('.session-item.active').forEach(s => s.classList.remove('active'));
|
| });
|
| }
|
|
|
|
|
|
|
|
|
|
|
| const mobileNewChat = el('mobile-new-chat-btn');
|
| if (mobileNewChat) {
|
| mobileNewChat.addEventListener('click', () => {
|
| if (!sessionModule) return;
|
| if (_closeCompareIfActive()) return;
|
| _deactivateIncognito();
|
| _startFreshChat();
|
| document.querySelectorAll('.session-item.active').forEach(s => s.classList.remove('active'));
|
|
|
|
|
|
|
| const _input = el('message-input');
|
| if (_input) { try { _input.focus(); } catch (_) {} }
|
| });
|
| }
|
|
|
|
|
| const brandBtn = el('sidebar-brand-btn');
|
| if (brandBtn) {
|
| brandBtn.addEventListener('click', async () => {
|
| if (!sessionModule) return;
|
| if (_closeCompareIfActive()) return;
|
| _deactivateIncognito();
|
| if (presetsModule && presetsModule.deactivateCharacter) presetsModule.deactivateCharacter();
|
|
|
| _syncResearchIndicator(false);
|
| if (await _createDirectChatFromPreferredModel()) return;
|
|
|
| sessionModule.setCurrentSessionId(null);
|
| if (documentModule && documentModule.isPanelOpen && documentModule.isPanelOpen()) documentModule.closePanel();
|
| const docBtn2 = el('overflow-doc-btn');
|
| if (docBtn2) docBtn2.classList.remove('active', 'has-docs');
|
| const box = el('chat-history');
|
| if (box) box.innerHTML = '';
|
| if (chatModule && chatModule.showWelcomeScreen) chatModule.showWelcomeScreen();
|
| document.querySelectorAll('.session-item.active').forEach(s => s.classList.remove('active'));
|
| });
|
| }
|
|
|
| const sidebarNewChatBtn = el('sidebar-new-chat-btn');
|
| if (sidebarNewChatBtn) {
|
| sidebarNewChatBtn.addEventListener('click', () => {
|
| const brandBtn = el('sidebar-brand-btn');
|
| if (brandBtn) brandBtn.click();
|
| });
|
| }
|
|
|
|
|
| const railDelete = el('rail-delete-session');
|
| if (railDelete) {
|
| railDelete.addEventListener('click', async () => {
|
| if (!sessionModule) return;
|
| const currentId = sessionModule.getCurrentSessionId();
|
| if (!currentId) return;
|
| const sessions = sessionModule.getSessions();
|
| const current = sessions.find(s => s.id === currentId);
|
| const name = current ? current.name : 'this session';
|
| if (!await uiModule.styledConfirm(`Delete "${name}"?`, { confirmText: 'Delete', danger: true })) return;
|
| try {
|
|
|
| const idx = sessions.findIndex(s => s.id === currentId);
|
| const nextSession = sessions.filter(s => !s.archived && s.id !== currentId)[Math.max(0, idx)] ||
|
| sessions.find(s => !s.archived && s.id !== currentId);
|
| const res = await fetch(`${API_BASE}/api/session/${currentId}`, { method: 'DELETE' });
|
| if (res.ok) {
|
| await sessionModule.loadSessions();
|
| if (nextSession) {
|
| await sessionModule.selectSession(nextSession.id);
|
| }
|
| uiModule.showToast('Session deleted');
|
| } else {
|
| uiModule.showError('Failed to delete session');
|
| }
|
| } catch (e) {
|
| uiModule.showError('Failed to delete session: ' + e);
|
| }
|
| });
|
| }
|
|
|
|
|
| const textarea = el('message');
|
| if (textarea) {
|
| uiModule.autoResize(textarea);
|
| textarea.addEventListener('input', () => {
|
| uiModule.autoResize(textarea);
|
| });
|
| textarea.addEventListener('paste', () => {
|
| setTimeout(() => uiModule.autoResize(textarea), 1);
|
| });
|
| textarea.addEventListener('keydown', (e) => {
|
| if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
|
|
|
| if (window._ghostAutocomplete && window._ghostAutocomplete.isActive()) {
|
| e.preventDefault();
|
| e.stopPropagation();
|
| window._ghostAutocomplete.accept();
|
| return;
|
| }
|
| e.preventDefault();
|
| e.stopPropagation();
|
|
|
| const form = el('chat-form');
|
| if (form) {
|
| const submitBtn = form.querySelector('button[type="submit"]');
|
| if (submitBtn) submitBtn.click();
|
| }
|
| }
|
| });
|
| }
|
|
|
|
|
| (function initGhostAutocomplete() {
|
| const textarea = el('message');
|
| const ghost = document.getElementById('message-ghost');
|
| if (!textarea || !ghost) return;
|
|
|
| let modelCache = null;
|
| let filtered = [];
|
| let cycleIdx = 0;
|
| let active = false;
|
| const CACHE_TTL = 60000;
|
| const CMD_RE = /^\/(new|create)\s/i;
|
|
|
| async function fetchModels() {
|
| if (modelCache && Date.now() - modelCache.ts < CACHE_TTL) return modelCache.models;
|
| try {
|
| const res = await fetch(`${API_BASE}/api/models`, { credentials: 'same-origin' });
|
| const data = await res.json();
|
| const models = [];
|
| (data.items || []).forEach(ep => {
|
| const displayNames = ep.models_display || ep.models || [];
|
| (ep.models || []).forEach((mid, i) => {
|
| models.push({
|
| mid,
|
| url: ep.url,
|
| endpointId: ep.endpoint_id || null,
|
| displayName: displayNames[i] || mid,
|
| });
|
| });
|
| });
|
| modelCache = { models, ts: Date.now() };
|
| return models;
|
| } catch (e) {
|
| console.warn('Ghost autocomplete: failed to fetch models', e);
|
| return modelCache ? modelCache.models : [];
|
| }
|
| }
|
|
|
| function hide() {
|
| active = false;
|
| filtered = [];
|
| cycleIdx = 0;
|
| ghost.textContent = '';
|
| ghost.style.display = 'none';
|
| }
|
|
|
| function show(typed, suggestion) {
|
| active = true;
|
| ghost.innerHTML = '';
|
|
|
| const span1 = document.createElement('span');
|
| span1.style.visibility = 'hidden';
|
| span1.textContent = typed;
|
|
|
| const span2 = document.createElement('span');
|
| span2.className = 'ghost-suggestion';
|
| span2.textContent = suggestion;
|
| ghost.appendChild(span1);
|
| ghost.appendChild(span2);
|
| ghost.style.display = 'block';
|
| }
|
|
|
| function syncSize() {
|
|
|
| const cs = getComputedStyle(textarea);
|
| ghost.style.width = cs.width;
|
| ghost.style.height = cs.height;
|
| }
|
|
|
| async function update() {
|
| const val = textarea.value;
|
| const match = val.match(CMD_RE);
|
| if (!match) { hide(); return; }
|
|
|
| const prefix = val.slice(match[0].length);
|
| const models = await fetchModels();
|
| if (!models.length) { hide(); return; }
|
|
|
|
|
| const lp = prefix.toLowerCase();
|
| filtered = models.filter(m =>
|
| m.mid.toLowerCase().startsWith(lp) || m.displayName.toLowerCase().startsWith(lp)
|
| );
|
|
|
| if (!filtered.length) { hide(); return; }
|
|
|
|
|
| cycleIdx = cycleIdx % filtered.length;
|
| const chosen = filtered[cycleIdx];
|
|
|
| const name = chosen.mid.toLowerCase().startsWith(lp) ? chosen.mid : chosen.displayName;
|
| const remainder = name.slice(prefix.length);
|
| if (!remainder && filtered.length <= 1) { hide(); return; }
|
|
|
| syncSize();
|
| show(val, remainder);
|
| }
|
|
|
|
|
|
|
| textarea.addEventListener('input', () => {
|
| cycleIdx = 0;
|
| update();
|
| });
|
|
|
| textarea.addEventListener('keydown', (e) => {
|
| if (!active) return;
|
|
|
| if (e.key === 'Tab') {
|
|
|
| e.preventDefault();
|
| e.stopPropagation();
|
| const val = textarea.value;
|
| const match = val.match(CMD_RE);
|
| if (match && filtered.length) {
|
| const prefix = val.slice(match[0].length);
|
| const chosen = filtered[cycleIdx % filtered.length];
|
| const lp = prefix.toLowerCase();
|
| const name = chosen.mid.toLowerCase().startsWith(lp) ? chosen.mid : chosen.displayName;
|
| textarea.value = match[0] + name;
|
| textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
| }
|
| return;
|
| }
|
|
|
| if (e.key === 'ArrowDown') {
|
| e.preventDefault();
|
| e.stopPropagation();
|
| cycleIdx = (cycleIdx + 1) % filtered.length;
|
| update();
|
| return;
|
| }
|
|
|
| if (e.key === 'ArrowUp') {
|
| e.preventDefault();
|
| e.stopPropagation();
|
| cycleIdx = (cycleIdx - 1 + filtered.length) % filtered.length;
|
| update();
|
| return;
|
| }
|
|
|
| if (e.key === 'Escape') {
|
| e.preventDefault();
|
| hide();
|
| return;
|
| }
|
| });
|
|
|
| textarea.addEventListener('blur', hide);
|
|
|
|
|
| const ro = new ResizeObserver(() => { if (active) syncSize(); });
|
| ro.observe(textarea);
|
|
|
|
|
| window._ghostAutocomplete = {
|
| isActive() { return active && filtered.length > 0; },
|
| accept() {
|
| if (!active || !filtered.length) return;
|
| const val = textarea.value;
|
| const match = val.match(CMD_RE);
|
| if (!match) { hide(); return; }
|
| const prefix = val.slice(match[0].length);
|
| const chosen = filtered[cycleIdx % filtered.length];
|
| const lp = prefix.toLowerCase();
|
| const name = chosen.mid.toLowerCase().startsWith(lp) ? chosen.mid : chosen.displayName;
|
| textarea.value = match[0] + name;
|
| hide();
|
|
|
| textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
| setTimeout(() => {
|
| const form = el('chat-form');
|
| if (form) form.querySelector('button[type="submit"]').click();
|
| }, 0);
|
| }
|
| };
|
| })();
|
|
|
|
|
| initKeyboardShortcuts({
|
| el, Storage, sessionModule, uiModule, chatModule,
|
| adminModule, settingsModule, searchChatModule,
|
| _closeCompareIfActive, _deactivateIncognito, API_BASE
|
| });
|
|
|
| }
|
|
|
|
|
|
|
|
|
| function startOdysseusApp() {
|
| if (window.__odysseusAppStarted) return;
|
| window.__odysseusAppStarted = true;
|
|
|
| document.documentElement.style.setProperty('--line-height', '20px');
|
|
|
|
|
| if (window.visualViewport && 'ontouchstart' in window) {
|
| let _prevVPH = visualViewport.height;
|
| visualViewport.addEventListener('resize', () => {
|
| const delta = visualViewport.height - _prevVPH;
|
| _prevVPH = visualViewport.height;
|
|
|
| if (delta < -50) {
|
| const hist = document.getElementById('chat-history');
|
| if (hist) {
|
| hist.style.scrollBehavior = 'smooth';
|
| hist.scrollTop = hist.scrollHeight;
|
|
|
| setTimeout(() => { hist.style.scrollBehavior = ''; }, 300);
|
| }
|
| }
|
| });
|
| }
|
|
|
|
|
| try { initializeEventListeners(); } catch(e) { console.error('Event init error:', e); }
|
|
|
|
|
|
|
| const _inputBottom = document.querySelector('.chat-input-bottom');
|
| if (_inputBottom) _inputBottom.style.visibility = '';
|
|
|
| fileHandlerModule.init(API_BASE);
|
| modelsModule.init(API_BASE);
|
| ragModule.init(API_BASE);
|
| presetsModule.init(API_BASE);
|
| searchModule.init(API_BASE);
|
| chatModule.init(API_BASE);
|
| chatModule.initListeners();
|
| groupModule.init(API_BASE);
|
|
|
| if (compareModule) {
|
| compareModule.init(API_BASE);
|
| }
|
| researchPanelModule.init(API_BASE, markdownModule, sessionModule);
|
|
|
| if (documentModule) {
|
| documentModule.init(API_BASE);
|
|
|
| const _curSession = sessionModule && sessionModule.getCurrentSessionId();
|
| if (_curSession && localStorage.getItem('odysseus-doc-open-' + _curSession) === '1') {
|
| documentModule.loadSessionDocs(_curSession);
|
| }
|
| }
|
|
|
| if (searchChatModule) {
|
| searchChatModule.init(API_BASE);
|
| }
|
|
|
|
|
| const railSearchBtn = el('rail-search-btn');
|
| if (railSearchBtn) {
|
| railSearchBtn.addEventListener('click', () => {
|
| if (searchChatModule) searchChatModule.openSearch();
|
| });
|
| }
|
|
|
|
|
| const _railToolMap = {
|
| 'rail-compare': 'tool-compare-btn',
|
| 'rail-research': 'tool-research-btn',
|
| 'rail-cookbook': 'tool-cookbook-btn',
|
| 'rail-archive': 'tool-library-btn',
|
| 'rail-gallery': 'tool-gallery-btn',
|
| 'rail-tasks': 'tool-tasks-btn',
|
| 'rail-calendar': 'tool-calendar-btn',
|
| 'rail-notes': 'tool-notes-btn',
|
| 'rail-memory': 'tool-memory-btn',
|
| 'rail-theme': 'tool-theme-btn',
|
| 'rail-email': 'email-section-title',
|
| };
|
| Object.entries(_railToolMap).forEach(([railId, toolId]) => {
|
| const railBtn = el(railId);
|
| if (railBtn) {
|
| railBtn.addEventListener('click', () => {
|
| const toolBtn = el(toolId);
|
| if (toolBtn) toolBtn.click();
|
| });
|
| }
|
| });
|
|
|
|
|
| const _railChatsBtn = el('rail-chats');
|
| if (_railChatsBtn) {
|
| _railChatsBtn.addEventListener('click', () => {
|
| const targetSid = _railChatsBtn.dataset.targetSession;
|
| if (targetSid && window.sessionModule) {
|
| window.sessionModule.selectSession(targetSid);
|
| }
|
|
|
| _railChatsBtn.classList.remove('rail-notify', 'rail-notify-success');
|
| delete _railChatsBtn.dataset.targetSession;
|
| _syncRailDynamic();
|
| });
|
| }
|
|
|
|
|
| const _railDocsBtn = el('rail-documents');
|
| if (_railDocsBtn) {
|
| _railDocsBtn.addEventListener('click', () => {
|
| const ob = el('overflow-doc-btn');
|
| if (ob) ob.click();
|
| });
|
| }
|
|
|
|
|
| const _railSettings = el('rail-settings');
|
| if (_railSettings) {
|
| _railSettings.addEventListener('click', () => {
|
| const sidebar = document.getElementById('sidebar');
|
| if (sidebar) sidebar.classList.remove('hidden');
|
| syncRailSide();
|
|
|
| const sidebarInner = document.querySelector('.sidebar-inner');
|
| if (sidebarInner) sidebarInner.scrollTo({ top: sidebarInner.scrollHeight, behavior: 'smooth' });
|
| });
|
| }
|
|
|
|
|
| const _railAdmin = el('rail-admin');
|
| if (_railAdmin) {
|
| _railAdmin.addEventListener('click', () => {
|
|
|
| const adminBtn = document.querySelector('[data-modal="admin-modal"]') || el('tool-admin-btn');
|
| if (adminBtn) adminBtn.click();
|
| });
|
| }
|
|
|
|
|
|
|
|
|
|
|
| function _syncRailDynamic() {
|
|
|
| const docPanelOpen = window.documentModule && window.documentModule.isPanelOpen();
|
| const docIndicator = el('doc-indicator-btn');
|
| const hasDocs = docIndicator && docIndicator.classList.contains('visible');
|
| const docOpen = docPanelOpen || hasDocs;
|
| const hasChatNotif = el('rail-chats')?.classList.contains('rail-notify');
|
|
|
| const _show = (id, visible) => { const b = el(id); if (b) b.style.display = visible ? '' : 'none'; };
|
| _show('rail-documents', docOpen);
|
| _show('rail-chats', !!hasChatNotif);
|
| }
|
| window._syncRailDynamic = _syncRailDynamic;
|
|
|
| setInterval(_syncRailDynamic, 1000);
|
| document.addEventListener('overflow-state-change', _syncRailDynamic);
|
|
|
| const sidebarSearchBtn = el('sidebar-search-btn');
|
| if (sidebarSearchBtn) {
|
| sidebarSearchBtn.addEventListener('click', () => {
|
| if (searchChatModule) searchChatModule.openSearch();
|
| });
|
| }
|
|
|
| const chatForm = document.getElementById('chat-form');
|
| const originalSubmit = chatModule.handleChatSubmit;
|
| let _submitting = false;
|
|
|
| function handleSubmit(e) {
|
| if (e) e.preventDefault();
|
|
|
| if (_submitting) return;
|
| _submitting = true;
|
|
|
| setTimeout(() => { _submitting = false; }, 300);
|
|
|
|
|
| if (compareModule && compareModule.isActive()) {
|
| return compareModule.handleCompareSubmit(e);
|
| }
|
|
|
|
|
| if (groupModule && groupModule.isActive()) {
|
| console.log('[group] Submit intercepted');
|
| const msgInput = document.getElementById('message');
|
| const msg = msgInput ? msgInput.value.trim() : '';
|
| if (!msg) { console.log('[group] Empty message, skipping'); return; }
|
| console.log('[group] Sending:', msg);
|
| chatRenderer.hideWelcomeScreen();
|
| chatRenderer.addMessage('user', msg);
|
| msgInput.value = '';
|
| groupModule.sendMessage(msg);
|
| return;
|
| }
|
|
|
| return originalSubmit.call(chatModule, e);
|
| }
|
|
|
| chatForm.onsubmit = handleSubmit;
|
|
|
|
|
| const sendBtn = document.querySelector('.send-btn');
|
| const messageInput = el('message');
|
| const modelPickerWrap = document.getElementById('model-picker-wrap');
|
|
|
| const _sendIcon = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg>';
|
| const _micIcon = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>';
|
| const _stopIcon = '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>';
|
| const _newChatIcon = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>';
|
|
|
|
|
| window._odysseusBtnIcons = { send: _sendIcon, mic: _micIcon, stop: _stopIcon, newChat: _newChatIcon };
|
|
|
| function _isSttEnabled() {
|
| return voiceRecorderModule._sttProvider && voiceRecorderModule._sttProvider !== 'disabled';
|
| }
|
|
|
| function _hasAttachments() {
|
| return fileHandlerModule.getPendingCount && fileHandlerModule.getPendingCount() > 0;
|
| }
|
|
|
| function _updateSendBtnIcon() {
|
| if (!sendBtn) return;
|
|
|
| if (sendBtn.dataset.mode === 'streaming' || sendBtn.dataset.mode === 'recording') return;
|
| const prevMode = sendBtn.dataset.mode || '';
|
| const hasText = messageInput && messageInput.value.trim().length > 0;
|
| const hasFiles = _hasAttachments();
|
| let newMode;
|
| if (!hasText && !hasFiles && _isSttEnabled()) {
|
| clearTimeout(sendBtn._collapseTimer);
|
| sendBtn.innerHTML = _micIcon;
|
| sendBtn.title = 'Record voice';
|
| newMode = 'mic';
|
| sendBtn.classList.add('mic-mode');
|
| sendBtn.classList.remove('newchat-mode', 'newchat-expanded');
|
| } else if (!hasText && !hasFiles && !_isSttEnabled()) {
|
| clearTimeout(sendBtn._collapseTimer);
|
|
|
| if (groupModule && groupModule.isActive()) {
|
| sendBtn.innerHTML = _sendIcon;
|
| sendBtn.title = 'Send to group';
|
| newMode = 'idle';
|
| sendBtn.classList.remove('mic-mode', 'newchat-mode', 'newchat-expanded');
|
| } else {
|
|
|
| const isEmptySession = document.getElementById('chat-container')?.classList.contains('welcome-active');
|
| if (isEmptySession) {
|
|
|
| sendBtn.innerHTML = _sendIcon;
|
| sendBtn.title = 'Send message';
|
| newMode = 'idle';
|
| sendBtn.classList.add('newchat-mode');
|
| sendBtn.classList.remove('mic-mode', 'newchat-expanded');
|
| clearTimeout(sendBtn._expandTimer);
|
| } else {
|
| sendBtn.innerHTML = _newChatIcon + '<span class="send-btn-label">+ New</span>';
|
| sendBtn.title = 'New chat';
|
| newMode = 'newchat';
|
| sendBtn.classList.add('newchat-mode');
|
| sendBtn.classList.remove('mic-mode');
|
|
|
|
|
|
|
| clearTimeout(sendBtn._expandTimer);
|
| sendBtn.classList.remove('newchat-expanded');
|
| }
|
| }
|
| } else {
|
| newMode = 'send';
|
| clearTimeout(sendBtn._expandTimer);
|
| const wasExpanded = sendBtn.classList.contains('newchat-expanded');
|
| const wasNewchat = prevMode === 'newchat' || prevMode === 'mic';
|
| if (wasExpanded || wasNewchat) {
|
|
|
| if (wasExpanded) sendBtn.classList.remove('newchat-expanded');
|
| const delay = wasExpanded ? 300 : 0;
|
| setTimeout(() => {
|
| if (sendBtn.dataset.mode !== 'send') return;
|
| sendBtn.innerHTML = _sendIcon;
|
| sendBtn.title = 'Send message';
|
| sendBtn.classList.remove('mic-mode', 'newchat-mode', 'anim-spin-swap');
|
| sendBtn.classList.add('anim-spin');
|
| sendBtn.addEventListener('animationend', () => sendBtn.classList.remove('anim-spin'), { once: true });
|
| }, delay);
|
| } else {
|
| sendBtn.innerHTML = _sendIcon;
|
| sendBtn.title = 'Send message';
|
| sendBtn.classList.remove('mic-mode', 'newchat-mode', 'newchat-expanded', 'anim-spin', 'anim-launch', 'anim-land');
|
| }
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
| if (newMode !== prevMode && (newMode === 'newchat' || newMode === 'mic')) {
|
| if (!sendBtn.classList.contains('anim-spin')) {
|
| sendBtn.classList.remove('anim-launch', 'anim-land');
|
| sendBtn.classList.add('anim-spin');
|
| sendBtn.addEventListener('animationend', () => sendBtn.classList.remove('anim-spin'), { once: true });
|
| }
|
| }
|
| sendBtn.dataset.mode = newMode;
|
| }
|
|
|
| if (sendBtn) {
|
| sendBtn.addEventListener('click', (e) => {
|
| e.preventDefault();
|
|
|
|
|
| if (sendBtn.dataset.mode === 'recording' || voiceRecorderModule.getIsRecording()) {
|
| voiceRecorderModule.stopRecording();
|
| return;
|
| }
|
|
|
| const hasText = messageInput && messageInput.value.trim().length > 0;
|
| const hasFiles = _hasAttachments();
|
|
|
|
|
| if (!hasText && !hasFiles && sendBtn.dataset.mode === 'newchat') {
|
| if (sessionModule) {
|
| const sessions = sessionModule.getSessions();
|
| const currentId = sessionModule.getCurrentSessionId();
|
| const current = sessions.find(s => s.id === currentId);
|
| if (current && current.endpoint_url && current.model) {
|
| sessionModule.createDirectChat(current.endpoint_url, current.model, current.endpoint_id);
|
| } else {
|
|
|
| const railNew = el('rail-new-session');
|
| if (railNew) railNew.click();
|
| }
|
| }
|
| return;
|
| }
|
|
|
|
|
| if (!hasText && !hasFiles && _isSttEnabled()) {
|
| sendBtn.innerHTML = _stopIcon;
|
| sendBtn.title = 'Stop recording';
|
| sendBtn.dataset.mode = 'recording';
|
| sendBtn.classList.add('recording');
|
| voiceRecorderModule.startRecording(
|
| (audioFile) => fileHandlerModule.addFiles([audioFile]),
|
| uiModule.showToast,
|
| uiModule.showError
|
| );
|
| return;
|
| }
|
|
|
|
|
| handleSubmit(e);
|
| });
|
| }
|
|
|
|
|
| if (messageInput) {
|
| messageInput.addEventListener('keydown', (e) => {
|
| if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
|
| e.preventDefault();
|
|
|
|
|
|
|
| try { _updateSendBtnIcon(); } catch {}
|
| if (sendBtn && sendBtn.dataset.mode === 'newchat') {
|
| const railNew = el('rail-new-session');
|
| if (railNew) railNew.click();
|
| return;
|
| }
|
| handleSubmit(e);
|
| }
|
| });
|
| }
|
|
|
|
|
| if (messageInput) {
|
| const _debouncedUpdateIcon = uiModule.debounce(_updateSendBtnIcon, 50);
|
| const _MODEL_PICKER_HIDE_CHARS = 10;
|
| const _syncModelPickerAutohide = () => {
|
| const hidePicker = (messageInput.value || '').replace(/\s/g, '').length >= _MODEL_PICKER_HIDE_CHARS;
|
| if (modelPickerWrap) {
|
| modelPickerWrap.classList.toggle('model-picker-autohide', hidePicker);
|
| }
|
| };
|
| window._syncModelPickerAutohide = _syncModelPickerAutohide;
|
| _syncModelPickerAutohide();
|
| messageInput.addEventListener('input', () => {
|
| _syncModelPickerAutohide();
|
| _debouncedUpdateIcon();
|
| }, { passive: true });
|
| }
|
|
|
|
|
| const _chatScroll = document.getElementById('chat-container');
|
| if (_chatScroll && sendBtn) {
|
| _chatScroll.addEventListener('scroll', () => {
|
| if (sendBtn.classList.contains('newchat-expanded')) {
|
| sendBtn.classList.remove('newchat-expanded');
|
| }
|
| }, { passive: true });
|
| }
|
|
|
|
|
| window._updateSendBtnIcon = _updateSendBtnIcon;
|
|
|
|
|
| _updateSendBtnIcon();
|
|
|
|
|
| if (messageInput) {
|
| setTimeout(() => messageInput.focus(), 100);
|
| }
|
|
|
|
|
| const chatContainer = el('chat-container');
|
|
|
|
|
| const chatInputBar = chatContainer.querySelector('.chat-input-bar');
|
| function _showDropHighlight() {
|
| chatContainer.style.backgroundColor = 'rgba(0, 170, 255, 0.1)';
|
| chatContainer.style.transition = 'background-color 0.2s ease';
|
| if (chatInputBar) {
|
| chatInputBar.style.outline = '2px dashed color-mix(in srgb, var(--accent, #0af) 50%, transparent)';
|
| chatInputBar.style.outlineOffset = '-2px';
|
| chatInputBar.style.background = 'color-mix(in srgb, var(--accent, #0af) 8%, var(--bg))';
|
| chatInputBar.style.transition = 'outline 0.2s ease, background 0.2s ease';
|
| }
|
| }
|
| function _hideDropHighlight() {
|
| chatContainer.style.backgroundColor = '';
|
| if (chatInputBar) {
|
| chatInputBar.style.outline = '';
|
| chatInputBar.style.outlineOffset = '';
|
| chatInputBar.style.background = '';
|
| }
|
| }
|
|
|
| chatContainer.addEventListener('dragover', (e) => {
|
| e.preventDefault();
|
| e.stopPropagation();
|
| _showDropHighlight();
|
| });
|
|
|
| chatContainer.addEventListener('drop', (e) => {
|
| e.preventDefault();
|
| e.stopPropagation();
|
| _hideDropHighlight();
|
| const files = Array.from(e.dataTransfer.files);
|
| if (files.length === 0) return;
|
| fileHandlerModule.addFiles(files);
|
| fileHandlerModule.renderAttachStrip();
|
| uiModule.showToast(`Added ${files.length} file${files.length > 1 ? 's' : ''} to chat`);
|
| });
|
|
|
| chatContainer.addEventListener('dragleave', (e) => {
|
| e.preventDefault();
|
| _hideDropHighlight();
|
| });
|
|
|
|
|
| const attachStrip = el('attach-strip');
|
| attachStrip.addEventListener('dragover', (e) => {
|
| e.preventDefault();
|
| attachStrip.style.backgroundColor = 'rgba(0, 170, 255, 0.1)';
|
| attachStrip.style.borderRadius = '4px';
|
| });
|
|
|
| attachStrip.addEventListener('drop', (e) => {
|
| e.preventDefault();
|
| attachStrip.style.backgroundColor = '';
|
|
|
| const files = Array.from(e.dataTransfer.files);
|
| if (files.length === 0) return;
|
|
|
| uiModule.showToast(`Added ${files.length} file${files.length > 1 ? 's' : ''} to chat`);
|
|
|
| });
|
|
|
| attachStrip.addEventListener('dragleave', (e) => {
|
| e.preventDefault();
|
| attachStrip.style.backgroundColor = '';
|
| });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| let _cmpDropShield = null;
|
| const _isFileDrag = (e) => {
|
| const types = e.dataTransfer && e.dataTransfer.types;
|
| return !!types && Array.prototype.indexOf.call(types, 'Files') !== -1;
|
| };
|
| const _compareActive = () => {
|
| const c = el('chat-container');
|
| return !!c && c.classList.contains('compare-active');
|
| };
|
| const _showCmpShield = () => {
|
| if (!_cmpDropShield) {
|
| _cmpDropShield = document.createElement('div');
|
| _cmpDropShield.id = 'compare-drop-shield';
|
| _cmpDropShield.setAttribute('aria-hidden', 'true');
|
| _cmpDropShield.style.cssText = 'position:fixed;inset:0;z-index:2147483646;' +
|
| 'display:none;align-items:center;justify-content:center;' +
|
| 'background:color-mix(in srgb, var(--accent, #0af) 16%, rgba(0,0,0,0.5));' +
|
| 'backdrop-filter:blur(2px);';
|
| const _box = document.createElement('div');
|
| _box.style.cssText = 'pointer-events:none;border:2px dashed rgba(255,255,255,0.9);' +
|
| 'border-radius:14px;padding:20px 28px;background:rgba(0,0,0,0.4);' +
|
| 'font:600 16px/1.4 system-ui,sans-serif;color:#fff;';
|
| _box.textContent = 'Drop files to attach';
|
| _cmpDropShield.appendChild(_box);
|
| document.body.appendChild(_cmpDropShield);
|
| }
|
| _cmpDropShield.style.display = 'flex';
|
| };
|
| const _hideCmpShield = () => { if (_cmpDropShield) _cmpDropShield.style.display = 'none'; };
|
|
|
| window.addEventListener('dragenter', (e) => {
|
| if (_isFileDrag(e) && _compareActive()) _showCmpShield();
|
| }, true);
|
| window.addEventListener('dragover', (e) => {
|
| if (!_isFileDrag(e) || !_compareActive()) return;
|
| e.preventDefault();
|
| if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
|
| _showCmpShield();
|
| }, true);
|
| window.addEventListener('dragleave', (e) => {
|
|
|
| if (_compareActive() && !e.relatedTarget) _hideCmpShield();
|
| }, true);
|
| window.addEventListener('dragend', _hideCmpShield, true);
|
| window.addEventListener('drop', (e) => {
|
| if (!_isFileDrag(e) || !_compareActive()) return;
|
| e.preventDefault();
|
| _hideCmpShield();
|
| const files = Array.from(e.dataTransfer.files || []);
|
| if (!files.length) return;
|
| fileHandlerModule.addFiles(files);
|
| fileHandlerModule.renderAttachStrip();
|
| uiModule.showToast(`Added ${files.length} file${files.length > 1 ? 's' : ''} to attach`);
|
| }, true);
|
|
|
|
|
| presetsModule.loadPresets(uiModule.showError);
|
|
|
| if (sessionModule) {
|
| sessionModule.initDependencies({
|
| API_BASE: API_BASE,
|
| el: el,
|
| showToast: uiModule.showToast,
|
| showError: uiModule.showError,
|
| addMessage: chatModule.addMessage,
|
| renderContent: markdownModule.renderContent,
|
| scrollHistory: uiModule.scrollHistoryInstant
|
| });
|
|
|
|
|
| sessionModule.loadSessions()
|
| .catch(e => console.warn('loadSessions error:', e))
|
| .finally(() => {
|
| const loader = document.getElementById('app-loader');
|
| if (loader) { loader.style.opacity = '0'; setTimeout(() => loader.remove(), 300); }
|
|
|
|
|
| if (window._odysseusRouteOpener) {
|
| try { window._odysseusRouteOpener(); } catch (_) {}
|
| window._odysseusRouteOpener = null;
|
| }
|
| });
|
| } else {
|
| console.error('Session module not loaded!');
|
| }
|
|
|
|
|
| modelsModule.refreshModels(true).then(() => {
|
| const modelsBox = document.getElementById('models');
|
| const hasModels = modelsBox && modelsBox.querySelector('.models-row');
|
| if (!hasModels) {
|
| const tip = document.getElementById('welcome-tip');
|
| if (tip) tip.textContent = 'Add an AI endpoint from Settings in the sidebar, or paste an endpoint/API key into the chat.';
|
| }
|
| }).catch(() => {});
|
| modelsModule.refreshProviders();
|
| ragModule.loadPersonalDocs();
|
| memoryModule.loadMemories();
|
|
|
|
|
| setTimeout(async () => {
|
| await memoryModule.loadMemories();
|
| }, 1000);
|
|
|
|
|
| voiceRecorderModule.init();
|
| if (censorModule) censorModule.init();
|
|
|
|
|
| const msgEl = document.getElementById('message');
|
| if (msgEl) msgEl.focus();
|
|
|
|
|
| const sidebar = document.getElementById('sidebar');
|
| const sidebarInner = sidebar ? sidebar.querySelector('.sidebar-inner') : sidebar;
|
|
|
|
|
| if (sidebarInner) {
|
| const MAX_PULL = 8;
|
| let _overscroll = 0;
|
| let _resetTimer = null;
|
| sidebarInner.addEventListener('wheel', (e) => {
|
| const el = sidebarInner;
|
| const atTop = el.scrollTop <= 0 && e.deltaY < 0;
|
| const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 1 && e.deltaY > 0;
|
| if (!atTop && !atBottom) { _overscroll = 0; return; }
|
|
|
| _overscroll += Math.abs(e.deltaY) * 0.15;
|
| const pull = Math.min(_overscroll, MAX_PULL);
|
| const dir = atTop ? 1 : -1;
|
| el.style.transition = 'none';
|
| el.style.transform = `translateY(${dir * pull}px)`;
|
|
|
| clearTimeout(_resetTimer);
|
| _resetTimer = setTimeout(() => {
|
| el.style.transition = 'transform 0.3s cubic-bezier(0.25, 1, 0.5, 1)';
|
| el.style.transform = '';
|
| _overscroll = 0;
|
| }, 120);
|
| }, { passive: true });
|
| }
|
|
|
|
|
|
|
|
|
| if (sidebarInner && 'ontouchstart' in window) {
|
| let _sidebarTouchMoved = false;
|
| let _sidebarTouchStartY = 0;
|
| sidebarInner.addEventListener('touchstart', (e) => {
|
| _sidebarTouchMoved = false;
|
| _sidebarTouchStartY = e.touches[0].clientY;
|
| }, { passive: true });
|
| sidebarInner.addEventListener('touchmove', (e) => {
|
|
|
| if (Math.abs(e.touches[0].clientY - _sidebarTouchStartY) > 8) {
|
| _sidebarTouchMoved = true;
|
| }
|
| }, { passive: true });
|
| sidebarInner.addEventListener('click', (e) => {
|
| if (_sidebarTouchMoved) {
|
| e.stopPropagation();
|
| e.preventDefault();
|
| _sidebarTouchMoved = false;
|
| }
|
| }, true);
|
| }
|
|
|
|
|
| initSectionCollapse(Storage);
|
| initSectionDrag(Storage, loadUIVis);
|
|
|
|
|
| const sections = document.querySelectorAll('.section[draggable="true"]');
|
| sections.forEach(section => {
|
| section.addEventListener('dragover', (e) => {
|
| e.preventDefault();
|
| e.dataTransfer.dropEffect = 'move';
|
|
|
|
|
| const activeId = e.dataTransfer.getData('text/plain');
|
| if (activeId && activeId !== section.id) {
|
| section.setAttribute('dnd-over', 'true');
|
| }
|
| });
|
|
|
| section.addEventListener('dragleave', (e) => {
|
|
|
| const rect = section.getBoundingClientRect();
|
| if (e.clientY < rect.top || e.clientY > rect.bottom ||
|
| e.clientX < rect.left || e.clientX > rect.right) {
|
| section.setAttribute('dnd-over', 'false');
|
| }
|
| });
|
| });
|
|
|
|
|
| const savedOrder = Storage.get(Storage.KEYS.SECTION_ORDER);
|
| if (savedOrder) {
|
| try {
|
| const order = JSON.parse(savedOrder);
|
| const innerContainer = sidebarInner || document.getElementById('sidebar');
|
|
|
|
|
| const fragment = document.createDocumentFragment();
|
|
|
|
|
| for (const id of order) {
|
| const section = document.getElementById(id);
|
| if (section) {
|
| fragment.appendChild(section);
|
| }
|
| }
|
|
|
|
|
| sections.forEach(section => {
|
| if (!order.includes(section.id)) {
|
| fragment.appendChild(section);
|
| }
|
| });
|
|
|
|
|
| innerContainer.appendChild(fragment);
|
| } catch (e) {
|
| console.error('Failed to restore sidebar order:', e);
|
| }
|
| }
|
|
|
|
|
|
|
| if (window.hljs) {
|
| console.log('Highlighting all code blocks on page load');
|
| document.querySelectorAll('pre code:not(.hljs)').forEach(block => {
|
| window.hljs.highlightElement(block);
|
| });
|
| }
|
| }
|
|
|
| if (document.readyState === 'loading') {
|
| document.addEventListener('DOMContentLoaded', startOdysseusApp, { once: true });
|
| } else {
|
| startOdysseusApp();
|
| }
|
|
|