Your Name
feat: UI improvements and error suppression - Enhanced dashboard and market pages with improved header buttons, logo, and currency symbol display - Stopped animated ticker - Removed pie chart legends - Added error suppressor for external service errors (SSE, Permissions-Policy warnings) - Improved header button prominence and icon appearance - Enhanced logo with glow effects and better design - Fixed currency symbol visibility in market tables
8b7b267
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AI Tools - Crypto Intelligence Hub</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | |
| background: linear-gradient(135deg, #050816 0%, #0a1128 100%); | |
| color: #e2e8f0; | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| .header { | |
| text-align: center; | |
| margin-bottom: 40px; | |
| padding: 30px 20px; | |
| background: rgba(15, 23, 42, 0.6); | |
| border-radius: 16px; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .header h1 { | |
| font-size: 2.5rem; | |
| font-weight: 700; | |
| margin-bottom: 10px; | |
| background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .header p { | |
| color: #94a3b8; | |
| font-size: 1.1rem; | |
| } | |
| .container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| } | |
| .card { | |
| background: rgba(15, 23, 42, 0.8); | |
| border-radius: 16px; | |
| padding: 30px; | |
| margin-bottom: 30px; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); | |
| backdrop-filter: blur(10px); | |
| } | |
| .card-title { | |
| font-size: 1.8rem; | |
| font-weight: 600; | |
| margin-bottom: 25px; | |
| color: #f1f5f9; | |
| } | |
| .form-group { | |
| margin-bottom: 20px; | |
| } | |
| .form-label { | |
| display: block; | |
| margin-bottom: 8px; | |
| color: #cbd5e1; | |
| font-weight: 500; | |
| font-size: 0.95rem; | |
| } | |
| .form-input, | |
| .form-textarea, | |
| .form-select { | |
| width: 100%; | |
| padding: 12px 16px; | |
| background: rgba(30, 41, 59, 0.8); | |
| border: 1px solid rgba(255, 255, 255, 0.15); | |
| border-radius: 8px; | |
| color: #e2e8f0; | |
| font-size: 1rem; | |
| transition: all 0.3s ease; | |
| } | |
| .form-input:focus, | |
| .form-textarea:focus, | |
| .form-select:focus { | |
| outline: none; | |
| border-color: #60a5fa; | |
| box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1); | |
| } | |
| .form-textarea { | |
| min-height: 120px; | |
| resize: vertical; | |
| font-family: inherit; | |
| } | |
| .btn { | |
| padding: 12px 24px; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| border: none; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); | |
| color: white; | |
| } | |
| .btn-primary:hover:not(:disabled) { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 20px rgba(59, 130, 246, 0.4); | |
| } | |
| .btn-primary:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| } | |
| .btn-secondary { | |
| background: rgba(71, 85, 105, 0.8); | |
| color: #e2e8f0; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .btn-secondary:hover:not(:disabled) { | |
| background: rgba(100, 116, 139, 0.9); | |
| } | |
| .result-box { | |
| margin-top: 25px; | |
| padding: 20px; | |
| background: rgba(30, 41, 59, 0.6); | |
| border-radius: 12px; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .error-box { | |
| margin-top: 25px; | |
| padding: 16px; | |
| background: rgba(239, 68, 68, 0.1); | |
| border: 1px solid rgba(239, 68, 68, 0.3); | |
| border-radius: 8px; | |
| color: #fca5a5; | |
| } | |
| .success-box { | |
| margin-top: 25px; | |
| padding: 20px; | |
| background: rgba(34, 197, 94, 0.1); | |
| border: 1px solid rgba(34, 197, 94, 0.3); | |
| border-radius: 12px; | |
| } | |
| .badge { | |
| display: inline-block; | |
| padding: 6px 14px; | |
| border-radius: 20px; | |
| font-size: 0.9rem; | |
| font-weight: 600; | |
| margin-right: 10px; | |
| } | |
| .badge-positive { | |
| background: rgba(34, 197, 94, 0.2); | |
| color: #4ade80; | |
| border: 1px solid rgba(34, 197, 94, 0.3); | |
| } | |
| .badge-negative { | |
| background: rgba(239, 68, 68, 0.2); | |
| color: #f87171; | |
| border: 1px solid rgba(239, 68, 68, 0.3); | |
| } | |
| .badge-neutral { | |
| background: rgba(148, 163, 184, 0.2); | |
| color: #94a3b8; | |
| border: 1px solid rgba(148, 163, 184, 0.3); | |
| } | |
| .badge-success { | |
| background: rgba(34, 197, 94, 0.2); | |
| color: #4ade80; | |
| border: 1px solid rgba(34, 197, 94, 0.3); | |
| } | |
| .badge-danger { | |
| background: rgba(239, 68, 68, 0.2); | |
| color: #f87171; | |
| border: 1px solid rgba(239, 68, 68, 0.3); | |
| } | |
| .score-bar { | |
| margin-top: 15px; | |
| } | |
| .score-item { | |
| display: flex; | |
| align-items: center; | |
| margin-bottom: 8px; | |
| } | |
| .score-label { | |
| min-width: 80px; | |
| font-size: 0.9rem; | |
| color: #cbd5e1; | |
| } | |
| .score-progress { | |
| flex: 1; | |
| height: 8px; | |
| background: rgba(30, 41, 59, 0.8); | |
| border-radius: 4px; | |
| overflow: hidden; | |
| margin: 0 12px; | |
| } | |
| .score-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 100%); | |
| border-radius: 4px; | |
| transition: width 0.5s ease; | |
| } | |
| .score-value { | |
| min-width: 50px; | |
| text-align: right; | |
| font-weight: 600; | |
| color: #e2e8f0; | |
| } | |
| .table-container { | |
| overflow-x: auto; | |
| margin-top: 20px; | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| } | |
| th { | |
| background: rgba(30, 41, 59, 0.8); | |
| padding: 12px; | |
| text-align: left; | |
| font-weight: 600; | |
| color: #f1f5f9; | |
| border-bottom: 2px solid rgba(255, 255, 255, 0.1); | |
| } | |
| td { | |
| padding: 12px; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.05); | |
| color: #cbd5e1; | |
| } | |
| tr:hover { | |
| background: rgba(30, 41, 59, 0.4); | |
| } | |
| .info-box { | |
| padding: 16px; | |
| background: rgba(59, 130, 246, 0.1); | |
| border: 1px solid rgba(59, 130, 246, 0.3); | |
| border-radius: 8px; | |
| margin: 15px 0; | |
| color: #93c5fd; | |
| } | |
| .warning-box { | |
| padding: 16px; | |
| background: rgba(251, 191, 36, 0.1); | |
| border: 1px solid rgba(251, 191, 36, 0.3); | |
| border-radius: 8px; | |
| margin: 15px 0; | |
| color: #fcd34d; | |
| } | |
| .status-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 15px; | |
| margin: 20px 0; | |
| } | |
| .status-item { | |
| padding: 15px; | |
| background: rgba(30, 41, 59, 0.6); | |
| border-radius: 8px; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .status-label { | |
| font-size: 0.85rem; | |
| color: #94a3b8; | |
| margin-bottom: 5px; | |
| } | |
| .status-value { | |
| font-size: 1.3rem; | |
| font-weight: 700; | |
| color: #f1f5f9; | |
| } | |
| .summary-text { | |
| padding: 20px; | |
| background: rgba(30, 41, 59, 0.8); | |
| border-radius: 8px; | |
| border-left: 4px solid #60a5fa; | |
| font-size: 1.05rem; | |
| line-height: 1.7; | |
| color: #e2e8f0; | |
| margin-bottom: 20px; | |
| } | |
| .sentences-list { | |
| list-style: none; | |
| padding: 0; | |
| } | |
| .sentences-list li { | |
| padding: 12px 15px; | |
| background: rgba(30, 41, 59, 0.6); | |
| border-radius: 8px; | |
| margin-bottom: 10px; | |
| border-left: 3px solid #8b5cf6; | |
| color: #cbd5e1; | |
| } | |
| .sentences-list li:before { | |
| content: "→"; | |
| margin-right: 10px; | |
| color: #8b5cf6; | |
| font-weight: bold; | |
| } | |
| .loading { | |
| display: inline-block; | |
| width: 16px; | |
| height: 16px; | |
| border: 2px solid rgba(255, 255, 255, 0.3); | |
| border-top-color: #fff; | |
| border-radius: 50%; | |
| animation: spin 0.6s linear infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| .two-column { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 20px; | |
| } | |
| @media (max-width: 768px) { | |
| .header h1 { | |
| font-size: 1.8rem; | |
| } | |
| .header p { | |
| font-size: 0.95rem; | |
| } | |
| .card { | |
| padding: 20px; | |
| } | |
| .card-title { | |
| font-size: 1.4rem; | |
| } | |
| .two-column { | |
| grid-template-columns: 1fr; | |
| } | |
| .status-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .hidden { | |
| display: none; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>AI Tools – Crypto Intelligence Hub</h1> | |
| <p>Sentiment, Summaries, and Model Diagnostics</p> | |
| </div> | |
| <!-- Sentiment Playground --> | |
| <div class="card"> | |
| <h2 class="card-title">Sentiment Playground</h2> | |
| <div class="form-group"> | |
| <label class="form-label" for="sentiment-input">Enter Text</label> | |
| <textarea | |
| id="sentiment-input" | |
| class="form-textarea" | |
| placeholder="Enter text to analyze sentiment (tweets, news, or any text)..." | |
| ></textarea> | |
| </div> | |
| <div class="two-column"> | |
| <div class="form-group"> | |
| <label class="form-label" for="sentiment-source">Source Type</label> | |
| <select id="sentiment-source" class="form-select"> | |
| <option value="user">User Input</option> | |
| <option value="tweet">Tweet</option> | |
| <option value="news">News</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label" for="sentiment-model-key">Model Key (Optional)</label> | |
| <input | |
| type="text" | |
| id="sentiment-model-key" | |
| class="form-input" | |
| placeholder="Leave empty for default model" | |
| /> | |
| </div> | |
| </div> | |
| <button id="analyze-sentiment-btn" class="btn btn-primary"> | |
| Analyze Sentiment | |
| </button> | |
| <div id="sentiment-result" class="hidden"></div> | |
| </div> | |
| <!-- Text Summarizer --> | |
| <div class="card"> | |
| <h2 class="card-title">Text Summarizer</h2> | |
| <div class="form-group"> | |
| <label class="form-label" for="summary-input">Enter Long Text</label> | |
| <textarea | |
| id="summary-input" | |
| class="form-textarea" | |
| placeholder="Paste article or long text to summarize..." | |
| style="min-height: 180px;" | |
| ></textarea> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label" for="max-sentences">Maximum Sentences</label> | |
| <select id="max-sentences" class="form-select"> | |
| <option value="2">2 sentences</option> | |
| <option value="3" selected>3 sentences</option> | |
| <option value="4">4 sentences</option> | |
| <option value="5">5 sentences</option> | |
| </select> | |
| </div> | |
| <button id="summarize-btn" class="btn btn-primary"> | |
| Summarize | |
| </button> | |
| <div id="summary-result" class="hidden"></div> | |
| </div> | |
| <!-- Model Status & Diagnostics --> | |
| <div class="card"> | |
| <h2 class="card-title">Model Status & Diagnostics</h2> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;"> | |
| <h3 style="color: #cbd5e1; font-size: 1.2rem;">Registry Status</h3> | |
| <button id="refresh-status-btn" class="btn btn-secondary"> | |
| Refresh | |
| </button> | |
| </div> | |
| <div id="registry-status"></div> | |
| <h3 style="color: #cbd5e1; font-size: 1.2rem; margin: 30px 0 15px 0;">Models Table</h3> | |
| <div id="models-table"></div> | |
| </div> | |
| </div> | |
| <script> | |
| (function() { | |
| 'use strict'; | |
| const AITools = { | |
| // Sentiment Analysis | |
| async analyzeSentiment() { | |
| const text = document.getElementById('sentiment-input').value.trim(); | |
| const source = document.getElementById('sentiment-source').value; | |
| const modelKey = document.getElementById('sentiment-model-key').value.trim(); | |
| const btn = document.getElementById('analyze-sentiment-btn'); | |
| const resultDiv = document.getElementById('sentiment-result'); | |
| if (!text) { | |
| this.showError(resultDiv, 'Please enter text to analyze'); | |
| return; | |
| } | |
| btn.disabled = true; | |
| btn.innerHTML = '<span class="loading"></span> Analyzing...'; | |
| resultDiv.classList.add('hidden'); | |
| try { | |
| const payload = { text, source }; | |
| if (modelKey) payload.model_key = modelKey; | |
| const response = await fetch('/api/sentiment/analyze', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload) | |
| }); | |
| const data = await response.json(); | |
| if (!response.ok || !data.ok) { | |
| throw new Error(data.error || 'Sentiment analysis failed'); | |
| } | |
| this.displaySentimentResult(resultDiv, data); | |
| } catch (error) { | |
| this.showError(resultDiv, error.message); | |
| } finally { | |
| btn.disabled = false; | |
| btn.innerHTML = 'Analyze Sentiment'; | |
| } | |
| }, | |
| displaySentimentResult(container, data) { | |
| const label = data.label || 'unknown'; | |
| const score = (data.score * 100).toFixed(1); | |
| const labelClass = label.toLowerCase(); | |
| let html = '<div class="result-box">'; | |
| html += '<h3 style="margin-bottom: 15px; color: #f1f5f9;">Sentiment Analysis Result</h3>'; | |
| html += `<div style="margin-bottom: 15px;">`; | |
| html += `<span class="badge badge-${labelClass}">${label.toUpperCase()}</span>`; | |
| html += `<span style="font-size: 1.3rem; font-weight: 700; color: #e2e8f0;">${score}%</span>`; | |
| html += `</div>`; | |
| if (data.model) { | |
| html += `<p style="color: #94a3b8; font-size: 0.9rem; margin-bottom: 15px;">Model: ${data.model}</p>`; | |
| } | |
| if (data.details && data.details.labels && data.details.scores) { | |
| html += '<div class="score-bar">'; | |
| for (let i = 0; i < data.details.labels.length; i++) { | |
| const lbl = data.details.labels[i]; | |
| const scr = (data.details.scores[i] * 100).toFixed(1); | |
| html += '<div class="score-item">'; | |
| html += `<span class="score-label">${lbl}</span>`; | |
| html += '<div class="score-progress">'; | |
| html += `<div class="score-fill" style="width: ${scr}%"></div>`; | |
| html += '</div>'; | |
| html += `<span class="score-value">${scr}%</span>`; | |
| html += '</div>'; | |
| } | |
| html += '</div>'; | |
| } | |
| html += '</div>'; | |
| container.innerHTML = html; | |
| container.classList.remove('hidden'); | |
| }, | |
| // Text Summarization | |
| async summarizeText() { | |
| const text = document.getElementById('summary-input').value.trim(); | |
| const maxSentences = parseInt(document.getElementById('max-sentences').value); | |
| const btn = document.getElementById('summarize-btn'); | |
| const resultDiv = document.getElementById('summary-result'); | |
| if (!text) { | |
| this.showError(resultDiv, 'Please enter text to summarize'); | |
| return; | |
| } | |
| btn.disabled = true; | |
| btn.innerHTML = '<span class="loading"></span> Summarizing...'; | |
| resultDiv.classList.add('hidden'); | |
| try { | |
| const response = await fetch('/api/ai/summarize', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ text, max_sentences: maxSentences }) | |
| }); | |
| const data = await response.json(); | |
| if (!response.ok || !data.ok) { | |
| throw new Error(data.error || 'Summarization failed'); | |
| } | |
| this.displaySummaryResult(resultDiv, data); | |
| } catch (error) { | |
| this.showError(resultDiv, error.message); | |
| } finally { | |
| btn.disabled = false; | |
| btn.innerHTML = 'Summarize'; | |
| } | |
| }, | |
| displaySummaryResult(container, data) { | |
| let html = '<div class="result-box">'; | |
| html += '<h3 style="margin-bottom: 15px; color: #f1f5f9;">Summary</h3>'; | |
| if (data.summary) { | |
| html += `<div class="summary-text">${this.escapeHtml(data.summary)}</div>`; | |
| } | |
| if (data.sentences && data.sentences.length > 0) { | |
| html += '<h4 style="margin: 20px 0 10px 0; color: #cbd5e1; font-size: 1.1rem;">Key Sentences</h4>'; | |
| html += '<ul class="sentences-list">'; | |
| data.sentences.forEach(sentence => { | |
| html += `<li>${this.escapeHtml(sentence)}</li>`; | |
| }); | |
| html += '</ul>'; | |
| } | |
| html += '</div>'; | |
| container.innerHTML = html; | |
| container.classList.remove('hidden'); | |
| }, | |
| // Model Status & Diagnostics | |
| async loadModelStatus() { | |
| const statusDiv = document.getElementById('registry-status'); | |
| const tableDiv = document.getElementById('models-table'); | |
| const btn = document.getElementById('refresh-status-btn'); | |
| btn.disabled = true; | |
| btn.innerHTML = '<span class="loading"></span> Loading...'; | |
| try { | |
| const [statusRes, listRes] = await Promise.all([ | |
| fetch('/api/models/status'), | |
| fetch('/api/models/list') | |
| ]); | |
| const statusData = await statusRes.json(); | |
| const listData = await listRes.json(); | |
| this.displayRegistryStatus(statusDiv, statusData); | |
| this.displayModelsTable(tableDiv, listData); | |
| } catch (error) { | |
| this.showError(statusDiv, 'Failed to load model status: ' + error.message); | |
| } finally { | |
| btn.disabled = false; | |
| btn.innerHTML = 'Refresh'; | |
| } | |
| }, | |
| displayRegistryStatus(container, data) { | |
| let html = '<div class="status-grid">'; | |
| html += '<div class="status-item">'; | |
| html += '<div class="status-label">HF Mode</div>'; | |
| html += `<div class="status-value">${data.hf_mode || 'unknown'}</div>`; | |
| html += '</div>'; | |
| html += '<div class="status-item">'; | |
| html += '<div class="status-label">Overall Status</div>'; | |
| html += `<div class="status-value">${data.status || 'unknown'}</div>`; | |
| html += '</div>'; | |
| html += '<div class="status-item">'; | |
| html += '<div class="status-label">Models Loaded</div>'; | |
| html += `<div class="status-value">${data.models_loaded || 0}</div>`; | |
| html += '</div>'; | |
| html += '<div class="status-item">'; | |
| html += '<div class="status-label">Models Failed</div>'; | |
| html += `<div class="status-value">${data.models_failed || 0}</div>`; | |
| html += '</div>'; | |
| html += '</div>'; | |
| if (data.status === 'disabled' || data.hf_mode === 'off') { | |
| html += '<div class="info-box">'; | |
| html += '<strong>Note:</strong> HF models are disabled. To enable them, set HF_MODE=public or HF_MODE=auth in the environment.'; | |
| html += '</div>'; | |
| } else if (data.models_loaded === 0 && data.status !== 'disabled') { | |
| html += '<div class="warning-box">'; | |
| html += '<strong>Warning:</strong> No models could be loaded. Check model IDs or HF credentials.'; | |
| html += '</div>'; | |
| } | |
| if (data.error) { | |
| html += '<div class="error-box" style="margin-top: 15px;">'; | |
| html += `<strong>Error:</strong> ${this.escapeHtml(data.error)}`; | |
| html += '</div>'; | |
| } | |
| if (data.failed && data.failed.length > 0) { | |
| html += '<div style="margin-top: 20px;">'; | |
| html += '<h4 style="color: #cbd5e1; margin-bottom: 10px;">Failed Models</h4>'; | |
| html += '<div style="background: rgba(30, 41, 59, 0.6); border-radius: 8px; padding: 15px;">'; | |
| data.failed.forEach(([key, error]) => { | |
| html += `<div style="margin-bottom: 8px; padding: 8px; background: rgba(239, 68, 68, 0.1); border-left: 3px solid #ef4444; border-radius: 4px;">`; | |
| html += `<strong style="color: #fca5a5;">${key}:</strong> `; | |
| html += `<span style="color: #cbd5e1;">${this.escapeHtml(error)}</span>`; | |
| html += `</div>`; | |
| }); | |
| html += '</div>'; | |
| html += '</div>'; | |
| } | |
| container.innerHTML = html; | |
| }, | |
| displayModelsTable(container, data) { | |
| if (!data.models || data.models.length === 0) { | |
| container.innerHTML = '<div class="info-box">No models configured</div>'; | |
| return; | |
| } | |
| let html = '<div class="table-container">'; | |
| html += '<table>'; | |
| html += '<thead><tr>'; | |
| html += '<th>Key</th>'; | |
| html += '<th>Task</th>'; | |
| html += '<th>Model ID</th>'; | |
| html += '<th>Loaded</th>'; | |
| html += '<th>Error</th>'; | |
| html += '</tr></thead>'; | |
| html += '<tbody>'; | |
| data.models.forEach(model => { | |
| html += '<tr>'; | |
| html += `<td><strong>${model.key || 'N/A'}</strong></td>`; | |
| html += `<td>${model.task || 'N/A'}</td>`; | |
| html += `<td style="font-family: monospace; font-size: 0.85rem;">${model.model_id || 'N/A'}</td>`; | |
| html += '<td>'; | |
| if (model.loaded) { | |
| html += '<span class="badge badge-success">Yes</span>'; | |
| } else { | |
| html += '<span class="badge badge-danger">No</span>'; | |
| } | |
| html += '</td>'; | |
| html += `<td style="color: #f87171; font-size: 0.85rem;">${model.error ? this.escapeHtml(model.error) : '-'}</td>`; | |
| html += '</tr>'; | |
| }); | |
| html += '</tbody>'; | |
| html += '</table>'; | |
| html += '</div>'; | |
| container.innerHTML = html; | |
| }, | |
| // Utility functions | |
| showError(container, message) { | |
| container.innerHTML = `<div class="error-box"><strong>Error:</strong> ${this.escapeHtml(message)}</div>`; | |
| container.classList.remove('hidden'); | |
| }, | |
| escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| }, | |
| // Initialize | |
| init() { | |
| document.getElementById('analyze-sentiment-btn').addEventListener('click', () => this.analyzeSentiment()); | |
| document.getElementById('summarize-btn').addEventListener('click', () => this.summarizeText()); | |
| document.getElementById('refresh-status-btn').addEventListener('click', () => this.loadModelStatus()); | |
| this.loadModelStatus(); | |
| } | |
| }; | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', () => AITools.init()); | |
| } else { | |
| AITools.init(); | |
| } | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |