meridian-amd / vscode.html
Cukinator's picture
Fix tokenizer artifacts in Code Agent
44623b6
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Code Editor β€” Meridian</title>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #1e1e1e; color: #cccccc;
height: 100vh; overflow: hidden;
display: flex; flex-direction: column; font-size: 13px;
}
/* Titlebar */
.titlebar { height: 30px; background: #3c3c3c; display: flex; align-items: center; padding: 0 12px; flex-shrink: 0; }
.titlebar-dots { display: flex; gap: 6px; margin-right: 12px; }
.titlebar-dot { width: 12px; height: 12px; border-radius: 50%; }
.dot-close { background: #ff5f57; } .dot-min { background: #febc2e; } .dot-max { background: #28c840; }
.titlebar-title { flex: 1; text-align: center; font-size: 12px; color: #cccccc; }
/* Workbench */
.workbench { display: flex; flex: 1; overflow: hidden; }
/* Activity bar */
.activity-bar {
width: 48px; background: #333333;
display: flex; flex-direction: column; align-items: center;
padding-top: 4px; flex-shrink: 0; border-right: 1px solid #252526;
}
.act-icon {
width: 36px; height: 36px;
display: flex; align-items: center; justify-content: center;
cursor: pointer; border-radius: 4px; margin: 1px 0;
color: #858585;
}
.act-icon:hover { color: #cccccc; }
.act-icon.active { color: #fff; border-left: 2px solid #007acc; }
.act-icon svg { width: 22px; height: 22px; }
.act-spacer { flex: 1; }
/* Sidebar */
.sidebar {
width: 250px; background: #252526; flex-shrink: 0;
display: flex; flex-direction: column; border-right: 1px solid #3c3c3c; overflow: hidden;
}
#sidebar-explorer { display: flex; flex-direction: column; flex: 1; overflow: hidden; }
#sidebar-agent { display: none; flex-direction: column; flex: 1; overflow: hidden; }
.sidebar-header {
padding: 8px 12px; font-size: 11px; font-weight: 600;
color: #bbbbbb; text-transform: uppercase; letter-spacing: 0.8px;
flex-shrink: 0; display: flex; align-items: center; justify-content: space-between;
border-bottom: 1px solid #3c3c3c;
}
.sidebar-btn {
background: none; border: none; color: #858585;
cursor: pointer; padding: 2px 6px; border-radius: 3px; font-size: 18px; line-height: 1;
}
.sidebar-btn:hover { color: #cccccc; background: rgba(255,255,255,0.1); }
/* File tree */
.file-tree { flex: 1; overflow-y: auto; }
.file-tree::-webkit-scrollbar { width: 6px; }
.file-tree::-webkit-scrollbar-thumb { background: #424242; border-radius: 3px; }
.file-item {
display: flex; align-items: center; gap: 6px;
padding: 4px 12px; cursor: pointer; color: #cccccc;
}
.file-item:hover { background: #2a2d2e; }
.file-item.active { background: #37373d; color: #fff; }
.file-icon { font-size: 10px; font-weight: 700; font-family: monospace; width: 22px; text-align: center; flex-shrink: 0; }
.icon-html { color: #f16529; } .icon-js { color: #f7df1e; }
.icon-css { color: #569cd6; } .icon-json { color: #fbc02d; }
.icon-txt { color: #858585; }
.file-name { flex: 1; font-size: 13px; }
.file-del {
opacity: 0; background: none; border: none; color: #858585;
cursor: pointer; padding: 0 2px; font-size: 14px; line-height: 1;
}
.file-item:hover .file-del { opacity: 0.6; }
.file-del:hover { opacity: 1 !important; color: #f48771; }
.empty-state { padding: 20px 12px; color: #555; font-size: 12px; text-align: center; line-height: 1.7; }
/* Agent sidebar */
.agent-chat { flex: 1; overflow-y: auto; padding: 8px; display: flex; flex-direction: column; gap: 6px; }
.agent-chat::-webkit-scrollbar { width: 4px; }
.agent-chat::-webkit-scrollbar-thumb { background: #424242; border-radius: 3px; }
.chat-msg { padding: 7px 9px; border-radius: 6px; font-size: 12px; line-height: 1.5; word-break: break-word; }
.chat-msg.user { background: #0e639c; color: #fff; align-self: flex-end; max-width: 90%; }
.chat-msg.assistant { background: #2d2d2d; color: #cccccc; max-width: 100%; }
.chat-msg.tool { background: #1a2633; color: #4fc1ff; font-family: monospace; font-size: 11px; border-left: 2px solid #007acc; }
.chat-msg.err { background: #2d1f1f; color: #f48771; }
.chat-msg.thinking { color: #858585; font-style: italic; }
.chat-msg pre { white-space: pre-wrap; margin-top: 4px; font-size: 10px; }
.api-row { padding: 6px 8px; border-bottom: 1px solid #3c3c3c; flex-shrink: 0; }
.api-label { font-size: 10px; color: #858585; margin-bottom: 3px; }
.api-input {
width: 100%; background: #3c3c3c; border: 1px solid #555;
border-radius: 3px; color: #cccccc; padding: 4px 8px; font-size: 11px; outline: none;
}
.api-input:focus { border-color: #007acc; }
.chat-input-row { padding: 6px 8px; border-top: 1px solid #3c3c3c; display: flex; gap: 4px; flex-shrink: 0; }
.chat-input {
flex: 1; background: #3c3c3c; border: 1px solid #555; border-radius: 4px;
color: #cccccc; padding: 5px 8px; font-size: 12px; resize: none; font-family: inherit; outline: none;
}
.chat-input:focus { border-color: #007acc; }
.chat-send {
background: #0e639c; border: none; color: #fff;
padding: 5px 10px; border-radius: 4px; cursor: pointer; font-size: 13px; flex-shrink: 0;
}
.chat-send:hover { background: #1177bb; }
.chat-send:disabled { opacity: 0.4; cursor: not-allowed; }
/* Editor area */
.editor-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
/* Tab bar */
.tab-bar {
height: 35px; background: #2d2d2d;
display: flex; align-items: flex-end;
border-bottom: 1px solid #252526; overflow-x: auto; flex-shrink: 0;
}
.tab-bar::-webkit-scrollbar { display: none; }
.tab {
height: 35px; padding: 0 10px;
display: flex; align-items: center; gap: 5px;
font-size: 12px; color: #969696; cursor: pointer;
background: #2d2d2d; border-right: 1px solid #252526;
white-space: nowrap; flex-shrink: 0;
}
.tab.active { background: #1e1e1e; color: #fff; border-top: 1px solid #007acc; }
.tab:hover:not(.active) { background: #2a2d2e; }
.tab-close {
width: 14px; height: 14px; display: flex; align-items: center; justify-content: center;
border-radius: 3px; opacity: 0; font-size: 14px; color: #ccc;
}
.tab:hover .tab-close, .tab.active .tab-close { opacity: 0.6; }
.tab-close:hover { background: rgba(255,255,255,0.15); opacity: 1 !important; }
.run-btn {
margin-left: auto; height: 35px; padding: 0 12px;
display: flex; align-items: center; gap: 5px;
background: none; border: none; color: #4ec9b0; cursor: pointer;
font-size: 12px; white-space: nowrap; flex-shrink: 0;
}
.run-btn:hover:not(:disabled) { color: #fff; background: rgba(78,201,176,0.12); }
.run-btn:disabled { color: #454545; cursor: not-allowed; }
/* Welcome */
.editor-welcome {
flex: 1; display: flex; flex-direction: column;
align-items: center; justify-content: center;
color: #555; text-align: center; padding: 20px;
}
.editor-welcome h2 { color: #777; font-size: 20px; font-weight: 400; margin-bottom: 10px; }
.editor-welcome p { font-size: 13px; line-height: 1.7; max-width: 300px; }
.editor-welcome .ext { margin-top: 14px; font-size: 11px; color: #3a3a3a; }
/* Code editor */
.editor-wrapper { flex: 1; display: flex; overflow: hidden; }
.line-numbers {
width: 50px; background: #1e1e1e; padding: 8px 0;
text-align: right; padding-right: 14px; color: #858585;
font-family: 'Cascadia Code', Consolas, 'Courier New', monospace;
font-size: 13px; line-height: 21px; user-select: none;
overflow: hidden; flex-shrink: 0;
}
.code-textarea {
flex: 1; background: #1e1e1e; color: #d4d4d4;
border: none; outline: none;
font-family: 'Cascadia Code', Consolas, 'Courier New', monospace;
font-size: 13px; line-height: 21px;
padding: 8px 12px; resize: none; tab-size: 2;
white-space: pre; overflow: auto;
}
.code-textarea::-webkit-scrollbar { width: 10px; height: 10px; }
.code-textarea::-webkit-scrollbar-track { background: #1e1e1e; }
.code-textarea::-webkit-scrollbar-thumb { background: #424242; border-radius: 2px; }
/* Bottom panel */
.bottom-panel {
height: 190px; background: #1e1e1e;
border-top: 1px solid #3c3c3c;
display: flex; flex-direction: column; flex-shrink: 0;
transition: height 0.15s ease;
}
.bottom-panel.expanded { height: calc(100% - 35px); }
.panel-header {
height: 28px; background: #252526;
display: flex; align-items: center; padding: 0 8px;
border-bottom: 1px solid #3c3c3c; flex-shrink: 0; gap: 0;
}
.ptab {
padding: 0 12px; height: 100%; display: flex; align-items: center;
font-size: 11px; color: #969696; cursor: pointer; border-bottom: 1px solid transparent;
}
.ptab.active { color: #cccccc; border-bottom-color: #007acc; }
.ptab:hover:not(.active) { color: #cccccc; }
.panel-actions { margin-left: auto; display: flex; align-items: center; gap: 4px; }
.pbtn {
background: none; border: none; color: #858585;
cursor: pointer; padding: 2px 6px; font-size: 11px; border-radius: 3px;
}
.pbtn:hover { color: #cccccc; background: rgba(255,255,255,0.08); }
.terminal-body {
flex: 1; padding: 6px 12px; overflow-y: auto;
font-family: 'Cascadia Code', Consolas, 'Courier New', monospace;
font-size: 12px; line-height: 1.65;
}
.terminal-body::-webkit-scrollbar { width: 6px; }
.terminal-body::-webkit-scrollbar-thumb { background: #424242; border-radius: 3px; }
.t-prompt { color: #3fc2fb; } .t-cmd { color: #cccccc; } .t-out { color: #858585; }
.t-ok { color: #4ec9b0; } .t-warn { color: #ffcc02; } .t-err { color: #f44747; }
.t-cursor { display: inline-block; width: 7px; height: 13px; background: #cccccc; vertical-align: text-bottom; animation: blink 1.2s step-end infinite; }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }
.preview-body { flex: 1; background: #fff; display: none; }
.preview-iframe { width: 100%; height: 100%; border: none; display: block; }
/* Syntax tokens */
.kw { color: #569cd6; } .ty { color: #4ec9b0; }
.fn { color: #dcdcaa; } .va { color: #9cdcfe; }
.str { color: #ce9178; } .cmt { color: #6a9955; font-style: italic; }
.dec { color: #c586c0; } .cls { color: #4ec9b0; }
.num { color: #b5cea8; }
/* AI bar */
.ai-bar {
display: none; flex-shrink: 0;
padding: 5px 10px; background: #252526;
border-bottom: 1px solid #007acc;
align-items: center; gap: 8px;
}
.ai-bar.visible { display: flex; }
.ai-bar-icon { font-size: 14px; flex-shrink: 0; }
.ai-bar-input {
flex: 1; background: #3c3c3c; border: 1px solid #555;
border-radius: 4px; color: #cccccc; padding: 4px 10px;
font-size: 12px; outline: none; font-family: inherit;
}
.ai-bar-input:focus { border-color: #007acc; }
.ai-bar-btn {
background: #0e639c; border: none; color: #fff;
padding: 5px 14px; border-radius: 4px; cursor: pointer;
font-size: 12px; font-weight: 600; white-space: nowrap; flex-shrink: 0;
}
.ai-bar-btn:hover { background: #1177bb; }
.ai-bar-btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* Ready bar */
.ready-bar {
display: none; flex-shrink: 0;
padding: 5px 10px; background: #252526;
border-bottom: 1px solid #28a745;
align-items: center; gap: 10px; font-size: 12px;
}
.ready-bar.visible { display: flex; }
.ready-bar-ok { color: #4ec9b0; flex-shrink: 0; font-size: 14px; }
.ready-bar-info { flex: 1; color: #858585; }
.ready-bar-info strong { color: #cccccc; }
.commit-btn {
background: #0e639c; border: none; color: #fff;
padding: 5px 14px; border-radius: 4px; cursor: pointer;
font-size: 12px; font-weight: 600; white-space: nowrap; flex-shrink: 0;
}
.commit-btn:hover { background: #1177bb; }
/* Code highlighted view */
.code-hl-view {
display: none; flex: 1; overflow: auto;
font-family: 'Cascadia Code', Consolas, 'Courier New', monospace;
font-size: 13px; line-height: 21px;
padding: 8px 12px; color: #d4d4d4; background: #1e1e1e;
}
.code-hl-view::-webkit-scrollbar { width: 10px; height: 10px; }
.code-hl-view::-webkit-scrollbar-track { background: #1e1e1e; }
.code-hl-view::-webkit-scrollbar-thumb { background: #424242; border-radius: 2px; }
.ai-gen-cursor { display: inline-block; width: 2px; height: 14px; background: #007acc; vertical-align: text-bottom; animation: blink 0.6s step-end infinite; margin-left: 1px; }
/* Commit overlay */
.commit-overlay {
display: none; position: fixed; inset: 0;
background: rgba(0,0,0,0.65); z-index: 1000;
align-items: center; justify-content: center;
}
.commit-overlay.open { display: flex; }
.commit-dialog {
background: #252526; border: 1px solid #454545;
border-radius: 8px; width: 420px; max-width: 90%;
display: flex; flex-direction: column;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}
.commit-header {
padding: 12px 16px; border-bottom: 1px solid #3c3c3c;
font-size: 13px; font-weight: 600; color: #cccccc;
display: flex; align-items: center; justify-content: space-between;
}
.commit-x { background: none; border: none; color: #858585; cursor: pointer; font-size: 18px; line-height: 1; }
.commit-x:hover { color: #cccccc; }
.commit-body { padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; }
.commit-lbl { font-size: 11px; color: #858585; margin-bottom: 3px; }
.commit-msg-inp {
width: 100%; background: #3c3c3c; border: 1px solid #555;
border-radius: 4px; color: #cccccc; padding: 7px 10px;
font-size: 13px; outline: none; font-family: inherit; resize: none; box-sizing: border-box;
}
.commit-msg-inp:focus { border-color: #007acc; }
.commit-meta { font-size: 11px; color: #858585; display: flex; gap: 14px; align-items: center; }
.commit-add { color: #4ec9b0; }
.commit-footer {
padding: 11px 16px; border-top: 1px solid #3c3c3c;
display: flex; gap: 8px; justify-content: flex-end;
}
.cbtn { padding: 6px 16px; border-radius: 4px; font-size: 13px; cursor: pointer; border: none; }
.cbtn-sec { background: #3c3c3c; color: #cccccc; }
.cbtn-sec:hover { background: #505050; }
.cbtn-pri { background: #0e639c; color: #fff; font-weight: 600; }
.cbtn-pri:hover { background: #1177bb; }
.cbtn:disabled { opacity: 0.4; cursor: not-allowed; }
/* Status bar */
.status-bar {
height: 22px; background: #007acc;
display: flex; align-items: center; font-size: 11px; color: #fff; flex-shrink: 0;
}
.st-item { padding: 0 8px; cursor: pointer; display: flex; align-items: center; gap: 4px; height: 100%; white-space: nowrap; }
.st-item:hover { background: rgba(255,255,255,0.12); }
.st-right { margin-left: auto; display: flex; }
</style>
</head>
<body>
<div class="titlebar">
<div class="titlebar-dots">
<div class="titlebar-dot dot-close"></div>
<div class="titlebar-dot dot-min"></div>
<div class="titlebar-dot dot-max"></div>
</div>
<div class="titlebar-title" id="titlebar-title">Code Editor β€” Meridian</div>
</div>
<div class="workbench">
<!-- Activity bar -->
<div class="activity-bar">
<div class="act-icon active" id="act-explorer" title="Explorador" onclick="setSidebar('explorer')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6M8 13h8M8 17h6"/>
</svg>
</div>
<div class="act-icon" id="act-agent" title="Agente AI" onclick="setSidebar('agent')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6">
<rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>
<circle cx="12" cy="16" r="1.5" fill="currentColor"/>
</svg>
</div>
<div class="act-spacer"></div>
</div>
<!-- Sidebar -->
<div class="sidebar">
<!-- Explorer -->
<div id="sidebar-explorer">
<div class="sidebar-header">
Explorer
<button class="sidebar-btn" title="New file" onclick="newFile()">+</button>
</div>
<div class="file-tree" id="file-tree">
<div class="empty-state">No files.<br>Press <strong>+</strong> to create one.</div>
</div>
</div>
<!-- Agent -->
<div id="sidebar-agent">
<div class="sidebar-header">Code Agent</div>
<div class="api-row">
<div class="api-label">AMD Inference Endpoint URL</div>
<input type="text" class="api-input" id="api-key" placeholder="http://<IP>:8000/v1/chat/completions" />
</div>
<div class="agent-chat" id="agent-chat">
<div class="chat-msg assistant">Hi! I'm your code agent. I can create, edit and run HTML, CSS and JS files. What would you like to build?</div>
</div>
<div class="chat-input-row">
<textarea class="chat-input" id="chat-input" rows="2" placeholder="Ask me to build something..." onkeydown="chatKey(event)"></textarea>
<button class="chat-send" id="chat-send" onclick="sendChat()">β–Ά</button>
</div>
</div>
</div>
<!-- Editor area -->
<div class="editor-area">
<div class="tab-bar" id="tab-bar">
<button class="run-btn" id="run-btn" disabled onclick="runActive()">β–Ά Run</button>
</div>
<div class="ai-bar" id="ai-bar">
<span class="ai-bar-icon">✦</span>
<input class="ai-bar-input" id="ai-prompt" value="Generate a TypeScript notification service with retry logic and priority queues" />
<button class="ai-bar-btn" id="ai-gen-btn" onclick="generateCode()">Generate</button>
</div>
<div class="ready-bar" id="ready-bar">
<span class="ready-bar-ok">βœ“</span>
<span class="ready-bar-info" id="ready-info">code generated</span>
<button class="commit-btn" onclick="openCommit()">↑ Commit &amp; push</button>
</div>
<div class="editor-welcome" id="editor-welcome">
<h2>Meridian Code Editor</h2>
<p>Create a file with <strong>+</strong> in the explorer, or ask the <strong>AI Agent</strong> to do it.</p>
<div class="ext">HTML Β· JavaScript Β· CSS Β· JSON</div>
</div>
<div class="editor-wrapper" id="editor-wrapper" style="display:none">
<div class="line-numbers" id="line-numbers">1</div>
<textarea class="code-textarea" id="code-textarea" spellcheck="false"
oninput="onInput()" onscroll="syncScroll()" onkeydown="editorKey(event)"
onclick="updateCursor()" onkeyup="updateCursor()"></textarea>
<div class="code-hl-view" id="code-hl-view"></div>
</div>
<div class="bottom-panel" id="bottom-panel">
<div class="panel-header">
<div class="ptab active" id="ptab-terminal" onclick="setPanel('terminal')">TERMINAL</div>
<div class="ptab" id="ptab-preview" onclick="setPanel('preview')">PREVIEW</div>
<div class="panel-actions">
<button class="pbtn" id="expand-btn" onclick="toggleExpand()" title="Expand preview">β€’</button>
<button class="pbtn" onclick="clearTerm()">Clear</button>
</div>
</div>
<div class="terminal-body" id="terminal-body">
<div><span class="t-out" style="color:#858585">Meridian Code Editor ready. Ask the AI agent or press + to create a file.</span></div>
<div><span class="t-ok">βœ“</span> <span class="t-out">Session storage active β€” files persist across refreshes.</span></div>
<div style="display:flex; align-items:center; gap:6px; margin-top:4px" class="t-input-line">
<span class="t-prompt">$</span>
<input type="text" class="t-actual-input" style="background:none; border:none; color:#ccc; outline:none; font-family:inherit; font-size:inherit; flex:1" placeholder="Type a prompt for the AI agent and press Enter..." onkeydown="if(event.key==='Enter' && this.value.trim()) { setSidebar('agent'); document.getElementById('chat-input').value = this.value; sendChat(); this.value=''; }">
</div>
</div>
<div class="preview-body" id="preview-body">
<iframe class="preview-iframe" id="preview-iframe" sandbox="allow-scripts allow-same-origin"></iframe>
</div>
</div>
</div>
</div>
<div class="status-bar">
<div class="st-item" id="st-lang">Plain Text</div>
<div class="st-right">
<div class="st-item" id="st-cursor">Ln 1, Col 1</div>
<div class="st-item">UTF-8</div>
</div>
</div>
<script>
// ── Constants ──────────────────────────────────────────────────────────
const AMD_MODEL = 'llama-3.3-70b-versatile';
const LS_FILES = 'meridian-vscode-files';
// URL del endpoint AMD (recibida desde el app padre via postMessage o heredada de window)
let _amdUrlOverride = '';
// Receive API endpoint URL from parent Meridian app via postMessage
window.addEventListener('message', (e) => {
if (e.data?.type === 'AMD_URL' && e.data?.url) {
_amdUrlOverride = e.data.url;
const apiInput = document.getElementById('api-key');
if (apiInput && !apiInput.value.trim()) apiInput.value = _amdUrlOverride;
}
});
function getEffectiveAmdUrl() {
const fromInput = document.getElementById('api-key')?.value?.trim();
return fromInput || _amdUrlOverride || (window.getAmdUrl ? window.getAmdUrl() : '') || '';
}
// ── State ──────────────────────────────────────────────────────────────
const files = {}; // { name: content }
let openTabs = []; // ordered open file names
let active = null; // current file name
let panelMode = 'terminal';
let expanded = false;
let agentBusy = false;
let codeGenDone = false;
let codeStreaming = false;
const agentHistory = []; // chat messages array
// ── File icons / language ──────────────────────────────────────────────
function fileIcon(name) {
const ext = name.split('.').pop().toLowerCase();
const m = { html:'<span class="file-icon icon-html">HTML</span>', htm:'<span class="file-icon icon-html">HTML</span>',
js:'<span class="file-icon icon-js">JS</span>', mjs:'<span class="file-icon icon-js">JS</span>',
css:'<span class="file-icon icon-css">CSS</span>', json:'<span class="file-icon icon-json">JSON</span>' };
return m[ext] || '<span class="file-icon icon-txt">TXT</span>';
}
function fileLang(name) {
const ext = name.split('.').pop().toLowerCase();
const m = { html:'HTML', htm:'HTML', js:'JavaScript', mjs:'JavaScript', css:'CSS', json:'JSON', md:'Markdown', ts:'TypeScript' };
return m[ext] || 'Plain Text';
}
function defaultContent(name) {
const ext = name.split('.').pop().toLowerCase();
if (ext === 'html') return `<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <title>My page</title>\n <style>\n body { font-family: sans-serif; padding: 20px; }\n </style>\n</head>\n<body>\n <h1>Hello world</h1>\n <p>My first page.</p>\n</body>\n</html>`;
if (ext === 'js') return `// ${name}\nconsole.log('Hello from ${name}!');\n`;
if (ext === 'css') return `/* ${name} */\nbody {\n font-family: sans-serif;\n margin: 0;\n padding: 20px;\n}\n`;
if (ext === 'json') return `{\n \n}\n`;
return '';
}
function esc(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── Persistence ────────────────────────────────────────────────────────
function persistFiles() {
try { localStorage.setItem(LS_FILES, JSON.stringify(files)); } catch(e) {}
}
function loadPersistedFiles() {
try {
const saved = JSON.parse(localStorage.getItem(LS_FILES) || '{}');
Object.assign(files, saved);
if (Object.keys(files).length) {
openTabs = Object.keys(files).slice(0, 5);
active = openTabs[0];
}
} catch(e) {}
}
// ── File operations ────────────────────────────────────────────────────
function createFile(name, content) {
saveCurrent();
if (content === undefined) content = defaultContent(name);
files[name] = content;
if (!openTabs.includes(name)) openTabs.push(name);
active = name;
renderAll();
persistFiles();
}
function openFile(name) {
if (files[name] === undefined) return;
saveCurrent();
if (!openTabs.includes(name)) openTabs.push(name);
active = name;
renderAll();
document.getElementById('code-textarea').focus();
}
function closeTab(name) {
const idx = openTabs.indexOf(name);
if (idx < 0) return;
openTabs.splice(idx, 1);
if (active === name) active = openTabs.length ? openTabs[Math.max(0, idx - 1)] : null;
renderAll();
}
function deleteFile(name) {
closeTab(name);
delete files[name];
renderAll();
persistFiles();
}
function saveCurrent() {
if (!active) return;
const ta = document.getElementById('code-textarea');
if (ta) { files[active] = ta.value; persistFiles(); }
}
// ── New file prompt ────────────────────────────────────────────────────
function newFile() {
const name = prompt('File name (e.g., index.html, app.js):');
if (!name || !name.trim()) return;
const trimmed = name.trim();
if (files[trimmed] !== undefined) { openFile(trimmed); return; }
createFile(trimmed);
}
// ── Render ─────────────────────────────────────────────────────────────
function renderAll() {
renderTree();
renderTabs();
renderEditor();
renderStatus();
}
function renderTree() {
const tree = document.getElementById('file-tree');
const names = Object.keys(files);
if (!names.length) {
tree.innerHTML = '<div class="empty-state">No files.<br>Press <strong>+</strong> to create one.</div>';
return;
}
tree.innerHTML = names.map(n => `
<div class="file-item ${n === active ? 'active' : ''}" data-file="${esc(n)}">
${fileIcon(n)}
<span class="file-name">${esc(n)}</span>
<button class="file-del" data-del="${esc(n)}" title="Delete">Γ—</button>
</div>`).join('');
}
function isRunnable(name) {
if (!name) return false;
const ext = name.split('.').pop().toLowerCase();
return ext === 'html' || ext === 'htm';
}
function renderTabs() {
const bar = document.getElementById('tab-bar');
const canRun = isRunnable(active);
const prBtn = `<button class="run-btn" style="background:var(--violet);margin-right:8px" onclick="createPR()">βš‘ Create PR</button>`;
const runBtn = `<button class="run-btn" id="run-btn" ${canRun ? '' : 'disabled'} onclick="runActive()">β–Ά Run</button>`;
if (!openTabs.length) { bar.innerHTML = prBtn + runBtn; return; }
bar.innerHTML = openTabs.map(n => `
<div class="tab ${n === active ? 'active' : ''}" data-file="${esc(n)}">
${fileIcon(n)} ${esc(n)}
<span class="tab-close" data-close="${esc(n)}">Γ—</span>
</div>`).join('') + prBtn + runBtn;
}
function renderEditor() {
const welcome = document.getElementById('editor-welcome');
const wrapper = document.getElementById('editor-wrapper');
const ta = document.getElementById('code-textarea');
document.getElementById('titlebar-title').textContent = active ? `${active} β€” Code Editor` : 'Code Editor β€” Meridian';
if (!active) {
welcome.style.display = 'flex';
wrapper.style.display = 'none';
} else {
welcome.style.display = 'none';
wrapper.style.display = 'flex';
ta.value = files[active] || '';
updateLineNums();
}
}
function renderStatus() {
document.getElementById('st-lang').textContent = active ? fileLang(active) : 'Plain Text';
}
// ── Editor helpers ─────────────────────────────────────────────────────
function updateLineNums() {
const ta = document.getElementById('code-textarea');
const ln = document.getElementById('line-numbers');
const cnt = ta.value.split('\n').length;
ln.innerHTML = Array.from({length: cnt}, (_, i) => i + 1).join('<br>');
}
function syncScroll() {
const ta = document.getElementById('code-textarea');
document.getElementById('line-numbers').scrollTop = ta.scrollTop;
}
function onInput() {
saveCurrent();
updateLineNums();
updateCursor();
}
function updateCursor() {
const ta = document.getElementById('code-textarea');
const val = ta.value.substring(0, ta.selectionStart);
const lines = val.split('\n');
document.getElementById('st-cursor').textContent = `Ln ${lines.length}, Col ${lines[lines.length - 1].length + 1}`;
}
function editorKey(e) {
if (e.key === 'Tab') {
e.preventDefault();
const ta = e.target, s = ta.selectionStart, en = ta.selectionEnd;
ta.value = ta.value.substring(0, s) + ' ' + ta.value.substring(en);
ta.selectionStart = ta.selectionEnd = s + 2;
saveCurrent(); updateLineNums();
}
}
// ── Sidebar toggle ─────────────────────────────────────────────────────
function setSidebar(mode) {
const isExplorer = mode === 'explorer';
document.getElementById('sidebar-explorer').style.display = isExplorer ? 'flex' : 'none';
document.getElementById('sidebar-agent').style.display = isExplorer ? 'none' : 'flex';
document.getElementById('act-explorer').classList.toggle('active', isExplorer);
document.getElementById('act-agent').classList.toggle('active', !isExplorer);
if (!isExplorer) {
setTimeout(() => document.getElementById('chat-input').focus(), 50);
}
}
// ── Panel toggle ────────────────────────────────────────────────────────
function setPanel(mode) {
panelMode = mode;
document.getElementById('ptab-terminal').classList.toggle('active', mode === 'terminal');
document.getElementById('ptab-preview').classList.toggle('active', mode === 'preview');
document.getElementById('terminal-body').style.display = mode === 'terminal' ? 'block' : 'none';
document.getElementById('preview-body').style.display = mode === 'preview' ? 'block' : 'none';
document.getElementById('expand-btn').style.display = mode === 'preview' ? 'inline-flex' : 'none';
}
function toggleExpand() {
expanded = !expanded;
const panel = document.getElementById('bottom-panel');
const wrap = document.getElementById('editor-wrapper');
const wel = document.getElementById('editor-welcome');
panel.classList.toggle('expanded', expanded);
if (expanded) {
wrap.style.display = 'none';
wel.style.display = 'none';
} else {
if (active) wrap.style.display = 'flex';
else wel.style.display = 'flex';
}
document.getElementById('expand-btn').textContent = expanded ? '‑' : '‒';
}
// ── Terminal ────────────────────────────────────────────────────────────
function termWrite(html) {
const body = document.getElementById('terminal-body');
const cur = body.querySelector('.t-input-line');
if (cur) cur.remove();
const el = document.createElement('div');
el.innerHTML = html;
body.appendChild(el);
const curLine = document.createElement('div');
curLine.className = 't-input-line';
curLine.style = 'display:flex; align-items:center; gap:6px; margin-top:4px';
curLine.innerHTML = `<span class="t-prompt">$</span> <input type="text" class="t-actual-input" style="background:none; border:none; color:#ccc; outline:none; font-family:inherit; font-size:inherit; flex:1" placeholder="Type a prompt for the AI agent and press Enter..." onkeydown="if(event.key==='Enter' && this.value.trim()) { setSidebar('agent'); document.getElementById('chat-input').value = this.value; sendChat(); this.value=''; }">`;
body.appendChild(curLine);
body.scrollTop = body.scrollHeight;
}
function termLine(type, text) {
const c = { ok:'t-ok', warn:'t-warn', err:'t-err', out:'t-out', cmd:'t-cmd' }[type] || 't-out';
termWrite(`<span class="${c}">${esc(text)}</span>`);
}
function termCmd(cmd) {
termWrite(`<span class="t-prompt">$</span> <span class="t-cmd">${esc(cmd)}</span>`);
}
function clearTerm() {
document.getElementById('terminal-body').innerHTML = `<div style="display:flex; align-items:center; gap:6px; margin-top:4px" class="t-input-line"><span class="t-prompt">$</span> <input type="text" class="t-actual-input" style="background:none; border:none; color:#ccc; outline:none; font-family:inherit; font-size:inherit; flex:1" placeholder="Type a prompt for the AI agent and press Enter..." onkeydown="if(event.key==='Enter' && this.value.trim()) { setSidebar('agent'); document.getElementById('chat-input').value = this.value; sendChat(); this.value=''; }"></div>`;
}
// ── Run ────────────────────────────────────────────────────────────────
function runActive() {
if (!active) return;
saveCurrent();
runFile(active);
}
async function runFile(name) {
const content = files[name];
if (content === undefined) return 'Error: file not found';
const ext = name.split('.').pop().toLowerCase();
if (ext === 'html' || ext === 'htm') return runHTML(name, content);
if (ext === 'js') return runJS(name, content);
if (ext === 'css') {
termLine('warn', `CSS files cannot run standalone. Include ${name} in an HTML file.`);
return `Not directly runnable. Tip: create an HTML file that links to ${name}.`;
}
termLine('warn', `To run this file: open ${name} in a browser or with node ${name}`);
return `Not runnable: .${ext}`;
}
function runHTML(name, content) {
termCmd(`open ${name}`);
const lines = content.split('\n').length;
termLine('out', ` Loading ${name} (${lines} lines)…`);
termLine('ok', `βœ“ ${name} rendered β€” Preview active`);
document.getElementById('preview-iframe').srcdoc = content;
setPanel('preview');
return `HTML preview opened. Command to run locally: npx serve .`;
}
function runJS(name, content) {
termCmd(`node ${name}`);
return new Promise(resolve => {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.setAttribute('sandbox', 'allow-scripts');
document.body.appendChild(iframe);
const tid = setTimeout(() => { cleanup(); termLine('err', 'Timeout (5s)'); resolve('Timeout'); }, 5000);
function cleanup() { clearTimeout(tid); window.removeEventListener('message', handler); try { document.body.removeChild(iframe); } catch(e){} }
function handler(e) {
if (e.source !== iframe.contentWindow) return;
if (!e.data || e.data.type !== '_meridian_result') return;
cleanup();
const { logs, error } = e.data;
logs.forEach(([t, msg]) => termLine(t === 'error' ? 'err' : t === 'warn' ? 'warn' : 'out', msg));
if (error) { termLine('err', `⚠ ${error}`); resolve(`Error: ${error}`); }
else { termLine('ok', `βœ“ ${name} executed`); resolve(logs.map(([,m]) => m).join('\n') || '(no output)'); }
}
window.addEventListener('message', handler);
const src = content.replace(/<\/script>/gi, '<\\/script>');
iframe.srcdoc = `<!DOCTYPE html><html><body><script>
const _L=[];
const console={
log:(...a)=>_L.push(['log',a.map(x=>typeof x==='object'?JSON.stringify(x):String(x)).join(' ')]),
error:(...a)=>_L.push(['error',a.map(x=>String(x)).join(' ')]),
warn:(...a)=>_L.push(['warn',a.map(x=>String(x)).join(' ')]),
info:(...a)=>_L.push(['log',a.map(x=>String(x)).join(' ')]),
};
try{${src};parent.postMessage({type:'_meridian_result',logs:_L,error:null},'*');}
catch(e){parent.postMessage({type:'_meridian_result',logs:_L,error:e.message},'*');}
<\/script></body></html>`;
});
}
// ── AI Agent (AMD vLLM Endpoint / OpenAI-compatible) ───────────────────────────────
const SYSTEM = `You are a code agent embedded in the Meridian Code Editor. You create, edit and run web files (HTML, CSS, JavaScript) for the user.
Rules:
- Always produce complete, functional, beautiful code (no placeholders, no TODO comments).
- Use modern HTML5/CSS3/JS. For HTML pages, inline styles or a <style> tag is fine (no build step).
- After creating a file, always call run_file so the user sees the result immediately.
- Mention the terminal command to run the file (e.g. "npx serve ." or just open index.html in a browser).
- Be concise in chat β€” let the code speak.
- Respond in English.`;
// OpenAI-compatible function definitions for AMD Endpoint
const TOOLS = [
{ type: 'function', function: { name: 'create_file', description: 'Create a new file with the given name and content. Use for HTML, CSS, JS.',
parameters: { type:'object', properties: { name:{type:'string',description:'Filename, e.g. index.html'}, content:{type:'string',description:'Full file content'} }, required:['name','content'] } } },
{ type: 'function', function: { name: 'edit_file', description: 'Replace the entire content of an existing file.',
parameters: { type:'object', properties: { name:{type:'string'}, content:{type:'string'} }, required:['name','content'] } } },
{ type: 'function', function: { name: 'read_file', description: 'Read the current content of a file.',
parameters: { type:'object', properties: { name:{type:'string'} }, required:['name'] } } },
{ type: 'function', function: { name: 'list_files', description: 'List all files in the editor.',
parameters: { type:'object', properties: {} } } },
{ type: 'function', function: { name: 'run_file', description: 'Execute a file β€” HTML opens live preview, JS runs in sandbox.',
parameters: { type:'object', properties: { name:{type:'string'} }, required:['name'] } } },
{ type: 'function', function: { name: 'run_command', description: 'Run a terminal command (e.g., "node file.js", "npm install").',
parameters: { type:'object', properties: { command:{type:'string', description:'The terminal command to run'} }, required:['command'] } } },
];
function chatKey(e) {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChat(); }
}
function addMsg(role, text) {
const box = document.getElementById('agent-chat');
const div = document.createElement('div');
div.className = `chat-msg ${role}`;
div.innerHTML = window.marked ? window.marked.parse(text) : esc(text).replace(/\n/g,'<br>');
box.appendChild(div);
box.scrollTop = box.scrollHeight;
return div;
}
function addToolMsg(name, input) {
const box = document.getElementById('agent-chat');
const div = document.createElement('div');
div.className = 'chat-msg tool';
const inp = JSON.stringify(input, null, 2);
div.innerHTML = `πŸ”§ <strong>${esc(name)}</strong><pre>${esc(inp.length > 300 ? inp.slice(0,300)+'…' : inp)}</pre>`;
box.appendChild(div);
box.scrollTop = box.scrollHeight;
}
async function sendChat() {
if (agentBusy) return;
// Prefer URL from input field; fall back to app-level AMD_ENDPOINT_URL
const amdUrl = getEffectiveAmdUrl();
const inputEl = document.getElementById('chat-input');
const msg = inputEl.value.trim();
if (!msg) return;
if (!amdUrl || amdUrl.includes('<IP-PUBLICA>')) { addMsg('err', '⚠ Endpoint AMD no configurado. ActualizÑ AMD_ENDPOINT_URL en ai-engine.jsx con la IP de tu instancia AMD.'); return; }
inputEl.value = '';
agentBusy = true;
document.getElementById('chat-send').disabled = true;
addMsg('user', msg);
agentHistory.push({ role: 'user', content: msg });
const thinking = addMsg('thinking', 'β‹― Thinking…');
try {
await agentLoop(amdUrl, thinking);
} catch(e) {
thinking.remove();
addMsg('assistant', `⚠ API Error: ${e?.message || String(e)}`);
} finally {
agentBusy = false;
document.getElementById('chat-send').disabled = false;
}
}
async function agentLoop(amdUrl, thinkingEl) {
let first = true;
const MAX_ITERS = 8;
for (let iter = 0; iter < MAX_ITERS; iter++) {
const res = await fetch(amdUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer dummy-key` },
body: JSON.stringify({
model: AMD_MODEL,
max_tokens: 4096,
messages: [{ role: 'system', content: SYSTEM }, ...agentHistory],
tools: TOOLS,
tool_choice: 'auto',
})
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error?.message || `AMD API HTTP ${res.status}`);
}
const data = await res.json();
if (first) { thinkingEl.remove(); first = false; }
const choice = data.choices?.[0];
const msg = choice?.message;
if (!msg) break;
agentHistory.push(msg);
// Text content
if (msg.content) {
msg.content = msg.content.replace(/Ġ/g, ' ').replace(/Ċ/g, '\n').replace(/ď/g, "'").replace(/č/g, "c");
addMsg('assistant', msg.content);
}
// Tool calls (OpenAI format)
const toolCalls = msg.tool_calls || [];
if (!toolCalls.length || choice.finish_reason === 'stop') break;
const results = [];
for (const tc of toolCalls) {
let input;
try { input = JSON.parse(tc.function.arguments); } catch { input = {}; }
addToolMsg(tc.function.name, input);
const out = await execTool(tc.function.name, input);
results.push({ role: 'tool', tool_call_id: tc.id, content: String(out) });
}
agentHistory.push(...results);
}
}
async function execTool(name, input) {
switch (name) {
case 'create_file':
createFile(input.name, input.content);
return `File "${input.name}" created.`;
case 'edit_file':
if (files[input.name] === undefined) return `Error: "${input.name}" does not exist.`;
files[input.name] = input.content;
if (active === input.name) { document.getElementById('code-textarea').value = input.content; updateLineNums(); }
renderTree(); renderTabs();
return `File "${input.name}" edited.`;
case 'read_file':
if (files[input.name] === undefined) return `Error: "${input.name}" does not exist.`;
return files[input.name];
case 'list_files': {
const ns = Object.keys(files);
return ns.length ? ns.join('\n') : 'No files.';
}
case 'run_file':
if (files[input.name] === undefined) return `Error: "${input.name}" does not exist.`;
openFile(input.name);
return await runFile(input.name);
case 'run_command':
termCmd(input.command);
if (input.command.startsWith('node ')) {
const f = input.command.split(' ')[1];
if (files[f]) { openFile(f); return await runFile(f); }
termLine('err', `Error: file ${f} not found`);
return `Error: file ${f} not found`;
}
termLine('out', `(Simulated) ${input.command}`);
return `Simulated command execution success.`;
default:
return `Unknown tool: ${name}`;
}
}
// ── Demo: AI Code Streaming ────────────────────────────────────────────
const notifierLines = [
`<span class="cmt">// notifier.ts β€” generated by Meridian AI</span>`,
`<span class="cmt">// Notification service with retry logic and priority queues</span>`,
``,
`<span class="kw">export type</span> <span class="ty">Priority</span> = <span class="str">'low'</span> | <span class="str">'medium'</span> | <span class="str">'high'</span> | <span class="str">'critical'</span>;`,
``,
`<span class="kw">export interface</span> <span class="cls">Notification</span> {`,
` <span class="va">id</span>: <span class="ty">string</span>;`,
` <span class="va">recipient</span>: <span class="ty">string</span>;`,
` <span class="va">subject</span>: <span class="ty">string</span>;`,
` <span class="va">body</span>: <span class="ty">string</span>;`,
` <span class="va">priority</span>: <span class="ty">Priority</span>;`,
` <span class="va">retries</span>: <span class="ty">number</span>;`,
` <span class="va">maxRetries</span>: <span class="ty">number</span>;`,
` <span class="va">createdAt</span>: <span class="ty">Date</span>;`,
`}`,
``,
`<span class="kw">interface</span> <span class="cls">QueueItem</span> {`,
` <span class="va">notification</span>: <span class="ty">Notification</span>;`,
` <span class="va">score</span>: <span class="ty">number</span>;`,
`}`,
``,
`<span class="kw">const</span> <span class="va">PRIORITY_SCORES</span>: <span class="ty">Record</span>&lt;<span class="ty">Priority</span>, <span class="ty">number</span>&gt; = {`,
` <span class="str">low</span>: <span class="num">1</span>,`,
` <span class="str">medium</span>: <span class="num">2</span>,`,
` <span class="str">high</span>: <span class="num">4</span>,`,
` <span class="str">critical</span>: <span class="num">8</span>,`,
`};`,
``,
`<span class="kw">export class</span> <span class="cls">NotificationService</span> {`,
` <span class="kw">private</span> <span class="va">queue</span>: <span class="ty">QueueItem</span>[] = [];`,
` <span class="kw">private readonly</span> <span class="va">maxConcurrent</span> = <span class="num">3</span>;`,
` <span class="kw">private</span> <span class="va">running</span> = <span class="num">0</span>;`,
``,
` <span class="fn">enqueue</span>(<span class="va">notification</span>: <span class="ty">Notification</span>): <span class="ty">void</span> {`,
` <span class="kw">const</span> <span class="va">score</span> = <span class="va">PRIORITY_SCORES</span>[<span class="va">notification</span>.<span class="va">priority</span>];`,
` <span class="kw">this</span>.<span class="va">queue</span>.<span class="fn">push</span>({ <span class="va">notification</span>, <span class="va">score</span> });`,
` <span class="kw">this</span>.<span class="va">queue</span>.<span class="fn">sort</span>((<span class="va">a</span>, <span class="va">b</span>) =&gt; <span class="va">b</span>.<span class="va">score</span> - <span class="va">a</span>.<span class="va">score</span>);`,
` <span class="kw">this</span>.<span class="fn">flush</span>();`,
` }`,
``,
` <span class="kw">private async</span> <span class="fn">flush</span>(): <span class="ty">Promise</span>&lt;<span class="ty">void</span>&gt; {`,
` <span class="kw">while</span> (<span class="kw">this</span>.<span class="va">queue</span>.<span class="va">length</span> &gt; <span class="num">0</span> &amp;&amp; <span class="kw">this</span>.<span class="va">running</span> &lt; <span class="kw">this</span>.<span class="va">maxConcurrent</span>) {`,
` <span class="kw">const</span> <span class="va">item</span> = <span class="kw">this</span>.<span class="va">queue</span>.<span class="fn">shift</span>()!;`,
` <span class="kw">this</span>.<span class="va">running</span>++;`,
` <span class="kw">this</span>.<span class="fn">dispatch</span>(<span class="va">item</span>.<span class="va">notification</span>)`,
` .<span class="fn">finally</span>(() =&gt; { <span class="kw">this</span>.<span class="va">running</span>--; <span class="kw">this</span>.<span class="fn">flush</span>(); });`,
` }`,
` }`,
``,
` <span class="kw">private async</span> <span class="fn">dispatch</span>(<span class="va">n</span>: <span class="ty">Notification</span>): <span class="ty">Promise</span>&lt;<span class="ty">void</span>&gt; {`,
` <span class="kw">for</span> (<span class="kw">let</span> <span class="va">attempt</span> = <span class="num">0</span>; <span class="va">attempt</span> &lt;= <span class="va">n</span>.<span class="va">maxRetries</span>; <span class="va">attempt</span>++) {`,
` <span class="kw">try</span> {`,
` <span class="kw">await</span> <span class="kw">this</span>.<span class="fn">send</span>(<span class="va">n</span>);`,
` <span class="kw">return</span>;`,
` } <span class="kw">catch</span> (<span class="va">err</span>) {`,
` <span class="kw">if</span> (<span class="va">attempt</span> === <span class="va">n</span>.<span class="va">maxRetries</span>) <span class="kw">throw</span> <span class="va">err</span>;`,
` <span class="kw">await</span> <span class="kw">this</span>.<span class="fn">delay</span>(<span class="ty">Math</span>.<span class="fn">pow</span>(<span class="num">2</span>, <span class="va">attempt</span>) * <span class="num">200</span>);`,
` }`,
` }`,
` }`,
``,
` <span class="kw">private async</span> <span class="fn">send</span>(<span class="va">n</span>: <span class="ty">Notification</span>): <span class="ty">Promise</span>&lt;<span class="ty">void</span>&gt; {`,
` <span class="cmt">// Simulate async transport (replace with real implementation)</span>`,
` <span class="kw">await</span> <span class="kw">this</span>.<span class="fn">delay</span>(<span class="num">50</span> + <span class="ty">Math</span>.<span class="fn">random</span>() * <span class="num">150</span>);`,
` <span class="va">console</span>.<span class="fn">log</span>(<span class="str">'Sent ['</span> + <span class="va">n</span>.<span class="va">priority</span> + <span class="str">'] to '</span> + <span class="va">n</span>.<span class="va">recipient</span>);`,
` }`,
``,
` <span class="kw">private</span> <span class="fn">delay</span>(<span class="va">ms</span>: <span class="ty">number</span>): <span class="ty">Promise</span>&lt;<span class="ty">void</span>&gt; {`,
` <span class="kw">return new</span> <span class="cls">Promise</span>(<span class="va">resolve</span> =&gt; <span class="fn">setTimeout</span>(<span class="va">resolve</span>, <span class="va">ms</span>));`,
` }`,
`}`,
];
function updateLineNumsForHL() {
const ln = document.getElementById('line-numbers');
ln.innerHTML = Array.from({length: notifierLines.length}, (_, i) => i + 1).join('<br>');
}
function generateCode() {
if (codeStreaming || codeGenDone) return;
const btn = document.getElementById('ai-gen-btn');
btn.disabled = true;
btn.textContent = 'Generating…';
codeStreaming = true;
const hlView = document.getElementById('code-hl-view');
const ta = document.getElementById('code-textarea');
const welcome = document.getElementById('editor-welcome');
const wrapper = document.getElementById('editor-wrapper');
const aiBar = document.getElementById('ai-bar');
welcome.style.display = 'none';
wrapper.style.display = 'flex';
ta.style.display = 'none';
hlView.style.display = 'block';
hlView.innerHTML = '';
updateLineNumsForHL();
let i = 0;
function streamLine() {
if (i >= notifierLines.length) {
const cur = hlView.querySelector('.ai-gen-cursor');
if (cur) cur.remove();
codeStreaming = false;
codeGenDone = true;
files['notifier.ts'] = notifierLines.map(l => l.replace(/<[^>]*>/g, '')).join('\n');
aiBar.classList.remove('visible');
const readyBar = document.getElementById('ready-bar');
document.getElementById('ready-info').innerHTML =
`<strong>notifier.ts</strong> β€” ${notifierLines.length} lines generated`;
readyBar.classList.add('visible');
renderStatus();
return;
}
const cur = hlView.querySelector('.ai-gen-cursor');
if (cur) cur.remove();
const div = document.createElement('div');
div.innerHTML = notifierLines[i] + '<span class="ai-gen-cursor"></span>';
hlView.appendChild(div);
hlView.scrollTop = hlView.scrollHeight;
i++;
setTimeout(streamLine, 28 + Math.random() * 55);
}
streamLine();
}
function openCommit() {
document.getElementById('commit-additions').textContent = '+' + notifierLines.length;
document.getElementById('commit-overlay').classList.add('open');
}
function closeCommit() {
document.getElementById('commit-overlay').classList.remove('open');
}
function doCommit() {
const msg = document.getElementById('commit-msg').value.trim() || 'feat: add NotificationService';
const goBtn = document.getElementById('commit-go');
goBtn.disabled = true;
goBtn.textContent = 'Pushing…';
const steps = [
{ type: 'cmd', text: 'git add notifier.ts' },
{ type: 'cmd', text: `git commit -m "${msg}"` },
{ type: 'out', text: ` 1 file changed, ${notifierLines.length} insertions(+)` },
{ type: 'cmd', text: 'git push origin feat/ai-generated' },
{ type: 'out', text: 'Enumerating objects: 4, done.' },
{ type: 'out', text: 'Writing objects: 100% (3/3), 1.42 KiB, done.' },
{ type: 'ok', text: `βœ“ feat/ai-generated β†’ origin [new branch]` },
];
closeCommit();
let idx = 0;
function nextStep() {
if (idx >= steps.length) {
goBtn.disabled = false;
goBtn.textContent = 'Commit & push';
window.parent.postMessage({
type: 'meridian:vscode-commit',
fileName: 'notifier.ts',
commitMessage: msg,
branch: 'feat/ai-generated',
additions: notifierLines.length,
}, '*');
return;
}
const { type, text } = steps[idx++];
if (type === 'cmd') termCmd(text); else termLine(type, text);
setTimeout(nextStep, 200 + Math.random() * 220);
}
nextStep();
}
// ── Event delegation ───────────────────────────────────────────────────
document.getElementById('file-tree').addEventListener('click', e => {
const del = e.target.closest('[data-del]');
const item = e.target.closest('[data-file]');
if (del) { e.stopPropagation(); if (confirm(`Delete ${del.dataset.del}?`)) deleteFile(del.dataset.del); }
else if (item) openFile(item.dataset.file);
});
document.getElementById('tab-bar').addEventListener('click', e => {
const close = e.target.closest('[data-close]');
const tab = e.target.closest('[data-file]');
if (close) { e.stopPropagation(); closeTab(close.dataset.close); }
else if (tab) openFile(tab.dataset.file);
});
// Ctrl+Enter or F5 to run
document.addEventListener('keydown', e => {
if ((e.ctrlKey && e.key === 'Enter') || e.key === 'F5') { e.preventDefault(); runActive(); }
});
// ── Init ───────────────────────────────────────────────────────────────
// Load persisted files from localStorage
loadPersistedFiles();
// Pre-fill AMD endpoint URL desde la constante central (si el campo estΓ‘ vacΓ­o)
(function() {
const apiInput = document.getElementById('api-key');
if (apiInput && !apiInput.value.trim() && window.AMD_ENDPOINT_URL) {
apiInput.value = window.AMD_ENDPOINT_URL;
}
})();
setPanel('terminal');
renderAll();
async function createPR() {
const prBtn = document.querySelector('.run-btn[onclick="createPR()"]');
if (prBtn) { prBtn.disabled = true; prBtn.innerText = '⏳ Creating PR...'; }
try {
const amdUrl = getEffectiveAmdUrl();
if (!amdUrl || amdUrl.includes('<IP-PUBLICA>')) throw new Error("Endpoint AMD no configurado. ActualizΓ‘ AMD_ENDPOINT_URL en ai-engine.jsx.");
// Generate AI Summary based on code
const fileContents = Object.entries(files).map(([name, content]) => `// File: ${name}\n${content}`).join('\n\n');
const res = await fetch(amdUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer dummy-key` },
body: JSON.stringify({
model: 'llama-3.3-70b-versatile',
max_tokens: 200,
messages: [
{ role: 'system', content: 'You are a senior developer. Based on the file contents provided, generate a brief, one-sentence PR title and a very short description (max 2 sentences) summarizing what this code does. Format strictly as JSON: {"title": "the title", "description": "the description"}' },
{ role: 'user', content: fileContents || 'Empty project' }
],
response_format: { type: "json_object" }
})
});
if (!res.ok) throw new Error("Failed to contact AMD API for PR summary.");
const data = await res.json();
let content = data.choices[0].message.content || '{}';
content = content.replace(/Ġ/g, ' ').replace(/Ċ/g, '\n').replace(/ď/g, "'").replace(/č/g, "c");
const summary = JSON.parse(content);
// Count rough lines of code for additions
let lines = 0;
for (const key in files) lines += files[key].split('\\n').length;
// Use window.parent to dispatch to the main app state
if (window.parent && window.parent.apiFetch) {
await window.parent.apiFetch('POST', '/api/prs', {
title: summary.title,
branch: 'feature/vscode-ai-gen',
base: 'main',
status: 'open',
additions: lines,
deletions: 0
});
window.parent.toast("Pull Request created successfully!");
if (window.parent.meridianRefresh) window.parent.meridianRefresh();
} else {
termLine('err', 'βœ— Could not connect to parent app window to create PR.');
}
} catch (err) {
termLine('err', `⚠ API Error: ${err?.message || String(err)}`);
} finally {
if (prBtn) { prBtn.disabled = false; prBtn.innerText = 'βš‘ Create PR'; }
}
}
// Greet in terminal
termLine('out', ' Meridian Code Editor ready. Ask the AI agent or press + to create a file.');
termLine('ok', 'βœ“ Session storage active β€” files persist across refreshes.');
</script>
<div class="commit-overlay" id="commit-overlay">
<div class="commit-dialog">
<div class="commit-header">
↑ Commit &amp; push
<button class="commit-x" onclick="closeCommit()">Γ—</button>
</div>
<div class="commit-body">
<div>
<div class="commit-lbl">Commit message</div>
<textarea class="commit-msg-inp" id="commit-msg" rows="2">feat: add NotificationService with retry logic and priority queues</textarea>
</div>
<div class="commit-meta">
<span>1 file changed</span>
<span class="commit-add" id="commit-additions">+59</span>
<span>β†’ feat/ai-generated</span>
</div>
</div>
<div class="commit-footer">
<button class="cbtn cbtn-sec" onclick="closeCommit()">Cancel</button>
<button class="cbtn cbtn-pri" id="commit-go" onclick="doCommit()">Commit &amp; push</button>
</div>
</div>
</div>
</body>
</html>