Spaces:
Running
Running
| // Bot Card Web Component - Quantum Theme | |
| class BotCard extends HTMLElement { | |
| constructor() { | |
| super(); | |
| this.attachShadow({ mode: "open" }); | |
| this._bot = null; | |
| this._logs = []; | |
| this._metrics = new Map(); | |
| this._quotes = { bid: "-", ask: "-", spread: "-" }; | |
| this._progress = 0; | |
| this._pausedOverride = null; | |
| } | |
| set bot(b) { | |
| this._bot = JSON.parse(JSON.stringify(b)); | |
| this.render(); | |
| } | |
| get bot() { | |
| return this._bot; | |
| } | |
| setPaused(p) { | |
| if (!this._bot) return; | |
| this._bot.paused = p; | |
| this.render(); | |
| } | |
| togglePaused() { | |
| if (!this._bot) return; | |
| this._bot.paused = !this._bot.paused; | |
| this.render(); | |
| } | |
| log(msg) { | |
| const line = `[${new Date().toLocaleTimeString()}] ${msg}`; | |
| this._logs.unshift(line); | |
| if (this._logs.length > 4) this._logs.length = 4; | |
| this.updateUI(); | |
| } | |
| setMetric(key, val) { | |
| this._metrics.set(key, val); | |
| this.updateUI(); | |
| } | |
| setProgress(pct) { | |
| this._progress = pct; | |
| this.updateUI(); | |
| } | |
| setQuotes({ bid, ask, spread }) { | |
| this._quotes = { | |
| bid: bid != null ? bid.toFixed(2) : "-", | |
| ask: ask != null ? ask.toFixed(2) : "-", | |
| spread: spread != null ? spread.toFixed(2) : "-", | |
| }; | |
| this.updateUI(); | |
| } | |
| updateUI() { | |
| if (!this.shadowRoot) return; | |
| const root = this.shadowRoot.querySelector("#root"); | |
| if (!root) return; | |
| const pausedBadge = root.querySelector("#pausedBadge"); | |
| const pauseBtn = root.querySelector("#pauseBtn"); | |
| const quotes = root.querySelector("#quotes"); | |
| const metrics = root.querySelector("#metrics"); | |
| const progress = root.querySelector("#progress"); | |
| const progressFill = root.querySelector("#progressFill"); | |
| const logs = root.querySelector("#logs"); | |
| // Status badge | |
| if (this._bot?.paused) { | |
| pausedBadge.classList.remove("hidden"); | |
| } else { | |
| pausedBadge.classList.add("hidden"); | |
| } | |
| // Pause/Resume button | |
| if (pauseBtn) { | |
| pauseBtn.innerHTML = this._bot?.paused | |
| ? `<i data-feather="play-circle" class="w-4 h-4"></i> Resume` | |
| : `<i data-feather="pause-circle" class="w-4 h-4"></i> Pause`; | |
| } | |
| // Quotes | |
| quotes.innerHTML = ` | |
| <div class="space-y-1"> | |
| <div class="flex justify-between text-xs"> | |
| <span class="text-gray-400">Bid</span> | |
| <span class="font-mono font-semibold">${this._quotes.bid}</span> | |
| </div> | |
| <div class="flex justify-between text-xs"> | |
| <span class="text-gray-400">Ask</span> | |
| <span class="font-mono font-semibold">${this._quotes.ask}</span> | |
| </div> | |
| <div class="flex justify-between text-xs"> | |
| <span class="text-gray-400">Spread</span> | |
| <span class="font-mono font-semibold text-accent">${this._quotes.spread}</span> | |
| </div> | |
| </div> | |
| `; | |
| // Metrics | |
| const metricHtml = Array.from(this._metrics.entries()) | |
| .map(([k, v]) => `<div class="flex justify-between text-xs"><span class="text-gray-400">${k}</span><span class="font-mono text-indigo-300">${v}</span></div>`) | |
| .join(""); | |
| metrics.innerHTML = metricHtml; | |
| // Progress | |
| progress.classList.toggle("hidden", this._bot?.type !== "TWAP"); | |
| if (!progress.classList.contains("hidden")) { | |
| progressFill.style.width = `${Math.max(0, Math.min(100, this._progress))}%`; | |
| } | |
| // Logs | |
| logs.innerHTML = this._logs.map((l) => `<div class="text-[11px] text-gray-500 truncate font-mono">${l}</div>`).join(""); | |
| // Re-apply feather icons if needed | |
| if (typeof feather !== "undefined") feather.replace(); | |
| } | |
| render() { | |
| if (!this._bot) return; | |
| const colorMap = { | |
| green: "bg-gradient-to-br from-emerald-500 to-green-600", | |
| blue: "bg-gradient-to-br from-blue-500 to-cyan-600", | |
| purple: "bg-gradient-to-br from-purple-500 to-pink-600", | |
| amber: "bg-gradient-to-br from-amber-500 to-orange-600", | |
| }; | |
| const color = colorMap[this._bot.color] || "bg-gradient-to-br from-indigo-500 to-purple-600"; | |
| const paused = this._bot.paused; | |
| this.shadowRoot.innerHTML = ` | |
| <style> | |
| :host { display: block; } | |
| .card { | |
| background: rgba(15, 23, 42, 0.6); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid rgba(99, 102, 241, 0.2); | |
| border-radius: 12px; | |
| overflow: hidden; | |
| transition: all 0.3s ease; | |
| } | |
| .card:hover { | |
| border-color: rgba(99, 102, 241, 0.4); | |
| box-shadow: 0 0 20px rgba(99, 102, 241, 0.2); | |
| } | |
| .header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 8px; | |
| padding: 12px; | |
| border-bottom: 1px solid rgba(99, 102, 241, 0.1); | |
| } | |
| .left { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .dot { | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 50%; | |
| box-shadow: 0 0 10px rgba(99, 102, 241, 0.5); | |
| } | |
| .body { | |
| padding: 12px; | |
| display: grid; | |
| grid-template-columns: 1fr; | |
| gap: 12px; | |
| } | |
| .section { | |
| padding: 8px; | |
| background: rgba(30, 41, 59, 0.5); | |
| border-radius: 8px; | |
| border: 1px solid rgba(99, 102, 241, 0.1); | |
| } | |
| .meta { | |
| font-size: 11px; | |
| color: #94a3b8; | |
| } | |
| button { | |
| background: rgba(99, 102, 241, 0.1); | |
| border: 1px solid rgba(99, 102, 241, 0.3); | |
| color: #fff; | |
| border-radius: 6px; | |
| padding: 4px 8px; | |
| font-size: 11px; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| } | |
| button:hover { | |
| background: rgba(99, 102, 241, 0.2); | |
| border-color: rgba(99, 102, 241, 0.5); | |
| } | |
| #progress { | |
| height: 8px; | |
| background: rgba(30, 41, 59, 0.8); | |
| border-radius: 4px; | |
| overflow: hidden; | |
| } | |
| #progressFill { | |
| height: 100%; | |
| background: linear-gradient(to right, #6366f1, #a855f7); | |
| width: 0%; | |
| transition: width 0.3s ease; | |
| } | |
| #logs { | |
| background: rgba(15, 23, 42, 0.8); | |
| border: 1px solid rgba(99, 102, 241, 0.1); | |
| padding: 8px; | |
| border-radius: 6px; | |
| max-height: 80px; | |
| overflow: hidden; | |
| } | |
| </style> | |
| <div class="card" id="root"> | |
| <div class="header"> | |
| <div class="left"> | |
| <div class="dot ${color}"></div> | |
| <div> | |
| <div class="font-semibold text-sm text-white">${this._bot.name}</div> | |
| <div class="meta">${this._bot.type} • ${this._bot.symbol}</div> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-2"> | |
| <span id="pausedBadge" class="text-[10px] px-2 py-1 rounded-full bg-red-500/20 text-red-400 border border-red-500/30 ${paused ? "" : "hidden"}">PAUSED</span> | |
| <button id="pauseBtn" title="Pause/Resume"> | |
| ${paused ? "Resume" : "Pause"} | |
| </button> | |
| </div> | |
| </div> | |
| <div class="body"> | |
| <div class="section"> | |
| <div class="text-xs font-semibold text-gray-300 mb-2">Quotes</div> | |
| <div id="quotes"></div> | |
| </div> | |
| <div class="section"> | |
| <div class="text-xs font-semibold text-gray-300 mb-2">Metrics</div> | |
| <div id="metrics"></div> | |
| </div> | |
| <div id="progress" class="hidden"> | |
| <div id="progressFill"></div> | |
| </div> | |
| <div class="section"> | |
| <div class="text-xs font-semibold text-gray-300 mb-2">Activity Log</div> | |
| <div id="logs"></div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| this.updateUI(); | |
| const pauseBtn = this.shadowRoot.querySelector("#pauseBtn"); | |
| pauseBtn.addEventListener("click", () => this.togglePaused()); | |
| } | |
| } | |
| customElements.define("bot-card", BotCard); | |