diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/core/module-chatstudio.js | 44 | ||||
| -rw-r--r-- | modules/core/module-searxng.js | 65 | ||||
| -rw-r--r-- | modules/ext/tutorial.js | 14 |
3 files changed, 95 insertions, 28 deletions
diff --git a/modules/core/module-chatstudio.js b/modules/core/module-chatstudio.js index 5777acf..762eae3 100644 --- a/modules/core/module-chatstudio.js +++ b/modules/core/module-chatstudio.js @@ -114,10 +114,16 @@ class ChatStudioModule { window._csGlobalsInited = true; } - // Fetch HTML template + // Fetch HTML template (cached after first load) + if (this._overlayHtml) { + // Overlay div already exists (just hidden). No-op. + resolve(); + return; + } fetch('chat/chat-ui.html?v=4') .then(r => r.text()) .then(html => { + this._overlayHtml = html; // cache for re-open // Insert overlay into body if not already there if (!document.getElementById('chat-overlay')) { const temp = document.createElement('div'); @@ -190,10 +196,10 @@ class ChatStudioModule { window.showToast('Chat overlay failed: ' + err.message); } - // Clean up any partial overlay DOM + // Clean up any partial overlay DOM — hide instead of remove so cached HTML path still works const partialOverlay = document.getElementById('chat-overlay'); if (partialOverlay) { - partialOverlay.remove(); + partialOverlay.style.display = 'none'; } // Reset state so user can retry @@ -301,17 +307,35 @@ class ChatStudioModule { el.style.height = Math.min(el.scrollHeight, 150) + 'px'; } + // Close sidebar when clicking outside on mobile + var backdrop = document.getElementById('sidebarBackdrop'); + if (!backdrop) { + backdrop = document.createElement('div'); + backdrop.id = 'sidebarBackdrop'; + backdrop.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;z-index:99;display:none;background:transparent'; + backdrop.onclick = function() { closeSidebar(); }; + document.getElementById('chat-overlay')?.appendChild(backdrop); + } + function toggleSidebar() { var s = document.getElementById('sidebar'); - if (s) s.classList.toggle('open'); - var b = document.getElementById('sidebarBackdrop'); - if (b) b.classList.toggle('open'); + if (!s) return; + // On mobile (<700px) use .open, on desktop use .collapsed + var isMobile = window.innerWidth <= 700; + if (isMobile) { + s.classList.toggle('open'); + } else { + s.classList.toggle('collapsed'); + // Toggle backdrop on mobile regardless + } + if (backdrop) { + backdrop.style.display = s.classList.contains('open') || s.classList.contains('collapsed') ? 'block' : 'none'; + } } function closeSidebar() { var s = document.getElementById('sidebar'); - if (s) s.classList.remove('open'); - var b = document.getElementById('sidebarBackdrop'); - if (b) b.classList.remove('open'); + if (s) { s.classList.remove('open'); s.classList.remove('collapsed'); } + if (backdrop) backdrop.style.display = 'none'; } // ── Log System ── @@ -403,7 +427,7 @@ class ChatStudioModule { OLLAMA_API_KEY = document.getElementById('ollamaApiKey').value.trim(); DEFAULT_MODEL = document.getElementById('defaultModel').value.trim() || 'llama3.2'; USE_STREAMING = document.getElementById('streamingToggle').checked; - API_BASE = document.getElementById('apiBase').value.trim() || '/api'; + API_BASE = (document.getElementById('apiBase')?.value || '/api').trim(); try { var settings = { ollamaUrl: OLLAMA_URL, diff --git a/modules/core/module-searxng.js b/modules/core/module-searxng.js index f2c2e72..24db0f5 100644 --- a/modules/core/module-searxng.js +++ b/modules/core/module-searxng.js @@ -5,29 +5,41 @@ class SearXNGModule { } getHelpInfo() { return { - 'search': 'search <query> [--summarize] [--fetch] — Search using SearXNG; --summarize/-s adds AI overview, --fetch/-f fetches page content', - 's': 's <query> [--summarize] [--fetch] — Alias for search', + 'search': 'search <query> [--summarize/-s] [--fetch/-f] [--images/-i] [--videos/-v] — Search using SearXNG; --summarize/-s adds AI overview, --fetch/-f fetches page content, --images/-i searches for images, --videos/-v searches for videos', + 's': 's <query> [--summarize] [--fetch] [--images] [--videos] — Alias for search', + 'images': 'images <query> — Search for images', + 'videos': 'videos <query> — Search for videos', }; } registerCommands() { this.commands['search'] = (args) => this.search(args); this.commands['s'] = (args) => this.search(args); + this.commands['images'] = (args) => this.search(args, 'images'); + this.commands['videos'] = (args) => this.search(args, 'videos'); } - async search(args) { - if (!args[0]) { - this.terminal.print("Usage: search <query> [--summarize] [--fetch]", "warning"); + async search(args, forceCategory) { + if (!args[0] && !forceCategory) { + this.terminal.print("Usage: search <query> [--summarize] [--fetch] [--images] [--videos]", "warning"); this.terminal.print("Example: search quantum computing --summarize", "info"); + this.terminal.print("Example: search cats --images", "info"); + this.terminal.print("Example: search cats --videos", "info"); + this.terminal.print("Shortcuts: images <query> or videos <query>", "info"); return; } const argsList = args.slice(); const summarize = argsList.includes('--summarize') || argsList.includes('-s'); const fetchContent = argsList.includes('--fetch') || argsList.includes('-f'); + const wantImages = forceCategory === 'images' || argsList.includes('--images') || argsList.includes('-i'); + const wantVideos = forceCategory === 'videos' || argsList.includes('--videos') || argsList.includes('-v'); const query = argsList.filter(a => !a.startsWith('-')).join(' '); if (!query) { this.terminal.print('Search query is required', 'warning'); return; } - this.terminal.print(`🔍 Searching: ${query}`, 'info'); + const category = wantImages ? 'images' : (wantVideos ? 'videos' : 'general'); + const categoryLabel = category === 'images' ? '🖼️' : (category === 'videos' ? '🎬' : '🔍'); - let params = 'q=' + encodeURIComponent(query); + this.terminal.print(`${categoryLabel} Searching ${category}: ${query}`, 'info'); + + let params = 'q=' + encodeURIComponent(query) + '&categories=' + category; if (summarize) params += '&summarize=true'; if (fetchContent) params += '&fetch_content=true'; @@ -46,7 +58,7 @@ class SearXNGModule { return; } - this.terminal.print(`\n=== ${results.length} results (via ${data.source || 'unknown'}) ===\n`, 'success'); + this.terminal.print(`\n=== ${results.length} results (${category}, via ${data.source || 'unknown'}) ===\n`, 'success'); if (data.ai_overview) { this.terminal.print('🤖 AI Overview:', 'info'); @@ -57,14 +69,35 @@ class SearXNGModule { results.forEach((r, i) => { const snippet = r.content || ''; const contentSnippet = r.content_snippet || ''; - this.terminal.printHTML(` - <div class="search-result"> - <strong>${i + 1}. <a href="${r.url}" target="_blank" style="color: #00aaff;">${r.title || 'No Title'}</a></strong><br/> - <span style="color: #888888; font-size: 0.85em;">${r.url || 'No URL'}</span><br/> - <span style="color: #00ff00; font-size: 0.85em;">${(snippet || '').substring(0, 200)}...</span> - ${contentSnippet ? `<br/><span style="color: #aaaaaa; font-size: 0.8em;">📄 ${contentSnippet.substring(0, 150)}...</span>` : ''} - </div> - `); + const imgSrc = r.img_src || r.thumbnail_src || ''; + const videoThumb = r.thumbnail || ''; + + if (category === 'images' && imgSrc) { + this.terminal.printHTML(` + <div class="search-result search-result-image"> + <a href="${r.url}" target="_blank"><img src="${imgSrc}" style="max-width:200px;max-height:150px;border-radius:4px;object-fit:cover;" alt="${r.title || ''}" loading="lazy"></a> + <br/><strong>${i + 1}. <a href="${r.url}" target="_blank" style="color: #00aaff;">${r.title || 'No Title'}</a></strong> + </div> + `); + } else if (category === 'videos' && videoThumb) { + this.terminal.printHTML(` + <div class="search-result search-result-video"> + <a href="${r.url}" target="_blank"><img src="${videoThumb}" style="max-width:200px;max-height:150px;border-radius:4px;object-fit:cover;" alt="${r.title || ''}" loading="lazy"></a> + <br/><strong>${i + 1}. <a href="${r.url}" target="_blank" style="color: #00aaff;">${r.title || 'No Title'}</a></strong><br/> + <span style="color: #888888; font-size: 0.85em;">${r.url || ''}</span> + </div> + `); + } else { + this.terminal.printHTML(` + <div class="search-result"> + <strong>${i + 1}. <a href="${r.url}" target="_blank" style="color: #00aaff;">${r.title || 'No Title'}</a></strong><br/> + <span style="color: #888888; font-size: 0.85em;">${r.url || 'No URL'}</span><br/> + <span style="color: #00ff00; font-size: 0.85em;">${(snippet || '').substring(0, 200)}...</span> + ${contentSnippet ? `<br/><span style="color: #aaaaaa; font-size: 0.8em;">📄 ${contentSnippet.substring(0, 150)}...</span>` : ''} + ${imgSrc && category === 'general' ? `<br/><a href="${imgSrc}" target="_blank" style="color: #ff8800; font-size: 0.8em;">🖼️ View image</a>` : ''} + </div> + `); + } }); } catch (e) { this.terminal.print('Search failed: ' + e.message, 'error'); diff --git a/modules/ext/tutorial.js b/modules/ext/tutorial.js index 8983322..af4f4fa 100644 --- a/modules/ext/tutorial.js +++ b/modules/ext/tutorial.js @@ -147,6 +147,10 @@ class TutorialModule { /** Called by welcome modal to begin the tutorial */ startTutorial() { + // Close welcome modal if it's open + const modal = document.getElementById('welcome-modal'); + if (modal) modal.style.display = 'none'; + this._started = true; this._completed = new Set(); this._activeIndex = 0; @@ -232,6 +236,10 @@ class TutorialModule { } _showCompletion() { + // Guard: only fire once + if (this._completionFired) return; + this._completionFired = true; + this.terminal.print(''); this.terminal.print('🎉🎉🎉 CONGRATULATIONS! 🎉🎉🎉', 'success'); this.terminal.print('═'.repeat(40), 'success'); @@ -255,10 +263,12 @@ class TutorialModule { } _fireConfetti() { - const emojis = ['🎉', '✨', '⭐', '🌟', '💫', '🎊', '🔷', '🟢', '🔶']; + const emojis = ['🎉', '✨', '⭐', '🌟', '💫']; const container = document.body; if (!container) return; - for (let i = 0; i < 40; i++) { + // Remove any previous confetti first + document.querySelectorAll('.confetti-piece').forEach(el => el.remove()); + for (let i = 0; i < 15; i++) { const el = document.createElement('div'); el.className = 'confetti-piece'; el.textContent = emojis[i % emojis.length]; |