diff options
Diffstat (limited to 'modules/core/terminal.js')
| -rw-r--r-- | modules/core/terminal.js | 117 |
1 files changed, 71 insertions, 46 deletions
diff --git a/modules/core/terminal.js b/modules/core/terminal.js index 607c51a..f0ef036 100644 --- a/modules/core/terminal.js +++ b/modules/core/terminal.js @@ -7,7 +7,7 @@ this.lastRouteTime = 0; this.lastRouteCmd = ''; this.blockedCount = 0; - this.destructiveCmds = new Set(['rm','delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot']); + this.destructiveCmds = new Set(['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot']); this.systemCmds = new Set(['reboot','shutdown','format','kill','halt','poweroff']); this.suspiciousFlags = new Set(['--dump','--execute','--eval','--exec','--shell','--script']); this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history']); @@ -374,8 +374,16 @@ } print(text, type = '') { const line = document.createElement('div'); - line.className = `output-line ${type}`; - line.textContent = text; + // Use card layout for typed messages (error/warning/info/success/text-muted), plain for bare text + const isTyped = ['error','warning','success'].includes(type); + if (isTyped) { + line.className = 'output-line msg-card system ' + type; + const safeText = String(text).replace(/</g, "<").replace(/>/g, ">"); + line.innerHTML = '<div class="msg-card-header"><span class="msg-role">⚙ System</span><span class="msg-buttons"><button class="card-btn copy-btn" onclick="webx11.copyToClipboard(this)">Copy</button></span></div><div class="msg-card-body">' + safeText + '</div>'; + } else { + line.className = 'output-line'; + line.textContent = text; + } this.outputContainer.appendChild(line); this._historyBuffer.push({ l: text, t: type }); if (this._historyBuffer.length > this._historyMaxLines) { @@ -411,7 +419,7 @@ const before = text.slice(lastIndex, match.index); if (before.trim()) { const safeBefore = before.replace(/</g, "<").replace(/>/g, ">"); - fragments.push({ type: 'text', html: `<button class="copy-btn" onclick="webx11.copyToClipboard(this)">Copy</button><div class="ai-content">${safeBefore}</div>` }); + fragments.push({ type: 'text', html: '<div class="ai-content">' + safeBefore + '</div>' }); } fragments.push({ type: 'render', html: this._renderHTMLPreview(match[1].trim()) }); lastIndex = match.index + match[0].length; @@ -419,7 +427,7 @@ const after = text.slice(lastIndex); if (after.trim()) { const safeAfter = after.replace(/</g, "<").replace(/>/g, ">"); - fragments.push({ type: 'text', html: `<button class="copy-btn" onclick="webx11.copyToClipboard(this)">Copy</button><div class="ai-content">${safeAfter}</div>` }); + fragments.push({ type: 'text', html: '<div class="ai-content">' + safeAfter + '</div>' }); } return fragments.length > 0 ? fragments : null; } @@ -427,23 +435,24 @@ this._flushStreamChunks(); const fragments = this._renderHTMLBlock(text); if (fragments) { + const wrapper = document.createElement('div'); + wrapper.className = 'output-line msg-card ai'; + wrapper.innerHTML = '<div class="msg-card-header"><span class="msg-role">🤖 AI</span><span class="msg-buttons"><button class="card-btn copy-btn" onclick="webx11.copyToClipboard(this)">Copy</button></span></div><div class="msg-card-body"></div>'; + const body = wrapper.querySelector('.msg-card-body'); for (const frag of fragments) { - const wrapper = document.createElement('div'); - wrapper.className = 'output-line' + (frag.type === 'text' ? ' ollama' : ''); - wrapper.innerHTML = frag.html; - this.outputContainer.appendChild(wrapper); + const div = document.createElement('div'); + div.innerHTML = frag.html; + body.appendChild(div); } + this.outputContainer.appendChild(wrapper); this.scrollToBottom(); this._trimOutput(); return; } const wrapper = document.createElement('div'); - wrapper.className = 'output-line ollama'; + wrapper.className = 'output-line msg-card ai'; const safeText = text.replace(/</g, "<").replace(/>/g, ">"); - wrapper.innerHTML = ` - <button class="copy-btn" onclick="webx11.copyToClipboard(this)">Copy</button> - <div class="ai-content">${safeText}</div> - `; + wrapper.innerHTML = '<div class="msg-card-header"><span class="msg-role">🤖 AI</span><span class="msg-buttons"><button class="card-btn copy-btn" onclick="webx11.copyToClipboard(this)">Copy</button></span></div><div class="msg-card-body">' + safeText + '</div>'; this.outputContainer.appendChild(wrapper); this.scrollToBottom(); this._trimOutput(); @@ -453,15 +462,12 @@ if (!append) { const safeText = text.replace(/</g, "<").replace(/>/g, ">"); const wrapper = document.createElement('div'); - wrapper.className = 'output-line ollama'; - wrapper.innerHTML = ` - <button class="copy-btn" onclick="webx11.copyToClipboard(this)">Copy</button> - <div class="ai-content">${safeText}</div> - `; + wrapper.className = 'output-line msg-card ai'; + wrapper.innerHTML = '<div class="msg-card-header"><span class="msg-role">🤖 AI</span><span class="msg-buttons"><button class="card-btn copy-btn" onclick="webx11.copyToClipboard(this)">Copy</button></span></div><div class="msg-card-body">' + safeText + '</div>'; this.outputContainer.appendChild(wrapper); this._streamChunks = []; } else { - const contentDiv = this.outputContainer.querySelector('.output-line.ollama:last-child .ai-content'); + const contentDiv = this.outputContainer.querySelector('.msg-card.ai:last-child .msg-card-body'); if (contentDiv) { this._streamChunks.push(text); if (this._streamChunks.length >= 5) { @@ -477,7 +483,7 @@ this._trimOutput(); } _flushStreamChunks() { - const contentDiv = this.outputContainer.querySelector('.output-line.ollama:last-child .ai-content'); + const contentDiv = this.outputContainer.querySelector('.msg-card.ai:last-child .msg-card-body'); if (contentDiv && this._streamChunks && this._streamChunks.length > 0) { contentDiv.textContent += this._streamChunks.join(''); this._streamChunks = []; @@ -485,17 +491,13 @@ } printUserPrompt(rawInput) { const wrapper = document.createElement('div'); - wrapper.className = 'output-line user-prompt-line'; + wrapper.className = 'output-line msg-card user'; const safe = rawInput.replace(/</g, '<').replace(/>/g, '>'); const hasHtml = /<[a-z][\s\S]*>/i.test(rawInput.trim()); - const previewBtn = hasHtml ? `<button class="copy-btn" onclick="webx11.previewHtmlFromPrompt(this)" data-content="${safe.replace(/"/g, '"')}" title="Preview HTML">👁 Preview</button>` : ''; - wrapper.innerHTML = ` - <span class="prompt-prefix">webx11@module:~$ </span> - <span class="prompt-text">${safe}</span> - <button class="copy-btn user-copy-btn" onclick="webx11.copyToClipboard(this)" title="Copy prompt">Copy</button> - ${previewBtn} - <button class="edit-btn" onclick="webx11.editPrompt('${safe.replace(/'/g, "\\'")}')" title="Edit prompt">Edit</button> - `; + let buttons = '<button class="card-btn copy-btn" onclick="webx11.copyToClipboard(this)">Copy</button>'; + if (hasHtml) buttons += '<button class="card-btn preview-btn" onclick="webx11.previewHtmlFromPrompt(this)" data-content="' + safe.replace(/"/g, '"') + '">Preview</button>'; + buttons += '<button class="card-btn edit-btn" onclick="webx11.editPrompt(\'' + safe.replace(/'/g, "\\'") + '\')">Edit</button>'; + wrapper.innerHTML = '<div class="msg-card-header"><span class="msg-role">👤 User</span><span class="msg-buttons">' + buttons + '</span></div><div class="msg-card-body">' + safe + '</div>'; this.outputContainer.appendChild(wrapper); this._historyBuffer.push({ l: rawInput, t: 'command-prompt' }); if (this._historyBuffer.length > this._historyMaxLines) this._historyBuffer.shift(); @@ -508,22 +510,37 @@ this.inputField.setSelectionRange(text.length, text.length); } copyToClipboard(btn) { - let text; - if (btn.classList.contains('user-copy-btn')) { - // User prompt: text is in .prompt-text sibling - const pSpan = btn.parentElement.querySelector('.prompt-text'); - if (pSpan) text = pSpan.textContent; + let text = ''; + const card = btn.closest('.msg-card'); + if (card) { + const cardBody = card.querySelector('.msg-card-body'); + if (cardBody) text = cardBody.textContent; } else { - // AI response: text is in next sibling .ai-content - const contentDiv = btn.nextElementSibling; - if (contentDiv) text = contentDiv.innerText; + const nd = btn.nextElementSibling; + if (nd) text = nd.innerText || nd.textContent; } if (!text) return; - navigator.clipboard.writeText(text).then(() => { - const originalText = btn.textContent; - btn.textContent = 'Copied!'; - setTimeout(() => { btn.textContent = originalText; }, 2000); - }).catch(err => { console.error('Failed to copy: ', err); }); + btn.textContent = 'Copied!'; + setTimeout(function(){ btn.textContent = 'Copy'; }, 2000); + // Use navigator.clipboard on secure contexts (iOS 14+, modern browsers). + // This is the preferred API — NOTHING blocks it on HTTPS pages. + if (navigator.clipboard) { + navigator.clipboard.writeText(text).catch(function(){}); + } + // Also try textarea+execCommand synchronously for older browsers. + // DO NOT wrap in setTimeout — iOS Safari requires execCommand to run + // in the same microtask as the user's click event. + try { + var ta = document.createElement('textarea'); + ta.value = text; + ta.style.cssText = 'position:fixed;left:10px;top:10px;width:200px;height:40px;font-size:16px;padding:4px;z-index:-1;opacity:0.01'; + document.body.appendChild(ta); + ta.focus(); + ta.select(); + ta.setSelectionRange(0, text.length); + document.execCommand('copy'); + document.body.removeChild(ta); + } catch(e) {} } printClickable(items) { const line = document.createElement('div'); @@ -1853,7 +1870,13 @@ const saved = localStorage.getItem('webx11_font_size'); if (saved) { const s = parseInt(saved); - if (s >= 8 && s <= 32) document.documentElement.style.fontSize = s + 'px'; + if (s >= 8 && s <= 32) { + document.body.style.fontSize = s + 'px'; + const tc = document.getElementById('terminal-container'); + if (tc) tc.style.fontSize = s + 'px'; + const inp = document.getElementById('command-input'); + if (inp) inp.style.fontSize = s + 'px'; + } } } catch (e) {} } @@ -2004,8 +2027,10 @@ this.print('Usage: font-size <8-32>', 'warning'); return; } - document.documentElement.style.fontSize = s + 'px'; + document.body.style.fontSize = s + 'px'; + document.getElementById('terminal-container').style.fontSize = s + 'px'; + document.getElementById('command-input').style.fontSize = s + 'px'; try { localStorage.setItem('webx11_font_size', String(s)); } catch (e) {} - this.print('Font size set to ' + s + 'px', 'success'); + this.print('Font size set to ' + s + 'px 👆', 'success'); } } |