Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>StableCog Dashboard</title> | |
| <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🎨</text></svg>"> | |
| <style> | |
| :root { | |
| --primary-color: #667eea; | |
| --secondary-color: #764ba2; | |
| --success-color: #4CAF50; | |
| --warning-color: #FF9800; | |
| --danger-color: #F44336; | |
| --light-color: #90EE90; | |
| --dark-color: #333; | |
| --card-bg: rgba(255, 255, 255, 0.1); | |
| --text-light: #ffffff; | |
| --text-dark: #333333; | |
| --shadow: 0 10px 30px rgba(0, 0, 0, 0.2); | |
| --border-radius: 15px; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; | |
| background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); | |
| min-height: 100vh; | |
| padding: 20px; | |
| color: var(--text-light); | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| } | |
| .header { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| padding: 20px; | |
| } | |
| .header h1 { | |
| font-size: 2.8rem; | |
| margin-bottom: 10px; | |
| font-weight: 700; | |
| } | |
| .header p { | |
| font-size: 1.1rem; | |
| opacity: 0.9; | |
| max-width: 600px; | |
| margin: 0 auto; | |
| } | |
| .dashboard { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); | |
| gap: 25px; | |
| margin-bottom: 30px; | |
| } | |
| .card { | |
| background: var(--card-bg); | |
| backdrop-filter: blur(10px); | |
| border-radius: var(--border-radius); | |
| padding: 25px; | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| box-shadow: var(--shadow); | |
| transition: transform 0.3s ease; | |
| } | |
| .card:hover { | |
| transform: translateY(-5px); | |
| } | |
| .card-title { | |
| font-size: 1.5rem; | |
| margin-bottom: 20px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .card-title i { | |
| font-size: 1.8rem; | |
| } | |
| .stat-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 15px; | |
| margin-bottom: 20px; | |
| } | |
| .stat-box { | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 10px; | |
| padding: 15px; | |
| text-align: center; | |
| } | |
| .stat-value { | |
| font-size: 2.2rem; | |
| font-weight: bold; | |
| margin-bottom: 5px; | |
| } | |
| .stat-label { | |
| font-size: 0.9rem; | |
| opacity: 0.8; | |
| } | |
| .progress-container { | |
| margin: 20px 0; | |
| } | |
| .progress-header { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-bottom: 8px; | |
| } | |
| .progress-bar { | |
| height: 22px; | |
| background: rgba(255, 255, 255, 0.15); | |
| border-radius: 11px; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| border-radius: 11px; | |
| transition: width 0.5s ease-in-out; | |
| box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); | |
| } | |
| .progress-text { | |
| position: absolute; | |
| right: 10px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| font-size: 12px; | |
| font-weight: bold; | |
| text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); | |
| } | |
| .status-badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 8px 16px; | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 20px; | |
| font-weight: bold; | |
| margin-top: 15px; | |
| } | |
| .models-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | |
| gap: 15px; | |
| max-height: 500px; | |
| overflow-y: auto; | |
| padding-right: 10px; | |
| } | |
| .model-card { | |
| background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%); | |
| border-radius: 10px; | |
| padding: 15px; | |
| border-left: 4px solid var(--primary-color); | |
| } | |
| .model-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 8px; | |
| } | |
| .model-name { | |
| font-weight: bold; | |
| font-size: 1rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .model-badge { | |
| padding: 2px 8px; | |
| border-radius: 12px; | |
| font-size: 0.7rem; | |
| font-weight: bold; | |
| } | |
| .badge-default { background: var(--success-color); } | |
| .badge-public { background: #2196F3; } | |
| .badge-community { background: var(--danger-color); } | |
| .badge-other { background: #9E9E9E; } | |
| .model-id { | |
| font-size: 0.8rem; | |
| opacity: 0.7; | |
| } | |
| .model-description { | |
| font-size: 0.9rem; | |
| opacity: 0.9; | |
| margin-bottom: 10px; | |
| line-height: 1.4; | |
| } | |
| .model-meta { | |
| display: flex; | |
| gap: 10px; | |
| font-size: 0.8rem; | |
| opacity: 0.7; | |
| } | |
| .controls { | |
| display: flex; | |
| gap: 15px; | |
| margin-top: 25px; | |
| flex-wrap: wrap; | |
| } | |
| .btn { | |
| padding: 12px 24px; | |
| border: none; | |
| border-radius: 10px; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| transition: all 0.3s ease; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); | |
| color: white; | |
| } | |
| .btn-secondary { | |
| background: rgba(255, 255, 255, 0.2); | |
| color: white; | |
| border: 1px solid rgba(255, 255, 255, 0.3); | |
| } | |
| .btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); | |
| } | |
| .btn:active { | |
| transform: translateY(0); | |
| } | |
| .api-key-input { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| flex-wrap: wrap; | |
| } | |
| .api-key-input input { | |
| flex: 1; | |
| min-width: 300px; | |
| padding: 12px 15px; | |
| border: 1px solid rgba(255, 255, 255, 0.3); | |
| border-radius: 10px; | |
| background: rgba(255, 255, 255, 0.1); | |
| color: white; | |
| font-size: 1rem; | |
| } | |
| .api-key-input input::placeholder { | |
| color: rgba(255, 255, 255, 0.6); | |
| } | |
| .api-key-input input:focus { | |
| outline: none; | |
| border-color: var(--primary-color); | |
| box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.3); | |
| } | |
| .error-container { | |
| background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); | |
| border-radius: var(--border-radius); | |
| padding: 25px; | |
| margin: 20px 0; | |
| text-align: center; | |
| } | |
| .error-title { | |
| font-size: 1.5rem; | |
| margin-bottom: 15px; | |
| } | |
| .error-content { | |
| background: rgba(255, 255, 255, 0.15); | |
| backdrop-filter: blur(10px); | |
| padding: 20px; | |
| border-radius: 10px; | |
| margin-bottom: 20px; | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| } | |
| .loading { | |
| display: inline-block; | |
| width: 20px; | |
| height: 20px; | |
| border: 3px solid rgba(255, 255, 255, 0.3); | |
| border-radius: 50%; | |
| border-top-color: white; | |
| animation: spin 1s ease-in-out infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| .credit-breakdown { | |
| margin-top: 20px; | |
| padding-top: 20px; | |
| border-top: 1px solid rgba(255, 255, 255, 0.2); | |
| } | |
| .credit-item { | |
| background: rgba(255, 255, 255, 0.05); | |
| border-radius: 8px; | |
| padding: 12px; | |
| margin-bottom: 10px; | |
| } | |
| .credit-header { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-bottom: 5px; | |
| } | |
| .credit-name { | |
| font-weight: bold; | |
| } | |
| .credit-amount { | |
| font-weight: bold; | |
| } | |
| .credit-description { | |
| font-size: 0.8rem; | |
| opacity: 0.8; | |
| margin-bottom: 8px; | |
| } | |
| .credit-progress { | |
| height: 6px; | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 3px; | |
| overflow: hidden; | |
| } | |
| .credit-progress-fill { | |
| height: 100%; | |
| border-radius: 3px; | |
| } | |
| .last-updated { | |
| text-align: center; | |
| font-size: 0.9rem; | |
| opacity: 0.7; | |
| margin-top: 10px; | |
| } | |
| .tabs { | |
| display: flex; | |
| gap: 5px; | |
| margin-bottom: 25px; | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 12px; | |
| padding: 5px; | |
| } | |
| .tab { | |
| flex: 1; | |
| padding: 12px 20px; | |
| text-align: center; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| transition: all 0.3s ease; | |
| } | |
| .tab.active { | |
| background: white; | |
| color: var(--primary-color); | |
| } | |
| .tab-content { | |
| display: none; | |
| } | |
| .tab-content.active { | |
| display: block; | |
| } | |
| .footer { | |
| text-align: center; | |
| margin-top: 40px; | |
| padding-top: 20px; | |
| border-top: 1px solid rgba(255, 255, 255, 0.2); | |
| font-size: 0.9rem; | |
| opacity: 0.7; | |
| } | |
| @media (max-width: 768px) { | |
| .dashboard { | |
| grid-template-columns: 1fr; | |
| } | |
| .stat-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .models-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .header h1 { | |
| font-size: 2rem; | |
| } | |
| .api-key-input input { | |
| min-width: 100%; | |
| } | |
| .controls { | |
| flex-direction: column; | |
| } | |
| .btn { | |
| width: 100%; | |
| justify-content: center; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>🎯 StableCog Dashboard</h1> | |
| <p>Monitor your credits and explore available AI models for image generation</p> | |
| </div> | |
| <!-- API Key Input --> | |
| <div class="api-key-input"> | |
| <input type="password" | |
| id="apiKeyInput" | |
| placeholder="Enter your StableCog API key (or leave empty for demo)" | |
| autocomplete="off"> | |
| <button class="btn btn-primary" onclick="saveApiKey()"> | |
| <span>🔑</span> Save API Key | |
| </button> | |
| <button class="btn btn-secondary" onclick="clearApiKey()"> | |
| <span>🗑️</span> Clear | |
| </button> | |
| </div> | |
| <!-- Tabs --> | |
| <div class="tabs"> | |
| <div class="tab active" onclick="switchTab('credits')">💰 Credits</div> | |
| <div class="tab" onclick="switchTab('models')">🤖 Models</div> | |
| <div class="tab" onclick="switchTab('info')">📚 Info</div> | |
| </div> | |
| <!-- Credits Tab --> | |
| <div id="creditsTab" class="tab-content active"> | |
| <div class="dashboard"> | |
| <!-- Credit Status Card --> | |
| <div class="card"> | |
| <h2 class="card-title">🎨 Credit Status</h2> | |
| <div class="stat-grid"> | |
| <div class="stat-box"> | |
| <div class="stat-value" id="totalCredits">0</div> | |
| <div class="stat-label">Total Credits</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-value" id="remainingCredits">0</div> | |
| <div class="stat-label">Remaining Credits</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-value" id="usedCredits">0</div> | |
| <div class="stat-label">Used Credits</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-value" id="usagePercentage">0%</div> | |
| <div class="stat-label">Usage</div> | |
| </div> | |
| </div> | |
| <div class="progress-container"> | |
| <div class="progress-header"> | |
| <span>Overall Usage</span> | |
| <span id="usageText">0%</span> | |
| </div> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" id="progressFill"></div> | |
| <div class="progress-text" id="progressText">0%</div> | |
| </div> | |
| </div> | |
| <div class="status-badge" id="statusBadge"> | |
| <span id="statusEmoji">🔄</span> | |
| <span id="statusText">Loading...</span> | |
| </div> | |
| <!-- Credit Breakdown --> | |
| <div class="credit-breakdown" id="creditBreakdown"> | |
| <h3 style="margin-bottom: 15px; font-size: 18px;">📊 Credit Breakdown</h3> | |
| <!-- Credit items will be inserted here --> | |
| </div> | |
| <div class="last-updated" id="creditsLastUpdated"> | |
| ⏰ Last checked: Never | |
| </div> | |
| </div> | |
| <!-- Recommendation Card --> | |
| <div class="card"> | |
| <h2 class="card-title">🎯 AI Recommendation</h2> | |
| <div id="recommendation" style="font-size: 1.1rem; line-height: 1.6; opacity: 0.9;"> | |
| Enter your API key and check credits to see personalized recommendations. | |
| </div> | |
| </div> | |
| </div> | |
| <div class="controls"> | |
| <button class="btn btn-primary" onclick="checkCredits()" id="checkCreditsBtn"> | |
| <span id="creditsIcon">🔄</span> | |
| <span id="creditsText">Check Credits</span> | |
| </button> | |
| <button class="btn btn-secondary" onclick="viewRawResponse('credits')"> | |
| <span>📋</span> View Raw Response | |
| </button> | |
| </div> | |
| <!-- Raw Response Modal --> | |
| <div id="creditsRawModal" style="display: none; margin-top: 20px;"> | |
| <div class="card"> | |
| <h2 class="card-title">📋 Raw API Response</h2> | |
| <pre id="creditsRawResponse" style=" | |
| background: rgba(0, 0, 0, 0.2); | |
| padding: 15px; | |
| border-radius: 8px; | |
| overflow: auto; | |
| max-height: 300px; | |
| font-family: 'Courier New', monospace; | |
| font-size: 12px; | |
| white-space: pre-wrap; | |
| "></pre> | |
| <button class="btn btn-secondary" onclick="hideRawResponse('credits')" style="margin-top: 15px;"> | |
| <span>✕</span> Close | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Models Tab --> | |
| <div id="modelsTab" class="tab-content"> | |
| <div class="dashboard"> | |
| <div class="card"> | |
| <h2 class="card-title">🤖 Available Models</h2> | |
| <div class="stat-grid"> | |
| <div class="stat-box"> | |
| <div class="stat-value" id="totalModels">0</div> | |
| <div class="stat-label">Total Models</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-value" id="publicModels">0</div> | |
| <div class="stat-label">Public Models</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-value" id="defaultModels">0</div> | |
| <div class="stat-label">Default Models</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-value" id="communityModels">0</div> | |
| <div class="stat-label">Community Models</div> | |
| </div> | |
| </div> | |
| <div class="models-grid" id="modelsList"> | |
| <!-- Models will be inserted here --> | |
| <div style="text-align: center; padding: 40px; opacity: 0.7;"> | |
| Click "Load Models" to see available models | |
| </div> | |
| </div> | |
| <div class="last-updated" id="modelsLastUpdated"> | |
| ⏰ Last updated: Never | |
| </div> | |
| </div> | |
| </div> | |
| <div class="controls"> | |
| <button class="btn btn-primary" onclick="loadModels()" id="loadModelsBtn"> | |
| <span id="modelsIcon">🔄</span> | |
| <span id="modelsText">Load Models</span> | |
| </button> | |
| <button class="btn btn-secondary" onclick="viewRawResponse('models')"> | |
| <span>📋</span> View Raw Response | |
| </button> | |
| </div> | |
| <!-- Raw Response Modal --> | |
| <div id="modelsRawModal" style="display: none; margin-top: 20px;"> | |
| <div class="card"> | |
| <h2 class="card-title">📋 Raw API Response</h2> | |
| <pre id="modelsRawResponse" style=" | |
| background: rgba(0, 0, 0, 0.2); | |
| padding: 15px; | |
| border-radius: 8px; | |
| overflow: auto; | |
| max-height: 300px; | |
| font-family: 'Courier New', monospace; | |
| font-size: 12px; | |
| white-space: pre-wrap; | |
| "></pre> | |
| <button class="btn btn-secondary" onclick="hideRawResponse('models')" style="margin-top: 15px;"> | |
| <span>✕</span> Close | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Info Tab --> | |
| <div id="infoTab" class="tab-content"> | |
| <div class="card"> | |
| <h2 class="card-title">📚 How to Use & Setup</h2> | |
| <div style="line-height: 1.6; opacity: 0.9;"> | |
| <h3 style="margin: 20px 0 10px 0;">Getting Started:</h3> | |
| <ol style="margin-left: 20px; margin-bottom: 20px;"> | |
| <li>Enter your StableCog API key in the input field above</li> | |
| <li>Click "Save API Key" to store it in your browser</li> | |
| <li>Switch to "Credits" tab and click "Check Credits"</li> | |
| <li>Switch to "Models" tab and click "Load Models"</li> | |
| </ol> | |
| <h3 style="margin: 20px 0 10px 0;">Understanding Credits:</h3> | |
| <ul style="margin-left: 20px; margin-bottom: 20px;"> | |
| <li><strong>Total Credits:</strong> Sum of all initial credits received</li> | |
| <li><strong>Remaining Credits:</strong> Currently available credits</li> | |
| <li><strong>Used Credits:</strong> Credits spent on image generation</li> | |
| <li><strong>Credit Types:</strong> Different sources (Free, Refund, etc.)</li> | |
| </ul> | |
| <h3 style="margin: 20px 0 10px 0;">Privacy & Security:</h3> | |
| <ul style="margin-left: 20px; margin-bottom: 20px;"> | |
| <li>Your API key is stored only in your browser's localStorage</li> | |
| <li>No data is sent to any server except StableCog API</li> | |
| <li>You can clear the API key anytime using the "Clear" button</li> | |
| <li>All API calls are made directly from your browser</li> | |
| </ul> | |
| <h3 style="margin: 20px 0 10px 0;">Troubleshooting:</h3> | |
| <ul style="margin-left: 20px;"> | |
| <li>Ensure your API key is correct and has proper permissions</li> | |
| <li>Check your internet connection</li> | |
| <li>Verify StableCog API is available</li> | |
| <li>Use "View Raw Response" to see detailed error messages</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Error Container (hidden by default) --> | |
| <div id="errorContainer" class="error-container" style="display: none;"> | |
| <h2 class="error-title" id="errorTitle">⚠️ API Error</h2> | |
| <div class="error-content"> | |
| <p id="errorMessage"></p> | |
| </div> | |
| <button class="btn btn-secondary" onclick="hideError()"> | |
| <span>✕</span> Dismiss | |
| </button> | |
| </div> | |
| <div class="footer"> | |
| Built with ❤️ for StableCog users | | |
| <a href="https://stablecog.com/docs/api" target="_blank" style="color: var(--light-color); text-decoration: none;"> | |
| API Documentation | |
| </a> | | |
| <a href="https://github.com/stability-ai/stablecog/issues" target="_blank" style="color: var(--light-color); text-decoration: none;"> | |
| Report Issues | |
| </a> | |
| </div> | |
| </div> | |
| <script> | |
| // Configuration | |
| const API_HOST = 'https://api.stablecog.com'; | |
| const CREDITS_ENDPOINT = '/v1/credits'; | |
| const MODELS_ENDPOINT = '/v1/image/generation/models'; | |
| // State | |
| let apiKey = ''; | |
| let creditsData = null; | |
| let modelsData = null; | |
| // Initialize on page load | |
| document.addEventListener('DOMContentLoaded', function() { | |
| loadApiKey(); | |
| // Check if API key exists, if so auto-check credits | |
| if (apiKey) { | |
| setTimeout(() => { | |
| checkCredits(); | |
| loadModels(); | |
| }, 500); | |
| } | |
| }); | |
| // API Key Management | |
| function loadApiKey() { | |
| apiKey = localStorage.getItem('stablecog_api_key') || ''; | |
| document.getElementById('apiKeyInput').value = apiKey; | |
| } | |
| function saveApiKey() { | |
| const input = document.getElementById('apiKeyInput'); | |
| apiKey = input.value.trim(); | |
| if (apiKey) { | |
| localStorage.setItem('stablecog_api_key', apiKey); | |
| showMessage('API key saved successfully!'); | |
| // Auto-check after saving | |
| setTimeout(() => { | |
| checkCredits(); | |
| loadModels(); | |
| }, 500); | |
| } else { | |
| localStorage.removeItem('stablecog_api_key'); | |
| showMessage('API key cleared. Using demo mode.'); | |
| } | |
| } | |
| function clearApiKey() { | |
| document.getElementById('apiKeyInput').value = ''; | |
| saveApiKey(); | |
| } | |
| // Tab Switching | |
| function switchTab(tabName) { | |
| // Update tabs | |
| document.querySelectorAll('.tab').forEach(tab => { | |
| tab.classList.remove('active'); | |
| }); | |
| document.querySelectorAll('.tab-content').forEach(content => { | |
| content.classList.remove('active'); | |
| }); | |
| // Activate selected tab | |
| document.querySelector(`.tab[onclick="switchTab('${tabName}')"]`).classList.add('active'); | |
| document.getElementById(`${tabName}Tab`).classList.add('active'); | |
| } | |
| // API Calls | |
| async function checkCredits() { | |
| const btn = document.getElementById('checkCreditsBtn'); | |
| const icon = document.getElementById('creditsIcon'); | |
| const text = document.getElementById('creditsText'); | |
| // Show loading state | |
| btn.disabled = true; | |
| icon.textContent = ''; | |
| icon.innerHTML = '<div class="loading"></div>'; | |
| text.textContent = 'Checking...'; | |
| try { | |
| const response = await fetch(`${API_HOST}${CREDITS_ENDPOINT}`, { | |
| headers: { | |
| 'Authorization': `Bearer ${apiKey || 'demo'}`, | |
| 'Content-Type': 'application/json' | |
| } | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`API Error: ${response.status} - ${response.statusText}`); | |
| } | |
| const data = await response.json(); | |
| creditsData = data; | |
| updateCreditsDisplay(data); | |
| hideError(); | |
| } catch (error) { | |
| showError('Failed to check credits', error.message); | |
| // For demo purposes, show sample data | |
| if (!apiKey) { | |
| showSampleCredits(); | |
| } | |
| } finally { | |
| // Reset button state | |
| btn.disabled = false; | |
| icon.textContent = '🔄'; | |
| text.textContent = 'Check Credits'; | |
| } | |
| } | |
| async function loadModels() { | |
| const btn = document.getElementById('loadModelsBtn'); | |
| const icon = document.getElementById('modelsIcon'); | |
| const text = document.getElementById('modelsText'); | |
| // Show loading state | |
| btn.disabled = true; | |
| icon.textContent = ''; | |
| icon.innerHTML = '<div class="loading"></div>'; | |
| text.textContent = 'Loading...'; | |
| try { | |
| const response = await fetch(`${API_HOST}${MODELS_ENDPOINT}`, { | |
| headers: { | |
| 'Authorization': `Bearer ${apiKey || 'demo'}`, | |
| 'Content-Type': 'application/json' | |
| } | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`API Error: ${response.status} - ${response.statusText}`); | |
| } | |
| const data = await response.json(); | |
| modelsData = data; | |
| updateModelsDisplay(data); | |
| hideError(); | |
| } catch (error) { | |
| showError('Failed to load models', error.message); | |
| // For demo purposes, show sample data | |
| if (!apiKey) { | |
| showSampleModels(); | |
| } | |
| } finally { | |
| // Reset button state | |
| btn.disabled = false; | |
| icon.textContent = '🔄'; | |
| text.textContent = 'Load Models'; | |
| } | |
| } | |
| // Display Functions | |
| function updateCreditsDisplay(data) { | |
| // Calculate totals | |
| const totalRemaining = data.total_remaining_credits || 0; | |
| const creditsList = data.credits || []; | |
| let totalInitial = 0; | |
| let totalUsed = 0; | |
| creditsList.forEach(credit => { | |
| const initial = credit.type?.amount || 0; | |
| const remaining = credit.remaining_amount || 0; | |
| totalInitial += initial; | |
| totalUsed += (initial - remaining); | |
| }); | |
| const totalCredits = Math.max(totalInitial, totalRemaining + totalUsed); | |
| const usagePercentage = totalCredits > 0 ? (totalUsed / totalCredits * 100) : 0; | |
| // Update UI | |
| document.getElementById('totalCredits').textContent = totalCredits; | |
| document.getElementById('remainingCredits').textContent = totalRemaining; | |
| document.getElementById('usedCredits').textContent = totalUsed; | |
| document.getElementById('usagePercentage').textContent = `${usagePercentage.toFixed(1)}%`; | |
| document.getElementById('usageText').textContent = `${usagePercentage.toFixed(1)}%`; | |
| document.getElementById('progressText').textContent = `${usagePercentage.toFixed(1)}%`; | |
| // Update progress bar | |
| const progressFill = document.getElementById('progressFill'); | |
| progressFill.style.width = `${Math.min(usagePercentage, 100)}%`; | |
| // Set progress bar color based on usage | |
| if (usagePercentage < 50) { | |
| progressFill.style.background = '#4CAF50'; | |
| } else if (usagePercentage < 80) { | |
| progressFill.style.background = '#FF9800'; | |
| } else { | |
| progressFill.style.background = '#F44336'; | |
| } | |
| // Update status | |
| const statusBadge = document.getElementById('statusBadge'); | |
| const statusEmoji = document.getElementById('statusEmoji'); | |
| const statusText = document.getElementById('statusText'); | |
| if (totalRemaining === 0) { | |
| statusEmoji.textContent = '💸'; | |
| statusText.textContent = 'No Credits'; | |
| statusBadge.style.color = '#F44336'; | |
| } else if (usagePercentage >= 90) { | |
| statusEmoji.textContent = '🔴'; | |
| statusText.textContent = 'Critically Low'; | |
| statusBadge.style.color = '#F44336'; | |
| } else if (usagePercentage >= 75) { | |
| statusEmoji.textContent = '🟡'; | |
| statusText.textContent = 'Running Low'; | |
| statusBadge.style.color = '#FF9800'; | |
| } else if (totalRemaining < 10) { | |
| statusEmoji.textContent = '📝'; | |
| statusText.textContent = 'Limited'; | |
| statusBadge.style.color = '#FF9800'; | |
| } else if (totalRemaining < 50) { | |
| statusEmoji.textContent = '✨'; | |
| statusText.textContent = 'Available'; | |
| statusBadge.style.color = '#4CAF50'; | |
| } else { | |
| statusEmoji.textContent = '🚀'; | |
| statusText.textContent = 'Plenty'; | |
| statusBadge.style.color = '#4CAF50'; | |
| } | |
| // Update recommendation | |
| updateRecommendation(totalRemaining, usagePercentage); | |
| // Update credit breakdown | |
| updateCreditBreakdown(creditsList); | |
| // Update timestamp | |
| const now = new Date(); | |
| document.getElementById('creditsLastUpdated').textContent = | |
| `⏰ Last checked: ${now.toLocaleString()}`; | |
| } | |
| function updateModelsDisplay(data) { | |
| const models = data.models || []; | |
| // Update stats | |
| document.getElementById('totalModels').textContent = models.length; | |
| document.getElementById('publicModels').textContent = | |
| models.filter(m => m.is_public).length; | |
| document.getElementById('defaultModels').textContent = | |
| models.filter(m => m.is_default).length; | |
| document.getElementById('communityModels').textContent = | |
| models.filter(m => m.is_community).length; | |
| // Update models list | |
| const modelsList = document.getElementById('modelsList'); | |
| modelsList.innerHTML = ''; | |
| models.sort((a, b) => { | |
| // Sort: default first, then public, then by name | |
| if (a.is_default !== b.is_default) return b.is_default - a.is_default; | |
| if (a.is_public !== b.is_public) return b.is_public - a.is_public; | |
| return a.name.localeCompare(b.name); | |
| }); | |
| models.forEach((model, index) => { | |
| const modelCard = document.createElement('div'); | |
| modelCard.className = 'model-card'; | |
| // Determine badge | |
| let badgeClass = 'badge-other'; | |
| let badgeText = model.type?.toUpperCase() || 'UNKNOWN'; | |
| if (model.is_community) { | |
| badgeClass = 'badge-community'; | |
| badgeText = 'COMMUNITY'; | |
| } else if (model.is_default) { | |
| badgeClass = 'badge-default'; | |
| badgeText = 'DEFAULT'; | |
| } else if (model.is_public) { | |
| badgeClass = 'badge-public'; | |
| badgeText = 'PUBLIC'; | |
| } | |
| // Shorten ID for display | |
| const shortId = model.id.length > 8 ? model.id.substring(0, 8) + '...' : model.id; | |
| modelCard.innerHTML = ` | |
| <div class="model-header"> | |
| <div class="model-name"> | |
| #${index + 1}. ${model.name} | |
| <span class="model-badge ${badgeClass}">${badgeText}</span> | |
| </div> | |
| <div class="model-id">ID: ${shortId}</div> | |
| </div> | |
| <div class="model-description">${model.description || 'No description available'}</div> | |
| <div class="model-meta"> | |
| <span>📅 Created: ${model.created_at ? model.created_at.substring(0, 10) : 'Unknown'}</span> | |
| <span>🔄 Updated: ${model.updated_at ? model.updated_at.substring(0, 10) : 'Unknown'}</span> | |
| </div> | |
| `; | |
| modelsList.appendChild(modelCard); | |
| }); | |
| // Update timestamp | |
| const now = new Date(); | |
| document.getElementById('modelsLastUpdated').textContent = | |
| `⏰ Last updated: ${now.toLocaleString()}`; | |
| } | |
| function updateRecommendation(remainingCredits, usagePercentage) { | |
| const recommendation = document.getElementById('recommendation'); | |
| if (remainingCredits === 0) { | |
| recommendation.innerHTML = "💸 <strong>No credits remaining.</strong> Please add credits to continue using StableCog services."; | |
| } else if (usagePercentage >= 90) { | |
| recommendation.innerHTML = "🛑 <strong>Critically low credits!</strong> Consider purchasing more credits before starting new projects."; | |
| } else if (usagePercentage >= 75) { | |
| recommendation.innerHTML = "⚠️ <strong>Credits are running low.</strong> You can still do some work, but plan ahead for larger projects."; | |
| } else if (remainingCredits < 10) { | |
| recommendation.innerHTML = "📝 <strong>Limited credits available.</strong> Good for small tasks, testing, or single images."; | |
| } else if (remainingCredits < 50) { | |
| recommendation.innerHTML = "✨ <strong>Credits available!</strong> Suitable for several medium-sized projects or batch processing."; | |
| } else { | |
| recommendation.innerHTML = "🚀 <strong>Plenty of credits!</strong> Ready for extensive image generation work and experimentation."; | |
| } | |
| } | |
| function updateCreditBreakdown(creditsList) { | |
| const breakdownContainer = document.getElementById('creditBreakdown'); | |
| const itemsContainer = breakdownContainer.querySelector('.credit-items') || | |
| document.createElement('div'); | |
| itemsContainer.className = 'credit-items'; | |
| itemsContainer.innerHTML = ''; | |
| creditsList.forEach(credit => { | |
| const initial = credit.type?.amount || 0; | |
| const remaining = credit.remaining_amount || 0; | |
| const used = initial - remaining; | |
| const percentage = initial > 0 ? (used / initial * 100) : 0; | |
| const item = document.createElement('div'); | |
| item.className = 'credit-item'; | |
| item.innerHTML = ` | |
| <div class="credit-header"> | |
| <span class="credit-name">${credit.type?.name || 'Unknown'}</span> | |
| <span class="credit-amount">${remaining} / ${initial} ⭐</span> | |
| </div> | |
| <div class="credit-description">${credit.type?.description || ''}</div> | |
| <div class="credit-progress"> | |
| <div class="credit-progress-fill" style="width: ${Math.min(percentage, 100)}%; background: ${getProgressColor(percentage)};"></div> | |
| </div> | |
| `; | |
| itemsContainer.appendChild(item); | |
| }); | |
| if (!breakdownContainer.querySelector('.credit-items')) { | |
| breakdownContainer.appendChild(itemsContainer); | |
| } | |
| } | |
| function getProgressColor(percentage) { | |
| if (percentage < 50) return '#4CAF50'; | |
| if (percentage < 80) return '#FF9800'; | |
| return '#F44336'; | |
| } | |
| // Raw Response Viewer | |
| function viewRawResponse(type) { | |
| const modal = document.getElementById(`${type}RawModal`); | |
| const pre = document.getElementById(`${type}RawResponse`); | |
| const data = type === 'credits' ? creditsData : modelsData; | |
| pre.textContent = JSON.stringify(data, null, 2); | |
| modal.style.display = 'block'; | |
| } | |
| function hideRawResponse(type) { | |
| document.getElementById(`${type}RawModal`).style.display = 'none'; | |
| } | |
| // Error Handling | |
| function showError(title, message) { | |
| document.getElementById('errorTitle').textContent = title; | |
| document.getElementById('errorMessage').textContent = message; | |
| document.getElementById('errorContainer').style.display = 'block'; | |
| } | |
| function hideError() { | |
| document.getElementById('errorContainer').style.display = 'none'; | |
| } | |
| function showMessage(message) { | |
| // Simple toast notification | |
| const toast = document.createElement('div'); | |
| toast.style.cssText = ` | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| background: rgba(76, 175, 80, 0.9); | |
| color: white; | |
| padding: 12px 20px; | |
| border-radius: 8px; | |
| box-shadow: 0 5px 15px rgba(0,0,0,0.3); | |
| z-index: 1000; | |
| animation: slideIn 0.3s ease; | |
| `; | |
| toast.textContent = message; | |
| document.body.appendChild(toast); | |
| setTimeout(() => { | |
| toast.style.animation = 'slideOut 0.3s ease'; | |
| setTimeout(() => toast.remove(), 300); | |
| }, 3000); | |
| } | |
| // Sample data for demo | |
| function showSampleCredits() { | |
| const sampleData = { | |
| "total_remaining_credits": 5, | |
| "credits": [ | |
| { | |
| "id": "cd813ce3-b5fe-4a74-ae5c-e077ce49b984", | |
| "remaining_amount": 0, | |
| "expires_at": "2100-01-01T05:00:00Z", | |
| "type": { | |
| "id": "7ca94fd6-c201-4ca6-a9bf-4473c83e30b4", | |
| "name": "Refund", | |
| "amount": 0, | |
| "description": "For generate/upscale failure refunds" | |
| } | |
| }, | |
| { | |
| "id": "b8a508a2-a5dc-4329-ad25-015b50fbc51f", | |
| "remaining_amount": 5, | |
| "expires_at": "2100-01-01T05:00:00Z", | |
| "type": { | |
| "id": "3b12b23e-478b-4c18-8e34-70b3f0af1ee6", | |
| "name": "Free", | |
| "amount": 50, | |
| "description": "Base free credits for user" | |
| } | |
| } | |
| ] | |
| }; | |
| creditsData = sampleData; | |
| updateCreditsDisplay(sampleData); | |
| } | |
| function showSampleModels() { | |
| const sampleData = { | |
| "models": [ | |
| { | |
| "id": "sd-xl-1.0", | |
| "name": "Stable Diffusion XL", | |
| "description": "High-quality image generation model with excellent detail", | |
| "type": "image", | |
| "is_public": true, | |
| "is_default": true, | |
| "is_community": false, | |
| "created_at": "2024-01-01T00:00:00Z", | |
| "updated_at": "2024-06-01T00:00:00Z" | |
| }, | |
| { | |
| "id": "dreamshaper-v7", | |
| "name": "DreamShaper v7", | |
| "description": "Artistic style model for creative image generation", | |
| "type": "image", | |
| "is_public": true, | |
| "is_default": false, | |
| "is_community": false, | |
| "created_at": "2024-02-01T00:00:00Z", | |
| "updated_at": "2024-05-01T00:00:00Z" | |
| }, | |
| { | |
| "id": "anime-diffusion", | |
| "name": "Anime Diffusion", | |
| "description": "Specialized model for anime-style artwork", | |
| "type": "image", | |
| "is_public": true, | |
| "is_default": false, | |
| "is_community": true, | |
| "created_at": "2024-03-01T00:00:00Z", | |
| "updated_at": "2024-04-01T00:00:00Z" | |
| } | |
| ] | |
| }; | |
| modelsData = sampleData; | |
| updateModelsDisplay(sampleData); | |
| } | |
| // Add CSS animations | |
| const style = document.createElement('style'); | |
| style.textContent = ` | |
| @keyframes slideIn { | |
| from { transform: translateX(100%); opacity: 0; } | |
| to { transform: translateX(0); opacity: 1; } | |
| } | |
| @keyframes slideOut { | |
| from { transform: translateX(0); opacity: 1; } | |
| to { transform: translateX(100%); opacity: 0; } | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| </script> | |
| </body> | |
| </html> |