| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Vultr Inference Usage Dashboard</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --primary: #00bfb3; | |
| --primary-dark: #009d93; | |
| --bg-light: #f8f9fa; | |
| --bg-card-light: #ffffff; | |
| --text-light: #212529; | |
| --text-muted-light: #6c757d; | |
| --border-light: #dee2e6; | |
| --error: #dc3545; | |
| --success: #28a745; | |
| --warning: #ffc107; | |
| --bg-dark: #1a1a2e; | |
| --bg-card-dark: #16213e; | |
| --text-dark: #e4e4e4; | |
| --text-muted-dark: #a0a0a0; | |
| --border-dark: #2d3748; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| background: var(--bg-light); | |
| color: var(--text-light); | |
| line-height: 1.6; | |
| transition: all 0.3s ease; | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| body.dark-mode { | |
| background: var(--bg-dark); | |
| color: var(--text-dark); | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| } | |
| /* Header */ | |
| .header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 30px; | |
| padding: 20px; | |
| background: var(--bg-card-light); | |
| border-radius: 12px; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| } | |
| body.dark-mode .header { | |
| background: var(--bg-card-dark); | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.3); | |
| } | |
| .header h1 { | |
| font-size: 1.8rem; | |
| background: linear-gradient(135deg, var(--primary), var(--primary-dark)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .header-controls { | |
| display: flex; | |
| gap: 15px; | |
| align-items: center; | |
| } | |
| /* Buttons */ | |
| .btn { | |
| padding: 10px 20px; | |
| border: none; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 0.9rem; | |
| font-weight: 600; | |
| transition: all 0.2s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--primary), var(--primary-dark)); | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px rgba(0, 191, 179, 0.4); | |
| } | |
| .btn-primary.tracking { | |
| background: linear-gradient(135deg, #f5576c, #f093fb); | |
| } | |
| .btn-primary.tracking:hover { | |
| box-shadow: 0 4px 12px rgba(245, 87, 108, 0.4); | |
| } | |
| .btn-secondary { | |
| background: var(--border-light); | |
| color: var(--text-light); | |
| } | |
| body.dark-mode .btn-secondary { | |
| background: var(--border-dark); | |
| color: var(--text-dark); | |
| } | |
| /* Theme Toggle */ | |
| .theme-toggle { | |
| background: none; | |
| border: 2px solid var(--border-light); | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.2rem; | |
| transition: all 0.3s ease; | |
| } | |
| body.dark-mode .theme-toggle { | |
| border-color: var(--border-dark); | |
| } | |
| .theme-toggle:hover { | |
| background: var(--border-light); | |
| } | |
| body.dark-mode .theme-toggle:hover { | |
| background: var(--border-dark); | |
| } | |
| /* API Key Input */ | |
| .api-key-section { | |
| background: var(--bg-card-light); | |
| padding: 20px; | |
| border-radius: 12px; | |
| margin-bottom: 30px; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| } | |
| body.dark-mode .api-key-section { | |
| background: var(--bg-card-dark); | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.3); | |
| } | |
| .input-group { | |
| display: flex; | |
| gap: 10px; | |
| align-items: center; | |
| } | |
| .input-wrapper { | |
| position: relative; | |
| flex: 1; | |
| } | |
| .api-input { | |
| width: 100%; | |
| padding: 12px 45px 12px 15px; | |
| border: 2px solid var(--border-light); | |
| border-radius: 8px; | |
| font-size: 0.95rem; | |
| background: var(--bg-light); | |
| color: var(--text-light); | |
| transition: all 0.3s ease; | |
| } | |
| body.dark-mode .api-input { | |
| background: var(--bg-dark); | |
| color: var(--text-dark); | |
| border-color: var(--border-dark); | |
| } | |
| .api-input:focus { | |
| outline: none; | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 3px rgba(0, 191, 179, 0.1); | |
| } | |
| .toggle-password { | |
| position: absolute; | |
| right: 12px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| background: none; | |
| border: none; | |
| cursor: pointer; | |
| font-size: 1.1rem; | |
| color: var(--text-muted-light); | |
| } | |
| body.dark-mode .toggle-password { | |
| color: var(--text-muted-dark); | |
| } | |
| /* Refresh Controls */ | |
| .refresh-controls { | |
| display: flex; | |
| gap: 15px; | |
| align-items: center; | |
| margin-top: 15px; | |
| flex-wrap: wrap; | |
| } | |
| .interval-selector { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .interval-selector select { | |
| padding: 8px 12px; | |
| border: 2px solid var(--border-light); | |
| border-radius: 6px; | |
| background: var(--bg-light); | |
| color: var(--text-light); | |
| cursor: pointer; | |
| } | |
| body.dark-mode .interval-selector select { | |
| background: var(--bg-dark); | |
| color: var(--text-dark); | |
| border-color: var(--border-dark); | |
| } | |
| .auto-refresh-toggle { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| cursor: pointer; | |
| } | |
| .toggle-switch { | |
| width: 50px; | |
| height: 26px; | |
| background: var(--border-light); | |
| border-radius: 13px; | |
| position: relative; | |
| transition: all 0.3s ease; | |
| } | |
| body.dark-mode .toggle-switch { | |
| background: var(--border-dark); | |
| } | |
| .toggle-switch.active { | |
| background: var(--primary); | |
| } | |
| .toggle-switch::after { | |
| content: ''; | |
| position: absolute; | |
| width: 22px; | |
| height: 22px; | |
| background: white; | |
| border-radius: 50%; | |
| top: 2px; | |
| left: 2px; | |
| transition: all 0.3s ease; | |
| } | |
| .toggle-switch.active::after { | |
| left: 26px; | |
| } | |
| .countdown { | |
| font-size: 0.9rem; | |
| color: var(--text-muted-light); | |
| font-weight: 600; | |
| } | |
| body.dark-mode .countdown { | |
| color: var(--text-muted-dark); | |
| } | |
| /* Status Indicator */ | |
| .status-bar { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| padding: 12px 20px; | |
| background: var(--bg-card-light); | |
| border-radius: 8px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.05); | |
| } | |
| body.dark-mode .status-bar { | |
| background: var(--bg-card-dark); | |
| } | |
| .status-dot { | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 50%; | |
| background: var(--success); | |
| animation: pulse 2s infinite; | |
| } | |
| .status-dot.loading { | |
| background: var(--warning); | |
| animation: blink 1s infinite; | |
| } | |
| .status-dot.error { | |
| background: var(--error); | |
| animation: none; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| } | |
| @keyframes blink { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.3; } | |
| } | |
| .status-text { | |
| font-weight: 600; | |
| font-size: 0.95rem; | |
| } | |
| .last-updated { | |
| margin-left: auto; | |
| color: var(--text-muted-light); | |
| font-size: 0.85rem; | |
| } | |
| body.dark-mode .last-updated { | |
| color: var(--text-muted-dark); | |
| } | |
| /* Grid */ | |
| .grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); | |
| gap: 20px; | |
| margin-bottom: 30px; | |
| } | |
| .card { | |
| background: var(--bg-card-light); | |
| padding: 24px; | |
| border-radius: 12px; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| transition: transform 0.3s ease, box-shadow 0.3s ease; | |
| } | |
| body.dark-mode .card { | |
| background: var(--bg-card-dark); | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.3); | |
| } | |
| .card:hover { | |
| transform: translateY(-4px); | |
| box-shadow: 0 4px 16px rgba(0,0,0,0.15); | |
| } | |
| .card-title { | |
| font-size: 0.85rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| color: var(--text-muted-light); | |
| margin-bottom: 8px; | |
| font-weight: 700; | |
| } | |
| body.dark-mode .card-title { | |
| color: var(--text-muted-dark); | |
| } | |
| .card-value { | |
| font-size: 2rem; | |
| font-weight: 700; | |
| background: linear-gradient(135deg, var(--primary), var(--primary-dark)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .card-subtitle { | |
| font-size: 0.8rem; | |
| color: var(--text-muted-light); | |
| margin-top: 4px; | |
| } | |
| body.dark-mode .card-subtitle { | |
| color: var(--text-muted-dark); | |
| } | |
| /* Sections */ | |
| .section { | |
| background: var(--bg-card-light); | |
| padding: 30px; | |
| border-radius: 12px; | |
| margin-bottom: 20px; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| } | |
| body.dark-mode .section { | |
| background: var(--bg-card-dark); | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.3); | |
| } | |
| .section-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| } | |
| .section-title { | |
| font-size: 1.3rem; | |
| font-weight: 700; | |
| } | |
| /* Usage Items */ | |
| .usage-item { | |
| margin-bottom: 20px; | |
| } | |
| .usage-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 8px; | |
| } | |
| .usage-label { | |
| font-weight: 600; | |
| font-size: 0.95rem; | |
| } | |
| .usage-value { | |
| font-weight: 700; | |
| font-size: 1.1rem; | |
| } | |
| .usage-bar-bg { | |
| width: 100%; | |
| height: 8px; | |
| background: var(--border-light); | |
| border-radius: 4px; | |
| overflow: hidden; | |
| } | |
| body.dark-mode .usage-bar-bg { | |
| background: var(--border-dark); | |
| } | |
| .usage-bar-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--primary), var(--primary-dark)); | |
| border-radius: 4px; | |
| transition: width 0.5s ease; | |
| } | |
| .usage-bar-fill.chat { | |
| background: linear-gradient(90deg, #667eea, #764ba2); | |
| } | |
| .usage-bar-fill.tts { | |
| background: linear-gradient(90deg, #f093fb, #f5576c); | |
| } | |
| .usage-bar-fill.image { | |
| background: linear-gradient(90deg, #4facfe, #00f2fe); | |
| } | |
| .usage-details { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-top: 6px; | |
| font-size: 0.8rem; | |
| color: var(--text-muted-light); | |
| } | |
| body.dark-mode .usage-details { | |
| color: var(--text-muted-dark); | |
| } | |
| /* Cost Section */ | |
| .cost-breakdown { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 15px; | |
| margin-top: 20px; | |
| } | |
| .cost-item { | |
| padding: 15px; | |
| background: var(--bg-light); | |
| border-radius: 8px; | |
| border-left: 4px solid var(--primary); | |
| } | |
| body.dark-mode .cost-item { | |
| background: var(--bg-dark); | |
| } | |
| .cost-label { | |
| font-size: 0.85rem; | |
| color: var(--text-muted-light); | |
| margin-bottom: 4px; | |
| } | |
| body.dark-mode .cost-label { | |
| color: var(--text-muted-dark); | |
| } | |
| .cost-value { | |
| font-size: 1.3rem; | |
| font-weight: 700; | |
| } | |
| .cost-value.dollars { | |
| color: var(--success); | |
| } | |
| /* Error Display */ | |
| .error-message { | |
| background: rgba(220, 53, 69, 0.1); | |
| border: 1px solid var(--error); | |
| color: var(--error); | |
| padding: 15px 20px; | |
| border-radius: 8px; | |
| margin-bottom: 20px; | |
| display: none; | |
| } | |
| .error-message.show { | |
| display: block; | |
| } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| .header { | |
| flex-direction: column; | |
| gap: 15px; | |
| text-align: center; | |
| } | |
| .header-controls { | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| } | |
| .refresh-controls { | |
| justify-content: center; | |
| } | |
| .input-group { | |
| flex-direction: column; | |
| } | |
| .grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| /* Loading Spinner */ | |
| .spinner { | |
| display: inline-block; | |
| width: 20px; | |
| height: 20px; | |
| border: 3px solid rgba(0, 191, 179, 0.3); | |
| border-top-color: var(--primary); | |
| border-radius: 50%; | |
| animation: spin 0.8s linear infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <!-- Header --> | |
| <div class="header"> | |
| <h1>🚀 Vultr Inference Usage Dashboard</h1> | |
| <div class="header-controls"> | |
| <button class="theme-toggle" id="themeToggle" title="Toggle Dark Mode">🌙</button> | |
| </div> | |
| </div> | |
| <!-- API Key Section --> | |
| <div class="api-key-section"> | |
| <div class="input-group"> | |
| <div class="input-wrapper"> | |
| <input | |
| type="password" | |
| id="apiKeyInput" | |
| class="api-input" | |
| placeholder="Enter your Vultr API Key..." | |
| autocomplete="off" | |
| > | |
| <button class="toggle-password" id="togglePassword" title="Show/Hide API Key">👁️</button> | |
| </div> | |
| <button class="btn btn-primary" id="saveKeyBtn"> | |
| 💾 Save Key | |
| </button> | |
| <button class="btn btn-secondary" id="refreshBtn"> | |
| 🔄 Refresh | |
| </button> | |
| </div> | |
| <div class="refresh-controls"> | |
| <div class="interval-selector"> | |
| <label for="intervalSelect">Refresh every:</label> | |
| <select id="intervalSelect"> | |
| <option value="5">5 seconds</option> | |
| <option value="10">10 seconds</option> | |
| <option value="30">30 seconds</option> | |
| <option value="60">1 minute</option> | |
| </select> | |
| </div> | |
| <div class="auto-refresh-toggle" id="autoRefreshToggle"> | |
| <div class="toggle-switch" id="toggleSwitch"></div> | |
| <span>Auto-refresh</span> | |
| </div> | |
| <span class="countdown" id="countdown"></span> | |
| </div> | |
| </div> | |
| <!-- Error Message --> | |
| <div class="error-message" id="errorMessage"></div> | |
| <!-- Status Bar --> | |
| <div class="status-bar"> | |
| <div class="status-dot" id="statusDot"></div> | |
| <span class="status-text" id="statusText">Ready - Enter API Key to begin</span> | |
| <span class="last-updated" id="lastUpdated"></span> | |
| </div> | |
| <!-- Session Tracking --> | |
| <div class="section" style="background: linear-gradient(135deg, rgba(0, 191, 179, 0.1), rgba(0, 157, 147, 0.1)); border: 2px solid var(--primary);"> | |
| <div class="section-header"> | |
| <h2 class="section-title">⏱️ Session Tracking</h2> | |
| <button class="btn btn-primary" id="startTrackingBtn"> | |
| ▶️ Start Tracking | |
| </button> | |
| </div> | |
| <div class="grid" style="margin-bottom: 0;"> | |
| <div class="card"> | |
| <div class="card-title">Chat Completion Tokens</div> | |
| <div class="card-value" id="sessionCompletionTokens">0</div> | |
| <div class="card-subtitle" id="sessionCompletionCost">$0.00 cost</div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-title">Chat Input Tokens</div> | |
| <div class="card-value" id="sessionInputTokens">0</div> | |
| <div class="card-subtitle" id="sessionInputCost">$0.00 cost</div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-title">Total Session Tokens</div> | |
| <div class="card-value" id="sessionTokens">0</div> | |
| <div class="card-subtitle">Completion + Input</div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-title">Total Session Cost</div> | |
| <div class="card-value" id="sessionCost">$0.00</div> | |
| <div class="card-subtitle">Completion + Input</div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-title">Session Duration</div> | |
| <div class="card-value" id="sessionDuration">00:00:00</div> | |
| <div class="card-subtitle">Time tracking</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Summary Cards --> | |
| <div class="grid"> | |
| <div class="card"> | |
| <div class="card-title">Current Month Chat Tokens</div> | |
| <div class="card-value" id="currentTokens">0</div> | |
| <div class="card-subtitle">Total (input + completion)</div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-title">Current Month Cost</div> | |
| <div class="card-value" id="currentCost">$0.00</div> | |
| <div class="card-subtitle">Estimated based on usage</div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-title">TTS Characters</div> | |
| <div class="card-value" id="ttsChars">0</div> | |
| <div class="card-subtitle">Text-to-speech this month</div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-title">Images Generated</div> | |
| <div class="card-value" id="imagePixels">0</div> | |
| <div class="card-subtitle">Megapixels this month</div> | |
| </div> | |
| </div> | |
| <!-- Current Month Section --> | |
| <div class="section"> | |
| <div class="section-header"> | |
| <h2 class="section-title">📊 Current Month Usage</h2> | |
| </div> | |
| <div class="usage-item"> | |
| <div class="usage-header"> | |
| <span class="usage-label">💬 Chat Completion Tokens</span> | |
| <span class="usage-value" id="currentChat">0 tokens</span> | |
| </div> | |
| <div class="usage-bar-bg"> | |
| <div class="usage-bar-fill chat" id="currentChatBar" style="width: 0%"></div> | |
| </div> | |
| <div class="usage-details"> | |
| <span>$2.75 per 1M tokens</span> | |
| <span id="currentChatCost">$0.00</span> | |
| </div> | |
| </div> | |
| <div class="usage-item"> | |
| <div class="usage-header"> | |
| <span class="usage-label">📥 Chat Input Tokens</span> | |
| <span class="usage-value" id="currentChatInput">0 tokens</span> | |
| </div> | |
| <div class="usage-bar-bg"> | |
| <div class="usage-bar-fill chat" id="currentChatInputBar" style="width: 0%"></div> | |
| </div> | |
| <div class="usage-details"> | |
| <span>$0.55 per 1M tokens</span> | |
| <span id="currentChatInputCost">$0.00</span> | |
| </div> | |
| </div> | |
| <div class="usage-item"> | |
| <div class="usage-header"> | |
| <span class="usage-label">🔊 TTS (HD Model)</span> | |
| <span class="usage-value" id="currentTts">0 characters</span> | |
| </div> | |
| <div class="usage-bar-bg"> | |
| <div class="usage-bar-fill tts" id="currentTtsBar" style="width: 0%"></div> | |
| </div> | |
| <div class="usage-details"> | |
| <span>Standard HD model</span> | |
| </div> | |
| </div> | |
| <div class="usage-item"> | |
| <div class="usage-header"> | |
| <span class="usage-label">🔉 TTS (Basic Model)</span> | |
| <span class="usage-value" id="currentTtsSm">0 characters</span> | |
| </div> | |
| <div class="usage-bar-bg"> | |
| <div class="usage-bar-fill tts" id="currentTtsSmBar" style="width: 0%"></div> | |
| </div> | |
| <div class="usage-details"> | |
| <span>Basic model</span> | |
| </div> | |
| </div> | |
| <div class="usage-item"> | |
| <div class="usage-header"> | |
| <span class="usage-label">🖼️ Images (Standard)</span> | |
| <span class="usage-value" id="currentImage">0 MP</span> | |
| </div> | |
| <div class="usage-bar-bg"> | |
| <div class="usage-bar-fill image" id="currentImageBar" style="width: 0%"></div> | |
| </div> | |
| <div class="usage-details"> | |
| <span>Standard model megapixels</span> | |
| </div> | |
| </div> | |
| <div class="usage-item"> | |
| <div class="usage-header"> | |
| <span class="usage-label">🖼️ Images (Small)</span> | |
| <span class="usage-value" id="currentImageSm">0 MP</span> | |
| </div> | |
| <div class="usage-bar-bg"> | |
| <div class="usage-bar-fill image" id="currentImageSmBar" style="width: 0%"></div> | |
| </div> | |
| <div class="usage-details"> | |
| <span>Small model megapixels</span> | |
| </div> | |
| </div> | |
| <!-- Cost Breakdown --> | |
| <div class="cost-breakdown"> | |
| <div class="cost-item"> | |
| <div class="cost-label">Completion Cost</div> | |
| <div class="cost-value dollars" id="completionCost">$0.00</div> | |
| </div> | |
| <div class="cost-item"> | |
| <div class="cost-label">Input Cost</div> | |
| <div class="cost-value dollars" id="inputCost">$0.00</div> | |
| </div> | |
| <div class="cost-item"> | |
| <div class="cost-label">Total Chat Cost</div> | |
| <div class="cost-value dollars" id="totalChatCost">$0.00</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Previous Month Section --> | |
| <div class="section"> | |
| <div class="section-header"> | |
| <h2 class="section-title">📈 Previous Month Usage</h2> | |
| </div> | |
| <div class="usage-item"> | |
| <div class="usage-header"> | |
| <span class="usage-label">💬 Chat Completion Tokens</span> | |
| <span class="usage-value" id="prevChat">0 tokens</span> | |
| </div> | |
| <div class="usage-bar-bg"> | |
| <div class="usage-bar-fill chat" id="prevChatBar" style="width: 0%"></div> | |
| </div> | |
| <div class="usage-details"> | |
| <span id="prevChatCost">$0.00</span> | |
| </div> | |
| </div> | |
| <div class="usage-item"> | |
| <div class="usage-header"> | |
| <span class="usage-label">📥 Chat Input Tokens</span> | |
| <span class="usage-value" id="prevChatInput">0 tokens</span> | |
| </div> | |
| <div class="usage-bar-bg"> | |
| <div class="usage-bar-fill chat" id="prevChatInputBar" style="width: 0%"></div> | |
| </div> | |
| <div class="usage-details"> | |
| <span id="prevChatInputCost">$0.00</span> | |
| </div> | |
| </div> | |
| <div class="cost-breakdown"> | |
| <div class="cost-item"> | |
| <div class="cost-label">Total Month Cost</div> | |
| <div class="cost-value dollars" id="prevTotalCost">$0.00</div> | |
| </div> | |
| <div class="cost-item"> | |
| <div class="cost-label">Total Tokens</div> | |
| <div class="cost-value" id="prevTotalTokens">0</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Pricing constants | |
| const PRICING = { | |
| completion_per_million: 2.75, | |
| input_per_million: 0.55 | |
| }; | |
| // State | |
| let state = { | |
| apiKey: localStorage.getItem('vultr_api_key') || '', | |
| autoRefresh: false, | |
| refreshInterval: 5, | |
| countdownValue: 5, | |
| intervalId: null, | |
| sessionTracking: false, | |
| sessionStart: null, | |
| sessionStartData: null, | |
| sessionIntervalId: null | |
| }; | |
| // DOM Elements | |
| const elements = { | |
| apiKeyInput: document.getElementById('apiKeyInput'), | |
| startTrackingBtn: document.getElementById('startTrackingBtn'), | |
| sessionTokens: document.getElementById('sessionTokens'), | |
| sessionCompletionTokens: document.getElementById('sessionCompletionTokens'), | |
| sessionInputTokens: document.getElementById('sessionInputTokens'), | |
| sessionCompletionCost: document.getElementById('sessionCompletionCost'), | |
| sessionInputCost: document.getElementById('sessionInputCost'), | |
| sessionCost: document.getElementById('sessionCost'), | |
| sessionDuration: document.getElementById('sessionDuration'), | |
| togglePassword: document.getElementById('togglePassword'), | |
| togglePassword: document.getElementById('togglePassword'), | |
| saveKeyBtn: document.getElementById('saveKeyBtn'), | |
| refreshBtn: document.getElementById('refreshBtn'), | |
| intervalSelect: document.getElementById('intervalSelect'), | |
| autoRefreshToggle: document.getElementById('autoRefreshToggle'), | |
| toggleSwitch: document.getElementById('toggleSwitch'), | |
| countdown: document.getElementById('countdown'), | |
| themeToggle: document.getElementById('themeToggle'), | |
| statusDot: document.getElementById('statusDot'), | |
| statusText: document.getElementById('statusText'), | |
| lastUpdated: document.getElementById('lastUpdated'), | |
| errorMessage: document.getElementById('errorMessage'), | |
| // Current month | |
| currentTokens: document.getElementById('currentTokens'), | |
| currentCost: document.getElementById('currentCost'), | |
| ttsChars: document.getElementById('ttsChars'), | |
| imagePixels: document.getElementById('imagePixels'), | |
| currentChat: document.getElementById('currentChat'), | |
| currentChatBar: document.getElementById('currentChatBar'), | |
| currentChatCost: document.getElementById('currentChatCost'), | |
| currentChatInput: document.getElementById('currentChatInput'), | |
| currentChatInputBar: document.getElementById('currentChatInputBar'), | |
| currentChatInputCost: document.getElementById('currentChatInputCost'), | |
| currentTts: document.getElementById('currentTts'), | |
| currentTtsBar: document.getElementById('currentTtsBar'), | |
| currentTtsSm: document.getElementById('currentTtsSm'), | |
| currentTtsSmBar: document.getElementById('currentTtsSmBar'), | |
| currentImage: document.getElementById('currentImage'), | |
| currentImageBar: document.getElementById('currentImageBar'), | |
| currentImageSm: document.getElementById('currentImageSm'), | |
| currentImageSmBar: document.getElementById('currentImageSmBar'), | |
| completionCost: document.getElementById('completionCost'), | |
| inputCost: document.getElementById('inputCost'), | |
| totalChatCost: document.getElementById('totalChatCost'), | |
| // Previous month | |
| prevChat: document.getElementById('prevChat'), | |
| prevChatBar: document.getElementById('prevChatBar'), | |
| prevChatCost: document.getElementById('prevChatCost'), | |
| prevChatInput: document.getElementById('prevChatInput'), | |
| prevChatInputBar: document.getElementById('prevChatInputBar'), | |
| prevChatInputCost: document.getElementById('prevChatInputCost'), | |
| prevTotalCost: document.getElementById('prevTotalCost'), | |
| prevTotalTokens: document.getElementById('prevTotalTokens') | |
| }; | |
| // Format numbers with commas | |
| function formatNumber(num) { | |
| return num.toLocaleString('en-US'); | |
| } | |
| // Format currency | |
| function formatCurrency(amount) { | |
| return '$' + amount.toFixed(2); | |
| } | |
| // Calculate cost | |
| function calculateCost(tokens, pricePerMillion) { | |
| return (tokens / 1_000_000) * pricePerMillion; | |
| } | |
| // Update UI with data | |
| function updateUI(data) { | |
| // Handle wrapped response structure | |
| const usageData = data.usage || data; | |
| const current = usageData.current_month || {}; | |
| const previous = usageData.previous_month || {}; | |
| // Current month | |
| const currentTotalTokens = (current.chat || 0) + (current.chat_input || 0); | |
| const currentCompletionCost = calculateCost(current.chat || 0, PRICING.completion_per_million); | |
| const currentInputCost = calculateCost(current.chat_input || 0, PRICING.input_per_million); | |
| const currentTotalCost = currentCompletionCost + currentInputCost; | |
| elements.currentTokens.textContent = formatNumber(currentTotalTokens); | |
| elements.currentCost.textContent = formatCurrency(currentTotalCost); | |
| elements.ttsChars.textContent = formatNumber((current.tts || 0) + (current.tts_sm || 0)); | |
| elements.imagePixels.textContent = ((current.image || 0) + (current.image_sm || 0)).toFixed(1) + ' MP'; | |
| elements.currentChat.textContent = formatNumber(current.chat || 0) + ' tokens'; | |
| elements.currentChatBar.style.width = Math.min((current.chat || 0) / 10000, 100) + '%'; | |
| elements.currentChatCost.textContent = formatCurrency(currentCompletionCost); | |
| elements.currentChatInput.textContent = formatNumber(current.chat_input || 0) + ' tokens'; | |
| elements.currentChatInputBar.style.width = Math.min((current.chat_input || 0) / 10000, 100) + '%'; | |
| elements.currentChatInputCost.textContent = formatCurrency(currentInputCost); | |
| elements.currentTts.textContent = formatNumber(current.tts || 0) + ' characters'; | |
| elements.currentTtsBar.style.width = Math.min((current.tts || 0) / 10000, 100) + '%'; | |
| elements.currentTtsSm.textContent = formatNumber(current.tts_sm || 0) + ' characters'; | |
| elements.currentTtsSmBar.style.width = Math.min((current.tts_sm || 0) / 10000, 100) + '%'; | |
| elements.currentImage.textContent = (current.image || 0).toFixed(1) + ' MP'; | |
| elements.currentImageBar.style.width = Math.min((current.image || 0) * 10, 100) + '%'; | |
| elements.currentImageSm.textContent = (current.image_sm || 0).toFixed(1) + ' MP'; | |
| elements.currentImageSmBar.style.width = Math.min((current.image_sm || 0) * 10, 100) + '%'; | |
| elements.completionCost.textContent = formatCurrency(currentCompletionCost); | |
| elements.inputCost.textContent = formatCurrency(currentInputCost); | |
| elements.totalChatCost.textContent = formatCurrency(currentTotalCost); | |
| // Previous month | |
| const prevTotalTokens = (previous.chat || 0) + (previous.chat_input || 0); | |
| const prevCompletionCost = calculateCost(previous.chat || 0, PRICING.completion_per_million); | |
| const prevInputCost = calculateCost(previous.chat_input || 0, PRICING.input_per_million); | |
| const prevTotalCost = prevCompletionCost + prevInputCost; | |
| elements.prevChat.textContent = formatNumber(previous.chat || 0) + ' tokens'; | |
| elements.prevChatBar.style.width = Math.min((previous.chat || 0) / 10000, 100) + '%'; | |
| elements.prevChatCost.textContent = formatCurrency(prevCompletionCost); | |
| elements.prevChatInput.textContent = formatNumber(previous.chat_input || 0) + ' tokens'; | |
| elements.prevChatInputBar.style.width = Math.min((previous.chat_input || 0) / 10000, 100) + '%'; | |
| elements.prevChatInputCost.textContent = formatCurrency(prevInputCost); | |
| elements.prevTotalCost.textContent = formatCurrency(prevTotalCost); | |
| elements.prevTotalTokens.textContent = formatNumber(prevTotalTokens); | |
| // Update session display if tracking | |
| updateSessionDisplay(); | |
| } | |
| // Show error | |
| function showError(message) { | |
| elements.errorMessage.textContent = '❌ ' + message; | |
| elements.errorMessage.classList.add('show'); | |
| elements.statusDot.className = 'status-dot error'; | |
| elements.statusText.textContent = 'Error: ' + message; | |
| } | |
| // Hide error | |
| function hideError() { | |
| elements.errorMessage.classList.remove('show'); | |
| } | |
| // Set loading state | |
| function setLoading(isLoading) { | |
| if (isLoading) { | |
| elements.statusDot.className = 'status-dot loading'; | |
| elements.statusText.textContent = 'Fetching usage data...'; | |
| hideError(); | |
| } else { | |
| elements.statusDot.className = 'status-dot'; | |
| } | |
| } | |
| // Fetch usage data | |
| async function fetchUsage() { | |
| if (!state.apiKey) { | |
| showError('Please enter your Vultr API Key first'); | |
| return; | |
| } | |
| setLoading(true); | |
| try { | |
| const response = await fetch('https://api.vultrinference.com/v1/usage', { | |
| method: 'GET', | |
| headers: { | |
| 'Authorization': `Bearer ${state.apiKey}`, | |
| 'Content-Type': 'application/json' | |
| } | |
| }); | |
| if (!response.ok) { | |
| if (response.status === 401) { | |
| throw new Error('Invalid API Key'); | |
| } else if (response.status === 403) { | |
| throw new Error('Forbidden - Check API permissions'); | |
| } else { | |
| throw new Error(`HTTP ${response.status}: ${response.statusText}`); | |
| } | |
| } | |
| const data = await response.json(); | |
| state.currentData = data; // Store for session tracking | |
| updateUI(data); | |
| elements.statusText.textContent = '✅ Data updated successfully'; | |
| elements.lastUpdated.textContent = 'Last updated: ' + new Date().toLocaleTimeString(); | |
| hideError(); | |
| } catch (error) { | |
| showError(error.message || 'Failed to fetch usage data'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| // Start countdown | |
| function startCountdown() { | |
| if (!state.autoRefresh) return; | |
| state.countdownValue = state.refreshInterval; | |
| updateCountdownDisplay(); | |
| state.intervalId = setInterval(() => { | |
| state.countdownValue--; | |
| updateCountdownDisplay(); | |
| if (state.countdownValue <= 0) { | |
| fetchUsage(); | |
| state.countdownValue = state.refreshInterval; | |
| } | |
| }, 1000); | |
| } | |
| // Stop countdown | |
| function stopCountdown() { | |
| if (state.intervalId) { | |
| clearInterval(state.intervalId); | |
| state.intervalId = null; | |
| } | |
| elements.countdown.textContent = ''; | |
| } | |
| // Update countdown display | |
| function updateCountdownDisplay() { | |
| elements.countdown.textContent = `Refreshing in ${state.countdownValue}s...`; | |
| } | |
| // Toggle auto-refresh | |
| function toggleAutoRefresh() { | |
| state.autoRefresh = !state.autoRefresh; | |
| elements.toggleSwitch.classList.toggle('active', state.autoRefresh); | |
| if (state.autoRefresh) { | |
| startCountdown(); | |
| } else { | |
| stopCountdown(); | |
| } | |
| } | |
| // Format duration | |
| function formatDuration(ms) { | |
| const seconds = Math.floor(ms / 1000); | |
| const hours = Math.floor(seconds / 3600); | |
| const minutes = Math.floor((seconds % 3600) / 60); | |
| const secs = seconds % 60; | |
| return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; | |
| } | |
| // Update session display | |
| function updateSessionDisplay() { | |
| if (!state.sessionTracking) { | |
| return; | |
| } | |
| if (!state.sessionStartData || !state.currentData) { | |
| console.log('Session tracking: waiting for data...'); | |
| return; | |
| } | |
| // Handle wrapped response structure | |
| const currentUsage = state.currentData.usage || state.currentData; | |
| const startUsage = state.sessionStartData.usage || state.sessionStartData; | |
| const current = currentUsage.current_month || {}; | |
| const start = startUsage.current_month || {}; | |
| // Calculate session usage | |
| const sessionCompletionTokens = Math.max(0, (current.chat || 0) - (start.chat || 0)); | |
| const sessionInputTokens = Math.max(0, (current.chat_input || 0) - (start.chat_input || 0)); | |
| const sessionTotalTokens = sessionCompletionTokens + sessionInputTokens; | |
| const sessionCompletionCost = calculateCost(sessionCompletionTokens, PRICING.completion_per_million); | |
| const sessionInputCost = calculateCost(sessionInputTokens, PRICING.input_per_million); | |
| const sessionTotalCost = sessionCompletionCost + sessionInputCost; | |
| console.log('Session update:', { | |
| sessionCompletionTokens, | |
| sessionInputTokens, | |
| sessionTotalTokens, | |
| sessionTotalCost | |
| }); | |
| // Update all session tracking fields | |
| elements.sessionCompletionTokens.textContent = formatNumber(sessionCompletionTokens); | |
| elements.sessionInputTokens.textContent = formatNumber(sessionInputTokens); | |
| elements.sessionTokens.textContent = formatNumber(sessionTotalTokens); | |
| elements.sessionCompletionCost.textContent = '$' + sessionCompletionCost.toFixed(2) + ' cost'; | |
| elements.sessionInputCost.textContent = '$' + sessionInputCost.toFixed(2) + ' cost'; | |
| elements.sessionCost.textContent = formatCurrency(sessionTotalCost); | |
| } | |
| // Update session duration | |
| function updateSessionDuration() { | |
| if (!state.sessionTracking || !state.sessionStart) { | |
| return; | |
| } | |
| const elapsed = Date.now() - state.sessionStart; | |
| elements.sessionDuration.textContent = formatDuration(elapsed); | |
| } | |
| // Start session tracking | |
| function startSessionTracking() { | |
| if (state.sessionTracking) { | |
| // Stop tracking | |
| state.sessionTracking = false; | |
| state.sessionStart = null; | |
| state.sessionStartData = null; | |
| if (state.sessionIntervalId) { | |
| clearInterval(state.sessionIntervalId); | |
| state.sessionIntervalId = null; | |
| } | |
| elements.startTrackingBtn.innerHTML = '▶️ Start Tracking'; | |
| elements.startTrackingBtn.classList.remove('tracking'); | |
| // Reset all session tracking fields | |
| elements.sessionCompletionTokens.textContent = '0'; | |
| elements.sessionInputTokens.textContent = '0'; | |
| elements.sessionTokens.textContent = '0'; | |
| elements.sessionCompletionCost.textContent = '$0.00 cost'; | |
| elements.sessionInputCost.textContent = '$0.00 cost'; | |
| elements.sessionCost.textContent = '$0.00'; | |
| elements.sessionDuration.textContent = '00:00:00'; | |
| } else { | |
| // Start tracking | |
| console.log('Starting session tracking'); | |
| console.log('Current data available:', !!state.currentData); | |
| if (!state.currentData) { | |
| showError('Please fetch usage data first - click the Refresh button'); | |
| return; | |
| } | |
| state.sessionTracking = true; | |
| state.sessionStart = Date.now(); | |
| state.sessionStartData = JSON.parse(JSON.stringify(state.currentData)); | |
| console.log('Session start data:', state.sessionStartData); | |
| elements.startTrackingBtn.innerHTML = '⏹️ Stop Tracking'; | |
| elements.startTrackingBtn.classList.add('tracking'); | |
| // Initial update | |
| updateSessionDisplay(); | |
| updateSessionDuration(); | |
| state.sessionIntervalId = setInterval(() => { | |
| updateSessionDuration(); | |
| updateSessionDisplay(); | |
| }, 1000); | |
| } | |
| } | |
| // Event Listeners | |
| elements.apiKeyInput.value = state.apiKey; | |
| elements.saveKeyBtn.addEventListener('click', () => { | |
| state.apiKey = elements.apiKeyInput.value.trim(); | |
| localStorage.setItem('vultr_api_key', state.apiKey); | |
| if (state.apiKey) { | |
| fetchUsage(); | |
| } | |
| }); | |
| elements.togglePassword.addEventListener('click', () => { | |
| const type = elements.apiKeyInput.type === 'password' ? 'text' : 'password'; | |
| elements.apiKeyInput.type = type; | |
| elements.togglePassword.textContent = type === 'password' ? '👁️' : '🙈'; | |
| }); | |
| elements.refreshBtn.addEventListener('click', fetchUsage); | |
| elements.intervalSelect.addEventListener('change', (e) => { | |
| state.refreshInterval = parseInt(e.target.value); | |
| if (state.autoRefresh) { | |
| stopCountdown(); | |
| startCountdown(); | |
| } | |
| }); | |
| elements.autoRefreshToggle.addEventListener('click', toggleAutoRefresh); | |
| elements.startTrackingBtn.addEventListener('click', startSessionTracking); | |
| elements.themeToggle.addEventListener('click', () => { | |
| document.body.classList.toggle('dark-mode'); | |
| const isDark = document.body.classList.contains('dark-mode'); | |
| elements.themeToggle.textContent = isDark ? '☀️' : '🌙'; | |
| localStorage.setItem('darkMode', isDark); | |
| }); | |
| // Load dark mode preference | |
| if (localStorage.getItem('darkMode') === 'true') { | |
| document.body.classList.add('dark-mode'); | |
| elements.themeToggle.textContent = '☀️'; | |
| } | |
| // Auto-fetch if API key exists | |
| if (state.apiKey) { | |
| fetchUsage(); | |
| } | |
| </script> | |
| </body> | |
| </html> |