diff options
| -rw-r--r-- | chat/chat-commands.js | 276 | ||||
| -rw-r--r-- | chat/chat-modules.js | 365 | ||||
| -rw-r--r-- | chat/chat-ui.css | 747 | ||||
| -rw-r--r-- | chat/chat-ui.html | 156 | ||||
| -rw-r--r-- | main.html | 14 | ||||
| -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 |
8 files changed, 1652 insertions, 29 deletions
diff --git a/chat/chat-commands.js b/chat/chat-commands.js new file mode 100644 index 0000000..d5cfdd9 --- /dev/null +++ b/chat/chat-commands.js @@ -0,0 +1,276 @@ +/* + * Version: 1.0.0-lite + * ChatStudio Lite - Command Handlers + * Handles /search, /fetch, /explain, /help, /status, /clear, /modules, etc. + */ + +async function handleCommand(cmd) { + var parts = cmd.split(' '); + var command = parts[0].toLowerCase(); + var args = parts.slice(1).join(' '); + + // === /help === + if (command === '/help' || command === '?') { + addMessage('ai', + '**ChatStudio Lite v' + CHATSTUDIO_VERSION + '**\n\n' + + '**Built-in Commands:**\n' + + '`/help` or `?` — Show this help\n' + + '`/module [name]` — Switch AI module\n' + + '`/modules` — List all modules\n' + + '`/search [query]` — Web search via DuckDuckGo (with AI overview)\n' + + '`/fetch [url]` — Fetch and display a web page\n' + + '`/extract [url]` — Extract readable content from a URL\n' + + '`/summarize [url]` — AI summarization of a URL\n' + + '`/explain [code]` — Get AI code explanation\n' + + '`/status` — Check Ollama connection status\n' + + '`/test` — Ping the AI API\n' + + '`/models` — List all available AI models\n' + + '`/clear` or `/purge` — Clear chat history\n\n' + + '**Modules:**\n' + + 'Modules in the sidebar customize AI behavior. Each module has a system prompt and commands.\n' + + 'Use `/module [name]` to switch, or edit them from the sidebar.\n\n' + + '**Links:**\n' + + '<a href="/spaces/chatstudio/">Full ChatStudio (Pro)</a>\n' + + '<a href="/spaces/dashboard.html">User Dashboard</a>\n\n' + + 'Type any question to chat with the AI!' + ); + return; + } + + // === /module or /switch === + if (command === '/module' || command === '/switch') { + var target = args.toLowerCase(); + var found = modules.find(function (m) { + return m.id === target || m.name.toLowerCase().includes(target); + }); + if (found) { + switchModule(found.id); + addMessage('ai', 'Switched to ' + found.name + ' module!'); + } else { + addMessage('ai', 'Module not found. Available: ' + modules.map(function (m) { return m.name; }).join(', ')); + } + return; + } + + // === /modules or /list === + if (command === '/modules' || command === '/list' || command === '/mods') { + addMessage('ai', '**Available Modules:**\n\n' + + modules.map(function (m) { return '- **' + m.name + '** - ' + m.desc; }).join('\n')); + return; + } + + // === /explain === + if (command === '/explain' && args) { + showTyping(); + try { + var headers = { 'Content-Type': 'application/json' }; + if (typeof OLLAMA_API_KEY !== 'undefined' && OLLAMA_API_KEY) { + headers['Authorization'] = 'Bearer ' + OLLAMA_API_KEY; + } + var response = await fetch((typeof OLLAMA_URL !== 'undefined' ? OLLAMA_URL : '/ollama') + '/api/generate', { + method: 'POST', + headers: headers, + body: JSON.stringify({ + model: document.getElementById('modelSelect').value, + prompt: 'Explain this code clearly, line by line, what it does:\n\n' + args, + stream: false + }) + }); + hideTyping(); + var data = await response.json(); + addMessage('ai', '**Code Explanation:**\n\n' + (data.response || 'No response')); + } catch (e) { + hideTyping(); + addMessage('ai', 'Failed to explain code: ' + e.message); + } + return; + } + + // === /search or /find === + if (command === '/search' || command === '/find') { + var query = args || cmd.split(' ').slice(1).join(' '); + if (!query) { + addMessage('ai', 'Usage: /search [query]\n\nExample: /search Python tutorials'); + return; + } + showTyping(); + try { + var response = await fetch('/api/search?q=' + encodeURIComponent(query) + '&summarize=true'); + var data = await response.json(); + hideTyping(); + if (data.error) { + addMessage('ai', 'Search failed: ' + data.error); + return; + } + if (data.results && data.results.length > 0) { + var source = data.source === 'searxng' ? '🔍 (SearXNG)' : '🔍 (DDG)'; + var msg = ''; + if (data.ai_overview) { + msg += '🌿 **AI Overview:**\n' + data.ai_overview + '\n\n---\n\n'; + } + msg += source + ' **Search results for "' + query + '":**\n\n'; + data.results.slice(0, 10).forEach(function(r) { + msg += '• **' + (r.title || 'Untitled') + '**\n'; + msg += ' ' + (r.url || '') + '\n'; + if (r.content && r.content.length > 5) { + msg += ' _' + r.content.substring(0, 200) + (r.content.length > 200 ? '...' : '') + '_\n'; + } + msg += '\n'; + }); + if (data.count > 10) { + msg += '_... and ' + (data.count - 10) + ' more results_\n'; + } + addMessage('ai', msg); + } else { + addMessage('ai', '🔍 No results found for "' + query + '".'); + } + } catch (e) { + hideTyping(); + addMessage('ai', 'Search failed: ' + e.message); + } + return; + } + + // === /fetch or /get === + if (command === '/fetch' || command === '/get') { + var url = args || (cmd.match(/(https?:\/\/[^\s]+)/) ? cmd.match(/(https?:\/\/[^\s]+)/)[1] : null); + if (!url) { + addMessage('ai', 'Usage: /fetch [url]\n\nExample: /fetch https://example.com'); + return; + } + showTyping(); + try { + var extractResp = await fetch('/api/extract?url=' + encodeURIComponent(url)); + var extractOk = extractResp.ok; + var extractData = extractOk ? await extractResp.json() : null; + + var htmlResp = await fetch('/api/fetch?url=' + encodeURIComponent(url)); + if (!htmlResp.ok) throw new Error('HTTP ' + htmlResp.status); + var html = await htmlResp.text(); + var htmlPreview = html.substring(0, 262144) + (html.length > 262144 ? '\n\n... (truncated)' : ''); + + hideTyping(); + var msg = ''; + if (extractData && !extractData.error) { + msg += '📄 **Fetched:** ' + extractData.url + '\n\n'; + if (extractData.title) msg += '**Title:** ' + extractData.title + '\n'; + msg += '**Words:** ' + (extractData.word_count || '?') + '\n'; + msg += '**Method:** ' + (extractData.method || '?') + '\n'; + if (extractData.excerpt) msg += '\n**Preview:**\n' + extractData.excerpt + '\n'; + } else { + msg += '📄 **Fetched:** ' + url + '\n'; + } + msg += '\n--- Raw HTML (truncated) ---\n```html\n' + htmlPreview + '\n```'; + addMessage('ai', msg); + } catch (e) { + hideTyping(); + addMessage('ai', 'Fetch failed: ' + e.message); + } + return; + } + + // === /summarize === + if (command === '/summarize') { + var url = args || (cmd.match(/(https?:\/\/[^\s]+)/) ? cmd.match(/(https?:\/\/[^\s]+)/)[1] : null); + if (!url) { + addMessage('ai', 'Usage: /summarize [url]\n\nExample: /summarize https://example.com'); + return; + } + showTyping(); + try { + var response = await fetch('/api/summarize?url=' + encodeURIComponent(url)); + if (!response.ok) throw new Error('HTTP ' + response.status); + var data = await response.json(); + hideTyping(); + var msg = '🤖 **AI Summary**\n'; + if (data.summary) { + msg += '\n' + data.summary + '\n'; + } + msg += '\n_Model: ' + (data.model || 'gemma4:31b-cloud') + ' | Processed in ' + (data.processing_time_ms || '?') + 'ms_'; + addMessage('ai', msg); + } catch (e) { + hideTyping(); + addMessage('ai', 'Summarize failed: ' + e.message); + } + return; + } + + // === /extract === + if (command === '/extract') { + var url = args || (cmd.match(/(https?:\/\/[^\s]+)/) ? cmd.match(/(https?:\/\/[^\s]+)/)[1] : null); + if (!url) { + addMessage('ai', 'Usage: /extract [url]\n\nExample: /extract https://example.com'); + return; + } + showTyping(); + try { + var response = await fetch('/api/extract?url=' + encodeURIComponent(url)); + if (!response.ok) throw new Error('HTTP ' + response.status); + var data = await response.json(); + hideTyping(); + var msg = '📄 **Extracted:** ' + (data.url || url) + '\n\n'; + if (data.title) msg += '**Title:** ' + data.title + '\n'; + msg += '**Words:** ' + (data.word_count || '?') + '\n'; + msg += '**Method:** ' + (data.method || '?') + '\n'; + if (data.excerpt) msg += '\n**Preview:**\n' + data.excerpt + '\n'; + if (data.content) msg += '\n**Full Content:** (first 2000 chars)\n' + data.content.substring(0, 2000) + (data.content.length > 2000 ? '...' : '') + '\n'; + addMessage('ai', msg); + } catch (e) { + hideTyping(); + addMessage('ai', 'Extract failed: ' + e.message); + } + return; + } + + // === /status === + if (command === '/status') { + try { + var response = await fetch((typeof OLLAMA_URL !== 'undefined' ? OLLAMA_URL : '/ollama') + '/api/tags'); + var data = await response.json(); + var modelCount = data.models ? data.models.length : 0; + var current = document.getElementById('modelSelect').value; + addMessage('ai', '**Ollama Status**\n\n- Status: Online\n- Available models: ' + modelCount + '\n- Current model: ' + current + '\n- API: ' + (typeof OLLAMA_URL !== 'undefined' ? OLLAMA_URL : '/ollama')); + } catch (e) { + addMessage('ai', '**Ollama Status**\n\n- Status: Offline\n- Error: ' + e.message); + } + return; + } + + // === /test === + if (command === '/test') { + showTyping(); + try { + var start = Date.now(); + var response = await fetch((typeof OLLAMA_URL !== 'undefined' ? OLLAMA_URL : '/ollama') + '/api/tags'); + var ms = Date.now() - start; + hideTyping(); + addMessage('ai', '**Connection Test**\n\n- Response time: ' + ms + 'ms\n- Status: OK'); + } catch (e) { + hideTyping(); + addMessage('ai', '**Connection Test Failed**\n\n- Error: ' + e.message); + } + return; + } + + // === /models or /list models === + if (command === '/models') { + try { + var response = await fetch((typeof OLLAMA_URL !== 'undefined' ? OLLAMA_URL : '/ollama') + '/api/tags'); + var data = await response.json(); + var list = data.models ? data.models.map(function (m) { return '- ' + m.name; }).join('\n') : 'No models'; + addMessage('ai', '**Available Models:**\n\n' + list); + } catch (e) { + addMessage('ai', 'Failed to list models: ' + e.message); + } + return; + } + + // === /clear, /purge === + if (command === '/clear' || command === '/purge') { + clearChat(); + return; + } + + // Unknown command + addMessage('ai', 'Unknown command. Try /help, /modules, /explain [code], /search [query], /summarize [url], /extract [url], /fetch [url], /status, /test, /models, or /switch [name]'); +} diff --git a/chat/chat-modules.js b/chat/chat-modules.js new file mode 100644 index 0000000..8483d48 --- /dev/null +++ b/chat/chat-modules.js @@ -0,0 +1,365 @@ +/* + * Version: 1.0.0-lite + * ChatStudio Lite - Module System + * Module CRUD, import/export, module list rendering, AI module generator. + * Module format: { id, name, desc, prompt, help, commands } + */ + +const DEFAULT_MODULES = [ + { + id: 'default', + name: 'AI Chat', + desc: 'General purpose AI assistant', + prompt: 'You are a helpful AI assistant on WebX11. You have access to these tools:\n\n1. **Search:** /api/search?q=QUERY - Searches the web via DuckDuckGo. Returns HTML results. Use this for current events, facts, and web information.\n\n2. **Fetch:** /api/fetch?url=URL - Fetches content from any URL. Use this to read web pages, APIs, or documents.\n\n3. **Commands:** Users can type /help, /search [query], /fetch [url], /explain [code], /status, /clear directly.\n\nWhen a user asks about current events, recent info, or something you dont know, suggest they use /search or tell them to ask you to search the web. If they provide a URL, offer to fetch it.\n\nBe concise, helpful, and use the available tools when appropriate.', + help: 'Just type any question! I can help with:\n- Current events (I can search the web)\n- Web pages (I can fetch URLs)\n- Coding questions\n- General knowledge\n- Problem solving\n\nTry: /search latest news', + commands: '/help, /search, /fetch, /explain, /status, /clear' + }, + { + id: 'search', + name: 'Search', + desc: 'Search the web for information', + prompt: 'You are a web search assistant. When asked about current events, recent information, or things you dont know, suggest searching the web. Provide helpful search suggestions.', + help: 'Use /search [query] to get search suggestions.\n\nExample: /search Python tutorials', + commands: '/search, /find' + }, + { + id: 'fetch', + name: 'URL Fetch', + desc: 'Fetch content from URLs', + prompt: 'You can fetch content from URLs using the fetch tool. When a user provides a URL, use the fetch tool to get its content.', + help: 'Provide a URL and I can fetch its content.\n\nJust paste any URL in the chat!', + commands: '/fetch, /get' + }, + { + id: 'summarize', + name: 'Summarizer', + desc: 'Summarize long text', + prompt: 'You are a text summarization expert. When given text, provide a clear summary highlighting the key points.', + help: 'Paste long text and Ill summarize it.\n\nUse /summary for a brief overview.', + commands: '/summarize, /summary' + }, + { + id: 'plan', + name: 'Planner', + desc: 'Create plans and todo lists', + prompt: 'You are a planning assistant. Help users break down tasks into steps, create schedules, and organize work.', + help: 'Tell me what you want to accomplish and Ill help create a plan!\n\nCommands:\n- /plan [goal] - Create a plan\n- /todo - Show current todos', + commands: '/plan, /todo' + }, + { + id: 'code', + name: 'Code Helper', + desc: 'Programming assistance', + prompt: 'You are a coding expert. Help with code explanations, debugging, writing, and best practices. Provide clear, working code examples.', + help: 'I can help with:\n- Explaining code\n- Debugging errors\n- Writing new code\n- Code reviews\n\nJust paste your code and ask!', + commands: '/code, /debug, /explain' + }, + { + id: 'translate', + name: 'Translator', + desc: 'Translate between languages', + prompt: 'You are a multilingual translator. Translate text between languages accurately while preserving meaning.', + help: 'Translate text by specifying languages:\n\n- /translate [text] to [language]\n- /es Hello - translate to Spanish\n- /en Hola - translate to English', + commands: '/translate, /es, /fr, /de, /zh' + }, + { + id: 'webx11-help', + name: 'WebX11 Help', + desc: 'Help with WebX11 Spaces', + prompt: 'You are a WebX11 Spaces help assistant. Help users with:\n- Creating and managing their web space\n- Using the dashboard\n- Uploading files\n- Custom domains\n- Module system\n- Any technical questions', + help: 'WebX11 Spaces Help:\n\n- Dashboard: Upload & manage files\n- Register: Create your space\n- Chat: AI assistant\n- Modules: Specialized tools\n\nType /help for more!', + commands: '/help, /webx11, /space' + } +]; + +var modules = []; +var currentModule = 'default'; +var editingModuleId = null; + +var STORAGE_KEY = 'webx11_chat_modules'; + +function loadModules() { + try { + var saved = localStorage.getItem(STORAGE_KEY); + if (saved) { + modules = JSON.parse(b64_to_utf8(saved)); + } else { + modules = JSON.parse(JSON.stringify(DEFAULT_MODULES)); + saveModules(); + } + } catch (e) { + console.warn('Failed to load modules:', e); + modules = JSON.parse(JSON.stringify(DEFAULT_MODULES)); + } + renderModuleList(); + updateHelpPanel(); +} + +function saveModules() { + try { + var encoded = utf8_to_b64(JSON.stringify(modules)); + localStorage.setItem(STORAGE_KEY, encoded); + } catch (e) { + console.warn('Failed to save modules:', e); + } +} + +function renderModuleList() { + var list = document.getElementById('moduleList'); + list.innerHTML = modules.map(function (m) { + var isActive = m.id === currentModule; + var actions = m.id !== 'default' + ? '<div class="module-actions">' + + '<button class="module-action-btn" onclick="event.stopPropagation(); copyModule(\'' + escapeHtml(m.id) + '\')" title="Copy">Copy</button>' + + '<button class="module-action-btn delete" onclick="event.stopPropagation(); deleteModule(\'' + escapeHtml(m.id) + '\')" title="Delete">Del</button>' + + '</div>' + : ''; + return '<div class="module-item' + (isActive ? ' active' : '') + '" onclick="switchModule(\'' + escapeHtml(m.id) + '\')" oncontextmenu="moduleContextMenu(event, \'' + escapeHtml(m.id) + '\')">' + + '<div class="module-name">' + escapeHtml(m.name) + '</div>' + + '<div class="module-desc">' + escapeHtml(m.desc) + '</div>' + + actions + + '</div>'; + }).join(''); +} + +function getCurrentModule() { + return modules.find(function (m) { return m.id === currentModule; }); +} + +function switchModule(id) { + currentModule = id; + renderModuleList(); + updateHelpPanel(); + var found = modules.find(function (m) { return m.id === id; }); + showToast('Switched to ' + (found ? found.name : 'module')); + addLog('Module', 'Switched to ' + (found ? found.name : id)); +} + +function updateHelpPanel() { + var mod = modules.find(function (m) { return m.id === currentModule; }); + if (mod) { + document.getElementById('helpTitle').textContent = mod.name; + document.getElementById('helpText').textContent = mod.help || 'No help available.'; + var cmds = mod.commands ? mod.commands.split(',').map(function (c) { return c.trim(); }) : []; + document.getElementById('helpCommands').innerHTML = cmds.map(function (c) { + return '<span class="help-cmd">' + escapeHtml(c) + '</span>'; + }).join(''); + } +} + +function moduleContextMenu(e, id) { + e.preventDefault(); + if (id === 'default') return; + var action = prompt('Module actions: edit, copy, delete (or cancel)'); + if (!action) return; + var a = action.toLowerCase().trim(); + if (a === 'edit') { + editModule(id); + } else if (a === 'copy') { + copyModule(id); + } else if (a === 'delete') { + deleteModule(id); + } +} + +function copyModule(id) { + var mod = modules.find(function (m) { return m.id === id; }); + if (!mod) return; + var newId = id + '_copy_' + Date.now(); + var newModule = JSON.parse(JSON.stringify(mod)); + newModule.id = newId; + newModule.name = mod.name + ' (Copy)'; + modules.push(newModule); + saveModules(); + renderModuleList(); + showToast('Module copied!'); +} + +function deleteModule(id) { + if (!confirm('Delete this module?')) return; + modules = modules.filter(function (m) { return m.id !== id; }); + if (currentModule === id) { + currentModule = 'default'; + } + saveModules(); + renderModuleList(); + updateHelpPanel(); + showToast('Module deleted'); +} + +function editModule(id) { + openModuleModal(id); +} + +function openModuleModal(editId) { + editId = editId || null; + editingModuleId = editId; + document.getElementById('moduleModalTitle').textContent = editId ? 'Edit Module' : 'Add Module'; + + if (editId) { + var mod = modules.find(function (m) { return m.id === editId; }); + if (mod) { + document.getElementById('moduleName').value = mod.name; + document.getElementById('moduleDesc').value = mod.desc; + document.getElementById('modulePrompt').value = mod.prompt; + document.getElementById('moduleHelp').value = mod.help; + document.getElementById('moduleCommands').value = mod.commands; + } + } else { + document.getElementById('moduleName').value = ''; + document.getElementById('moduleDesc').value = ''; + document.getElementById('modulePrompt').value = ''; + document.getElementById('moduleHelp').value = ''; + document.getElementById('moduleCommands').value = ''; + } + + document.getElementById('aiModuleDesc').value = ''; + document.getElementById('moduleModal').classList.add('open'); +} + +function closeModuleModal() { + document.getElementById('moduleModal').classList.remove('open'); + editingModuleId = null; +} + +function saveModule() { + var name = document.getElementById('moduleName').value.trim(); + var desc = document.getElementById('moduleDesc').value.trim(); + var prompt = document.getElementById('modulePrompt').value.trim(); + var help = document.getElementById('moduleHelp').value.trim(); + var commands = document.getElementById('moduleCommands').value.trim(); + + if (!name) { + showToast('Please enter a module name'); + return; + } + + if (editingModuleId) { + var idx = modules.findIndex(function (m) { return m.id === editingModuleId; }); + if (idx !== -1) { + modules[idx] = { + id: modules[idx].id, + name: name, + desc: desc, + prompt: prompt, + help: help, + commands: commands + }; + } + } else { + var id = name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''); + modules.push({ + id: id, + name: name, + desc: desc, + prompt: prompt, + help: help, + commands: commands + }); + } + + saveModules(); + renderModuleList(); + closeModuleModal(); + showToast(editingModuleId ? 'Module updated!' : 'Module added!'); +} + +// === AI Module Generator === + +async function generateModuleWithAI() { + var desc = document.getElementById('aiModuleDesc').value.trim(); + if (!desc) { + showToast('Please describe what the module should do'); + return; + } + + var btn = event.target; + btn.disabled = true; + btn.textContent = 'Generating...'; + + try { + var headers = { 'Content-Type': 'application/json' }; + if (typeof OLLAMA_API_KEY !== 'undefined' && OLLAMA_API_KEY) { + headers['Authorization'] = 'Bearer ' + OLLAMA_API_KEY; + } + + var response = await fetch((typeof OLLAMA_URL !== 'undefined' ? OLLAMA_URL : '/ollama') + '/api/generate', { + method: 'POST', + headers: headers, + body: JSON.stringify({ + model: document.getElementById('modelSelect').value, + prompt: 'Generate a chat module specification for WebX11 AI Chat.\n\nModule description: ' + desc + '\n\nRespond with a JSON object containing:\n{\n "name": "Module Name",\n "desc": "Short description (5-10 words)",\n "prompt": "System prompt/instructions for the AI (2-3 sentences)",\n "help": "How to use this module (3-5 bullet points)",\n "commands": "comma-separated commands, at least 3"\n}\n\nOnly respond with valid JSON, no other text.', + stream: false + }) + }); + + if (!response.ok) throw new Error('AI request failed'); + + var data = await response.json(); + var result = JSON.parse(data.response); + + document.getElementById('moduleName').value = result.name || ''; + document.getElementById('moduleDesc').value = result.desc || ''; + document.getElementById('modulePrompt').value = result.prompt || ''; + document.getElementById('moduleHelp').value = result.help || ''; + document.getElementById('moduleCommands').value = result.commands || ''; + + showToast('Module generated! Review and save.'); + } catch (e) { + console.error('Generation error:', e); + showToast('Failed to generate: ' + e.message); + } finally { + btn.disabled = false; + btn.textContent = 'Generate with AI'; + } +} + +// === Import / Export === + +function exportModules() { + var data = JSON.stringify(modules, null, 2); + var blob = new Blob([data], { type: 'application/json' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = 'webx11-modules.json'; + a.click(); + URL.revokeObjectURL(url); + showToast('Modules exported!'); +} + +function importModules() { + var input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.onchange = async function (e) { + var file = e.target.files[0]; + if (!file) return; + try { + var text = await file.text(); + var imported = JSON.parse(text); + if (Array.isArray(imported)) { + for (var i = 0; i < imported.length; i++) { + var m = imported[i]; + m.id = m.id + '_imported'; + modules.push(m); + } + saveModules(); + renderModuleList(); + updateHelpPanel(); + showToast('Modules imported!'); + } else { + showToast('Invalid format'); + } + } catch (err) { + showToast('Import failed: ' + err.message); + } + }; + input.click(); +} + + +// Load modules from storage (runs after core.js) + +// Auto-init once DOM + deps are ready +document.addEventListener('DOMContentLoaded', function() { loadModules(); }); diff --git a/chat/chat-ui.css b/chat/chat-ui.css new file mode 100644 index 0000000..fd43ee8 --- /dev/null +++ b/chat/chat-ui.css @@ -0,0 +1,747 @@ +/* + * ChatStudio Lite - Terminal Aesthetic Styles + * Uses CSS variables from /spaces/assets/css/theme.css with terminal overrides + */ + +:root { + --bg: #000000; + --bg2: #0d0d0d; + --bg3: #141414; + --border: #1e1e1e; + --text: #e5e5e5; + --text2: #6b6b6b; + --accent: #6366f1; + --accent-dim: #818cf8; + --accent-glow: rgba(99, 102, 241, 0.15); + --danger: #ef4444; + --user-msg: #0a0a14; + --ai-msg: #0d0d0d; + --font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace; + --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; +} + +/* App body: sidebar + main in a row, fills remaining height after sticky header */ + +.app-body { + flex: 1; + display: flex; + min-height: 0; + overflow: hidden; +} + +/* Header */ + +.header { + position: sticky; + top: 0; + z-index: 50; + background: var(--bg2); + border-bottom: 1px solid var(--border); + padding: 12px 20px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.header-brand { + color: var(--accent); + font-weight: bold; + font-size: 1.1em; + text-decoration: none; + text-shadow: 0 0 12px rgba(99, 102, 241, 0.3); +} + +.header-brand:hover { + color: var(--accent-dim); +} + +.header-actions { + display: flex; + gap: 8px; +} + +/* Buttons */ + +#chat-overlay .btn { + padding: 8px 14px; + border-radius: 8px; + border: none; + cursor: pointer; + font-size: 0.9em; + font-family: inherit; + transition: all 0.2s; + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 6px; +} + +#chat-overlay .btn-primary { + background: var(--accent); + color: #000; + font-weight: bold; +} + +#chat-overlay .btn-primary:hover { + background: var(--accent-dim); + box-shadow: 0 0 12px rgba(99, 102, 241, 0.25); +} + +#chat-overlay .btn-secondary { + background: var(--bg3); + color: var(--text); + border: 1px solid var(--border); +} + +#chat-overlay .btn-secondary:hover { + border-color: var(--accent); + color: var(--accent); + box-shadow: 0 0 8px rgba(99, 102, 241, 0.15); +} + +#chat-overlay #chat-overlay .btn-danger { + background: var(--danger); + color: #fff; +} + +#chat-overlay .btn-sm { + padding: 5px 10px; + font-size: 0.8em; +} + +/* Hamburger */ + +.hamburger { + display: block; + background: none; + border: none; + color: var(--text); + font-size: 1.4em; + cursor: pointer; + padding: 4px 8px; +} + +@media (max-width: 700px) { + .hamburger { + display: block; + } +} + +#chat-overlay .sidebar.collapsed { + display: none; +} + +/* Sidebar */ + +.sidebar { + width: 340px; + background: var(--bg2); + border-right: 1px solid var(--border); + padding: 20px; + display: flex; + flex-direction: column; + gap: 16px; + overflow-y: auto; + height: 100%; +} + +@media (max-width: 700px) { + .sidebar { + display: none; + position: fixed; + top: 56px; + left: 0; + bottom: 0; + z-index: 100; + width: 100%; + } + .sidebar.open { + display: flex; + } +} + +.sidebar h3 { + color: var(--accent); + font-size: 0.85em; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 8px; +} + +.module-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.module-item { + padding: 10px 12px; + background: var(--bg3); + border-radius: 8px; + cursor: pointer; + transition: all 0.2s; + border: 1px solid transparent; + position: relative; +} + +.module-item:hover { + border-color: var(--accent); + box-shadow: 0 0 8px rgba(99, 102, 241, 0.1); +} + +.module-item.active { + border-color: var(--accent); + background: rgba(99, 102, 241, 0.1); + box-shadow: 0 0 12px rgba(99, 102, 241, 0.15); +} + +.module-name { + font-weight: 600; + font-size: 0.95em; + font-family: var(--font-mono); +} + +.module-desc { + font-size: 0.75em; + color: var(--text2); + margin-top: 4px; +} + +.module-actions { + position: absolute; + top: 6px; + right: 6px; + display: none; + gap: 4px; +} + +.module-item:hover .module-actions { + display: flex; +} + +.module-action-btn { + background: var(--bg2); + border: 1px solid var(--border); + color: var(--text2); + padding: 3px 6px; + border-radius: 4px; + cursor: pointer; + font-size: 0.75em; + font-family: var(--font-mono); +} + +.module-action-btn:hover { + color: var(--accent); + border-color: var(--accent); +} + +.module-action-btn.delete:hover { + color: var(--danger); + border-color: var(--danger); +} + +.add-module-btn { + border: 1px dashed var(--border); + background: transparent; + color: var(--text2); + justify-content: center; + width: 100%; +} + +.add-module-btn:hover { + border-color: var(--accent); + color: var(--accent); +} + +/* Main area */ + +.main { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; + overflow: hidden; +} + +.chat-container { + flex: 1; + overflow-y: auto; + padding: 20px; + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; + max-width: 100%; + scroll-behavior: smooth; +} + +/* Messages */ + +.message { + max-width: 100%; + width: 100%; + padding: 14px 18px; + border-radius: 16px; + line-height: 1.5; + white-space: pre-wrap; + word-break: break-word; + font-family: var(--font-mono); + font-size: 0.95em; + animation: msgFadeIn 0.3s ease-out; +} + +@keyframes msgFadeIn { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.message.user { + align-self: flex-end; + background: var(--user-msg); + border-bottom-right-radius: 4px; + border: 1px solid rgba(99, 102, 241, 0.12); +} + +.message.ai { + align-self: flex-start; + background: var(--ai-msg); + border: 1px solid var(--border); + border-bottom-left-radius: 4px; +} + +.message .role { + font-size: 0.7em; + color: var(--accent); + margin-bottom: 6px; + font-family: var(--font-sans); + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; +} + +.message.user .role { + color: var(--accent); +} + +.message.ai .role { + color: var(--accent-dim); +} + +.msg-copy { + background: none; + border: none; + color: var(--text2); + cursor: pointer; + font-size: 0.9em; + padding: 2px 6px; + border-radius: 4px; + opacity: 0; + transition: opacity 0.2s; +} + +.message:hover .msg-copy { + opacity: 1; +} + +.msg-copy:hover { + color: var(--accent); + background: rgba(99, 102, 241, 0.1); +} + +.message a { + color: var(--accent); + text-decoration: underline; +} + +.message a:hover { + text-shadow: 0 0 8px rgba(99, 102, 241, 0.35); +} + +.message code { + background: #222; + padding: 2px 6px; + border-radius: 4px; + font-family: var(--font-mono); + font-size: 0.9em; +} + +.message pre { + background: #1a1a22; + padding: 12px; + border-radius: 8px; + overflow-x: auto; + margin: 10px 0; + border: 1px solid var(--border); +} + +.message pre code { + background: none; + padding: 0; +} + +/* Input area */ + +.input-area { + position: sticky; + bottom: 0; + z-index: 10; + background: var(--bg2); + border-top: 1px solid var(--border); + padding: 16px 20px; + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.input-area textarea { + flex: 1; + min-width: 200px; + padding: 12px 16px; + background: var(--bg3); + border: 1px solid var(--border); + border-radius: 16px; + color: var(--text); + font-size: 1em; + font-family: var(--font-mono); + outline: none; + resize: none; +} + +.input-area textarea:focus { + border-color: var(--accent); + box-shadow: 0 0 10px rgba(99, 102, 241, 0.15); +} + +.input-area textarea::placeholder { + color: var(--text2); +} + +.input-area button { + padding: 12px 24px; +} + +/* Typing indicator */ + +.typing { + color: var(--text2); + font-style: italic; + padding: 10px; + display: flex; + align-items: center; + gap: 8px; + font-family: var(--font-mono); +} + +.typing-dots { + display: inline-flex; + gap: 4px; +} + +.typing-dots span { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--accent); + display: inline-block; + animation: typingDot 1.4s infinite ease-in-out both; +} + +.typing-dots span:nth-child(1) { + animation-delay: 0s; +} + +.typing-dots span:nth-child(2) { + animation-delay: 0.16s; +} + +.typing-dots span:nth-child(3) { + animation-delay: 0.32s; +} + +@keyframes typingDot { + 0%, 80%, 100% { + opacity: 0.2; + transform: scale(0.8); + } + 40% { + opacity: 1; + transform: scale(1); + } +} + +/* Streaming cursor */ + +.streaming-cursor { + display: inline-block; + width: 2px; + height: 1.1em; + background: var(--accent); + margin-left: 2px; + vertical-align: text-bottom; + animation: blink 0.8s step-end infinite; +} + +@keyframes blink { + 50% { + opacity: 0; + } +} + +/* Model select */ + +.model-select { + padding: 6px 8px; + background: var(--bg3); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text); + font-size: 0.75em; + font-family: var(--font-mono); + cursor: pointer; + max-width: 140px; +} + +.model-select:focus { + border-color: var(--accent); + outline: none; +} + +/* Modal */ + +.modal { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.9); + z-index: 200; + display: none; + align-items: center; + justify-content: center; + padding: 20px; +} + +.modal.open { + display: flex; +} + +#chat-overlay .modal-content { + background: var(--bg2); + border: 1px solid var(--border); + border-radius: 16px; + width: 100%; + max-width: 500px; + max-height: 90vh; + overflow-y: auto; +} + +#chat-overlay .modal-header { + padding: 16px 20px; + border-bottom: 1px solid var(--border); + display: flex; + justify-content: space-between; + align-items: center; +} + +#chat-overlay .modal-header h2 { + color: var(--accent); + font-size: 1.1em; +} + +.modal-close { + background: none; + border: none; + color: var(--text2); + font-size: 1.5em; + cursor: pointer; +} + +.modal-close:hover { + color: var(--danger); +} + +#chat-overlay .modal-body { + padding: 20px; +} + +.form-group { + margin-bottom: 16px; +} + +.form-group label { + display: block; + margin-bottom: 6px; + font-size: 0.9em; + color: var(--text2); +} + +.form-group input, +.form-group textarea { + width: 100%; + padding: 10px 14px; + background: var(--bg3); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text); + font-size: 0.95em; + font-family: var(--font-mono); +} + +.form-group textarea { + min-height: 120px; + resize: vertical; +} + +.form-group input:focus, +.form-group textarea:focus { + border-color: var(--accent); + outline: none; + box-shadow: 0 0 8px rgba(99, 102, 241, 0.1); +} + +#chat-overlay .modal-footer { + padding: 16px 20px; + border-top: 1px solid var(--border); + display: flex; + gap: 10px; + justify-content: flex-end; +} + +/* Help panel */ + +.help-panel { + background: var(--bg3); + border-radius: 12px; + padding: 16px; + margin-bottom: 16px; +} + +.help-panel h4 { + color: var(--accent); + margin-bottom: 10px; + font-size: 0.95em; +} + +.help-panel p { + font-size: 0.85em; + color: var(--text2); + line-height: 1.5; + font-family: var(--font-mono); +} + +.help-commands { + margin-top: 10px; + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.help-cmd { + background: var(--bg); + padding: 4px 10px; + border-radius: 6px; + font-size: 0.8em; + color: var(--accent); + font-family: var(--font-mono); +} + +/* Toast */ + +.toast { + position: fixed; + bottom: 20px; + right: 20px; + background: var(--bg2); + border: 1px solid var(--accent); + padding: 12px 20px; + border-radius: 8px; + z-index: 300; + animation: slideIn 0.3s; + font-family: var(--font-mono); +} + +@keyframes slideIn { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +/* Settings */ + +.settings-row { + display: flex; + gap: 10px; + align-items: center; + margin-bottom: 10px; +} + +.settings-row label { + min-width: 80px; + color: var(--text2); + font-size: 0.9em; +} + +.settings-row input { + flex: 1; +} + +/* Logs panel */ + +.logs-panel { + background: var(--bg3); + border-radius: 12px; + padding: 16px; + max-height: 300px; + overflow-y: auto; +} + +.log-entry { + font-size: 0.8em; + padding: 8px 0; + border-bottom: 1px solid var(--border); + display: flex; + gap: 10px; + font-family: var(--font-mono); +} + +.log-entry:last-child { + border-bottom: none; +} + +.log-time { + color: var(--text2); + min-width: 70px; +} + +.log-action { + min-width: 60px; +} + +.log-detail { + flex: 1; + color: var(--text); + word-break: break-all; +} + + +/* Touch-friendly close buttons */ +#chat-overlay .modal-close { + min-width: 44px; + min-height: 44px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.3em; +} diff --git a/chat/chat-ui.html b/chat/chat-ui.html new file mode 100644 index 0000000..5eedce6 --- /dev/null +++ b/chat/chat-ui.html @@ -0,0 +1,156 @@ +<div id="chat-overlay" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;z-index:10000;flex-direction:column;background:var(--bg);"> + <header class="header"> + <div style="display:flex;align-items:center;gap:10px;"> + <button class="hamburger" onclick="toggleSidebar()">☰</button> + <span class="header-brand">🤖 WebX11 AI</span> + </div> + <div class="header-actions"> + <button class="btn btn-secondary btn-sm" onclick="openSettingsModal()" title="Settings">⚙️</button> + <select class="model-select" id="modelSelect" title="Select AI Model"> + <option value="">Loading...</option> + </select> + <button class="btn btn-secondary btn-sm" onclick="shareChat()" title="Share chat">📤</button> + <button class="btn btn-secondary btn-sm" onclick="clearChat()" title="Clear chat">🗑️</button> + <button class="btn btn-secondary btn-sm" onclick="closeChatOverlay()" title="Close (Esc)">✕</button> + </div> + </header> + + <div class="app-body"> + <aside class="sidebar" id="sidebar"> + <button class="modal-close" onclick="toggleSidebar()" style="align-self:flex-end;margin-bottom:8px;">×</button> + <div> + <h3>Modules</h3> + <div class="module-list" id="moduleList"></div> + <button class="btn add-module-btn" onclick="openModuleModal()">+ Add Module</button> + <div style="display:flex;gap:4px;margin-top:8px;"> + <button class="btn btn-secondary btn-sm" style="flex:1;" onclick="saveModules();showToast('Modules saved!')">💾 Save</button> + </div> + <div style="display:flex;gap:4px;margin-top:4px;"> + <button class="btn btn-secondary btn-sm" style="flex:1;" onclick="exportModules()">📤 Export</button> + <button class="btn btn-secondary btn-sm" style="flex:1;" onclick="importModules()">📥 Import</button> + </div> + </div> + <div> + <h3>Current Module Help</h3> + <div class="help-panel" id="helpPanel"> + <h4 id="helpTitle">AI Chat</h4> + <p id="helpText">Default AI assistant. Ask anything!</p> + <div class="help-commands" id="helpCommands"></div> + </div> + </div> + <div> + <h3>Activity Log <button class="msg-copy" onclick="copyLogs()" title="Copy logs" style="opacity:1;font-size:0.8em;padding:2px 6px;">📋</button><button class="msg-copy" onclick="clearLogs()" title="Clear logs" style="opacity:1;font-size:0.8em;padding:2px 6px;">🗑️</button></h3> + <div class="logs-panel" id="logsPanel"> + <div id="logsContent"></div> + </div> + </div> + </aside> + + <main class="main"> + <div class="chat-container" id="chatContainer"> + <div class="message ai"> + <div class="role">AI</div> + Welcome to <strong>ChatStudio Lite</strong> 🚀<br><br> + <strong>Commands:</strong><br> + <code>/help</code> - Show help<br> + <code>/search [query]</code> - Web search<br> + <code>/fetch [url]</code> - Fetch a page<br> + <code>/explain [code]</code> - Explain code<br> + <code>/clear</code> - Clear chat<br> + <code>/status</code> - System status<br> + </div> + </div> + + <div class="input-area"> + <textarea id="chatInput" placeholder="Type your message..." autocomplete="off" rows="1" onfocus="this.style.height='80px'" oninput="autoExpand(this);if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMessage();}"></textarea> + <button class="btn btn-primary" onclick="sendMessage()">Send</button> + </div> + </main> + </div> + + <div class="modal" id="moduleModal"> + <div class="modal-content"> + <div class="modal-header"> + <h2 id="moduleModalTitle">Add Module</h2> + <button class="modal-close" onclick="closeModuleModal()">×</button> + </div> + <div class="modal-body"> + <div class="form-group"> + <label>Module Name</label> + <input type="text" id="moduleName" placeholder="e.g., Search, Code Helper"> + </div> + <div class="form-group"> + <label>Description</label> + <input type="text" id="moduleDesc" placeholder="What does this module do?"> + </div> + <div class="form-group"> + <label>System Prompt (Instructions)</label> + <textarea id="modulePrompt" placeholder="You are a helpful assistant specialized in..."></textarea> + </div> + <div class="form-group"> + <label>Help Text (how to use)</label> + <textarea id="moduleHelp" placeholder="Explain how to use this module..."></textarea> + </div> + <div class="form-group"> + <label>Commands (comma-separated)</label> + <input type="text" id="moduleCommands" placeholder="e.g., /search, /find, /web"> + </div> + </div> + <div class="modal-footer"> + <button class="btn btn-secondary" onclick="closeModuleModal()">Cancel</button> + <button class="btn btn-primary" onclick="saveModule()">Save Module</button> + </div> + </div> + </div> + + <div class="modal" id="settingsModal"> + <div class="modal-content"> + <div class="modal-header"> + <h2>⚙️ Settings</h2> + <button class="modal-close" onclick="closeSettingsModal()">×</button> + </div> + <div class="modal-body"> + <div class="form-group"> + <label>Ollama API URL</label> + <input type="text" id="ollamaUrl" placeholder="/ollama"> + </div> + <div class="form-group"> + <label>API Key (optional)</label> + <input type="password" id="ollamaApiKey" placeholder="Enter API key if required"> + </div> + <div class="form-group"> + <label>Default Model</label> + <input type="text" id="defaultModel" placeholder="llama2"> + </div> + <div class="form-group" style="margin-top:16px;padding-top:16px;border-top:1px solid var(--border);"> + <label style="display:flex;align-items:center;gap:10px;cursor:pointer;"> + <input type="checkbox" id="streamingToggle" checked> + <span>Streaming responses</span> + </label> + </div> + <div style="margin-top:16px;padding-top:16px;border-top:1px solid var(--border);"> + <button class="btn btn-secondary" style="width:100%;" onclick="testConnection()">🔗 Test Connection</button> + <p id="connectionStatus" style="font-size:0.85em;margin-top:10px;text-align:center;"></p> + </div> + </div> + <div class="modal-footer"> + <button class="btn btn-secondary" onclick="closeSettingsModal()">Cancel</button> + <button class="btn btn-primary" onclick="saveSettings()">Save Settings</button> + </div> + </div> + </div> + + <div class="toast" id="toast" style="display:none;"></div> + + <div class="modal" id="previewModal"> + <div class="modal-content" style="max-width:95vw;max-height:95vh;width:100%;height:100%;"> + <div class="modal-header"> + <h2>HTML Preview</h2> + <button class="modal-close" onclick="closePreviewModal()">×</button> + </div> + <div class="modal-body" style="height:calc(100% - 60px);padding:0;"> + <iframe id="previewFrame" style="width:100%;height:100%;border:none;"></iframe> + </div> + </div> + </div> +</div>
\ No newline at end of file @@ -407,6 +407,18 @@ .search-result a:hover { text-decoration: underline; } + .search-result-image, .search-result-video { + margin-bottom: 10px; + padding: 8px; + background: var(--bg-tertiary); + border-radius: 4px; + } + .search-result-image img, .search-result-video img { + max-width: 200px; + max-height: 150px; + border-radius: 4px; + object-fit: cover; + } /* ── Splash Screen ── */ #splash-overlay { @@ -824,7 +836,7 @@ Take the <strong>2-minute guided tour</strong> to learn the basics. </p> <div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap;"> - <button class="btn" onclick="if(webx11&&webx11.tutorialModule)webx11.tutorialModule.startTutorial();else document.getElementById('welcome-modal').style.display='none'" + <button class="btn" onclick="var m=document.getElementById('welcome-modal');if(m)m.style.display='none';if(webx11&&webx11.tutorialModule)webx11.tutorialModule.startTutorial()" style="border-color:var(--success-color);color:var(--success-color);padding:12px 28px;font-size:1em;"> 🎮 Start Tutorial </button> 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]; |