Loading...
Recent Pages
Proof Activity
Loading proof-bearing activity...
Portfolio Works Manages aleqth.com/portfolio
View Portfolio ↗
Loading works…
All Pages
Create
New Page
Create a page from a template
Blank Canvas
Freeform canvas — drop images, text, anything
Build with AI
Describe a site and generate it
Import a Site
Paste a URL — fork it into an editable page
New Work
Add a piece to your portfolio
Upload Media
Host an image/GIF/video, get a link
New Post
Write a quick update or thought
New Reminder
Set a reminder for later
Choose a Template
View live ↗
canvas
Add to canvas
Page settings
⌘ Hermes — describe a change
Images
Drop media here or click to upload
Images, Videos — JPG, PNG, GIF, MP4, WebM
generating…
Ready to apply
@⌘ terminal
loading @⌘...
@⌘
paired agent ready
My Collection
Media items. Full inventory (sites · agents · worlds · dada · domains · files) → /wallet
Access Codes
Generate New Code
Active Codes
Broadcast
All Users
Credits
Balance
Use
Service credits
not cash-redeemable
Boundary
Credits cannot be redeemed for cash.
Top Up
Credit History
Profile
+
JPG, PNG, GIF, or WebP. Max 25 MB.
0/500
Loading private preferences… If unavailable, profile saves will leave them unchanged.
View Feed
Wallet connection & authority
Your wallet stays under your control. Your paired agent can prepare requests, but cannot sign or spend unless you explicitly enable a future, limited permission.
Loading...
Agent can prepare a request for you. It cannot sign, spend, trade, mint, or move funds. Permission controls will require an explicit limit, expiry, approval, and instant revoke.
Stack
What powers aleqth.com · ankle96 component library
UI Kit
/elements — ankle96 component library
23 copy-paste UI snippets: bevel buttons, ticker, master-detail, tabs, form rows, status bars. Self-contained HTML, CORS-enabled API.
Open /elements →
Thanks To — Powered By
What Built With
Web frameworkFastAPI | SQLAlchemy
StorageSQLite (WAL) | Pinata IPFS
AI cascadeGroq | Anthropic | Gemini | OpenRouter
Image genImagen 4 | moondream
On-chainZora | Base | Whop
AuthGoogle OAuth | Whop
AgentsClaudeCommander · Codex · Hermes · BOTTA
Front-endVanilla JS · Times · Verdana · CommonMark
ankle96 build · 2026.04.28 · ankle.website
Agent Lab
Task queue · Experiments · Autoresearch loop
Agent Task Queue
Experiments

Add website to a world

The website will appear in that world's scrolling Sites grid. Enter a world name or its full address. A private page becomes unlisted so the world can display it without adding it to public discovery.

