| <!DOCTYPE html> |
| <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> |
| |
| <script src="/static/js/api-config.js"></script> |
| <script> |
| |
| window.apiReady = new Promise((resolve) => { |
| if (window.apiClient) { |
| console.log('✅ API Client ready'); |
| resolve(window.apiClient); |
| } else { |
| console.error('❌ API Client not loaded'); |
| } |
| }); |
| </script> |
|
|
| </head> |
| <body> |
| <div class="container"> |
| <div class="header"> |
| <h1>AI Tools – Crypto Intelligence Hub</h1> |
| <p>Sentiment, Summaries, and Model Diagnostics</p> |
| </div> |
|
|
| |
| <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">Analysis Mode</label> |
| <select id="sentiment-source" class="form-select"> |
| <option value="auto">Auto (Crypto)</option> |
| <option value="crypto">Crypto</option> |
| <option value="financial">Financial</option> |
| <option value="social">Social/Twitter</option> |
| <option value="news">News</option> |
| </select> |
| </div> |
|
|
| <div class="form-group"> |
| <label class="form-label" for="sentiment-symbol">Asset Symbol (Optional)</label> |
| <input |
| type="text" |
| id="sentiment-symbol" |
| class="form-input" |
| placeholder="e.g., BTC, ETH" |
| style="text-transform: uppercase;" |
| /> |
| </div> |
| </div> |
|
|
| <button id="analyze-sentiment-btn" class="btn btn-primary"> |
| Analyze Sentiment |
| </button> |
|
|
| <div id="sentiment-result" class="hidden"></div> |
| </div> |
|
|
| |
| <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> |
|
|
| |
| <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 = { |
| |
| async analyzeSentiment() { |
| const text = document.getElementById('sentiment-input').value.trim(); |
| const mode = document.getElementById('sentiment-source').value; |
| const symbol = document.getElementById('sentiment-symbol').value.trim().toUpperCase(); |
| 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, mode, source: 'ai_tools' }; |
| if (symbol) payload.symbol = symbol; |
| |
| 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(); |
| const engine = data.engine || 'unknown'; |
| |
| |
| let displayLabel = label; |
| if (label === 'bullish' || label === 'positive') displayLabel = 'Bullish/Positive'; |
| else if (label === 'bearish' || label === 'negative') displayLabel = 'Bearish/Negative'; |
| else if (label === 'neutral') displayLabel = 'Neutral'; |
| |
| let html = '<div class="result-box">'; |
| html += '<h3 style="margin-bottom: 15px; color: #f1f5f9;">Sentiment Analysis Result</h3>'; |
| html += `<div style="margin-bottom: 15px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">`; |
| html += `<div>`; |
| html += `<span class="badge badge-${labelClass}">${displayLabel.toUpperCase()}</span>`; |
| html += `<span style="font-size: 1.3rem; font-weight: 700; color: #e2e8f0; margin-left: 10px;">${score}%</span>`; |
| html += `</div>`; |
| html += `<div style="font-size: 0.85rem; color: #94a3b8;">Engine: ${engine}</div>`; |
| 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>'; |
| } |
| |
| |
| if (engine === 'fallback_lexical') { |
| html += '<div class="info-box" style="margin-top: 15px;">'; |
| html += '<strong>Note:</strong> Using fallback lexical analysis. HF models may be unavailable.'; |
| html += '</div>'; |
| } |
| |
| html += '</div>'; |
| container.innerHTML = html; |
| container.classList.remove('hidden'); |
| }, |
| |
| |
| 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'); |
| }, |
| |
| |
| 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; |
| }, |
| |
| |
| 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; |
| }, |
| |
| |
| 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> |