/* * 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 newOS4. Current date is injected dynamically. You have access to these tools:\n\n1. **Search:** /api/search?q=QUERY - Searches the web via DuckDuckGo/SearXNG. Returns results with titles, URLs, and snippets. Use this for current events, facts, and web information.\n2. **Fetch:** /api/fetch?url=URL - Fetches raw HTML from any URL. Use this to read web pages, APIs, or documents.\n3. **Extract:** /api/extract?url=URL - Extracts readable content (title, word count, excerpt, full text) from a URL. Prefer this over /fetch for reading articles.\n4. **Summarize:** /api/summarize?url=URL - AI summarization of a URL.\n\n**Tool-Use Rules:**\n- NEVER announce that you are about to use a tool (no "Hold on...", "Let me search...", "Fetching...", or waiting indicators). Just use the tool and present the result immediately.\n- If a /search returns generic/portal results (e.g., google.com homepages), automatically retry with a more specific query using site: operators or exact phrases in quotes.\n- If a user types a malformed command (e.g., /extraxt instead of /extract, /serch instead of /search), recognize their intent and suggest the correct command immediately: "Did you mean /extract [url]? Here are the available commands: /search, /fetch, /extract, /summarize"\n- When a user shares a URL, proactively suggest /extract or /fetch with the URL pre-filled.\n- Be concise. Deliver results, not commentary about the process.\n\n**Available slash commands:** /help, /search [query], /fetch [url], /extract [url], /summarize [url], /explain [code], /status, /models, /clear', help: 'Just type any question! I can help with:\n- Current events (I can search the web)\n- Web pages (I can extract/fetch URLs)\n- Coding questions\n- General knowledge\n- Problem solving\n\nCommands: /search, /fetch, /extract, /summarize, /explain, /status, /clear\n\nTry: /search latest news', commands: '/help, /search, /fetch, /extract, /summarize, /explain, /status, /models, /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' } ]; // modules array lives on window.modules (set by _initLiteEngine in module-chatstudio.js) // Do NOT redeclare here — just reference the global 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(evt) { var e = evt || window.event; var desc = document.getElementById('aiModuleDesc').value.trim(); if (!desc) { showToast('Please describe what the module should do'); return; } var btn = e ? e.target : null; if (!btn) { var modal = document.getElementById('moduleModal'); btn = modal ? modal.querySelector('button[onclick*="generateModule"]') : null; } 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 _csFetch((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; try { result = JSON.parse(data.response); } catch (e) { showToast('AI response was not valid JSON. Try a different model.'); btn.disabled = false; btn.textContent = 'Generate with AI'; return; } 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; if (file.size > 1048576) { showToast('File too large (max 1MB)'); return; } try { var text = await file.text(); var imported = JSON.parse(text); if (!Array.isArray(imported)) { showToast('Invalid format: expected array'); return; } var valid = []; for (var i = 0; i < imported.length; i++) { var m = imported[i]; if (!m || typeof m !== 'object') continue; if (typeof m.name !== 'string' || m.name.length > 80) continue; if (typeof m.desc !== 'string' || m.desc.length > 200) continue; if (typeof m.prompt !== 'string' || m.prompt.length > 10000) continue; if (m.help && typeof m.help !== 'string') continue; if (m.commands && typeof m.commands !== 'string') continue; // Sanitize ID — only allow a-z0-9- var rawId = typeof m.id === 'string' ? m.id : m.name.toLowerCase().replace(/\s+/g, '-'); var safeId = rawId.toLowerCase().replace(/[^a-z0-9-]/g, '').substring(0, 64); if (!safeId) safeId = 'imported_' + Date.now(); valid.push({ id: safeId + '_imported', name: m.name, desc: m.desc || '', prompt: m.prompt || '', help: m.help || '', commands: m.commands || '' }); } if (valid.length === 0) { showToast('No valid modules found in file'); return; } for (var i = 0; i < valid.length; i++) { modules.push(valid[i]); } saveModules(); renderModuleList(); updateHelpPanel(); showToast(valid.length + ' module' + (valid.length > 1 ? 's' : '') + ' imported!'); } catch (err) { showToast('Import failed: ' + err.message); } }; input.click(); } // Load modules from storage (runs after core.js) // Auto-init — DOM is already ready when this script loads dynamically if (document.getElementById('chat-overlay')) { loadModules(); } // ═══ TTS + SST Integration ═══ var _chatTTS = false; var _chatVoices = []; var _chatMicRecording = false; var _chatMediaRecorder = null; var _chatAudioChunks = []; var _chatRecordingTimeout = null; var _chatSTTEndpoint = '/vmai-api/stt'; // Pre-load voices if (typeof speechSynthesis !== 'undefined') { _chatVoices = speechSynthesis.getVoices() || []; if (_chatVoices.length === 0) { speechSynthesis.onvoiceschanged = function() { _chatVoices = speechSynthesis.getVoices() || []; }; } } function toggleChatTTS() { _chatTTS = !_chatTTS; var btn = document.getElementById('chat-tts-btn'); if (btn) { btn.style.borderColor = _chatTTS ? 'var(--success-color)' : ''; btn.style.color = _chatTTS ? 'var(--success-color)' : ''; } if (typeof addLog === 'function') addLog('TTS ' + (_chatTTS ? 'ON' : 'OFF')); if (typeof showToast === 'function') showToast('🔊 TTS ' + (_chatTTS ? 'ON' : 'OFF')); } function chatSpeakText(text) { if (!_chatTTS || typeof speechSynthesis === 'undefined') return; speechSynthesis.cancel(); var clean = text.replace(/```[\s\S]*?```/g, ' code block ').replace(/[*_#|`>]/g, '').replace(/\n+/g, '. ').trim(); if (!clean) return; function doSpeak(txt) { var utter = new SpeechSynthesisUtterance(txt); utter.rate = 1.0; utter.pitch = 1.0; var voice = _chatVoices.find(function(v) { return v.lang && v.lang.startsWith('en'); }) || (_chatVoices.length > 0 ? _chatVoices[0] : null); if (voice) utter.voice = voice; if (_chatVoices.length === 0) { var spoken = false; speechSynthesis.onvoiceschanged = function() { if (spoken) return; spoken = true; _chatVoices = speechSynthesis.getVoices() || []; var v = _chatVoices.find(function(v) { return v.lang && v.lang.startsWith('en'); }) || _chatVoices[0]; if (v) utter.voice = v; speechSynthesis.cancel(); speechSynthesis.speak(utter); speechSynthesis.onvoiceschanged = null; }; setTimeout(function() { if (!spoken) { spoken = true; speechSynthesis.onvoiceschanged = null; speechSynthesis.cancel(); speechSynthesis.speak(utter); } }, 2000); } else { speechSynthesis.speak(utter); } } if (clean.length > 500) { var chunks = clean.match(/.{1,500}/g); for (var i = 0; i < chunks.length; i++) doSpeak(chunks[i]); } else { doSpeak(clean); } } function toggleChatMic() { if (_chatMicRecording) { stopChatRecording(); } else { startChatRecording(); } } function startChatRecording() { if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { if (typeof showToast === 'function') showToast('🎤 Audio recording not supported'); return; } _chatMicRecording = true; _chatAudioChunks = []; var btn = document.getElementById('chat-mic-btn'); if (btn) { btn.textContent = '🔴'; btn.style.borderColor = 'var(--success-color)'; btn.style.color = 'var(--success-color)'; } if (typeof addLog === 'function') addLog('🎤 Recording... (tap mic again to stop)'); navigator.mediaDevices.getUserMedia({ audio: true }) .then(function(stream) { var mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' : ''; _chatMediaRecorder = new MediaRecorder(stream, mimeType ? { mimeType: mimeType } : {}); _chatMediaRecorder.ondataavailable = function(e) { if (e.data.size > 0) _chatAudioChunks.push(e.data); }; _chatMediaRecorder.onstop = function() { stream.getTracks().forEach(function(t) { t.stop(); }); transcribeChatAudio(); }; _chatMediaRecorder.start(); _chatRecordingTimeout = setTimeout(function() { if (_chatMediaRecorder && _chatMediaRecorder.state === 'recording') _chatMediaRecorder.stop(); }, 10000); }) .catch(function(err) { if (typeof showToast === 'function') showToast('🎤 Mic denied: ' + err.message); _chatMicRecording = false; var btn = document.getElementById('chat-mic-btn'); if (btn) { btn.textContent = '🎤'; btn.style.borderColor = ''; btn.style.color = ''; } }); } function stopChatRecording() { if (_chatMediaRecorder && _chatMediaRecorder.state === 'recording') { _chatMediaRecorder.stop(); clearTimeout(_chatRecordingTimeout); } _chatMicRecording = false; var btn = document.getElementById('chat-mic-btn'); if (btn) { btn.textContent = '🎤'; btn.style.borderColor = ''; btn.style.color = ''; } } function transcribeChatAudio() { var blob = new Blob(_chatAudioChunks, { type: _chatMediaRecorder ? _chatMediaRecorder.mimeType || 'audio/webm' : 'audio/webm' }); if (blob.size < 100) { if (typeof addLog === 'function') addLog('🎤 No audio detected'); return; } if (typeof addLog === 'function') addLog('🎤 Transcribing...'); var reader = new FileReader(); reader.onload = async function() { try { var ctx = new (window.AudioContext || window.webkitAudioContext)(); var audioBuffer = await ctx.decodeAudioData(reader.result); var pcmData = chatAudioBufferToWav(audioBuffer); var b64 = btoa(String.fromCharCode.apply(null, new Uint8Array(pcmData))); var resp = await fetch(_chatSTTEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ audio: b64, sample_rate: 16000 }) }); var data = await resp.json(); var text = (data.text || '').trim(); if (text) { if (typeof addLog === 'function') addLog('🎤 Recognized: "' + text + '"'); var input = document.getElementById('chatInput'); if (input) { input.value = text; input.focus(); } } else { if (typeof addLog === 'function') addLog('🎤 No speech detected'); } } catch (err) { if (typeof addLog === 'function') addLog('🎤 Transcription error: ' + err.message); } }; reader.readAsArrayBuffer(blob); } function chatAudioBufferToWav(audioBuffer) { var targetSampleRate = 16000; var src = audioBuffer.getChannelData(0); var srcRate = audioBuffer.sampleRate; var ratio = srcRate / targetSampleRate; var newLen = Math.round(src.length / ratio); var pcm = new Int16Array(newLen); for (var i = 0; i < newLen; i++) { var srcIdx = Math.round(i * ratio); var sample = Math.max(-1, Math.min(1, src[Math.min(srcIdx, src.length - 1)])); pcm[i] = sample < 0 ? sample * 0x8000 : sample * 0x7FFF; } var buffer = new ArrayBuffer(44 + pcm.byteLength); var view = new DataView(buffer); var writeStr = function(off, str) { for (var j = 0; j < str.length; j++) view.setUint8(off + j, str.charCodeAt(j)); }; writeStr(0, 'RIFF'); view.setUint32(4, 36 + pcm.byteLength, true); writeStr(8, 'WAVE'); writeStr(12, 'fmt '); view.setUint32(16, 16, true); view.setUint16(20, 1, true); view.setUint16(22, 1, true); view.setUint32(24, targetSampleRate, true); view.setUint32(28, targetSampleRate * 2, true); view.setUint16(32, 2, true); view.setUint16(34, 16, true); writeStr(36, 'data'); view.setUint32(40, pcm.byteLength, true); new Uint8Array(buffer, 44).set(new Uint8Array(pcm.buffer)); return buffer; }