Spaces:
Paused
Paused
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>TEKDEV // AGENT</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=VT323&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --green: #00ff41; | |
| --green-dim: #00cc33; | |
| --green-dark: #003b00; | |
| --green-glow: rgba(0,255,65,0.15); | |
| --bg: #0a0a0a; | |
| --text-dim: #007a1f; | |
| --yellow: #ffd700; | |
| --red: #ff4444; | |
| --blue: #4af; | |
| } | |
| - { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| background: var(–bg); | |
| color: var(–green); | |
| font-family: ‘Share Tech Mono’, monospace; | |
| font-size: 13px; | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| body::before { | |
| content: ‘’; | |
| position: fixed; | |
| inset: 0; | |
| background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.07) 2px, rgba(0,0,0,0.07) 4px); | |
| pointer-events: none; | |
| z-index: 9999; | |
| } | |
| /* HEADER */ | |
| header { | |
| border-bottom: 1px solid var(–green-dark); | |
| padding: 7px 14px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| background: #060606; | |
| flex-shrink: 0; | |
| } | |
| .logo { font-family: ‘VT323’, monospace; font-size: 20px; letter-spacing: 4px; text-shadow: 0 0 10px var(–green); } | |
| .logo span { color: var(–text-dim); } | |
| .header-right { display: flex; align-items: center; gap: 10px; } | |
| .status-pill { | |
| display: flex; align-items: center; gap: 6px; | |
| font-size: 10px; color: var(–text-dim); | |
| } | |
| .dot { width: 6px; height: 6px; border-radius: 50%; background: var(–green); box-shadow: 0 0 6px var(–green); animation: pulse 2s infinite; } | |
| @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.3} } | |
| select, .hdr-btn { | |
| background: transparent; border: 1px solid var(–green-dark); | |
| color: var(–text-dim); font-family: ‘Share Tech Mono’, monospace; | |
| font-size: 10px; padding: 3px 8px; cursor: pointer; outline: none; | |
| transition: all 0.2s; | |
| } | |
| select:hover, .hdr-btn:hover { border-color: var(–green); color: var(–green); } | |
| option { background: #080808; } | |
| /* TABS */ | |
| .tabs { | |
| display: flex; border-bottom: 1px solid var(–green-dark); | |
| background: #060606; flex-shrink: 0; | |
| } | |
| .tab { | |
| padding: 6px 16px; font-size: 11px; cursor: pointer; | |
| color: var(–text-dim); border-right: 1px solid var(–green-dark); | |
| transition: all 0.2s; | |
| } | |
| .tab:hover { color: var(–green); background: var(–green-glow); } | |
| .tab.active { color: var(–green); border-bottom: 1px solid var(–green); background: rgba(0,255,65,0.05); } | |
| /* PANELS */ | |
| .panel { display: none; flex: 1; flex-direction: column; overflow: hidden; } | |
| .panel.active { display: flex; } | |
| /* TERMINAL */ | |
| #terminal { | |
| flex: 1; overflow-y: auto; padding: 12px 14px; | |
| display: flex; flex-direction: column; gap: 1px; | |
| } | |
| #terminal::-webkit-scrollbar { width: 3px; } | |
| #terminal::-webkit-scrollbar-thumb { background: var(–green-dark); } | |
| .line { line-height: 1.65; white-space: pre-wrap; word-break: break-word; } | |
| .line.user-cmd { color: var(–green); } | |
| .line.user-cmd::before { content: ’you › ’; color: var(–text-dim); } | |
| .line.agent-text { color: #00cc33; } | |
| .line.cmd-running { color: var(–yellow); } | |
| .line.cmd-running::before { content: ’⚡ exec › ’; } | |
| .line.cmd-out { color: #009920; padding-left: 8px; border-left: 2px solid var(–green-dark); } | |
| .line.cmd-err { color: #cc4444; padding-left: 8px; border-left: 2px solid #cc2222; } | |
| .line.sys { color: var(–text-dim); font-size: 11px; } | |
| .line.err { color: var(–red); } | |
| .line.blank { height: 5px; } | |
| .line.streaming::after { content: ‘▋’; animation: blink 0.6s infinite; } | |
| @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} } | |
| /* INPUT */ | |
| #input-area { | |
| border-top: 1px solid var(–green-dark); | |
| background: #060606; flex-shrink: 0; | |
| } | |
| #input-row { | |
| display: flex; align-items: flex-end; gap: 8px; | |
| padding: 8px 14px; | |
| } | |
| .prefix { color: var(–text-dim); white-space: nowrap; padding-bottom: 2px; } | |
| #cmd-input { | |
| flex: 1; background: transparent; border: none; | |
| color: var(–green); font-family: ‘Share Tech Mono’, monospace; | |
| font-size: 13px; outline: none; caret-color: var(–green); | |
| resize: none; min-height: 20px; max-height: 120px; | |
| overflow-y: auto; line-height: 1.5; | |
| } | |
| .run-btn { | |
| background: transparent; border: 1px solid var(–green); | |
| color: var(–green); font-family: ‘Share Tech Mono’, monospace; | |
| font-size: 11px; padding: 5px 14px; cursor: pointer; | |
| transition: all 0.2s; white-space: nowrap; align-self: flex-end; | |
| } | |
| .run-btn:hover { background: var(–green-glow); box-shadow: 0 0 10px var(–green-glow); } | |
| .run-btn:disabled { opacity: 0.3; cursor: not-allowed; } | |
| /* QUICK CMDS */ | |
| #quickbar { | |
| padding: 5px 14px 7px; display: flex; gap: 6px; flex-wrap: wrap; | |
| border-top: 1px solid #111; | |
| } | |
| .qbtn { | |
| background: transparent; border: 1px solid #1a2a1a; | |
| color: #005c10; font-family: ‘Share Tech Mono’, monospace; | |
| font-size: 10px; padding: 2px 8px; cursor: pointer; transition: all 0.2s; | |
| } | |
| .qbtn:hover { border-color: var(–green-dim); color: var(–green-dim); background: var(–green-glow); } | |
| /* SETTINGS PANEL */ | |
| #panel-settings { | |
| padding: 20px; overflow-y: auto; gap: 16px; | |
| } | |
| .setting-group { margin-bottom: 20px; } | |
| .setting-group h3 { color: var(–green); font-size: 12px; letter-spacing: 2px; margin-bottom: 10px; border-bottom: 1px solid var(–green-dark); padding-bottom: 6px; } | |
| .field { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; } | |
| .field label { font-size: 10px; color: var(–text-dim); } | |
| .field input { | |
| background: #0d0d0d; border: 1px solid var(–green-dark); | |
| color: var(–green); font-family: ‘Share Tech Mono’, monospace; | |
| font-size: 12px; padding: 6px 10px; outline: none; transition: border-color 0.2s; | |
| } | |
| .field input:focus { border-color: var(–green); box-shadow: 0 0 8px var(–green-glow); } | |
| .save-btn { | |
| background: transparent; border: 1px solid var(–green); | |
| color: var(–green); font-family: ‘Share Tech Mono’, monospace; | |
| font-size: 11px; padding: 6px 20px; cursor: pointer; transition: all 0.2s; | |
| } | |
| .save-btn:hover { background: var(–green-glow); } | |
| .hint { font-size: 10px; color: var(–text-dim); margin-top: 4px; line-height: 1.5; } | |
| /* FILES PANEL */ | |
| #panel-files { padding: 14px; overflow-y: auto; } | |
| #file-tree { font-size: 12px; color: #009920; line-height: 1.8; } | |
| .refresh-btn { | |
| background: transparent; border: 1px solid var(–green-dark); | |
| color: var(–text-dim); font-family: ‘Share Tech Mono’, monospace; | |
| font-size: 10px; padding: 3px 10px; cursor: pointer; margin-bottom: 12px; | |
| transition: all 0.2s; | |
| } | |
| .refresh-btn:hover { border-color: var(–green); color: var(–green); } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="logo">TEKDEV<span>//</span>AGENT</div> | |
| <div class="header-right"> | |
| <div class="status-pill"> | |
| <div class="dot" id="status-dot"></div> | |
| <span id="status-text">READY</span> | |
| </div> | |
| <select id="model-select"> | |
| <option value="google/gemma-4-31b-it:free">gemma-4-31b</option> | |
| <option value="google/gemma-4-26b-a4b-it:free">gemma-4-26b-moe</option> | |
| <option value="qwen/qwen3-235b-a22b:free">qwen3-235b</option> | |
| <option value="meta-llama/llama-4-maverick:free">llama-4-maverick</option> | |
| <option value="nvidia/nemotron-3-super-120b-a12b:free">nemotron-120b</option> | |
| </select> | |
| <button class="hdr-btn" onclick="clearTerminal()">CLEAR</button> | |
| </div> | |
| </header> | |
| <div class="tabs"> | |
| <div class="tab active" onclick="switchTab('terminal', this)">TERMINAL</div> | |
| <div class="tab" onclick="switchTab('files', this)">WORKSPACE</div> | |
| <div class="tab" onclick="switchTab('settings', this)">CONFIG</div> | |
| </div> | |
| <!-- TERMINAL PANEL --> | |
| <div class="panel active" id="panel-terminal"> | |
| <div id="terminal"></div> | |
| <div id="input-area"> | |
| <div id="input-row"> | |
| <span class="prefix">›</span> | |
| <textarea id="cmd-input" rows="1" placeholder="describe a task or type a command..." spellcheck="false"></textarea> | |
| <button class="run-btn" id="run-btn" onclick="sendCommand()">RUN</button> | |
| </div> | |
| <div id="quickbar"> | |
| <button class="qbtn" onclick="q('clone my repo and show me the structure')">clone repo</button> | |
| <button class="qbtn" onclick="q('show me all files in the workspace')">ls workspace</button> | |
| <button class="qbtn" onclick="q('git status and show recent commits')">git status</button> | |
| <button class="qbtn" onclick="q('stage all changes and commit with a smart message')">git commit</button> | |
| <button class="qbtn" onclick="q('push to origin main')">git push</button> | |
| <button class="qbtn" onclick="q('review the code and suggest improvements')">review code</button> | |
| <button class="qbtn" onclick="q('install dependencies and run the project')">npm install & run</button> | |
| <button class="qbtn" onclick="q('show git diff of latest changes')">git diff</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- FILES PANEL --> | |
| <div class="panel" id="panel-files"> | |
| <button class="refresh-btn" onclick="loadFiles()">⟳ REFRESH</button> | |
| <div id="file-tree">// loading workspace...</div> | |
| </div> | |
| <!-- SETTINGS PANEL --> | |
| <div class="panel" id="panel-settings"> | |
| <div class="setting-group"> | |
| <h3>// API KEY</h3> | |
| <div class="field"> | |
| <label>OPENROUTER API KEY</label> | |
| <input type="password" id="s-apikey" placeholder="sk-or-v1-..." /> | |
| </div> | |
| <p class="hint">Get a free key at openrouter.ai — stored in localStorage only.</p> | |
| </div> | |
| <div class="setting-group"> | |
| <h3>// GIT IDENTITY</h3> | |
| <div class="field"> | |
| <label>GIT NAME</label> | |
| <input type="text" id="s-gitname" placeholder="Your Name" /> | |
| </div> | |
| <div class="field"> | |
| <label>GIT EMAIL</label> | |
| <input type="text" id="s-gitemail" placeholder="you@email.com" /> | |
| </div> | |
| </div> | |
| <div class="setting-group"> | |
| <h3>// GITHUB</h3> | |
| <div class="field"> | |
| <label>GITHUB TOKEN (for push)</label> | |
| <input type="password" id="s-ghtoken" placeholder="ghp_..." /> | |
| </div> | |
| <p class="hint">Generate at github.com/settings/tokens — needs repo scope.<br>Used as: https://TOKEN@github.com/user/repo.git</p> | |
| </div> | |
| <button class="save-btn" onclick="saveConfig()">SAVE CONFIG</button> | |
| </div> | |
| <script> | |
| const terminal = document.getElementById('terminal'); | |
| const cmdInput = document.getElementById('cmd-input'); | |
| const runBtn = document.getElementById('run-btn'); | |
| let isRunning = false; | |
| let history = []; | |
| // Auto-resize textarea | |
| cmdInput.addEventListener('input', () => { | |
| cmdInput.style.height = 'auto'; | |
| cmdInput.style.height = Math.min(cmdInput.scrollHeight, 120) + 'px'; | |
| }); | |
| cmdInput.addEventListener('keydown', e => { | |
| if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendCommand(); } | |
| }); | |
| // Init | |
| window.onload = () => { | |
| loadSavedConfig(); | |
| boot(); | |
| }; | |
| function boot() { | |
| [ | |
| ['╔════════════════════════════════════╗', 'sys'], | |
| ['║ TEKDEV AGENT // DOCKER ONLINE ║', 'sys'], | |
| ['╚════════════════════════════════════╝', 'sys'], | |
| ['', 'blank'], | |
| ['// Real shell access — Ubuntu container', 'sys'], | |
| ['// Workspace: /data/workspace (persistent)', 'sys'], | |
| ['// Set your keys in CONFIG tab first', 'sys'], | |
| ['// Then try: "clone my repo and show structure"', 'sys'], | |
| ['', 'blank'], | |
| ].forEach((l, i) => setTimeout(() => printLine(l[0], l[1]), i * 50)); | |
| } | |
| function loadSavedConfig() { | |
| const k = localStorage.getItem('or_key'); | |
| const gn = localStorage.getItem('git_name'); | |
| const ge = localStorage.getItem('git_email'); | |
| const gh = localStorage.getItem('gh_token'); | |
| if (k) document.getElementById('s-apikey').value = k; | |
| if (gn) document.getElementById('s-gitname').value = gn; | |
| if (ge) document.getElementById('s-gitemail').value = ge; | |
| if (gh) document.getElementById('s-ghtoken').value = gh; | |
| } | |
| function saveConfig() { | |
| const key = document.getElementById('s-apikey').value.trim(); | |
| const gn = document.getElementById('s-gitname').value.trim(); | |
| const ge = document.getElementById('s-gitemail').value.trim(); | |
| const gh = document.getElementById('s-ghtoken').value.trim(); | |
| if (key) localStorage.setItem('or_key', key); | |
| if (gn) localStorage.setItem('git_name', gn); | |
| if (ge) localStorage.setItem('git_email', ge); | |
| if (gh) localStorage.setItem('gh_token', gh); | |
| // Send to backend | |
| fetch('/config', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ git_name: gn, git_email: ge, github_token: gh }) | |
| }); | |
| switchTab('terminal', document.querySelector('.tab')); | |
| printLine('// Config saved. Agent configured.', 'sys'); | |
| } | |
| function switchTab(name, el) { | |
| document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); | |
| document.querySelectorAll('.panel').forEach(p => p.classList.remove('active')); | |
| el.classList.add('active'); | |
| document.getElementById(`panel-${name}`).classList.add('active'); | |
| if (name === 'files') loadFiles(); | |
| } | |
| function printLine(text, cls = 'agent-text') { | |
| const div = document.createElement('div'); | |
| div.className = `line ${cls}`; | |
| div.textContent = text; | |
| terminal.appendChild(div); | |
| terminal.scrollTop = terminal.scrollHeight; | |
| return div; | |
| } | |
| function clearTerminal() { | |
| terminal.innerHTML = ''; | |
| history = []; | |
| printLine('// Cleared. Session reset.', 'sys'); | |
| } | |
| function q(text) { | |
| cmdInput.value = text; | |
| cmdInput.style.height = 'auto'; | |
| cmdInput.focus(); | |
| } | |
| function setStatus(text, color = 'var(--green)') { | |
| document.getElementById('status-text').textContent = text; | |
| const dot = document.getElementById('status-dot'); | |
| dot.style.background = color; | |
| dot.style.boxShadow = `0 0 6px ${color}`; | |
| } | |
| async function execCmd(cmd) { | |
| printLine(cmd, 'cmd-running'); | |
| try { | |
| const res = await fetch('/execute', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ cmd }) | |
| }); | |
| const data = await res.json(); | |
| if (data.output) { | |
| data.output.trim().split('\n').forEach(l => printLine(l, 'cmd-out')); | |
| } | |
| if (data.error) { | |
| data.error.trim().split('\n').forEach(l => printLine(l, 'cmd-err')); | |
| } | |
| return data; | |
| } catch (e) { | |
| printLine(`exec error: ${e.message}`, 'cmd-err'); | |
| return { error: e.message }; | |
| } | |
| } | |
| async function sendCommand() { | |
| if (isRunning) return; | |
| const input = cmdInput.value.trim(); | |
| if (!input) return; | |
| const key = localStorage.getItem('or_key'); | |
| if (!key) { | |
| printLine('// No API key — go to CONFIG tab and save your OpenRouter key.', 'err'); | |
| return; | |
| } | |
| cmdInput.value = ''; | |
| cmdInput.style.height = 'auto'; | |
| isRunning = true; | |
| runBtn.disabled = true; | |
| setStatus('THINKING...', '#ffd700'); | |
| printLine('', 'blank'); | |
| printLine(input, 'user-cmd'); | |
| printLine('', 'blank'); | |
| history.push({ role: 'user', content: input }); | |
| if (history.length > 20) history = history.slice(-20); | |
| const model = document.getElementById('model-select').value; | |
| let fullText = ''; | |
| const outputDiv = printLine('', 'agent-text streaming'); | |
| try { | |
| const response = await fetch('/chat', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ messages: history, api_key: key, model }) | |
| }); | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| setStatus('STREAMING...', '#ffd700'); | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| const chunk = decoder.decode(value); | |
| for (const line of chunk.split('\n')) { | |
| if (!line.startsWith('data: ')) continue; | |
| const data = line.slice(6).trim(); | |
| if (data === '[DONE]') break; | |
| try { | |
| const parsed = JSON.parse(data); | |
| if (parsed.error) throw new Error(parsed.error); | |
| if (parsed.delta) { | |
| fullText += parsed.delta; | |
| outputDiv.textContent = fullText; | |
| outputDiv.className = 'line agent-text streaming'; | |
| terminal.scrollTop = terminal.scrollHeight; | |
| } | |
| } catch(e) { | |
| if (e.message && !e.message.includes('JSON')) throw e; | |
| } | |
| } | |
| } | |
| outputDiv.className = 'line agent-text'; | |
| history.push({ role: 'assistant', content: fullText }); | |
| // Extract and execute <CMD> blocks | |
| const cmdRegex = /<CMD>([\s\S]*?)<\/CMD>/g; | |
| let match; | |
| const cmdsToRun = []; | |
| while ((match = cmdRegex.exec(fullText)) !== null) { | |
| cmdsToRun.push(match[1].trim()); | |
| } | |
| if (cmdsToRun.length > 0) { | |
| printLine('', 'blank'); | |
| setStatus('EXECUTING...', '#ff8c00'); | |
| const results = []; | |
| for (const cmd of cmdsToRun) { | |
| const result = await execCmd(cmd); | |
| results.push({ cmd, ...result }); | |
| } | |
| // Send results back to agent for interpretation | |
| const resultSummary = results.map(r => | |
| `CMD: ${r.cmd}\nSTDOUT: ${r.output || '(empty)'}\nSTDERR: ${r.error || '(none)'}\nEXIT: ${r.returncode}` | |
| ).join('\n\n'); | |
| history.push({ role: 'user', content: `Command results:\n${resultSummary}\n\nPlease interpret these results briefly.` }); | |
| // Get agent's interpretation | |
| printLine('', 'blank'); | |
| const interpDiv = printLine('', 'agent-text streaming'); | |
| let interpText = ''; | |
| const interpResp = await fetch('/chat', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ messages: history, api_key: key, model }) | |
| }); | |
| const reader2 = interpResp.body.getReader(); | |
| while (true) { | |
| const { done, value } = await reader2.read(); | |
| if (done) break; | |
| const chunk = decoder.decode(value); | |
| for (const line of chunk.split('\n')) { | |
| if (!line.startsWith('data: ')) continue; | |
| const data = line.slice(6).trim(); | |
| if (data === '[DONE]') break; | |
| try { | |
| const parsed = JSON.parse(data); | |
| if (parsed.delta) { | |
| interpText += parsed.delta; | |
| interpDiv.textContent = interpText; | |
| terminal.scrollTop = terminal.scrollHeight; | |
| } | |
| } catch {} | |
| } | |
| } | |
| interpDiv.className = 'line agent-text'; | |
| history.push({ role: 'assistant', content: interpText }); | |
| } | |
| printLine('', 'blank'); | |
| setStatus('READY'); | |
| } catch (err) { | |
| outputDiv.remove(); | |
| printLine(`// ERROR: ${err.message}`, 'err'); | |
| history.pop(); | |
| setStatus('ERROR', 'var(--red)'); | |
| setTimeout(() => setStatus('READY'), 3000); | |
| } | |
| isRunning = false; | |
| runBtn.disabled = false; | |
| cmdInput.focus(); | |
| } | |
| async function loadFiles() { | |
| const tree = document.getElementById('file-tree'); | |
| tree.textContent = '// scanning workspace...'; | |
| try { | |
| const res = await fetch('/workspace'); | |
| const data = await res.json(); | |
| if (data.files && data.files.length > 0) { | |
| tree.textContent = data.files.join('\n'); | |
| } else { | |
| tree.textContent = '// workspace is empty\n// use "clone repo" to get started'; | |
| } | |
| } catch { | |
| tree.textContent = '// could not reach backend'; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> |