aboutsummaryrefslogtreecommitdiff
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
parent212708b464c2bdbdd9670c00804623dca0729d2c (diff)
auto: 2026-05-28_06:02 changes
-rw-r--r--main.html14
-rw-r--r--modules/core/terminal.js50
2 files changed, 64 insertions, 0 deletions
diff --git a/main.html b/main.html
index 5b88872..9886b66 100644
--- a/main.html
+++ b/main.html
@@ -332,6 +332,20 @@
color: var(--text-color);
background: rgba(0, 255, 0, 0.1);
}
+ .ai-render-container {
+ margin: 10px 0;
+ border-radius: 6px;
+ overflow: hidden;
+ border: 1px solid var(--border-color);
+ background: #000;
+ }
+ .ai-render-iframe {
+ width: 100%;
+ border: none;
+ display: block;
+ background: #000;
+ min-height: 200px;
+ }
.module-badge {
display: inline-block;
padding: 2px 6px;
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;");