|
|
<!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> |