UNPKG

aura-ai

Version:

AI-powered marketing strategist CLI tool for developers

333 lines (291 loc) 8.31 kB
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Aura AI - Web Terminal</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" /> <style> * { margin: 0; padding: 0; box-sizing: border-box; } html, body { width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; background: #1e1e1e; } .terminal-container { position: absolute; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%; display: flex; flex-direction: column; background: #1e1e1e; animation: slideUp 0.5s ease-out; } #terminal { flex: 1; width: 100%; height: 100%; overflow: hidden; } .status { margin-top: 20px; text-align: center; color: white; font-size: 14px; } .status.connected { color: #27c93f; } .status.disconnected { color: #ff5f56; } .controls { margin-top: 20px; display: flex; gap: 10px; justify-content: center; } .btn { padding: 10px 20px; background: rgba(255, 255, 255, 0.2); color: white; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 6px; cursor: pointer; transition: all 0.3s; font-size: 14px; } .btn:hover { background: rgba(255, 255, 255, 0.3); transform: translateY(-2px); } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .loading { color: white; text-align: center; padding: 50px; font-size: 18px; } .emoji-spinner { font-size: 30px; animation: spin 1s linear infinite; display: inline-block; margin-bottom: 10px; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } </style> </head> <body> <div class="terminal-container"> <div id="terminal"> <div class="loading"> <div class="emoji-spinner">🌀</div> <div>Connecting to terminal...</div> </div> </div> </div> <!-- <div class="status" id="status">Connecting...</div> <div class="controls"> <button class="btn" onclick="reconnect()">🔄 Reconnect</button> <button class="btn" onclick="clearTerminal()">🗑️ Clear</button> <button class="btn" onclick="toggleFullscreen()">📺 Fullscreen</button> </div> --> <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.min.js"></script> <script> let term let fitAddon let socket let isConnected = false function initTerminal() { // 清除載入訊息 document.getElementById('terminal').innerHTML = '' // 建立終端 term = new Terminal({ cursorBlink: true, fontSize: 14, fontFamily: 'Menlo, Monaco, "Courier New", monospace', theme: { background: '#1e1e1e', foreground: '#d4d4d4', cursor: '#fffff', cursorAccent: '#000000', selection: 'rgba(255, 255, 255, 0.3)', black: '#000000', red: '#cd3131', green: '#0dbc79', yellow: '#e5e510', blue: '#2472c8', magenta: '#bc3fbc', cyan: '#11a8cd', white: '#e5e5e5', brightBlack: '#666666', brightRed: '#f14c4c', brightGreen: '#23d18b', brightYellow: '#f5f543', brightBlue: '#3b8eea', brightMagenta: '#d670d6', brightCyan: '#29b8db', brightWhite: '#ffffff', }, }) // 加入插件 fitAddon = new FitAddon.FitAddon() term.loadAddon(fitAddon) const webLinksAddon = new WebLinksAddon.WebLinksAddon() term.loadAddon(webLinksAddon) // 開啟終端 const terminalEl = document.getElementById('terminal') term.open(terminalEl) // 多次調用 fit 確保正確計算尺寸 fitAddon.fit() // 延遲再次調整以確保 DOM 完全渲染 setTimeout(() => { fitAddon.fit() term.scrollToBottom() }, 10) // 再次確保尺寸正確 requestAnimationFrame(() => { fitAddon.fit() }) // 自動調整大小 window.addEventListener('resize', () => { fitAddon.fit() if (socket && socket.readyState === WebSocket.OPEN) { socket.send( JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows, }) ) } }) } function connectWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' const wsUrl = `${protocol}//${window.location.host}/terminal` socket = new WebSocket(wsUrl) socket.onopen = () => { isConnected = true updateStatus('Connected', true) // 發送初始大小 socket.send( JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows, }) ) } socket.onmessage = event => { const message = JSON.parse(event.data) if (message.type === 'output') { term.write(message.data) // 自動捲動到底部 term.scrollToBottom() } } socket.onclose = () => { isConnected = false updateStatus('Disconnected', false) term.write('\r\n\x1b[31mConnection lost. Click Reconnect to continue.\x1b[0m\r\n') term.scrollToBottom() } socket.onerror = error => { updateStatus('Connection error', false) } // 處理終端輸入 term.onData(data => { if (socket && socket.readyState === WebSocket.OPEN) { socket.send( JSON.stringify({ type: 'input', data: data, }) ) } }) } function updateStatus(text, connected) { const statusEl = document.getElementById('status') if (statusEl) { statusEl.textContent = text statusEl.className = connected ? 'status connected' : 'status disconnected' } } function reconnect() { if (socket) { socket.close() } term.clear() connectWebSocket() } function clearTerminal() { if (term) { term.clear() } } function toggleFullscreen() { if (!document.fullscreenElement) { document.documentElement.requestFullscreen() } else { document.exitFullscreen() } } // 初始化 window.onload = () => { initTerminal() connectWebSocket() // 確保視窗完全載入後再次調整 window.addEventListener('load', () => { setTimeout(() => { if (fitAddon) { fitAddon.fit() } }, 100) }) } </script> </body> </html>