Spaces:
Running
Running
| <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 ; 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 ; } | |
| .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 & 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); | |
| } | |
| // ββ 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><<span class="ty">Priority</span>, <span class="ty">number</span>> = {`, | |
| ` <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>) => <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><<span class="ty">void</span>> {`, | |
| ` <span class="kw">while</span> (<span class="kw">this</span>.<span class="va">queue</span>.<span class="va">length</span> > <span class="num">0</span> && <span class="kw">this</span>.<span class="va">running</span> < <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>(() => { <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><<span class="ty">void</span>> {`, | |
| ` <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> <= <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><<span class="ty">void</span>> {`, | |
| ` <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><<span class="ty">void</span>> {`, | |
| ` <span class="kw">return new</span> <span class="cls">Promise</span>(<span class="va">resolve</span> => <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 & 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 & push</button> | |
| </div> | |
| </div> | |
| </div> | |
| </body> | |
| </html> | |