diff options
| author | Agando <roduccomm@gmail.com> | 2026-05-28 06:02:08 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-05-28 06:02:08 +0200 |
| commit | d1af24deb8ad344994c0a06b909b7a6f4bac4c94 (patch) | |
| tree | 27bdf7deff8cf51940f74fb666d2700ff39fdc9e /modules/core/terminal.js | |
| parent | 212708b464c2bdbdd9670c00804623dca0729d2c (diff) | |
auto: 2026-05-28_06:02 changes
Diffstat (limited to 'modules/core/terminal.js')
| -rw-r--r-- | modules/core/terminal.js | 50 |
1 files changed, 50 insertions, 0 deletions
diff --git a/modules/core/terminal.js b/modules/core/terminal.js index 2617f79..309e477 100644 --- a/modules/core/terminal.js +++ b/modules/core/terminal.js @@ -377,7 +377,57 @@ this.scrollToBottom(); this._trimOutput(); } + _renderHTMLPreview(content) { + // AI returns raw HTML. We must HTML-escape for the srcdoc attribute + // so the parser treats it as attribute content, not tags. + // srcdoc decodes entities back to actual HTML inside the iframe. + const hasOwnStyle = /<style|<html|<head/i.test(content); + const themedContent = hasOwnStyle + ? content + : `<style>body{background:#000;color:#0f0;margin:16px;font-family:monospace;}</style><body style="background:#000;color:#0f0;font-family:monospace;margin:16px">${content}</body>`; + const safeSrcdoc = themedContent + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/`/g, '`'); + return `<div class="ai-render-container"><iframe sandbox="allow-scripts" srcdoc="${safeSrcdoc}" class="ai-render-iframe" onload="var _s=this;setTimeout(function(){_s.style.height=Math.max((_s.contentWindow.document.documentElement.scrollHeight||200),200)+'px'},100)"></iframe></div>`; + } + _renderHTMLBlock(text) { + const renderRegex = /:::render-html\r?\n([\s\S]*?)\r?\n:::/g; + let match; + let lastIndex = 0; + const fragments = []; + while ((match = renderRegex.exec(text)) !== null) { + // Text before this block + 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: 'render', html: this._renderHTMLPreview(match[1].trim()) }); + lastIndex = match.index + match[0].length; + } + 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>` }); + } + return fragments.length > 0 ? fragments : null; + } printAIResponse(text) { + const fragments = this._renderHTMLBlock(text); + if (fragments) { + 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); + } + this.scrollToBottom(); + this._trimOutput(); + return; + } const wrapper = document.createElement('div'); wrapper.className = 'output-line ollama'; const safeText = text.replace(/</g, "<").replace(/>/g, ">"); |