From d8bf7fa398d6d928325be9b5c64ca24875bfeef8 Mon Sep 17 00:00:00 2001
From: Agando
Date: Sat, 30 May 2026 18:01:51 +0200
Subject: auto: 2026-05-30_18:00 changes
---
chat/chat-commands.js | 276 ++++++++++++++
chat/chat-modules.js | 365 +++++++++++++++++++
chat/chat-ui.css | 747 ++++++++++++++++++++++++++++++++++++++
chat/chat-ui.html | 156 ++++++++
main.html | 14 +-
modules/core/module-chatstudio.js | 44 ++-
modules/core/module-searxng.js | 65 +++-
modules/ext/tutorial.js | 14 +-
8 files changed, 1652 insertions(+), 29 deletions(-)
create mode 100644 chat/chat-commands.js
create mode 100644 chat/chat-modules.js
create mode 100644 chat/chat-ui.css
create mode 100644 chat/chat-ui.html
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' +
+ 'Full ChatStudio (Pro)\n' +
+ 'User Dashboard\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'
+ ? '' +
+ '' +
+ '' +
+ '
'
+ : '';
+ return '' +
+ '
' + escapeHtml(m.name) + '
' +
+ '
' + escapeHtml(m.desc) + '
' +
+ actions +
+ '
';
+ }).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 '' + escapeHtml(c) + '';
+ }).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 @@
+
+
+
+
+
+
+
+
+
+
AI
+ Welcome to
ChatStudio Lite 🚀
+
Commands:
+
/help - Show help
+
/search [query] - Web search
+
/fetch [url] - Fetch a page
+
/explain [code] - Explain code
+
/clear - Clear chat
+
/status - System status
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/main.html b/main.html
index 4795d97..2468c19 100644
--- a/main.html
+++ b/main.html
@@ -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 2-minute guided tour to learn the basics.
-
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
[--summarize] [--fetch] — Search using SearXNG; --summarize/-s adds AI overview, --fetch/-f fetches page content',
- 's': 's [--summarize] [--fetch] — Alias for search',
+ 'search': 'search [--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 [--summarize] [--fetch] [--images] [--videos] — Alias for search',
+ 'images': 'images — Search for images',
+ 'videos': 'videos — 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 [--summarize] [--fetch]", "warning");
+ async search(args, forceCategory) {
+ if (!args[0] && !forceCategory) {
+ this.terminal.print("Usage: search [--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 or videos ", "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(`
-
-
${i + 1}. ${r.title || 'No Title'}
-
${r.url || 'No URL'}
-
${(snippet || '').substring(0, 200)}...
- ${contentSnippet ? `
📄 ${contentSnippet.substring(0, 150)}...` : ''}
-
- `);
+ const imgSrc = r.img_src || r.thumbnail_src || '';
+ const videoThumb = r.thumbnail || '';
+
+ if (category === 'images' && imgSrc) {
+ this.terminal.printHTML(`
+
+ `);
+ } else if (category === 'videos' && videoThumb) {
+ this.terminal.printHTML(`
+
+ `);
+ } else {
+ this.terminal.printHTML(`
+
+
${i + 1}. ${r.title || 'No Title'}
+
${r.url || 'No URL'}
+
${(snippet || '').substring(0, 200)}...
+ ${contentSnippet ? `
📄 ${contentSnippet.substring(0, 150)}...` : ''}
+ ${imgSrc && category === 'general' ? `
🖼️ View image` : ''}
+
+ `);
+ }
});
} 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];
--
cgit v1.2.3