aboutsummaryrefslogtreecommitdiff
path: root/modules/core
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-05-28 06:02:08 +0200
committerAgando <roduccomm@gmail.com>2026-05-28 06:02:08 +0200
commitd1af24deb8ad344994c0a06b909b7a6f4bac4c94 (patch)
tree27bdf7deff8cf51940f74fb666d2700ff39fdc9e /modules/core
parent212708b464c2bdbdd9670c00804623dca0729d2c (diff)
auto: 2026-05-28_06:02 changes
Diffstat (limited to 'modules/core')
-rw-r--r--modules/core/terminal.js50
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, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ .replace(/"/g, '&quot;')
+ .replace(/`/g, '&#96;');
+ 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, "&lt;").replace(/>/g, "&gt;");
+ 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, "&lt;").replace(/>/g, "&gt;");
+ 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, "&lt;").replace(/>/g, "&gt;");