funny.ankle.website
' : baseHtml; var doc; try { doc = new DOMParser().parseFromString(html, 'text/html'); } catch(_e) { doc = new DOMParser().parseFromString('', 'text/html'); } if (!doc.head) { var head = doc.createElement('head'); doc.documentElement.insertBefore(head, doc.body || null); } if (!doc.body) { doc.documentElement.appendChild(doc.createElement('body')); } doc.querySelectorAll('#ankle-selected-wallpaper').forEach(function(n) { n.remove(); }); doc.body.querySelectorAll('p').forEach(function(p) { var txt = (p.textContent || '').toLowerCase(); if (txt.indexOf('generation unavailable') !== -1 && txt.indexOf('all providers busy') !== -1) p.remove(); }); if (wasErrorBase) { doc.body.style.display = ''; doc.body.style.alignItems = ''; doc.body.style.justifyContent = ''; doc.body.style.height = ''; doc.body.style.color = ''; doc.body.style.fontFamily = ''; } var style = doc.createElement('style'); style.id = 'ankle-selected-wallpaper'; style.textContent = 'html,body{min-height:100vh;}' + 'html body[data-wallpaper],html body[data-wallpaper][class]{' + 'background-image:url("' + _cssUrl(src) + '") !important;' + 'background-repeat:repeat !important;background-position:top left !important;background-size:auto !important;}'; doc.head.appendChild(style); doc.body.setAttribute('data-wallpaper', 'selected-element'); doc.body.setAttribute('data-wallpaper-intent', src); doc.documentElement.setAttribute('data-wallpaper-intent', src); return '\n' + doc.documentElement.outerHTML; } function _wallpaperSrcFromHtml(rawHtml) { if (!rawHtml) return ''; try { var doc = new DOMParser().parseFromString(String(rawHtml), 'text/html'); var intent = (doc.documentElement && doc.documentElement.getAttribute('data-wallpaper-intent')) || (doc.body && doc.body.getAttribute('data-wallpaper-intent')) || ''; if (intent) return intent; var style = doc.getElementById('ankle-selected-wallpaper'); var match = style && String(style.textContent || '').match(/background-image\s*:\s*url\((?:"|')?([^"')]+)(?:"|')?\)/i); return match && match[1] ? match[1] : ''; } catch(_e) { return ''; } } function _isSelectedMultiplyTrailPrompt(prompt) { var p = String(prompt || '').toLowerCase(); var wantsCopies = /\b(multiply|duplicate|clone|copy|copies|repeat|many|multiple|multiples|trail|layered)\b/.test(p); var wantsPath = /\b(snake|serpent|winding|wind|wave|wavy|squiggle|trail|chain|curve|curvy|layered|cascade)\b/.test(p); return wantsCopies && wantsPath; } function _applySelectedSnakeTrail(prompt, selectedElement) { if (!selectedElement || typeof canvasElements === 'undefined' || !Array.isArray(canvasElements)) return null; var idx = selectedElement.index; if (typeof idx !== 'number' || idx < 0 || !canvasElements[idx]) return null; if (!_isSelectedMultiplyTrailPrompt(prompt)) return null; var base = canvasElements[idx]; if (!base || base.type === 'zone') return null; var sourceKey = String(base.id || base.src || base.text || base.html || ('idx:' + idx)); var countMatch = String(prompt || '').match(/\b([3-9]|1[0-9]|2[0-4])\b/); var count = countMatch ? parseInt(countMatch[1], 10) : 11; if (/\b(many|lots|tons)\b/i.test(prompt || '')) count = Math.max(count, 15); count = Math.max(3, Math.min(24, count)); var area = document.getElementById('canvasArea'); var areaW = Math.max((area && area.clientWidth) || 1200, 900); var areaH = Math.max((area && area.clientHeight) || 680, 640); var w = Math.max(24, base.width || selectedElement.w || 160); var h = Math.max(24, base.height || selectedElement.h || 100); var step = Math.max(34, Math.min(120, w * 0.42)); var amp = Math.max(28, Math.min(120, h * 0.55)); var span = step * (count - 1); var centerX = (base.x || 0) + w / 2; var startX = Math.max(12, Math.min(areaW - w - span - 12, centerX - span / 2 - w / 2)); var centerY = Math.max(24 + amp, Math.min(areaH - h - amp - 24, (base.y || 0) + h / 2)); var baseZ = base.zIndex || canvasElements.length + 1; var cloneStamp = Date.now(); var clamp = function(v, min, max) { return Math.max(min, Math.min(max, v)); }; canvasElements = canvasElements.filter(function(el, elIdx) { if (elIdx === idx) return true; return !(el && el.generatedBy === 'selected-snake-trail' && el.sourceKey === sourceKey); }); idx = canvasElements.indexOf(base); if (idx < 0) { canvasElements.unshift(base); idx = 0; } delete base.generatedBy; delete base.sourceKey; base.x = Math.round(startX); base.y = Math.round(centerY - h / 2); base.zIndex = baseZ; base.rotation = base.rotation || 0; for (var i = 1; i < count; i++) { var copy = JSON.parse(JSON.stringify(base)); copy.id = 'snake_copy_' + cloneStamp + '_' + i; copy.generatedBy = 'selected-snake-trail'; copy.sourceKey = sourceKey; copy.x = Math.round(clamp(startX + step * i, 0, areaW - w - 8)); copy.y = Math.round(clamp(centerY - h / 2 + Math.sin(i * 0.92) * amp, 0, areaH - h - 8)); copy.rotation = Math.round(((base.rotation || 0) + Math.sin(i * 0.92) * 22) * 10) / 10; copy.zIndex = baseZ + i + 1; canvasElements.push(copy); } canvasSelectedIdx = idx; renderCanvasElements(); return { count: count, sourceKey: sourceKey }; } function _canvasPromptText(prompt) { return String(prompt || '').toLowerCase().replace(/\s+/g, ' ').trim(); } function _canvasCommandNumber(prompt, fallback) { var p = _canvasPromptText(prompt); var m = p.match(/\b([3-9]|1[0-9]|2[0-4])\b/); if (m) return parseInt(m[1], 10); var words = { three:3, four:4, five:5, six:6, seven:7, eight:8, nine:9, ten:10, eleven:11, twelve:12, thirteen:13, fourteen:14, fifteen:15, sixteen:16, seventeen:17, eighteen:18, nineteen:19, twenty:20 }; for (var k in words) { if (new RegExp('\\b' + k + '\\b').test(p)) return words[k]; } if (/\b(few|several)\b/.test(p)) return 5; if (/\b(many|lots)\b/.test(p)) return Math.max(fallback || 11, 15); if (/\b(tons|loads|dozens)\b/.test(p)) return 20; return fallback || 11; } function _canvasAreaMetrics() { var area = document.getElementById('canvasArea'); return { area: area, width: Math.max((area && area.clientWidth) || 1200, 900), height: Math.max((area && area.clientHeight) || 680, 640) }; } function _canvasElementSize(el, selectedElement) { return { w: Math.max(24, el && (el.width || el.w) || selectedElement && selectedElement.w || 160), h: Math.max(24, el && (el.height || el.h) || selectedElement && selectedElement.h || 100) }; } function _canvasOuterTransform(el) { var parts = []; if (el && el.rotation) parts.push('rotate(' + el.rotation + 'deg)'); if (el && el.flipX) parts.push('scaleX(-1)'); if (el && el.flipY) parts.push('scaleY(-1)'); return parts.join(' '); } function _canvasMotionClass(name) { name = String(name || '').toLowerCase(); var ok = { spin: true, counterspin: true, pulse: true, breathe: true, bounce: true, float: true, wiggle: true, shake: true, blink: true, orbit: true, 'drift-right': true, 'drift-left': true, 'drift-up': true, 'drift-down': true }; return ok[name] ? 'canvas-motion-' + name : ''; } function _applyCanvasElementPresentation(node, el) { if (!node || !el) return; var transform = _canvasOuterTransform(el); if (transform) node.style.transform = transform; else node.style.transform = ''; if (el.opacity !== undefined && el.opacity !== null && el.opacity !== '') node.style.opacity = String(el.opacity); if (el.filter) node.style.filter = el.filter; if (el.mixBlendMode) node.style.mixBlendMode = el.mixBlendMode; if (el.boxShadow) node.style.boxShadow = el.boxShadow; if (el.borderRadius !== undefined && el.borderRadius !== null && el.borderRadius !== '') { node.style.borderRadius = (typeof el.borderRadius === 'number') ? (el.borderRadius + 'px') : String(el.borderRadius); node.style.overflow = 'hidden'; } } function _applyCanvasMotionPresentation(node, motion) { if (!node) return; motion = motion || {}; var cls = _canvasMotionClass(motion.name || motion.type); if (!cls) return; node.className += ' ' + cls; node.style.setProperty('--motion-duration', (motion.duration || 3) + 's'); node.style.setProperty('--motion-distance', ((motion.distance || 56)) + 'px'); if (motion.delay) node.style.animationDelay = motion.delay + 's'; } function _currentSelectedElementDescriptor() { if (typeof canvasSelectedIdx !== 'number' || canvasSelectedIdx < 0) return null; if (!Array.isArray(canvasElements) || !canvasElements[canvasSelectedIdx]) return null; var el = canvasElements[canvasSelectedIdx]; if (!el || el.type === 'zone') return null; return { index: canvasSelectedIdx, type: el.type || 'unknown', x: el.x || 0, y: el.y || 0, w: el.width || el.w || 0, h: el.height || el.h || 0, rotation: el.rotation || 0, src: el.src || '', text: el.text || '' }; } function _localCanvasCommandTargets(prompt, selectedElement) { if (!Array.isArray(canvasElements)) return []; var p = _canvasPromptText(prompt); var explicitAll = /\b(all|everything|every element|all elements|whole canvas)\b/.test(p); if (explicitAll) { return canvasElements.map(function(el, idx) { return el && el.type !== 'zone' ? idx : -1; }).filter(function(idx) { return idx >= 0; }); } var idx = selectedElement && typeof selectedElement.index === 'number' ? selectedElement.index : canvasSelectedIdx; if (typeof idx === 'number' && idx >= 0 && canvasElements[idx] && canvasElements[idx].type !== 'zone') return [idx]; return []; } function _detectCanvasPattern(prompt) { var p = _canvasPromptText(prompt); if (!/\b(multiply|duplicate|clone|copy|copies|repeat|many|multiple|multiples|trail|array|arrange|spread|scatter|tile|grid|matrix|pattern|layer|layered|cascade|stack)\b/.test(p)) return ''; if (/\b(snake|serpent|winding|wind|wave|wavy|squiggle|trail|chain|curve|curvy)\b/.test(p)) return 'snake'; if (/\b(spiral|swirl|vortex)\b/.test(p)) return 'spiral'; if (/\b(circle|ring|halo|orbit|around)\b/.test(p)) return 'circle'; if (/\b(grid|matrix|tile|tiles|checker|wall)\b/.test(p)) return 'grid'; if (/\b(row|line|horizontal|across)\b/.test(p)) return 'row'; if (/\b(column|vertical|down)\b/.test(p)) return 'column'; if (/\b(scatter|random|cloud|sprinkle|spread)\b/.test(p)) return 'scatter'; if (/\b(stack|layer|layered|cascade|pile|stair)\b/.test(p)) return 'cascade'; return /\b(multiply|duplicate|clone|copy|copies|repeat|multiple|multiples)\b/.test(p) ? 'cascade' : ''; } function _applyCanvasDuplicatePattern(prompt, idx) { if (!Array.isArray(canvasElements) || idx < 0 || !canvasElements[idx]) return null; var base = canvasElements[idx]; if (!base || base.type === 'zone') return null; var pattern = _detectCanvasPattern(prompt); if (!pattern) return null; var count = _canvasCommandNumber(prompt, pattern === 'grid' ? 12 : 11); count = Math.max(2, Math.min(24, count)); var metrics = _canvasAreaMetrics(); var size = _canvasElementSize(base); var areaW = metrics.width, areaH = metrics.height, w = size.w, h = size.h; var sourceKey = String(base.sourceKey || base.id || base.src || base.text || ('idx:' + idx)); var cloneStamp = Date.now(); var clamp = function(v, min, max) { return Math.max(min, Math.min(max, v)); }; var centerX = (base.x || 0) + w / 2; var centerY = (base.y || 0) + h / 2; var points = []; var i; if (pattern === 'snake') { var step = Math.max(34, Math.min(120, w * 0.42)); var amp = Math.max(28, Math.min(120, h * 0.55)); var span = step * (count - 1); var startX = Math.max(12, Math.min(areaW - w - span - 12, centerX - span / 2 - w / 2)); var snakeCenterY = Math.max(24 + amp, Math.min(areaH - h - amp - 24, centerY)); for (i = 0; i < count; i++) { points.push({ x: Math.round(clamp(startX + step * i, 0, areaW - w - 8)), y: Math.round(clamp(snakeCenterY - h / 2 + Math.sin(i * 0.92) * amp, 0, areaH - h - 8)), rotation: Math.round(((base.rotation || 0) + Math.sin(i * 0.92) * 22) * 10) / 10 }); } } else if (pattern === 'circle') { var radius = Math.max(72, Math.min(220, Math.max(w, h) * 1.25)); centerX = clamp(centerX, radius + w / 2 + 12, areaW - radius - w / 2 - 12); centerY = clamp(centerY, radius + h / 2 + 12, areaH - radius - h / 2 - 12); for (i = 0; i < count; i++) { var a = (Math.PI * 2 * i) / count - Math.PI / 2; points.push({ x: Math.round(clamp(centerX + Math.cos(a) * radius - w / 2, 0, areaW - w - 8)), y: Math.round(clamp(centerY + Math.sin(a) * radius - h / 2, 0, areaH - h - 8)), rotation: Math.round(((a * 180 / Math.PI) + 90) * 10) / 10 }); } } else if (pattern === 'spiral') { var maxR = Math.max(96, Math.min(260, Math.min(areaW, areaH) * 0.28)); for (i = 0; i < count; i++) { var t = count <= 1 ? 0 : i / (count - 1); var sa = i * 0.82; var r = 24 + maxR * t; points.push({ x: Math.round(clamp(centerX + Math.cos(sa) * r - w / 2, 0, areaW - w - 8)), y: Math.round(clamp(centerY + Math.sin(sa) * r - h / 2, 0, areaH - h - 8)), rotation: Math.round(((base.rotation || 0) + sa * 32) * 10) / 10 }); } } else if (pattern === 'grid') { var cols = Math.ceil(Math.sqrt(count)); var rows = Math.ceil(count / cols); var gap = Math.max(8, Math.min(28, Math.min(w, h) * 0.16)); var gridW = cols * w + (cols - 1) * gap; var gridH = rows * h + (rows - 1) * gap; var gx = clamp(centerX - gridW / 2, 8, Math.max(8, areaW - gridW - 8)); var gy = clamp(centerY - gridH / 2, 8, Math.max(8, areaH - gridH - 8)); for (i = 0; i < count; i++) { points.push({ x: Math.round(gx + (i % cols) * (w + gap)), y: Math.round(gy + Math.floor(i / cols) * (h + gap)), rotation: base.rotation || 0 }); } } else if (pattern === 'row' || pattern === 'column') { var lineGap = Math.max(8, Math.min(34, (pattern === 'row' ? w : h) * 0.18)); for (i = 0; i < count; i++) { var dx = pattern === 'row' ? i * (w + lineGap) : 0; var dy = pattern === 'column' ? i * (h + lineGap) : 0; var lineSpanX = pattern === 'row' ? (count * w + (count - 1) * lineGap) : w; var lineSpanY = pattern === 'column' ? (count * h + (count - 1) * lineGap) : h; var lx = clamp(centerX - lineSpanX / 2 + dx, 8, areaW - w - 8); var ly = clamp(centerY - lineSpanY / 2 + dy, 8, areaH - h - 8); points.push({ x: Math.round(lx), y: Math.round(ly), rotation: base.rotation || 0 }); } } else if (pattern === 'scatter') { var scatterW = Math.min(areaW - w - 16, Math.max(320, w * 4)); var scatterH = Math.min(areaH - h - 16, Math.max(240, h * 3)); for (i = 0; i < count; i++) { var n1 = Math.abs(Math.sin((i + 1) * 12.9898) * 43758.5453) % 1; var n2 = Math.abs(Math.sin((i + 3) * 78.233) * 24634.6345) % 1; points.push({ x: Math.round(clamp(centerX - scatterW / 2 + n1 * scatterW, 8, areaW - w - 8)), y: Math.round(clamp(centerY - scatterH / 2 + n2 * scatterH, 8, areaH - h - 8)), rotation: Math.round(((base.rotation || 0) + (n1 - 0.5) * 48) * 10) / 10 }); } } else { var offX = /\b(left)\b/.test(_canvasPromptText(prompt)) ? -22 : 22; var offY = /\b(up)\b/.test(_canvasPromptText(prompt)) ? -22 : 22; for (i = 0; i < count; i++) { points.push({ x: Math.round(clamp((base.x || 0) + offX * i, 0, areaW - w - 8)), y: Math.round(clamp((base.y || 0) + offY * i, 0, areaH - h - 8)), rotation: (base.rotation || 0) + (/\b(fan)\b/.test(_canvasPromptText(prompt)) ? i * 7 : 0) }); } } canvasElements = canvasElements.filter(function(el, elIdx) { if (elIdx === idx) return true; return !(el && el.generatedBy === 'local-canvas-pattern' && el.sourceKey === sourceKey); }); idx = canvasElements.indexOf(base); if (idx < 0) { canvasElements.unshift(base); idx = 0; } var baseZ = base.zIndex || canvasElements.length + 1; delete base.generatedBy; delete base.sourceKey; delete base.pattern; base.x = points[0].x; base.y = points[0].y; base.rotation = points[0].rotation || 0; base.zIndex = baseZ; var indexes = [idx]; for (i = 1; i < points.length; i++) { var copy = JSON.parse(JSON.stringify(base)); copy.id = 'local_copy_' + cloneStamp + '_' + i; copy.generatedBy = 'local-canvas-pattern'; copy.sourceKey = sourceKey; copy.pattern = pattern; copy.x = points[i].x; copy.y = points[i].y; copy.rotation = points[i].rotation || 0; copy.zIndex = baseZ + i + 1; canvasElements.push(copy); indexes.push(canvasElements.length - 1); } canvasSelectedIdx = idx; return { pattern: pattern, count: points.length, indexes: indexes }; } function _isCanvasGroundFillPrompt(prompt) { var p = _canvasPromptText(prompt); return /\b(grass|grassy|field|meadow|lawn|ground|floor|terrain|landscape|garden)\b/.test(p) && /\b(make|more|fill|cover|spread|repeat|row|line|field|ground|bottom|look like|turn into)\b/.test(p); } function _applyCanvasGroundFillCommand(prompt, idx) { if (!Array.isArray(canvasElements) || idx < 0 || !canvasElements[idx]) return null; var base = canvasElements[idx]; if (!base || base.type === 'zone' || !base.src) return null; if (!_isCanvasGroundFillPrompt(prompt)) return null; var p = _canvasPromptText(prompt); var area = document.getElementById('canvasArea'); var areaW = Math.max((area && area.scrollWidth) || 0, (area && area.clientWidth) || 0, 1200); var areaH = Math.max((area && area.scrollHeight) || 0, (area && area.clientHeight) || 0, 800); var size = _canvasElementSize(base); var w = Math.max(36, size.w); var h = Math.max(24, size.h); var rows = /\b(field|grassy|meadow|lawn|cover|fill|terrain|landscape)\b/.test(p) ? 2 : 1; if (/\b(thick|dense|full|carpet)\b/.test(p)) rows = 3; var step = Math.max(42, Math.min(160, w * 0.66)); var cols = Math.ceil((areaW + step) / step); var explicit = _canvasCommandNumber(prompt, cols * rows); var total = Math.max(cols, Math.min(36, Math.max(explicit, cols * rows))); rows = Math.max(rows, Math.ceil(total / cols)); total = Math.min(36, cols * rows); var sourceKey = String(base.sourceKey || base.id || base.src || ('idx:' + idx)); var stamp = Date.now(); var baseZ = base.zIndex || _nextCanvasZIndex(); var indexes = []; canvasElements = canvasElements.filter(function(el, elIdx) { if (elIdx === idx) return true; return !(el && el.generatedBy === 'local-ground-fill' && el.sourceKey === sourceKey); }); idx = canvasElements.indexOf(base); if (idx < 0) { canvasElements.push(base); idx = canvasElements.length - 1; } for (var i = 0; i < total; i++) { var col = i % cols; var row = Math.floor(i / cols); var jitterX = Math.round(Math.sin((i + 1) * 8.13) * Math.min(18, step * 0.18)); var jitterY = Math.round(Math.sin((i + 3) * 5.91) * Math.min(18, h * 0.18)); var scale = 0.86 + (Math.abs(Math.sin((i + 5) * 4.7)) * 0.32); var nextW = Math.max(24, Math.round(w * scale)); var nextH = Math.max(24, Math.round(h * scale)); var x = Math.max(0, Math.min(areaW - nextW, Math.round(col * step - step * 0.25 + jitterX))); var y = Math.max(0, Math.min(areaH - nextH, Math.round(areaH - 32 - nextH - row * (nextH * 0.48) + jitterY))); var target = i === 0 ? base : JSON.parse(JSON.stringify(base)); target.id = i === 0 ? (target.id || ('el_' + stamp)) : ('ground_fill_' + stamp + '_' + i); target.generatedBy = i === 0 ? undefined : 'local-ground-fill'; target.sourceKey = sourceKey; target.x = x; target.y = y; target.width = nextW; target.height = nextH; target.rotation = Math.round(Math.sin((i + 2) * 2.7) * 4); target.zIndex = baseZ + i; if (i === 0) { delete target.generatedBy; canvasElements[idx] = target; indexes.push(idx); } else { target.generatedBy = 'local-ground-fill'; canvasElements.push(target); indexes.push(canvasElements.length - 1); } } canvasSelectedIdx = indexes[0] || idx; return { summary: 'grass field x' + total, indexes: indexes }; } function _canvasMotionFromPrompt(prompt) { var p = _canvasPromptText(prompt); if (/\b(stop|remove|clear|freeze|still|no)\s+(motion|animation|animate|moving|movement)\b|\b(stop|freeze)\b/.test(p)) { return { stop: true }; } var name = ''; if (/\b(counter ?spin|counterclockwise|anti-clockwise)\b/.test(p)) name = 'counterspin'; else if (/\b(spin|rotate endlessly|rotation animation)\b/.test(p)) name = 'spin'; else if (/\b(orbit|revolve)\b/.test(p)) name = 'orbit'; else if (/\b(pulse|throb)\b/.test(p)) name = 'pulse'; else if (/\b(breathe|breathing)\b/.test(p)) name = 'breathe'; else if (/\b(bounce|jump|hop)\b/.test(p)) name = 'bounce'; else if (/\b(float|hover|bob)\b/.test(p)) name = 'float'; else if (/\b(wiggle|wobble|jiggle)\b/.test(p)) name = 'wiggle'; else if (/\b(shake|vibrate)\b/.test(p)) name = 'shake'; else if (/\b(blink|flash|flicker)\b/.test(p)) name = 'blink'; else if (/\b(drift|slide|marquee|moving|movement)\b|\bmake (it|them|this) move\b|\bmotion\b/.test(p)) { if (/\b(left|west)\b/.test(p)) name = 'drift-left'; else if (/\b(up|north)\b/.test(p)) name = 'drift-up'; else if (/\b(down|south)\b/.test(p)) name = 'drift-down'; else name = 'drift-right'; } if (!name) return null; var duration = 3; if (/\b(very slow|super slow)\b/.test(p)) duration = 9; else if (/\b(slow|lazy)\b/.test(p)) duration = 6; else if (/\b(fast|quick|rapid)\b/.test(p)) duration = 1.2; else if (/\b(very fast|hyper)\b/.test(p)) duration = 0.65; var distance = /\b(big|wide|far|huge)\b/.test(p) ? 120 : (/\b(small|subtle|tiny)\b/.test(p) ? 24 : 56); return { name: name, duration: duration, distance: distance }; } function _applyCanvasMotionCommand(prompt, indexes) { var motion = _canvasMotionFromPrompt(prompt); if (!motion || !indexes.length) return null; indexes.forEach(function(idx, order) { var el = canvasElements[idx]; if (!el) return; if (motion.stop) { delete el.motion; delete el.animation; return; } el.motion = { name: motion.name, duration: motion.duration, distance: motion.distance, delay: Math.round(order * 0.08 * 100) / 100 }; }); return motion.stop ? 'stopped motion' : ('added ' + motion.name + ' motion'); } function _applyCanvasPlacementCommand(prompt, indexes) { var p = _canvasPromptText(prompt); if (!indexes.length || !/\b(center|middle|left|right|top|bottom|corner|fullscreen|full screen|cover|front|back|behind|forward|place|position|move|put|align|at x|x\s*\d)\b/.test(p)) return ''; if (/\bmake (it|them|this) move\b|\b(drift|slide|marquee|animation|animate)\b/.test(p) && !/\b(place|position|align|put|move to|center|middle|corner|at x|x\s*\d)\b/.test(p)) return ''; var metrics = _canvasAreaMetrics(); var xy = p.match(/\bx\s*[:=]?\s*(-?\d{1,5})\s*(?:,|\s+)\s*y\s*[:=]?\s*(-?\d{1,5})\b/) || p.match(/\b(?:place|position|move|put)\b[^\d-]*(-?\d{1,5})\s*,\s*(-?\d{1,5})\b/); var changed = false; indexes.forEach(function(idx) { var el = canvasElements[idx]; if (!el) return; var size = _canvasElementSize(el); if (xy) { el.x = Math.max(0, parseInt(xy[1], 10)); el.y = Math.max(0, parseInt(xy[2], 10)); changed = true; } else if (/\b(fullscreen|full screen|fill canvas|cover canvas|cover page)\b/.test(p)) { el.x = 0; el.y = 0; el.width = metrics.width; el.height = metrics.height; changed = true; } else { if (/\b(center|middle)\b/.test(p) && !/\b(left|right)\b/.test(p)) { el.x = Math.max(0, Math.round((metrics.width - size.w) / 2)); changed = true; } if (/\b(center|middle)\b/.test(p) && !/\b(top|bottom)\b/.test(p)) { el.y = Math.max(0, Math.round((metrics.height - size.h) / 2)); changed = true; } if (/\bleft\b/.test(p)) { el.x = 24; changed = true; } if (/\bright\b/.test(p)) { el.x = Math.max(0, metrics.width - size.w - 24); changed = true; } if (/\btop\b/.test(p)) { el.y = 24; changed = true; } if (/\bbottom\b/.test(p)) { el.y = Math.max(0, metrics.height - size.h - 24); changed = true; } } if (/\b(front|forward|on top|bring up)\b/.test(p)) { el.zIndex = Math.max.apply(null, canvasElements.map(function(e){ return e.zIndex || 1; })) + 10; changed = true; } if (/\b(back|behind|send back)\b/.test(p)) { el.zIndex = 0; changed = true; } }); return changed ? 'placed element' : ''; } function _applyCanvasTransformCommand(prompt, indexes) { var p = _canvasPromptText(prompt); if (!indexes.length) return ''; var changed = false; indexes.forEach(function(idx) { var el = canvasElements[idx]; if (!el) return; var size = _canvasElementSize(el); var scale = null; var pct = p.match(/\b(\d{2,3})\s*%/); var times = p.match(/\b([0-9]+(?:\.[0-9]+)?)\s*x\b/); if (pct && /\b(scale|size|resize|bigger|smaller|larger|shrink|grow)\b/.test(p)) scale = Math.max(0.1, Math.min(5, parseInt(pct[1], 10) / 100)); else if (times && /\b(scale|size|resize|bigger|smaller|larger|shrink|grow)\b/.test(p)) scale = Math.max(0.1, Math.min(5, parseFloat(times[1]))); else if (/\b(huge|giant|massive)\b/.test(p)) scale = 1.75; else if (/\b(bigger|larger|enlarge|grow)\b/.test(p)) scale = 1.28; else if (/\b(tiny|mini)\b/.test(p)) scale = 0.45; else if (/\b(smaller|shrink|reduce)\b/.test(p)) scale = 0.72; if (scale) { el.width = Math.max(24, Math.round(size.w * scale)); if (el.height || size.h) el.height = Math.max(24, Math.round(size.h * scale)); changed = true; } if (/\b(wider|wide)\b/.test(p)) { el.width = Math.round(size.w * 1.4); changed = true; } if (/\b(taller|tall)\b/.test(p)) { el.height = Math.round(size.h * 1.4); changed = true; } var deg = p.match(/\brotate\s+(-?\d{1,3})\b|\b(-?\d{1,3})\s*deg(?:rees)?\b/); if (deg && !/\b(spin|animation|motion)\b/.test(p)) { el.rotation = parseInt(deg[1] || deg[2], 10); changed = true; } else if (/\b(tilt|slant)\b/.test(p)) { el.rotation = /\bleft\b/.test(p) ? -15 : 15; changed = true; } if (/\bflip horizontal|mirror horizontal|flip x|mirror x\b/.test(p)) { el.flipX = !el.flipX; changed = true; } if (/\bflip vertical|mirror vertical|flip y|mirror y\b/.test(p)) { el.flipY = !el.flipY; changed = true; } }); return changed ? 'transformed element' : ''; } function _applyCanvasVisualCommand(prompt, indexes) { var p = _canvasPromptText(prompt); if (!indexes.length) return ''; var changed = false; indexes.forEach(function(idx) { var el = canvasElements[idx]; if (!el) return; if (/\b(transparent|fade|faded|ghost)\b/.test(p)) { el.opacity = /\b(very|mostly)\s+transparent\b/.test(p) ? 0.25 : 0.55; changed = true; } if (/\b(opaque|solid|full opacity)\b/.test(p)) { el.opacity = 1; changed = true; } if (/\b(glow|glowing|neon)\b/.test(p)) { el.boxShadow = '0 0 24px rgba(0,255,102,.9), 0 0 48px rgba(30,77,255,.45)'; changed = true; } if (/\b(no glow|remove glow|clear glow)\b/.test(p)) { delete el.boxShadow; changed = true; } if (/\b(blur|blurry|soft)\b/.test(p)) { el.filter = 'blur(2px)'; changed = true; } if (/\b(grayscale|black and white|monochrome)\b/.test(p)) { el.filter = 'grayscale(1)'; changed = true; } if (/\b(saturate|vivid|juicy)\b/.test(p)) { el.filter = 'saturate(1.8) contrast(1.08)'; changed = true; } if (/\b(no filter|clear filter|normal filter)\b/.test(p)) { delete el.filter; changed = true; } if (/\b(round|rounded)\b/.test(p)) { el.borderRadius = /\b(circle|pill|fully)\b/.test(p) ? '999px' : '14px'; changed = true; } if (/\b(sharp corners|square corners|no radius)\b/.test(p)) { delete el.borderRadius; changed = true; } if (/\b(multiply blend|blend multiply)\b/.test(p)) { el.mixBlendMode = 'multiply'; changed = true; } if (/\b(screen blend|blend screen)\b/.test(p)) { el.mixBlendMode = 'screen'; changed = true; } if (/\b(normal blend|no blend)\b/.test(p)) { delete el.mixBlendMode; changed = true; } }); return changed ? 'styled element' : ''; } function _isLocalCanvasCommandPrompt(prompt, selectedElement) { var p = _canvasPromptText(prompt); if (!p) return false; if (_isCanvasGroundFillPrompt(p)) return true; if (_detectCanvasPattern(p)) return true; if (_canvasMotionFromPrompt(p)) return true; if (/\b(center|middle|left|right|top|bottom|corner|fullscreen|cover canvas|front|back|behind|place|position|move|put|align|at x|x\s*\d)\b/.test(p)) return true; if (/\b(bigger|larger|enlarge|grow|smaller|shrink|tiny|huge|scale|resize|wide|wider|tall|taller|rotate|tilt|flip|mirror)\b/.test(p)) return true; if (/\b(transparent|fade|opaque|glow|blur|grayscale|monochrome|saturate|rounded|circle|blend)\b/.test(p)) return true; return false; } function _applyLocalCanvasCommand(prompt, selectedElement, opts) { if (!Array.isArray(canvasElements)) return null; if (!_isLocalCanvasCommandPrompt(prompt, selectedElement)) return null; var targets = _localCanvasCommandTargets(prompt, selectedElement); if (!targets.length) return null; var summaries = []; var groundFill = _applyCanvasGroundFillCommand(prompt, targets[0]); if (groundFill) { targets = groundFill.indexes || targets; summaries.push(groundFill.summary); } var pattern = groundFill ? null : _applyCanvasDuplicatePattern(prompt, targets[0]); if (pattern) { targets = pattern.indexes || targets; summaries.push(pattern.pattern + ' x' + pattern.count); } var motion = _applyCanvasMotionCommand(prompt, targets); if (motion) summaries.push(motion); var placement = _applyCanvasPlacementCommand(prompt, targets); if (placement) summaries.push(placement); var transform = _applyCanvasTransformCommand(prompt, targets); if (transform) summaries.push(transform); var visual = _applyCanvasVisualCommand(prompt, targets); if (visual) summaries.push(visual); if (!summaries.length) return null; renderCanvasElements(); if (opts && opts.autosave) { try { scheduleCanvasAutoSave(); } catch(_e) {} } return { summary: summaries.join(', '), indexes: targets }; } /* ─── Portfolio Works editor (native, replaces /admin iframe) ───────── * Same /api/works endpoints the legacy admin uses. Auth piggy-backs on the * dashboard session — admin_authenticated is set in identity_resolver after * dashboard sign-in for users with the aleqth/admin role. */ var _worksList = []; var _worksEditingId = null; async function loadWorksGrid() { var grid = document.getElementById('worksGridEditor'); if (!grid) return; grid.innerHTML = '
Loading works…
'; try { var r = await fetch('/api/works', { credentials: 'same-origin' }); if (!r.ok) throw new Error('HTTP ' + r.status); _worksList = await r.json(); renderPortfolioWorksGrid(); } catch(e) { grid.innerHTML = '
Could not load works: ' + (e.message || e) + '
'; } } function renderPortfolioWorksGrid() { var grid = document.getElementById('worksGridEditor'); if (!grid) return; if (!_worksList.length) { grid.innerHTML = '
No works yet — click + Add Work to start.
'; return; } grid.innerHTML = _worksList.map(function(w) { var isVid = w.type === 'video' || /\.(mp4|webm|ogg|mov|avi)$/i.test(w.file_path || ''); var src = (w.file_path || '').charAt(0) === '/' ? w.file_path : '/' + (w.file_path || ''); var media = isVid ? '' : ''; var tags = (w.tags || []).map(function(t) { return '' + escHtml(t) + ''; }).join(''); return '
' + media + '
' + '
' + escHtml(w.title || 'Untitled') + '
' + '
' + escHtml(w.year || '') + ' · ' + escHtml(w.medium || '') + ' · ' + escHtml(w.type || 'image') + '
' + (tags ? '
' + tags + '
' : '') + '
' + '' + '' + '
' + '
' + '
'; }).join(''); } function worksShowAddModal() { _worksEditingId = null; document.getElementById('worksModalTitle').textContent = 'Add New Work'; document.getElementById('worksForm').reset(); document.getElementById('works-id').disabled = false; document.getElementById('works-file-path').value = ''; document.getElementById('worksUploadText').textContent = 'Click or drag to upload'; document.getElementById('worksModal').style.display = 'flex'; } function worksCloseModal() { document.getElementById('worksModal').style.display = 'none'; } function worksEdit(id) { var w = _worksList.find(function(x) { return x.id === id; }); if (!w) return; _worksEditingId = id; document.getElementById('worksModalTitle').textContent = 'Edit Work'; document.getElementById('works-id').value = w.id; document.getElementById('works-id').disabled = true; document.getElementById('works-title').value = w.title || ''; document.getElementById('works-year').value = w.year || ''; document.getElementById('works-type').value = w.type || 'image'; document.getElementById('works-medium').value = w.medium || ''; document.getElementById('works-tags').value = (w.tags || []).join(', '); document.getElementById('works-description').value = w.description || ''; document.getElementById('works-file-path').value = w.file_path || ''; document.getElementById('worksUploadText').textContent = w.file_path ? ('Current: ' + w.file_path) : 'Click or drag to upload'; document.getElementById('worksModal').style.display = 'flex'; } async function worksDelete(id) { if (!confirm('Delete work "' + id + '"? This cannot be undone.')) return; try { var r = await fetch('/api/works/' + encodeURIComponent(id), { method: 'DELETE', credentials: 'same-origin' }); if (!r.ok) throw new Error('HTTP ' + r.status); if (typeof toast === 'function') toast('Deleted'); else alert('Deleted'); loadWorksGrid(); } catch(e) { if (typeof toast === 'function') toast('Delete failed: ' + e.message, true); else alert('Delete failed: ' + e.message); } } async function worksHandleFile(input) { if (!input.files || !input.files.length) return; var file = input.files[0]; if (file.size > 800 * 1024 * 1024) { alert('File too big (max 800 MB)'); input.value = ''; return; } document.getElementById('worksUploadText').textContent = 'Uploading ' + file.name + '…'; var fd = new FormData(); fd.append('file', file); try { var r = await fetch('/api/upload', { method: 'POST', body: fd, credentials: 'same-origin' }); if (!r.ok) throw new Error('HTTP ' + r.status); var d = await r.json(); var path = d.file_path || d.path || ''; if (!path) throw new Error('no path returned'); document.getElementById('works-file-path').value = path; document.getElementById('worksUploadText').textContent = 'Uploaded: ' + path; // Apply AI-suggested fields if present and not already filled var t = document.getElementById('works-title'); if (d.suggested_title && !t.value.trim()) t.value = d.suggested_title; var m = document.getElementById('works-medium'); if (d.suggested_medium && !m.value.trim()) m.value = d.suggested_medium; if (Array.isArray(d.suggested_tags) && d.suggested_tags.length) { var tg = document.getElementById('works-tags'); var current = (tg.value || '').split(',').map(function(s) { return s.trim(); }).filter(Boolean); var seen = {}; current.forEach(function(s) { seen[s.toLowerCase()] = true; }); d.suggested_tags.forEach(function(s) { var k = String(s || '').toLowerCase(); if (k && !seen[k]) { current.push(s); seen[k] = true; } }); tg.value = current.join(', '); } } catch(e) { document.getElementById('worksUploadText').textContent = 'Upload failed: ' + e.message; } } async function worksSubmit() { var payload = { id: document.getElementById('works-id').value.trim(), title: document.getElementById('works-title').value.trim(), year: parseInt(document.getElementById('works-year').value, 10) || null, type: document.getElementById('works-type').value, medium: document.getElementById('works-medium').value.trim(), tags: document.getElementById('works-tags').value.split(',').map(function(s) { return s.trim(); }).filter(Boolean), description: document.getElementById('works-description').value.trim(), file_path: document.getElementById('works-file-path').value.trim(), }; var url = _worksEditingId ? '/api/works/' + encodeURIComponent(_worksEditingId) : '/api/works'; var method = _worksEditingId ? 'PUT' : 'POST'; try { var r = await fetch(url, { method: method, headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify(payload), }); if (!r.ok) { var msg = ''; try { msg = (await r.json()).detail || ''; } catch(_) {} throw new Error(msg || 'HTTP ' + r.status); } if (typeof toast === 'function') toast('Saved'); else alert('Saved'); worksCloseModal(); loadWorksGrid(); } catch(e) { alert('Save failed: ' + e.message); } } async function worksExport() { try { var r = await fetch('/api/works', { credentials: 'same-origin' }); var data = await r.json(); var blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'works-' + new Date().toISOString().slice(0,10) + '.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch(e) { alert('Export failed: ' + e.message); } } function worksImport() { var input = document.createElement('input'); input.type = 'file'; input.accept = '.json,application/json'; input.onchange = async function() { if (!input.files || !input.files.length) return; var text = await input.files[0].text(); try { var data = JSON.parse(text); if (!Array.isArray(data)) throw new Error('Expected an array of works'); if (!confirm('Import ' + data.length + ' works? This adds/updates by id, does not delete existing.')) return; var ok = 0, failed = 0; for (var i = 0; i < data.length; i++) { var w = data[i]; try { var r = await fetch('/api/works/' + encodeURIComponent(w.id), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify(w), }); if (r.ok) ok++; else { var r2 = await fetch('/api/works', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify(w), }); if (r2.ok) ok++; else failed++; } } catch(_) { failed++; } } alert('Imported ' + ok + ' / ' + data.length + ' (' + failed + ' failed)'); loadWorksGrid(); } catch(e) { alert('Import failed: ' + e.message); } }; input.click(); } // Drag-drop on the upload zone (function setupWorksDragDrop() { document.addEventListener('DOMContentLoaded', function() { var z = document.getElementById('worksUploadZone'); if (!z) return; z.addEventListener('dragover', function(e) { e.preventDefault(); z.style.borderColor = 'var(--fg)'; }); z.addEventListener('dragleave', function(e) { z.style.borderColor = 'var(--line)'; }); z.addEventListener('drop', function(e) { e.preventDefault(); z.style.borderColor = 'var(--line)'; if (e.dataTransfer.files && e.dataTransfer.files.length) { var inp = document.getElementById('worksFileInput'); inp.files = e.dataTransfer.files; worksHandleFile(inp); } }); }); })(); async function savePage() { if (!editingPageId) return; // Name-required gate: users start unnamed (name-deferral), but Save won't go through until they // give it a real title. Keeps the save serializer untouched — pure pre-save validation. var _nm = (document.getElementById('editorTitle') && document.getElementById('editorTitle').value || '').trim(); if (!_nm || /^untitled(\b|-|\s)/i.test(_nm)) { toast('Name your page first — set a title at the top, then Save.', true); var _ti = document.getElementById('editorTitle'); if (_ti) { try { _ti.focus(); _ti.select(); } catch(e){} _ti.style.outline = '2px solid var(--bad, #cf3b3b)'; _ti.style.outlineOffset = '2px'; setTimeout(function(){ _ti.style.outline=''; _ti.style.outlineOffset=''; }, 2200); } return; } var _slugInput = document.getElementById('editorSlug'); var _requestedSlug = slugify((_slugInput && _slugInput.value) || '') || ((allPages || []).find(function(p) { return Number(p.id) === Number(editingPageId); }) || {}).slug || ''; // Inventory-zone search results remain in the transient UI cache and are // deliberately excluded from durable page and CanvasDocument writes. // If we have AI HTML, save the current iframe state. Match the autosave // priority order EXACTLY so manual Save and autosave are serializer-parity: // 1) .canvas-raw-backdrop (canvas-mode forked pages — source of truth) // 2) #editorLiveFrame (unified-mode live preview) // 3) _aiBuildHtml (last cached AI build) // Previously this only checked editorLiveFrame, so manual Save on a // canvas-mode forked page would read an empty/stale live preview iframe // (it's hidden in canvas mode) and could send '' // — which the server would treat as a real wipe of raw_html. The autosave // path was already correct; manual Save is now matched to it. // FIELDS templates (linktree, store, pitchdeck...) are content-driven: the field values are the // source of truth, NOT the rendered iframe. The live preview sets _aiBuildHtml, which used to make // Save persist raw_html and silently drop the user's field edits. Skip the html branch in fields-mode // so Save always writes the structured content (and clears stale raw_html so the live page re-renders). var _fieldsMode = !!document.querySelector('.editor-split.fields-mode'); if (_aiBuildHtml && !_fieldsMode) { try { // Same strip + serialize helper autosave uses — removes our hover/focus // affordance ' + elementHtml + ''; } else { var bodyClose = _studioHtml.lastIndexOf(''); if (bodyClose !== -1) { _studioHtml = _studioHtml.substring(0, bodyClose) + elementHtml + '\n' + _studioHtml.substring(bodyClose); } else { _studioHtml += elementHtml; } } var frame = document.getElementById('studioFrame'); if (frame) frame.srcdoc = _studioHtml; studioSyncActionButtons(); } // Click on canvas background to deselect all layers document.addEventListener('click', function(e) { if (!_isStudioActive()) return; var container = document.getElementById('studioLayerContainer'); if (!container) return; // If click is on the container itself (not a layer or its children) if (e.target === container || e.target.id === 'studioFrame') { _studioSelectedLayerId = null; var all = container.querySelectorAll('.studio-layer'); for (var i = 0; i < all.length; i++) all[i].classList.remove('sl-selected'); studioRenderLayerPanel(); } }); // Keyboard: Delete/Backspace removes selected layer, Escape deselects document.addEventListener('keydown', function(e) { if (!_isStudioActive() || !_studioSelectedLayerId) return; if (e.target.isContentEditable || e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; if (e.key === 'Delete' || e.key === 'Backspace') { e.preventDefault(); studioRemoveLayer(_studioSelectedLayerId); } else if (e.key === 'Escape') { _studioSelectedLayerId = null; var container = document.getElementById('studioLayerContainer'); if (container) { var all = container.querySelectorAll('.studio-layer'); for (var i = 0; i < all.length; i++) all[i].classList.remove('sl-selected'); } studioRenderLayerPanel(); } }); var GENERATION_TRIGGERS = /\b(make|create|build|generate|design|give me|can you make|can you build|i want)\b.{0,60}\b(website|site|page|web page|landing page|portfolio)\b|\b(website|site|page)\b.{0,40}\b(about|for|with|featuring|based on)\b/i; var EDIT_TRIGGER = /\b(edit|update|modify|change|fix|improve|redo|adjust)\b\s+\/?([\w][\w-]{0,39})\b/i; var PAYMENT_TRIGGER = /\b(create|make|generate|set up|spin up|add)\b.{0,40}\b(payment link|checkout link|buy link|purchase link|plan|credit pack|pay link)\b|\b(payment|checkout|purchase)\b.{0,20}\b(link|url|page)\b/i; var INVENTORY_TRIGGER = /\b(my inventory|from my inventory|use my|items from|my items|my images|my collection)\b/i; function studioEscapeRx(text) { return String(text || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function studioLog(msg, type) { var log = document.getElementById('studioChatLog'); if (!log) return; var div = document.createElement('div'); div.className = 'smsg smsg--' + (type || 'sys'); if (type === 'user') { div.style.background = '#0f1115'; div.style.border = '1px solid rgba(255,255,255,0.08)'; div.style.alignSelf = 'flex-end'; div.style.maxWidth = '85%'; div.style.setProperty('color', '#f3f4f6', 'important'); div.style.setProperty('-webkit-text-fill-color', '#f3f4f6'); } else if (type === 'agent') { div.style.background = 'var(--surface)'; div.style.alignSelf = 'flex-start'; div.style.maxWidth = '85%'; div.style.setProperty('color', 'var(--fg)', 'important'); } else { div.style.alignSelf = 'center'; div.style.fontSize = '10px'; div.style.setProperty('color', 'var(--dim)', 'important'); } div.textContent = msg; log.appendChild(div); log.scrollTop = log.scrollHeight; } function studioHandleFiles(files) { _studioAttachments = []; var preview = document.getElementById('studioAttachPreview'); if (preview) preview.innerHTML = ''; Array.from(files).slice(0, 3).forEach(function(f) { var item = { name: f.name, file: f, mime: f.type || 'application/octet-stream', size: f.size || 0, dataUrl: '', url: '', stored_name: '', is_image: /^image\//i.test(f.type || '') }; _studioAttachments.push(item); var reader = new FileReader(); reader.onload = function(e) { item.dataUrl = e.target.result; if (preview) { var chip = document.createElement('div'); chip.style.cssText = 'display:inline-flex;align-items:center;gap:6px;padding:4px 6px;border:1px solid rgba(255,255,255,0.08);border-radius: 0px;background:#111;color:var(--dim);'; if (item.is_image && item.dataUrl) { var img = document.createElement('img'); img.src = item.dataUrl; img.alt = f.name; img.style.cssText = 'width:18px;height:18px;border-radius: 0px;object-fit:cover;'; chip.appendChild(img); } var label = document.createElement('span'); label.textContent = f.name; chip.appendChild(label); preview.appendChild(chip); } }; reader.readAsDataURL(f); }); } async function studioEnsureUploads() { var pending = _studioAttachments.filter(function(item) { return item && item.file && !item.url; }); if (!pending.length) { return _studioAttachments.filter(function(item) { return item && item.url; }).map(function(item) { return { name: item.name, url: item.url, content_type: item.mime || '', is_image: !!item.is_image, size: item.size || 0, stored_name: item.stored_name || '' }; }); } var form = new FormData(); pending.forEach(function(item) { form.append('files', item.file); }); var res = await fetch('/api/atcmd/upload', { method: 'POST', body: form, credentials: 'same-origin' }); var payload = await res.json().catch(function() { return {}; }); if (!res.ok) throw new Error(payload.detail || payload.message || 'attachment upload failed'); (payload.files || []).forEach(function(saved, idx) { var item = pending[idx]; if (!item || !saved) return; item.url = saved.url || ''; item.stored_name = saved.stored_name || ''; item.is_image = !!saved.is_image; }); return _studioAttachments.filter(function(item) { return item && item.url; }).map(function(item) { return { name: item.name, url: item.url, content_type: item.mime || '', is_image: !!item.is_image, size: item.size || 0, stored_name: item.stored_name || '' }; }); } function studioApplyAttachmentRefs(html, attachments) { var out = String(html || ''); (attachments || []).forEach(function(att, idx) { var url = String(att && att.url || '').trim(); if (!url) return; var tokens = [ att.name, att.stored_name, 'attachment_' + (idx + 1), 'attachment-' + (idx + 1), 'attached_image_' + (idx + 1), 'asset://attachment/' + (idx + 1) ].filter(Boolean); tokens.forEach(function(token) { var rx = new RegExp("(src\\s*=\\s*['\\\"])" + studioEscapeRx(token) + "(['\\\"])", 'gi'); out = out.replace(rx, '$1' + url + '$2'); }); }); return out; } function studioPreviewUrl(slug) { var host = (window.location && window.location.hostname) ? window.location.hostname : 'aleqth.com'; var username = (currentUser && currentUser.username) ? currentUser.username : 'aleqth'; var safeSlug = slugify(slug || '') || 'my-site'; return host + '/' + username + '/' + safeSlug; } function studioRefreshSlugPreview() { var input = document.getElementById('studioSlugInput'); var preview = document.getElementById('studioUrlPreview'); if (!input || !preview) return; preview.textContent = studioPreviewUrl(input.value); } function studioSuggestSlug(prompt) { var text = String(prompt || '').trim().toLowerCase(); if (!text) return ''; text = text.replace(/^\/[a-z0-9-]+\s*/i, ''); text = text.replace(/^(can you|could you|please|make|build|create|generate|design)\s+/i, ''); text = text.replace(/^(a|an|the)\s+/i, ''); text = text.replace(/\b(page|site|website|landing page)\b/gi, ' '); text = text.replace(/\b(with|that has|featuring|using|put|this|image|images|on|the|some)\b/gi, ' '); text = slugify(text.replace(/\s+/g, '-')); if (text.length > 40) text = text.substring(0, 40).replace(/-+$/g, ''); return text; } function studioMaybeSuggestSlug(prompt) { var input = document.getElementById('studioSlugInput'); if (!input || _studioPageId || input.dataset.edited === '1') { studioRefreshSlugPreview(); return; } if (input.value.trim()) { studioRefreshSlugPreview(); return; } var suggested = studioSuggestSlug(prompt); if (suggested) input.value = suggested; studioRefreshSlugPreview(); } function studioSyncActionButtons() { var saveBtn = document.getElementById('studioSaveBtn'); var updateBtn = document.getElementById('studioUpdateBtn'); var saveListBtn = document.getElementById('studioSaveListBtn'); var hasManualLayers = _studioLayers && _studioLayers.length > 1; var hasHtml = !!(_studioHtml && _studioHtml.trim()) || hasManualLayers; if (saveBtn) { saveBtn.style.display = _studioPageId ? 'none' : ''; saveBtn.disabled = !hasHtml || _studioRunning; } if (updateBtn) { updateBtn.style.display = _studioPageId ? '' : 'none'; updateBtn.disabled = !hasHtml || _studioRunning; } if (saveListBtn) { saveListBtn.disabled = !hasHtml || _studioRunning; } } function openStudio(prompt, pageId) { _studioPageId = pageId || null; _studioHtml = ''; _studioConvHistory = []; _studioAttachments = []; studioResetLayers(); var _el = function(id) { return document.getElementById(id); }; var overlay = _el('studioOverlay'); if (!overlay) { // Studio overlay not in DOM — fall through to AI chat instead if (typeof termAIChat === 'function') termAIChat(prompt || 'create a website'); return; } overlay.classList.add('active'); var chatLog = _el('studioChatLog'); if (chatLog) chatLog.innerHTML = ''; var slugInput = _el('studioSlugInput'); if (slugInput) { slugInput.value = ''; slugInput.dataset.edited = ''; } var frame = _el('studioFrame'); if (frame) frame.srcdoc = ''; var attachPrev = _el('studioAttachPreview'); if (attachPrev) attachPrev.innerHTML = ''; var promptInput = _el('studioPromptInput'); if (promptInput) promptInput.value = ''; studioRefreshSlugPreview(); studioSyncActionButtons(); document.body.style.overflow = 'hidden'; if (pageId) { document.getElementById('studioStatus').textContent = 'loading page...'; apiFetch('/api/studio/page/' + pageId).then(function(d) { _studioHtml = d.html || ''; if (_studioHtml) document.getElementById('studioFrame').srcdoc = _studioHtml; document.getElementById('studioSlugInput').value = d.slug || ''; document.getElementById('studioSlugInput').dataset.edited = '1'; studioRefreshSlugPreview(); document.getElementById('studioStatus').textContent = 'editing ' + d.slug; studioSyncActionButtons(); if (prompt) { studioLog(prompt, 'user'); studioGenerate(prompt); } else { studioLog('Page loaded. Describe what you want to change.', 'sys'); document.getElementById('studioPromptInput').focus(); } }).catch(function() { studioLog('Could not load page. Starting fresh.', 'sys'); _studioPageId = null; studioMaybeSuggestSlug(prompt); studioSyncActionButtons(); if (prompt) studioGenerate(prompt); }); } else { studioMaybeSuggestSlug(prompt); if (prompt) { studioLog(prompt, 'user'); studioGenerate(prompt); } else { studioLog('Describe the website you want to create.', 'sys'); document.getElementById('studioPromptInput').focus(); } } } function closeStudio() { document.getElementById('studioOverlay').classList.remove('active'); document.body.style.overflow = ''; if (_studioGenDotsInterval) { clearInterval(_studioGenDotsInterval); _studioGenDotsInterval = null; } } async function studioGenerate(prompt) { if (_studioRunning) return; _studioRunning = true; document.getElementById('studioStatus').textContent = 'generating...'; document.getElementById('studioGenerating').style.display = 'block'; studioMaybeSuggestSlug(prompt); studioSyncActionButtons(); var dots = 0; _studioGenT0 = Date.now(); _studioGenAcc = ''; studioSetProgress(''); _studioGenDotsInterval = setInterval(function() { dots = (dots + 1) % 4; var el = document.getElementById('studioGenDots'); if (el) el.textContent = '.'.repeat(dots + 1); studioSetProgress(_studioGenAcc); }, 400); _studioConvHistory.push({ role: 'user', content: prompt }); var accumulated = ''; var lastUpdate = 0; var frame = document.getElementById('studioFrame'); var uploadedAttachments = []; // Streaming helper — reads plain text stream and updates iframe live async function streamInto(fetchRequest) { var res = null; for (var attempt = 0; attempt < 2; attempt++) { res = await (typeof fetchRequest === 'function' ? fetchRequest(attempt) : fetchRequest); if (res.ok) break; var retryable = (res.status === 502 || res.status === 504); if (retryable && attempt === 0) { studioLog('Server worker hiccup — retrying once...', 'sys'); try { await new Promise(function(resolve) { setTimeout(resolve, 1200); }); } catch(_wait) {} continue; } var errDetail = ''; try { var errBody = await res.json(); errDetail = errBody.detail || ''; } catch(_e) {} throw new Error(errDetail || ('HTTP ' + res.status)); } var reader = res.body.getReader(); var decoder = new TextDecoder(); accumulated = ''; while (true) { var chunk = await reader.read(); if (chunk.done) break; accumulated += decoder.decode(chunk.value, { stream: true }); _studioGenAcc = accumulated; var now = Date.now(); if (now - lastUpdate > 120) { lastUpdate = now; if (accumulated.includes(' 0) accumulated = accumulated.substring(start); frame.srcdoc = accumulated; return accumulated; } try { try { uploadedAttachments = await studioEnsureUploads(); } catch (uploadErr) { studioLog('Attachment upload failed — image references may not render.', 'sys'); } // Include manual layer context so the AI knows about overlaid elements var layerCtx = studioGetLayerContext(); var existingWithLayers = (_studioHtml && _studioConvHistory.length > 1) ? _studioHtml.substring(0, 28000) : ''; if (layerCtx) existingWithLayers = existingWithLayers + '\n' + layerCtx; var studioGenerationPayload = JSON.stringify({ prompt: prompt, existing_html: existingWithLayers, attachments: uploadedAttachments, page_id: _studioPageId, page_slug: (document.getElementById('studioSlugInput') || {}).value || '' }); accumulated = await streamInto(function(attempt) { return fetch('/api/terminal/generate', { method: 'POST', credentials: 'include', headers: _ankleAttachAuth({ 'Content-Type': 'application/json', 'X-Ankle-Generation-Attempt': String((attempt || 0) + 1) }), body: studioGenerationPayload }); }); } catch(err) { var _smsg = err && err.message ? String(err.message) : 'unavailable'; var _shint; if (/\b(401|403|Auth)/i.test(_smsg)) _shint = 'sign in expired — reload and try again.'; else if (/\b503\b/.test(_smsg)) _shint = 'no API keys configured on the server.'; else if (/\b50[024]\b|timed?\s*out|closed|reset|prematurely/i.test(_smsg)) _shint = 'server hiccup — try again in a moment.'; else if (/\b429\b|rate.?limit/i.test(_smsg)) _shint = 'rate limit — wait a moment and retry.'; else _shint = 'try again, and if it keeps failing share this message.'; studioLog('Generation failed: ' + _smsg + ' — ' + _shint, 'sys'); document.getElementById('studioStatus').textContent = 'error'; clearInterval(_studioGenDotsInterval); _studioGenDotsInterval = null; document.getElementById('studioGenerating').style.display = 'none'; _studioRunning = false; studioSyncActionButtons(); return; } accumulated = studioApplyAttachmentRefs(accumulated, uploadedAttachments); // Check if the response is an error page, not real content if (accumulated.indexOf('Generation unavailable') !== -1 || accumulated.indexOf('all providers busy') !== -1) { studioLog('AI providers are busy. Try again in a moment.', 'sys'); document.getElementById('studioStatus').textContent = 'providers busy'; clearInterval(_studioGenDotsInterval); _studioGenDotsInterval = null; document.getElementById('studioGenerating').style.display = 'none'; _studioRunning = false; studioSyncActionButtons(); return; } _studioHtml = accumulated; frame.srcdoc = accumulated; (function(){ var b=document.getElementById('studioGenBar'); if(b)b.style.width='100%'; var s=document.getElementById('studioGenStage'); if(s)s.textContent='done'; })(); _studioConvHistory.push({ role: 'assistant', content: '(generated HTML · ' + accumulated.length + ' chars)' }); studioLog('Generated · ' + accumulated.length + ' chars. Iterate or save.', 'sys'); document.getElementById('studioStatus').textContent = 'ready'; clearInterval(_studioGenDotsInterval); _studioGenDotsInterval = null; document.getElementById('studioGenerating').style.display = 'none'; _studioRunning = false; studioSyncActionButtons(); document.getElementById('studioPromptInput').focus(); } function studioSend() { var input = document.getElementById('studioPromptInput'); var prompt = input.value.trim(); if (!prompt || _studioRunning) return; input.value = ''; studioLog(prompt, 'user'); studioGenerate(prompt); } // Merge manual layers into HTML for saving function studioBakeLayersIntoHtml() { var html = _studioHtml || ''; var manualLayers = _studioLayers.filter(function(l) { return l.id !== 'L0' && l.visible; }); if (!manualLayers.length) return html; var layerHtml = '\n\n'; manualLayers.forEach(function(l) { var style = 'position:absolute;left:' + l.x + 'px;top:' + l.y + 'px;z-index:' + (10 + _studioLayers.indexOf(l)) + ';'; if (l.width) style += 'width:' + l.width + 'px;'; if (l.height) style += 'height:' + l.height + 'px;'; if (l.type === 'text') { layerHtml += '
' + (l.text || '').replace(//g, '>') + '
\n'; } else if (l.type === 'image' || l.type === 'inventory') { layerHtml += '\n'; } else if (l.type === 'video') { layerHtml += '\n'; } }); if (!html) { return '' + layerHtml + ''; } var bodyClose = html.lastIndexOf(''); if (bodyClose !== -1) { return html.substring(0, bodyClose) + layerHtml + html.substring(bodyClose); } return html + layerHtml; } async function studioSave() { var mergedHtml = studioBakeLayersIntoHtml(); if (!mergedHtml || !mergedHtml.trim()) { toast('Nothing generated yet'); return; } var siteName = document.getElementById('studioSlugInput').value.trim() || 'my-site'; var slug = siteName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').substring(0, 40); var title = siteName; var btn = document.getElementById(_studioPageId ? 'studioUpdateBtn' : 'studioSaveBtn'); btn.disabled = true; btn.textContent = 'saving...'; try { var d = await apiFetch('/api/studio/save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ html: mergedHtml, slug: slug, title: title, page_id: _studioPageId }) }); toast('Live → ' + d.url); window.open(d.url, '_blank'); closeStudio(); loadPages(); } catch(e) { toast('Save failed: ' + (e.message || 'error')); } finally { btn.textContent = _studioPageId ? 'Update Live' : 'Save Live'; studioSyncActionButtons(); } } async function studioSaveAndList() { var mergedHtml = studioBakeLayersIntoHtml(); if (!mergedHtml || !mergedHtml.trim()) { toast('Nothing generated yet'); return; } var priceStr = window.prompt('Set credit price for market listing (minimum 10):'); if (priceStr === null) return; // cancelled var price = parseInt(priceStr, 10); if (isNaN(price) || price < 10) { toast('Price must be at least 10 credits'); return; } var listBtn = document.getElementById('studioSaveListBtn'); listBtn.disabled = true; listBtn.textContent = 'saving...'; var siteName = document.getElementById('studioSlugInput').value.trim() || 'my-site'; var slug = siteName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').substring(0, 40); var title = siteName; try { // Step 1: Save the page var d = await apiFetch('/api/studio/save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ html: mergedHtml, slug: slug, title: title, page_id: _studioPageId }) }); var pageId = d.page_id || d.id; if (!pageId) { toast('Saved but could not get page ID for listing'); return; } // Step 2: List on market var listRes = await apiFetch('/api/pages/' + pageId + '/list', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ price_credits: price }) }); toast('Listed on market for ' + price + ' credits — ' + (d.url || slug)); window.open(d.url, '_blank'); closeStudio(); loadPages(); } catch(e) { toast('Save & List failed: ' + (e.message || 'error')); } finally { listBtn.textContent = 'Save & List on Market'; studioSyncActionButtons(); } } var KNOWN_PROTOCOLS = ['weather','space','quote','web','radio','surf','dada','gif','site','data','search','remix','compose','feed','profile','upload','status']; function termShowProfile() { if (!window._agentBriefing) { termAppendMsg('
@⌘
No profile yet. Keep creating, chatting, and collecting — the agent is learning.
'); return; } var rendered = window._agentBriefing.replace(/&/g,'&').replace(//g,'>').replace(/\n/g,'
'); termAppendMsg('
@⌘ profile
' + rendered + '
'); } var _termChatHistory = []; async function termSend() { if (_termRunning) return; var input = document.getElementById('termInput'); var raw = input.value.trim(); if (!raw) return; _termHistory.push(raw); _termHistIdx = -1; input.value = ''; _termRunning = true; /* termSendBtn never disabled — prevents stuck state */ // Safety: auto-reset if stuck for 30s setTimeout(function() { if (_termRunning) { _termRunning = false; document.getElementById('termSendBtn').disabled = false; var typing = document.getElementById('termTypingInd'); if (typing) typing.remove(); } }, 30000); // Detect if this is a known slash protocol or natural language var isSlash = raw.startsWith('/'); var slug = isSlash ? raw.replace(/^\//, '').split(/\s+/)[0].toLowerCase() : ''; var isKnownProtocol = isSlash && KNOWN_PROTOCOLS.indexOf(slug) !== -1; // /sync — re-synthesize agent profile if (slug === 'sync' || slug === 'profile') { termAppendMsg('
you
' + escHtml(raw) + '
'); input.value = ''; if (slug === 'profile') { termShowProfile(); return; } termAppendMsg('
synthesizing profile...
'); apiFetch('/api/agent/synthesize', { method: 'POST', headers: {'Content-Type':'application/json'}, body: '{}' }).then(function() { var t = document.getElementById('termTypingInd'); if (t) t.remove(); setTimeout(function() { fetch('/api/agent/profile').then(function(r){return r.json();}).then(function(d) { window._agentBriefing = d.briefing; var meta = document.getElementById('termInputMeta'); if (meta) meta.innerHTML = 'agent synced · ' + (d.interaction_count||0) + ' interactions · view profile'; termShowProfile(); }); }, 12000); }); return; } termAppendMsg( '
' + '
you
' + '
' + escHtml(raw) + '
' + '
' ); termAppendMsg( '
' + '
' + (isKnownProtocol ? 'running...' : 'thinking...') + '
' + '
' ); if (isKnownProtocol) { // Route to protocol runner var parts = raw.replace(/^\//, '').split(/\s+/); var rest = parts.slice(1).join(' '); var inputData = {}; if (slug === 'data' && rest) inputData.url = rest; else if (slug === 'search' && rest) inputData.q = rest; else if (slug === 'remix' && rest) inputData.remix_code = rest; else if (rest) inputData.query = rest; try { var res = await fetch('/api/terminal/run', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ protocol_slug: slug, input: inputData, share_to_feed: false }) }); var data = await res.json(); var typing = document.getElementById('termTypingInd'); if (typing) typing.remove(); if (!res.ok) { termAppendMsg( '
system
' + '
' + escHtml(data.detail || 'Error') + '
' ); } else { var output = data.output || {}; if (output.type === 'redirect') { termAppendMsg(termRenderOutput(output, data.status, data.duration_ms, data.artifacts)); setTimeout(function() { window.location.href = output.url; }, 600); } else { termAppendMsg(termRenderOutput(output, data.status, data.duration_ms, data.artifacts)); } } } catch(e) { var typing = document.getElementById('termTypingInd'); if (typing) typing.remove(); termAppendMsg( '
system
' + '
Network error: ' + escHtml(e.message) + '
' ); } } else { var genMatch = GENERATION_TRIGGERS.test(raw); var editMatch = EDIT_TRIGGER.exec(raw); var paymentMatch = PAYMENT_TRIGGER.test(raw); var inventoryMatch = INVENTORY_TRIGGER.test(raw); // Payment link creation if (paymentMatch && !genMatch) { var typing2 = document.getElementById('termTypingInd'); if (typing2) typing2.remove(); termAIPaymentLink(raw); return; } // Generation intent — open studio if (genMatch || (isSlash && (slug === 'create' || slug === 'make' || slug === 'generate' || slug === 'build'))) { var typing2 = document.getElementById('termTypingInd'); if (typing2) typing2.remove(); _termRunning = false; document.getElementById('termSendBtn').disabled = false; var cleanPrompt = raw.replace(/^\/(create|make|generate|build)\s*/i, ''); // Inject inventory context if requested if (inventoryMatch && window._dashboardInvItems && window._dashboardInvItems.length) { var invSummary = window._dashboardInvItems.slice(0, 20).map(function(item) { return (item.title || item.filename || 'item') + (item.url ? ' (' + item.url + ')' : ''); }).join('\n'); cleanPrompt = cleanPrompt + '\n\n[Inventory items available:\n' + invSummary + ']'; } termAppendMsg( '
@⌘
' + '
Opening studio...
' ); openStudio(cleanPrompt, null); return; } // Edit existing page if (editMatch) { var editSlug = editMatch[2]; var typing2 = document.getElementById('termTypingInd'); if (typing2) typing2.remove(); apiFetch('/api/pages').then(function(d) { var pages = (d.pages || d.recent || []); var found = pages.find(function(p) { return p.slug === editSlug; }); if (found) { _termRunning = false; document.getElementById('termSendBtn').disabled = false; termAppendMsg( '
@⌘
' + '
Opening studio for /' + escHtml(editSlug) + '...
' ); openStudio(raw, found.id); } else { _termRunning = false; document.getElementById('termSendBtn').disabled = false; termAIChat(raw); } }).catch(function() { _termRunning = false; document.getElementById('termSendBtn').disabled = false; termAIChat(raw); }); return; } // Default: AI chat try { await termAIChat(raw); } catch(e) { var typing = document.getElementById('termTypingInd'); if (typing) typing.remove(); } } _termRunning = false; document.getElementById('termSendBtn').disabled = false; document.getElementById('termInput').focus(); } window.termLegacyDashboardSend = termSend; async function termAIChat(raw) { _termChatHistory.push({ role: 'user', content: raw }); try { var res = await fetch('/api/terminal/chat', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ message: raw, history: _termChatHistory.slice(-10) }) }); var typing = document.getElementById('termTypingInd'); if (typing) typing.remove(); var data = await res.json(); if (!res.ok) { termAppendMsg( '
@⌘
' + '
' + escHtml(data.detail || 'Unknown command. Try /weather, /space, /quote, or just ask me anything.') + '
' ); } else { var reply = data.reply || ''; _termChatHistory.push({ role: 'assistant', content: reply }); // Detect HTML code block in reply var htmlMatch = reply.match(/```(?:html)?\s*()\s*```/i) || reply.match(/()/i); var extractedHtml = htmlMatch ? htmlMatch[1].trim() : null; // Extract suggested title from HTML var suggestedTitle = ''; if (extractedHtml) { var titleMatch = extractedHtml.match(/([^<]*)<\/title>/i); suggestedTitle = titleMatch ? titleMatch[1].trim() : ''; } // Render reply — if it has HTML, show truncated preview instead of full dump var displayReply = reply; if (extractedHtml) { var before = reply.substring(0, reply.indexOf(htmlMatch[0])).trim(); displayReply = (before ? before + '\n\n' : '') + '[ok] HTML generated (' + extractedHtml.length + ' chars)'; } var rendered = displayReply .replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') .replace(/`([^`]+)`/g,'<code style="background:rgba(255,255,255,0.08);padding:1px 5px;border-radius: 0px;font-family:monospace;">$1</code>') .replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>') .replace(/\n/g,'<br>'); // Build save UI if HTML detected var saveUi = ''; if (extractedHtml) { var safeHtml = extractedHtml.replace(/'/g, "\\'").replace(/\n/g, '\\n'); var uid = 'termSave_' + Date.now(); saveUi = '<div id="' + uid + '" style="margin-top:12px;padding:10px 12px;background:#111;border:1px solid #2a2a2a;border-radius: 0px;">' + '<div style="font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:#555;margin-bottom:8px;">Ready to deploy</div>' + '<input id="' + uid + '_title" class="input" placeholder="Site name (e.g. Orange Studio)" value="' + escHtml(suggestedTitle) + '" style="width:100%;margin-bottom:8px;font-size:16px;" />' + '<div style="display:flex;gap:8px;flex-wrap:wrap;">' + '<button class="btn btn--primary" onclick="termSaveAndOpen(\'' + uid + '\')" style="flex:1;">* Open Live</button>' + '<button class="btn" onclick="termOpenStudioFromTerm(\'' + uid + '\')" style="flex:1;">Edit in Studio</button>' + '</div>' + '<div id="' + uid + '_status" style="font-size:10px;color:var(--dim);margin-top:6px;display:none;"></div>' + '</div>'; // Store the HTML on the element after rendering window['_termHtml_' + uid] = extractedHtml; } var opsUi = ''; if (data.ops_suggestion && data.ops_suggestion.goal) { var opsUid = '_termOps_' + Date.now(); window[opsUid] = data.ops_suggestion.goal; opsUi = '<div style="margin-top:12px;padding:10px 12px;background:#111;border:1px solid #2a2a2a;border-radius: 0px;">' + '<div style="font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:#888;margin-bottom:8px;">Live execution available</div>' + '<div style="font-size:11px;color:#bbb;line-height:1.5;margin-bottom:10px;">Hermes can launch this as a real ops mission with file, deploy, and service tools.</div>' + '<div style="display:flex;gap:8px;flex-wrap:wrap;">' + '<button class="btn btn--primary" onclick="termLaunchOps(window[\'' + opsUid + '\'])" style="flex:1;">Launch Ops Mission</button>' + '<button class="btn" onclick="termTypeCmd(\'/ops \');document.getElementById(\'termInput\').focus();" style="flex:1;">Open /ops</button>' + '</div>' + '</div>'; } termAppendMsg( '<div class="term-msg term-msg--system"><div class="term-msg__label">@⌘</div>' + '<div class="term-msg__bubble"><div class="term-msg__text">' + rendered + saveUi + opsUi + '</div></div></div>' ); } } catch(e) { var typing = document.getElementById('termTypingInd'); if (typing) typing.remove(); termAppendMsg( '<div class="term-msg term-msg--system"><div class="term-msg__label">@⌘</div>' + '<div class="term-msg__bubble"><div class="term-msg__text" style="color:#e53935;">Agent offline. Try /weather, /space, /quote or other commands.</div></div></div>' ); } _termRunning = false; document.getElementById('termSendBtn').disabled = false; document.getElementById('termInput').focus(); } async function termLaunchOps(goal) { if (!goal) return; termAppendMsg( '<div class="term-msg term-msg--user">' + '<div class="term-msg__label term-msg__label--user">you</div>' + '<div class="term-msg__bubble"><div class="term-msg__text">' + escHtml('/ops ' + goal) + '</div></div>' + '</div>' ); termAppendMsg( '<div class="term-msg term-msg--system" id="termOpsLaunchTyping">' + '<div class="term-typing"><div class="term-typing__dots"><span></span><span></span><span></span></div> launching mission...</div>' + '</div>' ); try { var res = await fetch('/api/terminal/run', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ protocol_slug: 'ops', input: { query: goal }, share_to_feed: false }) }); var data = await res.json(); var typing = document.getElementById('termOpsLaunchTyping'); if (typing) typing.remove(); if (!res.ok) { termAppendMsg( '<div class="term-msg term-msg--system"><div class="term-msg__label">system</div>' + '<div class="term-msg__bubble"><div class="term-msg__text" style="color:#e53935;">' + escHtml(data.detail || 'Mission launch failed') + '</div></div></div>' ); return; } termAppendMsg(termRenderOutput(data.output || {}, data.status, data.duration_ms, data.artifacts)); } catch (e) { var typing = document.getElementById('termOpsLaunchTyping'); if (typing) typing.remove(); termAppendMsg( '<div class="term-msg term-msg--system"><div class="term-msg__label">system</div>' + '<div class="term-msg__bubble"><div class="term-msg__text" style="color:#e53935;">Network error: ' + escHtml(e.message || 'mission launch failed') + '</div></div></div>' ); } } async function termCheckOpsTask(taskId) { if (!taskId) return; try { var res = await fetch('/api/swarm-ops/task/' + encodeURIComponent(taskId)); var data = await res.json(); if (!res.ok) throw new Error(data.detail || 'Mission lookup failed'); var lastEvent = (data.log || []).length ? data.log[data.log.length - 1] : null; var lines = []; lines.push('Mission ' + taskId + ' · ' + (data.status || 'unknown')); lines.push('Agent: ' + (data.agent_name || data.agent_slug || 'Hermes')); if (data.deployed_url) lines.push('Live: ' + data.deployed_url); if (lastEvent && lastEvent.type === 'text' && lastEvent.text) lines.push(lastEvent.text); else if (lastEvent && lastEvent.type === 'error' && lastEvent.message) lines.push('Error: ' + lastEvent.message); else if (lastEvent && lastEvent.type) lines.push('Last event: ' + lastEvent.type); termAppendMsg( '<div class="term-msg term-msg--system"><div class="term-msg__label">ops</div>' + '<div class="term-msg__bubble"><div class="term-msg__text" style="white-space:pre-wrap;">' + escHtml(lines.join('\n')) + '</div></div></div>' ); } catch (e) { termAppendMsg( '<div class="term-msg term-msg--system"><div class="term-msg__label">ops</div>' + '<div class="term-msg__bubble"><div class="term-msg__text" style="color:#e53935;">' + escHtml(e.message || 'Could not fetch mission') + '</div></div></div>' ); } } async function termSaveAndOpen(uid) { var titleEl = document.getElementById(uid + '_title'); var statusEl = document.getElementById(uid + '_status'); var html = window['_termHtml_' + uid]; if (!html) { toast('No HTML to save'); return; } var title = (titleEl ? titleEl.value.trim() : '') || 'My Site'; var slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').substring(0, 40) || 'my-site'; if (statusEl) { statusEl.style.display = 'block'; statusEl.textContent = 'Saving...'; } try { var d = await apiFetch('/api/studio/save', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ html: html, slug: slug, title: title }) }); if (d && d.url) { if (statusEl) { statusEl.textContent = 'Live at ' + d.url; } toast('Live → ' + d.url); window.open(d.url, '_blank'); loadPages(); } } catch(e) { if (statusEl) { statusEl.textContent = 'Save failed'; } toast('Save failed: ' + (e.message || 'error')); } } async function termOpenStudioFromTerm(uid) { var html = window['_termHtml_' + uid]; var titleEl = document.getElementById(uid + '_title'); var title = (titleEl ? titleEl.value.trim() : '') || ''; if (!html) { toast('No HTML to edit'); return; } _studioHtml = html; openStudio(title); } async function termAIPaymentLink(raw) { var priceMatch = raw.match(/\$\s*(\d+(?:\.\d+)?)/); var creditsMatch = raw.match(/(\d[\d,]*)\s*(?:credits?|cr\b)/i); var labelMatch = raw.match(/(?:for|called|named|label(?:ed)?)\s+"?([^"$\d]{2,30})"?/i); var price = priceMatch ? parseFloat(priceMatch[1]) : 0; var credits = creditsMatch ? parseInt(creditsMatch[1].replace(/,/g, '')) : 0; var label = labelMatch ? labelMatch[1].trim() : (credits ? credits + ' Credits' : 'Credit Pack'); if (!price) { termAppendMsg('<div class="term-msg term-msg--system"><div class="term-msg__label">@⌘</div><div class="term-msg__bubble"><div class="term-msg__text">Specify a price — e.g. <em>create a payment link for $5 for 500 credits</em></div></div></div>'); _termRunning = false; document.getElementById('termSendBtn').disabled = false; return; } termAppendMsg('<div class="term-msg term-msg--system" id="termTypingInd"><div class="term-typing"><div class="term-typing__dots"><span></span><span></span><span></span></div> creating payment link...</div></div>'); try { var d = await apiFetch('/api/terminal/payment-link', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ price: price, credits: credits, label: label }) }); var typing = document.getElementById('termTypingInd'); if (typing) typing.remove(); var url = d.purchase_url || ''; termAppendMsg('<div class="term-msg term-msg--system"><div class="term-msg__label">@⌘</div><div class="term-msg__bubble"><div class="term-msg__text">[ok] Payment link — <strong>' + escHtml(label) + '</strong> $' + price.toFixed(2) + (credits ? ' · ' + credits.toLocaleString() + ' ◈' : '') + '<br><br><a href="' + escHtml(url) + '" target="_blank" style="color:var(--accent);word-break:break-all;">' + escHtml(url) + '</a></div></div></div>'); } catch(e) { var typing = document.getElementById('termTypingInd'); if (typing) typing.remove(); termAppendMsg('<div class="term-msg term-msg--system"><div class="term-msg__label">@⌘</div><div class="term-msg__bubble"><div class="term-msg__text" style="color:#e53935;">Failed: ' + escHtml(e.message||'error') + '. WHOP_API_KEY must be set.</div></div></div>'); } _termRunning = false; document.getElementById('termSendBtn').disabled = false; document.getElementById('termInput').focus(); } // ─── Inventory ─────────────────────────────────────────── async function loadInventory() { if (typeof _dashboardInventoryLoadPrimary === 'function') { return _dashboardInventoryLoadPrimary(); } } async function removeInventoryItem(id, el) { if (typeof _dashboardRemoveInventoryItemPrimary === 'function') { return _dashboardRemoveInventoryItemPrimary(id, el); } } </script> <!-- Template Preview Modal --> <div id="previewModal" class="preview-modal" style="display:none;"> <div class="preview-modal__chrome"> <div class="preview-modal__header"> <span class="preview-modal__title" id="previewTitle">Preview</span> <div class="preview-modal__actions"> <button class="btn btn--sm btn--primary" id="previewUseBtn">Use Template</button> <button class="preview-modal__close" id="previewCloseBtn">×</button> </div> </div> <iframe id="previewFrame" class="preview-modal__frame" src="about:blank"></iframe> </div> </div> <script src="/atcmd.js?v=20260310d"></script> <script> // ─── Inventory ─────────────────────────────────────────── async function loadInventory() { if (typeof _dashboardInventoryLoadPrimary === 'function') { return _dashboardInventoryLoadPrimary(); } } async function removeInventoryItem(id, el) { if (typeof _dashboardRemoveInventoryItemPrimary === 'function') { return _dashboardRemoveInventoryItemPrimary(id, el); } } function openStudioModal() { canvasActivateTextStudio(); } function closeStudioModal() { var modal = document.getElementById('studioModal'); var frame = document.getElementById('fontStudioFrame'); if (modal) modal.classList.remove('open'); if (frame) frame.src = 'about:blank'; } window.addEventListener('message', function(e) { if (!e.data || e.data.type !== 'studio-image') return; var src = e.data.src; var w = Math.min(e.data.width || 400, 600); var h = Math.min(e.data.height || 200, 400); if (typeof canvasElements !== 'undefined') { canvasElements.push({ type: 'image', src: src, x: 20, y: 20, width: w, height: h, zIndex: canvasElements.length + 1 }); renderCanvasElements(); } closeStudioModal(); }); </script> <div class="studio-modal" id="studioModal"> <div class="studio-modal__inner"> <div class="studio-modal__bar"> <span>Font Studio — design styled text, then click "Use in Page"</span> <button class="studio-modal__close" onclick="closeStudioModal()">Close</button> </div> <iframe id="fontStudioFrame" src="about:blank"></iframe> </div> </div> <script> var _codesData = []; function toggleCodeFields() { var t = document.getElementById('codeType').value; document.getElementById('cfItemLimit').style.display = t === 'item_limit' ? '' : 'none'; document.getElementById('cfDuration').style.display = (t === 'item_limit' || t === 'tier_grant') ? '' : 'none'; } async function loadCodes() { var wrap = document.getElementById('codesTableWrap'); wrap.innerHTML = '<div class="loading-state"><span class="spinner"></span></div>'; try { var d = await apiFetch('/api/archive/admin/codes'); _codesData = d.codes || []; renderCodesTable(); } catch(e) { wrap.innerHTML = '<div class="error-state">' + e.message + '</div>'; } } function renderCodesTable() { var wrap = document.getElementById('codesTableWrap'); if (!_codesData.length) { wrap.innerHTML = '<div class="empty-state">No codes generated yet.</div>'; return; } var html = '<table class="codes-table"><thead><tr>' + '<th>Code</th><th>Label</th><th>Type</th><th>Terms</th><th>Uses</th><th>Status</th><th>Created</th><th></th>' + '</tr></thead><tbody>'; _codesData.forEach(function(c) { var terms = ''; if (c.code_type === 'permanent') terms = c.tier + ' (permanent)'; else if (c.code_type === 'item_limit') { terms = (c.item_limit || 0).toLocaleString() + ' items'; if (c.duration_days) terms += ' / ' + c.duration_days + 'd'; } else if (c.code_type === 'tier_grant') { terms = c.tier + ' for ' + (c.duration_days || 30) + 'd'; } var statusCls = c.is_active ? 'code-badge--active' : 'code-badge--inactive'; var statusTxt = c.is_active ? 'active' : 'inactive'; if (c.max_uses && c.use_count >= c.max_uses) { statusCls = 'code-badge--inactive'; statusTxt = 'used up'; } var created = c.created_at ? c.created_at.split('T')[0] : ''; var redeemed = (c.redeemed_by || []).length; html += '<tr>' + '<td><span class="code-mono" onclick="copyCode(this)" title="Click to copy">' + c.code + '</span></td>' + '<td>' + (c.label || '<span style="color:var(--faint);">—</span>') + '</td>' + '<td><span class="code-badge">' + c.code_type + '</span></td>' + '<td class="code-terms">' + terms + '</td>' + '<td>' + c.use_count + '/' + (c.max_uses || '∞') + (redeemed ? ' (' + redeemed + ' users)' : '') + '</td>' + '<td><span class="code-badge ' + statusCls + '">' + statusTxt + '</span></td>' + '<td style="font-size:10px;color:var(--dim);">' + created + '</td>' + '<td>' + (c.is_active ? '<button class="btn btn--sm btn--danger" onclick="deactivateCode(' + c.id + ')">Revoke</button>' : '') + '</td>' + '</tr>'; }); html += '</tbody></table>'; wrap.innerHTML = html; } function copyCode(el) { navigator.clipboard.writeText(el.textContent).then(function() { toast('Code copied'); }); } async function generateCode() { var btn = document.getElementById('codeGenBtn'); btn.disabled = true; btn.textContent = 'Generating...'; try { var body = { label: document.getElementById('codeLabel').value.trim(), code_type: document.getElementById('codeType').value, tier: document.getElementById('codeTier').value, max_uses: parseInt(document.getElementById('codeMaxUses').value) || 1, }; var custom = document.getElementById('codeCustom').value.trim(); if (custom) body.code = custom; if (body.code_type === 'item_limit') { body.item_limit = parseInt(document.getElementById('codeItemLimit').value) || null; } if (body.code_type === 'item_limit' || body.code_type === 'tier_grant') { body.duration_days = parseInt(document.getElementById('codeDuration').value) || null; } var exp = document.getElementById('codeExpires').value; if (exp) body.expires_at = exp + 'T23:59:59'; var d = await apiFetch('/api/archive/admin/codes', { method: 'POST', credentials: 'same-origin', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body) }); toast('Code generated: ' + d.code.code); document.getElementById('codeLabel').value = ''; document.getElementById('codeCustom').value = ''; document.getElementById('codeItemLimit').value = ''; document.getElementById('codeDuration').value = ''; document.getElementById('codeExpires').value = ''; loadCodes(); } catch(e) { toast(e.message, true); } btn.disabled = false; btn.textContent = 'Generate Code'; } async function deactivateCode(id) { if (!confirm('Revoke this code? Users who already redeemed it keep their access.')) return; try { await apiFetch('/api/archive/admin/codes/' + id, { method: 'DELETE', credentials: 'same-origin' }); toast('Code revoked'); loadCodes(); } catch(e) { toast(e.message, true); } } document.getElementById('broadcastTarget').addEventListener('change', function() { document.getElementById('broadcastUsernamesField').style.display = this.value === 'specific' ? 'block' : 'none'; }); async function loadEmailStatus() { try { var d = await apiFetch('/api/admin/email/status'); var bar = document.getElementById('emailStatusBar'); bar.innerHTML = 'SMTP: ' + (d.smtp_configured ? '<span style="color:#34c759;">configured</span>' : '<span style="color:#e53935;">not configured</span>') + ' · ' + d.users_with_email + '/' + d.total_users + ' users have email on file'; } catch(e) {} } async function sendBroadcastEmail() { var subject = document.getElementById('broadcastSubject').value.trim(); var body = document.getElementById('broadcastBody').value.trim(); if (!subject || !body) { toast('Subject and message are required', true); return; } var target = document.getElementById('broadcastTarget').value; var payload = { subject: subject, body: body }; if (target === 'specific') { var usernames = document.getElementById('broadcastUsernames').value.split(',').map(function(u) { return u.trim(); }).filter(Boolean); if (!usernames.length) { toast('Enter at least one username', true); return; } payload.target = usernames; } else { payload.target = 'all'; } var btn = document.getElementById('broadcastEmailBtn'); btn.disabled = true; btn.textContent = 'Sending...'; try { var d = await apiFetch('/api/admin/broadcast/email', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); var msg = 'Email sent to ' + d.sent + ' users'; if (d.failed) msg += ', ' + d.failed + ' failed'; if (d.no_email) msg += ', ' + d.no_email + ' had no email'; if (!d.smtp_configured) msg += ' (SMTP not configured — notifications created only)'; toast(msg); } catch(e) { toast(e.message, true); } btn.disabled = false; btn.textContent = 'Send Email'; } async function sendBroadcastSms() { var body = document.getElementById('broadcastBody').value.trim(); if (!body) { toast('Message is required', true); return; } var btn = document.getElementById('broadcastSmsBtn'); btn.disabled = true; btn.textContent = 'Sending...'; try { var d = await apiFetch('/api/sms/broadcast', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: body }) }); toast('SMS broadcast: ' + (d.sent || 0) + ' sent, ' + (d.failed || 0) + ' failed'); } catch(e) { toast(e.message, true); } btn.disabled = false; btn.textContent = 'Send SMS'; } async function loadUsers() { var wrap = document.getElementById('usersTableWrap'); if (!wrap) return; wrap.innerHTML = '<div class="loading-state"><span class="spinner"></span></div>'; try { var users = await apiFetch('/api/admin/users'); if (!users.length) { wrap.innerHTML = '<div class="empty-state">No users.</div>'; return; } var html = '<table class="codes-table"><thead><tr><th></th><th>Username</th><th>Email</th><th>Provider</th><th>Tier</th><th>Joined</th></tr></thead><tbody>'; users.forEach(function(u) { var avatar = u.avatar_url ? '<img src="' + escHtml(u.avatar_url) + '" style="width:24px;height:24px;border-radius:50%;object-fit:cover;">' : '<div style="width:24px;height:24px;border-radius:50%;background:#eee;"></div>'; var joined = u.created_at ? new Date(u.created_at).toLocaleDateString() : ''; html += '<tr><td>' + avatar + '</td><td><a href="/' + escHtml(u.username) + '" target="_blank" style="color:var(--fg);">@' + escHtml(u.username) + '</a></td>' + '<td style="font-size:10px;color:var(--dim);">' + escHtml(u.email) + '</td>' + '<td style="font-size:10px;">' + escHtml(u.auth_provider) + '</td>' + '<td style="font-size:10px;">' + escHtml(u.tier) + '</td>' + '<td style="font-size:10px;color:var(--dim);">' + joined + '</td></tr>'; }); html += '</tbody></table>'; wrap.innerHTML = '<div style="margin-bottom:8px;font-size:10px;color:var(--dim);">' + users.length + ' total users</div>' + html; } catch(e) { wrap.innerHTML = '<div class="error-state" style="font-size:11px;color:var(--dim);">' + e.message + '</div>'; } } (function(){ var _termMirrorInitialized = false; var _termMirrorPanel = 'terminal'; var _termMirrorListenerBound = false; var _termMirrorTools = false; function termSafe(value) { if (window.escHtml) return escHtml(value == null ? '' : String(value)); var node = document.createElement('div'); node.textContent = value == null ? '' : String(value); return node.innerHTML; } function termBridge() { return window.__ALEQTH_ATCMD__ || null; } function termFmtDate(value) { if (!value) return ''; try { var date = new Date(value); if (isNaN(date.getTime())) return ''; return date.toLocaleString([], { month:'short', day:'numeric', hour:'2-digit', minute:'2-digit' }); } catch (e) { return ''; } } function termFmtSize(bytes) { var value = Number(bytes || 0); if (!value) return ''; if (value >= 1024 * 1024) return (value / (1024 * 1024)).toFixed(1) + ' MB'; if (value >= 1024) return Math.round(value / 1024) + ' KB'; return value + ' B'; } window.termNavigate = function(url, newTab) { var target = String(url || '').trim(); if (!target) return; if (window.bottaNavigate) { window.bottaNavigate(target, !!newTab); return; } if (newTab) { window.open(target, '_blank', 'noopener'); return; } window.location.href = target; }; function termRenderActionButtons(actions) { var items = Array.isArray(actions) ? actions.filter(function(action) { return action && action.url && action.label; }) : []; if (!items.length) return ''; return '<div class="term-msg__actions">' + items.map(function(action) { var primary = action.variant === 'primary' ? ' term-msg__action--primary' : ''; return '<button class="term-msg__action' + primary + '" type="button" data-url="' + termSafe(action.url) + '" data-target="' + termSafe(action.target || '') + '" onclick="termNavigate(this.dataset.url, this.dataset.target === \'blank\')">' + termSafe(action.label) + '</button>'; }).join('') + '</div>'; } function termRenderThinkingCard(state) { if (!state || !state.running) return ''; var provider = ''; if (state.ai && state.ai.providers && state.ai.providers.length) { provider = ((state.ai.providers[0].name || state.ai.preferred || 'ai') + ' · ' + (state.ai.providers[0].model || 'model')).toUpperCase(); } else if (state.ai && state.ai.preferred) { provider = String(state.ai.preferred).toUpperCase(); } return '<div class="term-msg term-msg--thinking">' + '<div class="term-msg__label">hermes</div>' + '<div class="term-msg__bubble">' + '<div class="term-typing"><div class="term-typing__dots"><span></span><span></span><span></span></div> ' + termSafe(state.runningNote || 'Hermes is formulating a response') + '</div>' + (provider ? ('<div class="term-msg__status">' + termSafe(provider) + '</div>') : '') + '</div>' + '</div>'; } function termRenderTranscript(state) { var log = document.getElementById('termLog'); if (!log) return; var transcript = state.transcript || []; if (!transcript.length && !state.running) { log.innerHTML = '<div class="term-msg term-msg--system"><div class="term-msg__label">system</div><div class="term-msg__bubble"><div class="term-msg__text">Ask @⌘ to do something. It can answer directly, execute a command, or build a workflow first.</div></div></div>'; return; } var html = transcript.map(function (entry) { var role = entry.role || 'system'; var label = role === 'assistant' ? '@⌘' : role; var attachments = (entry.attachments || []).map(function (file, idx) { var preview = file.preview_url || file.url || ''; var isImage = /^image\\//i.test(file.mime || ''); return '<div class="term-attach-chip">' + (isImage ? ('<img src="' + termSafe(preview) + '" alt="" loading="lazy">') : '<div style="width:28px;height:28px;border-radius: 0px;border:1px solid #202020;background:#090909;display:flex;align-items:center;justify-content:center;color:#666;font-size:10px;">' + termSafe((file.ext || 'file').slice(0,4)) + '</div>') + '<div class="term-attach-chip__body"><div class="term-attach-chip__name">' + termSafe(file.name || ('attachment ' + (idx + 1))) + '</div><div class="term-attach-chip__meta">' + termSafe(file.mime || file.kind || 'file') + (file.size ? ' · ' + termFmtSize(file.size) : '') + '</div></div>' + '</div>'; }).join(''); return '<div class="term-msg term-msg--' + termSafe(role) + '">' + '<div class="term-msg__label' + (role === 'user' ? ' term-msg__label--user' : '') + '">' + termSafe(label) + '</div>' + '<div class="term-msg__bubble">' + '<div class="term-msg__text">' + termSafe(entry.text || '') + '</div>' + (attachments ? ('<div class="term-attach-strip" style="padding:10px 0 0;background:none;">' + attachments + '</div>') : '') + termRenderActionButtons(entry.actions || []) + (entry.meta ? ('<div class="term-msg__status">' + termSafe(entry.meta) + '</div>') : '') + '</div></div>'; }).join(''); html += termRenderThinkingCard(state); log.innerHTML = html; log.scrollTop = log.scrollHeight; } function termRenderWorkflows(state) { var panel = document.getElementById('termWorkflowPanel'); if (!panel) return; var plan = state.plan || []; var html = '<div class="term-sidepanel__helper">Use workflows when you want @⌘ to break a request into steps before execution.</div>'; if (!plan.length) { html += '<div class="term-empty">No workflow yet. Use the workflow button when you want steps before execution.</div>'; } else { html += '<div class="term-workflow-list">'; plan.forEach(function (step, idx) { html += '<div class="term-workflow-step"><div class="term-ledger-item__meta">STEP ' + (idx + 1) + '</div><div class="term-ledger-item__title">' + termSafe(step.raw || step.normalized || step.name || '') + '</div></div>'; }); html += '</div><button class="btn btn--primary" style="width:100%;margin-top:4px;" onclick="termRunPlanned()">Run Workflow</button>'; } panel.innerHTML = html; } function termRenderLedger(state) { var panel = document.getElementById('termLedgerPanel'); if (!panel) return; var capsules = state.recentCapsules || []; var selected = state.selectedCapsule || null; var html = '<div class="term-sidepanel__helper">Ledger keeps recent capsules and a simple context graph so the paired agent can stay grounded in what it just made.</div>'; if (!capsules.length) { html += '<div class="term-empty">No capsules yet.</div>'; panel.innerHTML = html; return; } html += '<div class="term-ledger-list">'; capsules.forEach(function (capsule) { var active = selected && selected.id === capsule.id ? ' active' : ''; html += '<div class="term-ledger-item' + active + '" data-capsule-id="' + termSafe(capsule.id || '') + '" onclick="termSelectCapsule(this.dataset.capsuleId)">' + '<div class="term-ledger-item__title">' + termSafe(capsule.title || capsule.id || 'capsule') + '</div>' + '<div class="term-ledger-item__meta">' + termSafe(capsule.type || 'capsule') + (capsule.created_at ? ' · ' + termFmtDate(capsule.created_at) : '') + '</div>' + '</div>'; }); html += '</div>'; if (selected) { var media = (selected.media_refs || []).slice(0, 6).map(function (url) { return '<img src="' + termSafe(url) + '" alt="" loading="lazy">'; }).join(''); var context = (state.context || []).map(function (part) { return '<span class="term-ledger-detail__node">' + termSafe(part) + '</span>'; }).join('<span class="term-ledger-detail__arrow">→</span>'); var graph = context + (context ? '<span class="term-ledger-detail__arrow">→</span>' : '') + '<span class="term-ledger-detail__node">' + termSafe(selected.type || 'capsule') + '</span>' + '<span class="term-ledger-detail__arrow">→</span><span class="term-ledger-detail__node">' + termSafe(selected.id || 'selected') + '</span>'; html += '<div class="term-ledger-detail">' + '<div class="term-ledger-detail__title">' + termSafe(selected.title || selected.id || 'selected capsule') + '</div>' + '<div class="term-ledger-detail__meta">' + termSafe(selected.type || 'capsule') + (selected.created_at ? ' · ' + termFmtDate(selected.created_at) : '') + '</div>' + '<div class="term-ledger-detail__graph">' + graph + '</div>' + (selected.extracted_text ? '<div class="term-ledger-detail__body">' + termSafe(selected.extracted_text).slice(0, 500) + '</div>' : '') + (media ? '<div class="term-ledger-detail__media">' + media + '</div>' : '') + '</div>'; } panel.innerHTML = html; } function termRenderSuggestions(state) { var wrap = document.getElementById('termNextMoves'); if (!wrap) return; var suggestions = state.suggestions || []; if (!suggestions.length) { wrap.innerHTML = ''; return; } wrap.innerHTML = suggestions.map(function (suggestion) { return '<button class="term-sug-card" data-suggestion="' + termSafe(suggestion) + '" onclick="termUseSuggestion(this.dataset.suggestion)">' + termSafe(suggestion) + '</button>'; }).join(''); } function termRenderAttachments(state) { var strip = document.getElementById('termAttachStrip'); if (!strip) return; var attachments = state.pendingAttachments || []; if (!attachments.length) { strip.innerHTML = ''; strip.style.display = 'none'; return; } strip.style.display = 'flex'; strip.innerHTML = attachments.map(function (file, idx) { var preview = file.preview_url || file.url || ''; var isImage = /^image\\//i.test(file.mime || ''); return '<div class="term-attach-chip">' + (isImage ? ('<img src="' + termSafe(preview) + '" alt="" loading="lazy">') : '<div style="width:28px;height:28px;border-radius: 0px;border:1px solid #202020;background:#090909;display:flex;align-items:center;justify-content:center;color:#666;font-size:10px;">' + termSafe((file.ext || 'file').slice(0,4)) + '</div>') + '<div class="term-attach-chip__body"><div class="term-attach-chip__name">' + termSafe(file.name || ('attachment ' + (idx + 1))) + '</div><div class="term-attach-chip__meta">' + termSafe(file.mime || file.kind || 'file') + (file.size ? ' · ' + termFmtSize(file.size) : '') + '</div></div>' + '<button class="term-attach-chip__remove" onclick="termRemoveAttachment(' + idx + ')">×</button>' + '</div>'; }).join(''); } function termRenderStatus(state) { var status = document.getElementById('termMirrorStatus'); var chips = document.getElementById('termMirrorContext'); var meta = document.getElementById('termInputMeta'); if (status) { status.innerHTML = 'User Agent: <b>BOTTA_' + termSafe((state.user && state.user.username) || 'guest') + '</b> · Surface: ' + termSafe(state.page || '/') + ' · Context: ' + termSafe((state.context || []).join(' + ') || 'home') + ' · AI: ' + termSafe((state.ai && state.ai.preferred) || 'offline') + (state.runningNote ? (' · Status: ' + termSafe(state.runningNote)) : ''); } if (chips) { var current = state.context || []; chips.innerHTML = current.map(function (item) { return '<button class="term-chipbar__chip" data-context="' + termSafe(item) + '" onclick="termUseContext(this.dataset.context)">' + termSafe(item) + '</button>'; }).join('') + '<button class="term-chipbar__add" type="button" onclick="termAddContext()">+</button>'; } if (meta) { meta.textContent = state.running ? ('Hermes is working · ' + (state.runningNote || 'thinking')) : ('paired agent: BOTTA_' + (((state.user || {}).username) || 'guest') + ' · enter = execute · cmd/ctrl+enter = workflow'); } } function termSyncFromBridge() { var bridge = termBridge(); if (!bridge || typeof bridge.getState !== 'function') return; var state = bridge.getState() || {}; termRenderStatus(state); termRenderTranscript(state); termRenderWorkflows(state); termRenderLedger(state); termRenderSuggestions(state); termRenderAttachments(state); termSwitchPanel(_termMirrorPanel, true); } function termSetTools(value) { _termMirrorTools = !!value; var wrap = document.querySelector('.term-wrap'); var toggle = document.getElementById('termToolsToggle'); var show = _termMirrorTools || _termMirrorPanel !== 'terminal'; if (wrap) wrap.classList.toggle('term-wrap--tools', show); if (toggle) toggle.classList.toggle('active', show); } window.termUseSuggestion = function(value) { var bridge = termBridge(); var input = document.getElementById('termInput'); if (input) input.value = value || ''; if (bridge && bridge.setInput) bridge.setInput(value || ''); if (input) input.focus(); }; window.termUseContext = function(item) { var bridge = termBridge(); if (!bridge || !bridge.getState || !bridge.setContext) return; var state = bridge.getState() || {}; var stack = (state.context || []).slice(); if (stack.indexOf(item) === -1) stack.push(item); bridge.setContext(stack); }; window.termAddContext = function() { var input = window.prompt('Add context token'); if (!input) return; var bridge = termBridge(); if (!bridge || !bridge.getState || !bridge.setContext) return; var state = bridge.getState() || {}; var stack = (state.context || []).slice(); if (stack.indexOf(input) === -1) stack.push(input); bridge.setContext(stack); }; window.termAttach = function() { var input = document.getElementById('termAttachInput'); if (input) input.click(); }; window.termWorkflow = function() { var input = document.getElementById('termInput'); var bridge = termBridge(); if (!bridge || !bridge.planLine) return; bridge.planLine(input ? input.value : ''); termSwitchPanel('workflows'); }; window.termRunPlanned = function() { var bridge = termBridge(); if (!bridge || !bridge.dispatch) return; bridge.dispatch({ open: false, tab: 'workflows' }); var btn = document.getElementById('bottaRunPlan'); if (btn) btn.click(); }; window.termSelectCapsule = function(id) { var bridge = termBridge(); if (!bridge || !bridge.selectCapsule) return; bridge.selectCapsule(id); termSwitchPanel('ledger'); }; window.termRemoveAttachment = function(index) { var bridge = termBridge(); if (!bridge || !bridge.getState) return; var state = bridge.getState() || {}; var next = (state.pendingAttachments || []).filter(function(_, idx){ return idx !== index; }); bridge.clearAttachments && bridge.clearAttachments(); if (next.length && bridge.queueFiles && window.fetch) { /* no-op: removal only for current local mirror, shared shell stays source of truth */ } if (window.__ALEQTH_ATCMD__ && window.__ALEQTH_ATCMD__.dispatch) { window.__ALEQTH_ATCMD__.dispatch({}); } setTimeout(termSyncFromBridge, 10); }; window.termToggleTools = function() { termSetTools(!_termMirrorTools); }; window.termSwitchPanel = function(panel, silent) { _termMirrorPanel = panel || 'terminal'; document.querySelectorAll('.term-htab').forEach(function(tab){ tab.classList.toggle('active', tab.dataset.panel === _termMirrorPanel); }); var log = document.getElementById('termLog'); var workflows = document.getElementById('termWorkflowPanel'); var ledger = document.getElementById('termLedgerPanel'); var sugs = document.getElementById('termNextMoves'); if (log) log.style.display = _termMirrorPanel === 'terminal' ? 'flex' : 'none'; if (sugs) sugs.style.display = _termMirrorPanel === 'terminal' ? 'grid' : 'none'; if (workflows) workflows.style.display = _termMirrorPanel === 'workflows' ? 'flex' : 'none'; if (ledger) ledger.style.display = _termMirrorPanel === 'ledger' ? 'flex' : 'none'; termSetTools(_termMirrorTools); if (!silent) termSyncFromBridge(); }; // Save the original renderTerminal before overwriting window.termLegacyRenderTerminal = window.renderTerminal; window.renderTerminal = function() { var bridge = termBridge(); // Don't bail if bridge missing — terminal still works with inline handlers + termSend if (!bridge) { // Fall back to legacy terminal init if available if (typeof window.termLegacyRenderTerminal === 'function') { window.termLegacyRenderTerminal(); return; } } if (!_termMirrorInitialized) { _termMirrorInitialized = true; var composer = document.getElementById('termComposer'); var input = document.getElementById('termInput'); var fileInput = document.getElementById('termAttachInput'); var sendBtn = document.getElementById('termSendBtn'); if (composer && !composer.__termMirrorSubmitBound) { composer.__termMirrorSubmitBound = true; composer.addEventListener('submit', termHandleComposerSubmit); } if (sendBtn && !sendBtn.__termMirrorClickBound) { sendBtn.__termMirrorClickBound = true; sendBtn.addEventListener('click', termHandleComposerSubmit); } if (input) { input.addEventListener('input', function(){ if (bridge.setInput) bridge.setInput(input.value || ''); }); } if (fileInput) { fileInput.addEventListener('change', function(){ if (bridge.queueFiles) bridge.queueFiles(fileInput.files || []); }); } if (!_termMirrorListenerBound) { _termMirrorListenerBound = true; window.addEventListener('aleqth:atcmd:update', termSyncFromBridge); } } termSetTools(_termMirrorTools); termSyncFromBridge(); }; window.termSend = function() { if (typeof window.termLegacyDashboardSend === 'function') { window.termLegacyDashboardSend(); return; } if (termDispatchViaBridge('send')) { return; } }; })(); </script> <script> if (window.loadDashboardInventory) window.loadInventory = window.loadDashboardInventory; if (window.removeDashboardInventoryItem) window.removeInventoryItem = window.removeDashboardInventoryItem; // ── Page Capsules (ingested via /ingest) — remixable in canvas editor ── async function loadPageCapsules() { if (!currentUser || String(currentUser.username || '').toLowerCase() !== 'aleqth') return; var grid = document.getElementById('pageCapGrid'); var empty = document.getElementById('pageCapEmpty'); var count = document.getElementById('pageCapCount'); if (!grid) return; grid.innerHTML = ''; try { var r = await fetch('/api/surf/pages/mine?limit=200', { credentials: 'same-origin' }); if (!r.ok) { if (empty) empty.style.display = 'block'; return; } var d = await r.json(); var items = (d && d.items) || []; if (count) count.textContent = items.length ? (items.length + ' capsule' + (items.length === 1 ? '' : 's')) : ''; if (!items.length) { if (empty) empty.style.display = 'block'; return; } if (empty) empty.style.display = 'none'; items.forEach(function(it) { var card = document.createElement('div'); card.style.cssText = 'border:1px solid var(--border,#e0e0e0);border-radius: 0px;overflow:hidden;background:var(--bg2,#fff);display:flex;flex-direction:column;'; var thumb = it.thumb_url || (it.page_images && it.page_images[0]) || ''; card.innerHTML = (thumb ? '<div style="aspect-ratio:16/10;background:#111 center/cover no-repeat url(\'' + thumb.replace(/\'/g, "%27") + '\');"></div>' : '<div style="aspect-ratio:16/10;background:linear-gradient(135deg,#222,#444);display:flex;align-items:center;justify-content:center;color:#888;font-size:10px;">no preview</div>') + '<div style="padding:10px 12px;flex:1;display:flex;flex-direction:column;gap:6px;">' + '<div style="font-size:12px;font-weight:600;line-height:1.3;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;">' + escapeHTML(it.title || it.name || 'untitled') + '</div>' + '<div style="font-size:9px;color:var(--dim,#888);overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">' + escapeHTML(it.source || '') + '</div>' + '<div style="font-size:9px;color:var(--dim,#888);">' + (it.page_images ? (it.page_images.length + ' img · ') : '') + (it.has_snapshot ? 'snapshot [ok]' : 'no snapshot') + '</div>' + '<div style="display:flex;gap:6px;margin-top:4px;">' + '<button class="btn btn--sm btn--primary" data-remix="' + escapeHTML(it.id || it.url || '') + '" style="flex:1;font-size:10px;">remix in canvas</button>' + '<a href="' + escapeHTML(it.url || '#') + '" target="_blank" rel="noopener" class="btn btn--sm btn--ghost" style="font-size:10px;" title="open source">source</a>' + '</div>' + '</div>'; grid.appendChild(card); }); grid.querySelectorAll('[data-remix]').forEach(function(btn) { btn.addEventListener('click', function() { remixPageCapsule(btn.dataset.remix); }); }); } catch (err) { console.error('loadPageCapsules', err); if (empty) { empty.style.display = 'block'; empty.textContent = 'Could not load page capsules.'; } } } async function remixPageCapsule(itemId) { if (!itemId) return; try { var r = await fetch('/api/surf/pages/' + encodeURIComponent(itemId) + '/remix', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, }); if (!r.ok) { var e = await r.text(); alert('Remix failed: ' + e); return; } var d = await r.json(); // Open the new page in the canvas editor. if (typeof editPage === 'function' && d.page_id) { editPage(d.page_id); } else if (typeof switchTab === 'function') { switchTab('pages'); } } catch (err) { console.error('remixPageCapsule', err); alert('Remix failed: ' + err.message); } } function escapeHTML(s) { return String(s == null ? '' : s) .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') .replace(/"/g, '"').replace(/'/g, '''); } </script> <script> // ── Wallet / On-Chain Identity ───────────────────────────────────────────── async function loadWalletInfo() { try { var r = await fetch('/api/users/me/wallet', {credentials:'same-origin'}); if (!r.ok) return; var d = await r.json(); // Agent wallet display var aw = document.getElementById('agentWalletDisplay'); if (aw) { if (d.agent_wallet) { aw.innerHTML = d.agent_wallet + ' <a href="https://basescan.org/address/' + d.agent_wallet + '" target="_blank" style="color:var(--accent);font-size:9px;margin-left:8px">Basescan ↗</a>' + ' <a href="https://zora.co/' + d.agent_wallet + '" target="_blank" style="color:var(--accent);font-size:9px;margin-left:4px">Zora Profile ↗</a>'; } else { aw.textContent = 'Not set up. Connecting an address never gives the agent permission to spend.'; } } // Agent Zora coin var ac = document.getElementById('agentCoinDisplay'); if (ac) { if (d.agent_zora_coin) { ac.innerHTML = d.agent_zora_coin + ' <a href="' + d.agent_zora_url + '" target="_blank" style="color:var(--accent);font-size:9px;margin-left:8px">View Coin ↗</a>'; } else { ac.textContent = '— no agent coin minted yet'; } } // Personal wallet var pw = document.getElementById('personalWalletInput'); if (pw && d.personal_wallet) pw.value = d.personal_wallet; // Toggles var sat = document.getElementById('showAgentWalletToggle'); if (sat) sat.checked = d.show_agent_wallet !== false; var spt = document.getElementById('showPersonalWalletToggle'); if (spt) spt.checked = !!d.show_personal_wallet; } catch(e) {} } // Called after all scripts load — safe to call loadWalletInfo now document.addEventListener('DOMContentLoaded', function() { // Wait for currentUser to be set (main init is async) var _walletPoll = setInterval(function() { if (window.currentUser) { clearInterval(_walletPoll); loadWalletInfo(); } }, 200); setTimeout(function() { clearInterval(_walletPoll); }, 8000); }); async function saveWalletSettings() { var sat = document.getElementById('showAgentWalletToggle'); var spt = document.getElementById('showPersonalWalletToggle'); var pw = document.getElementById('personalWalletInput'); var status = document.getElementById('walletSettingsStatus'); try { var r = await fetch('/api/users/me/wallet-settings', { method: 'PATCH', credentials: 'same-origin', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ show_agent_wallet: sat ? sat.checked : true, show_personal_wallet: spt ? spt.checked : false, personal_wallet: pw ? pw.value.trim() : '', }) }); var d = await r.json(); if (d.ok) { if (status) { status.style.display = 'block'; setTimeout(function(){ status.style.display='none'; }, 2000); } } } catch(e) {} } // ─── Lab ────────────────────────────────────────────────────────────────── async function labLoad() { await Promise.all([labLoadTasks(), labLoadExps()]); } async function labLoadTasks() { var el = document.getElementById('labTaskList'); if (!el) return; var d = await apiFetch('/api/ankle/tasks?limit=30'); if (!d || !d.length) { el.innerHTML = '<div style="color:var(--faint);font-size:11px;padding:8px 0;">No tasks yet. Give an agent a command above.</div>'; return; } el.innerHTML = d.map(function(t) { var sc = {completed:'#8eff6d',failed:'#e53935',running:'#FFD700',pending:'var(--dim)'}[t.status] || 'var(--dim)'; return '<div style="padding:10px 12px;border:1px solid var(--line);border-radius: 0px;">' + '<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;">' + '<span style="font-size:16px;">' + escHtml(t.agent_sigil) + '</span>' + '<span style="font-size:9px;text-transform:uppercase;letter-spacing:.1em;color:var(--dim);">' + escHtml(t.command_type) + '</span>' + '<span style="margin-left:auto;font-size:9px;text-transform:uppercase;letter-spacing:.08em;color:' + sc + ';">' + escHtml(t.status) + '</span>' + '</div>' + '<div style="font-size:12px;margin-bottom:6px;color:var(--fg);">' + escHtml(t.command) + '</div>' + (t.result ? '<div style="font-size:11px;color:var(--dim);border-left:2px solid var(--line);padding-left:8px;white-space:pre-wrap;line-height:1.6;max-height:100px;overflow-y:auto;">' + escHtml(t.result.substring(0,400)) + '</div>' : '') + '</div>'; }).join(''); } async function labQueueTask() { var sigil = document.getElementById('labSigil').value.trim() || '⬡'; var type = document.getElementById('labType').value; var cmd = document.getElementById('labCmd').value.trim(); if (!cmd) { toast('Enter a command'); return; } var d = await apiFetch('/api/ankle/tasks', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({agent_sigil:sigil,command:cmd,command_type:type})}); if (d) { document.getElementById('labCmd').value = ''; toast(sigil + ' task queued'); labLoadTasks(); } } async function labExecNow() { var sigil = document.getElementById('labSigil').value.trim() || '⬡'; var type = document.getElementById('labType').value; var cmd = document.getElementById('labCmd').value.trim(); if (!cmd) { toast('Enter a command'); return; } toast(sigil + ' executing...'); var d = await apiFetch('/api/ankle/execute', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({agent_sigil:sigil,command:cmd,command_type:type})}); if (d) { document.getElementById('labCmd').value = ''; toast(sigil + ' done'); labLoadTasks(); } } async function labLoadExps() { var el = document.getElementById('labExpList'); if (!el) return; var d = await apiFetch('/api/experiments?limit=10'); if (!d || !d.length) { el.innerHTML = '<div style="color:var(--faint);font-size:11px;padding:8px 0;">No experiments yet. State a hypothesis above to start one.</div>'; return; } el.innerHTML = d.map(function(e) { var sc = {completed:'#8eff6d',running:'#FFD700',scoring:'#FFD700',generating:'var(--dim)',setup:'var(--dim)',failed:'#e53935'}[e.status] || 'var(--dim)'; var scores = e.scores || {}; var scoreHtml = Object.entries(scores).map(function(kv) { var win = e.winner_page_id && String(e.winner_page_id) === kv[0]; return '<a href="/' + (window._currentUser || '') + '/exp' + e.id + '-v" target="_blank" style="color:' + (win ? '#8eff6d' : 'var(--dim)') + ';text-decoration:none;">' + 'p' + kv[0] + ':' + Math.round(kv[1]) + 'pt' + (win ? '*' : '') + '</a>'; }).join(' · '); var endsAt = e.scoring_ends_at ? new Date(e.scoring_ends_at).toLocaleDateString() : ''; return '<div style="padding:12px 14px;border:1px solid var(--line);border-radius: 0px;">' + '<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:8px;margin-bottom:6px;">' + '<div style="font-size:12px;font-weight:500;">' + escHtml(e.title) + '</div>' + '<span style="font-size:9px;text-transform:uppercase;letter-spacing:.08em;color:' + sc + ';white-space:nowrap;">' + escHtml(e.status) + '</span>' + '</div>' + '<div style="font-size:11px;color:var(--dim);font-style:italic;margin-bottom:8px;">' + escHtml(e.hypothesis) + '</div>' + (scoreHtml ? '<div style="font-size:11px;margin-bottom:6px;display:flex;gap:12px;flex-wrap:wrap;">' + scoreHtml + '</div>' : '') + (endsAt ? '<div style="font-size:10px;color:var(--faint);margin-bottom:6px;">scores until ' + endsAt + '</div>' : '') + (e.synthesis ? '<div style="font-size:11px;color:var(--dim);border-left:2px solid var(--line);padding-left:8px;line-height:1.6;margin-bottom:8px;">' + escHtml(e.synthesis.substring(0,300)) + '...</div>' : '') + '<div style="display:flex;gap:6px;flex-wrap:wrap;">' + (e.variants && e.variants.length ? e.variants.map(function(pid,i) { return '<a href="/p/' + pid + '" target="_blank" class="btn" style="font-size:10px;padding:3px 8px;text-decoration:none;">V' + (i+1) + ' →</a>'; }).join('') : '') + '<button class="btn" style="font-size:10px;padding:3px 8px;" onclick="labScore(' + e.id + ')">↻ Scores</button>' + (e.status !== 'completed' && e.winner_page_id ? '<button class="btn" style="font-size:10px;padding:3px 8px;" onclick="labSynthesize(' + e.id + ')">Synthesize</button>' : '') + '</div>' + '</div>'; }).join(''); } async function labRunExperiment() { var hyp = document.getElementById('labHyp').value.trim(); var n = parseInt(document.getElementById('labVariants').value); if (!hyp) { toast('State a hypothesis'); return; } toast('Generating ' + n + ' variants...'); var d = await apiFetch('/api/experiments', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({title:'Exp: ' + hyp.substring(0,50), hypothesis:hyp, variant_count:n, scoring_window_hours:48})}); if (d) { document.getElementById('labHyp').value = ''; toast(n + ' variants live — watch scores over 48h'); labLoadExps(); } } async function labScore(id) { await apiFetch('/api/experiments/' + id + '/score', {method:'POST', headers:{'Content-Type':'application/json'}, body:'{}'}); labLoadExps(); } async function labSynthesize(id) { toast('⊛ synthesizing...'); await apiFetch('/api/experiments/' + id + '/synthesize', {method:'POST', headers:{'Content-Type':'application/json'}, body:'{}'}); toast('Synthesis complete'); labLoadExps(); } </script> <div class="inv-lightbox" id="invLightbox"> <div class="inv-modal"> <div class="inv-modal__img"> <img id="invLbImg" src="" alt=""> <div class="inv-modal__nav prev" id="invLbPrev">‹</div> <div class="inv-modal__nav next" id="invLbNext">›</div> </div> <div class="inv-modal__body"> <div class="inv-meta"> <div class="inv-title" id="invLbTitle"></div> <div id="invLbMeta"></div> </div> <div class="inv-actions"> <a id="invLbDownload" href="#" download>Download</a> <a id="invLbOpen" href="#" target="_blank">Open</a> <button onclick="closeInventoryLightbox()">Close</button> <div class="inv-counter" id="invLbCounter"></div> </div> </div> </div> </div> <div id="capsulePanel" style="display:none;position:fixed;bottom:24px;right:24px;width:380px;z-index:500;background:rgba(5,8,22,.97);border:1px solid rgba(142,255,109,.28);border-radius: 0px;padding:16px;box-shadow: 2px 2px 0px #000;font-family:inherit;"> <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;"> <div style="font-size:11px;letter-spacing:.12em;color:#8eff6d;">◈ CONTEXT CAPSULE  <span id="capsuleMode" style="color:#555;text-transform:uppercase;"></span></div> <button onclick="closeCapsulePanel()" style="background:none;border:none;color:#555;font:inherit;font-size:14px;cursor:pointer;padding:0 4px;line-height:1;" onmouseover="this.style.color='#f5f7ff'" onmouseout="this.style.color='#555'">✕</button> </div> <div id="capsuleResult" style="font-size:11px;line-height:1.8;color:rgba(245,247,255,.8);white-space:pre-wrap;max-height:220px;overflow-y:auto;padding:12px;background:rgba(255,255,255,.03);border-radius: 0px;margin-bottom:12px;border:1px solid rgba(255,255,255,.05);"></div> <div style="display:flex;gap:8px;"> <button onclick="navigator.clipboard.writeText(document.getElementById('capsuleResult').textContent)" style="flex:1;padding:8px 12px;border-radius: 0px;font:inherit;font-size:9px;letter-spacing:.1em;text-transform:uppercase;cursor:pointer;background:rgba(142,255,109,.1);border:1px solid rgba(142,255,109,.3);color:#8eff6d;" onmouseover="this.style.background='rgba(142,255,109,.18)'" onmouseout="this.style.background='rgba(142,255,109,.1)'">Copy</button> <button id="capsuleDlBtn" onclick="capsuleDownload()" style="flex:1;padding:8px 12px;border-radius: 0px;font:inherit;font-size:9px;letter-spacing:.1em;text-transform:uppercase;cursor:pointer;background:rgba(83,210,255,.08);border:1px solid rgba(83,210,255,.25);color:#53d2ff;display:none;" onmouseover="this.style.background='rgba(83,210,255,.15)'" onmouseout="this.style.background='rgba(83,210,255,.08)'">↓ Save .ankl</button> </div> </div> <script src="/theme.js" defer></script> </body> </html>