/**
* AI Tools Page - Comprehensive AI Analysis Suite
*/
class AIToolsPage {
constructor() {
this.history = this.loadHistory();
this.currentTab = 'sentiment';
this.init();
}
/**
* Initialize the page
*/
init() {
this.setupTabs();
this.setupEventListeners();
this.loadModelStatus();
this.updateStats();
this.renderHistory();
}
/**
* Setup tab navigation
*/
setupTabs() {
const tabs = document.querySelectorAll('#ai-tools-tabs .tab');
const panes = document.querySelectorAll('.tab-pane');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const targetTab = tab.dataset.tab;
// Update active tab
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// Update active pane
panes.forEach(p => p.classList.remove('active'));
const targetPane = document.getElementById(`tab-${targetTab}`);
if (targetPane) {
targetPane.classList.add('active');
this.currentTab = targetTab;
}
});
});
}
/**
* Setup event listeners
*/
setupEventListeners() {
// Sentiment
document.getElementById('analyze-sentiment-btn')?.addEventListener('click', () => this.analyzeSentiment());
// Summarize
document.getElementById('summarize-btn')?.addEventListener('click', () => this.summarizeText());
// News
document.getElementById('analyze-news-btn')?.addEventListener('click', () => this.analyzeNews());
// Trading
document.getElementById('get-trading-decision-btn')?.addEventListener('click', () => this.getTradingDecision());
// Batch
document.getElementById('process-batch-btn')?.addEventListener('click', () => this.processBatch());
// History
document.getElementById('clear-history-btn')?.addEventListener('click', () => this.clearHistory());
document.getElementById('export-history-btn')?.addEventListener('click', () => this.exportHistory());
// Model Status
document.getElementById('refresh-status-btn')?.addEventListener('click', () => this.loadModelStatus());
// Refresh All
document.getElementById('refresh-all-btn')?.addEventListener('click', () => {
this.loadModelStatus();
this.updateStats();
});
}
/**
* Update statistics cards - REAL DATA from API
*/
async updateStats() {
try {
const [statusRes, resourcesRes] = await Promise.allSettled([
fetch('/api/models/status', { signal: AbortSignal.timeout(10000) }),
fetch('/api/resources/summary', { signal: AbortSignal.timeout(10000) })
]);
// Update model stats
if (statusRes.status === 'fulfilled' && statusRes.value.ok) {
const statusData = await statusRes.value.json();
const modelsLoaded = document.getElementById('models-loaded');
const hfMode = document.getElementById('hf-mode');
const failedModels = document.getElementById('failed-models');
const hfStatus = document.getElementById('hf-status');
const loadedCount = statusData.models_loaded || statusData.models?.total_models || 0;
const totalModels = statusData.models?.total_models || statusData.models_loaded || 0;
const failedCount = totalModels - loadedCount;
if (modelsLoaded) modelsLoaded.textContent = loadedCount;
if (hfMode) hfMode.textContent = (statusData.hf_mode || 'off').toUpperCase();
if (failedModels) failedModels.textContent = failedCount;
if (hfStatus) {
if (statusData.status === 'ready' || statusData.models_loaded > 0) {
hfStatus.textContent = 'Ready';
hfStatus.className = 'stat-trend success';
} else {
hfStatus.textContent = 'Disabled';
hfStatus.className = 'stat-trend warning';
}
}
}
// Update analyses count
const analysesToday = document.getElementById('analyses-today');
if (analysesToday) {
const today = new Date().toDateString();
const todayCount = this.history.filter(h => new Date(h.timestamp).toDateString() === today).length;
analysesToday.textContent = todayCount;
}
// Update resources stats if available
if (resourcesRes.status === 'fulfilled' && resourcesRes.value.ok) {
const resourcesData = await resourcesRes.value.json();
if (resourcesData.resources) {
const hfModels = resourcesData.huggingface_models || {};
const totalModels = hfModels.total_models || 0;
const loadedModels = hfModels.loaded_models || 0;
// Update model stats with real data
if (modelsLoaded && !modelsLoaded.textContent) {
modelsLoaded.textContent = loadedModels;
}
}
}
} catch (error) {
console.error('Failed to update stats:', error);
}
}
/**
* Analyze sentiment of text
*/
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 = ' 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);
this.addToHistory('sentiment', { text, symbol, result: data });
this.updateStats();
} catch (error) {
this.showError(resultDiv, error.message);
} finally {
btn.disabled = false;
btn.innerHTML = ' Analyze Sentiment';
}
}
/**
* Display sentiment analysis result
*/
displaySentimentResult(container, data) {
if (!container) return;
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 = '
';
html += '
Sentiment Analysis Result
';
html += `
`;
html += `
`;
html += `${displayLabel.toUpperCase()}`;
html += `${score}%`;
html += `
`;
html += `
Engine: ${engine}
`;
html += `
`;
if (data.model) {
html += `
Model: ${data.model}
`;
}
if (data.details && data.details.labels && data.details.scores) {
html += '
';
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 += '
';
html += `
${lbl}`;
html += '
';
html += `
${scr}%`;
html += '
';
}
html += '
';
}
if (engine === 'fallback_lexical') {
html += '
';
html += 'Note: Using fallback lexical analysis. HF models may be unavailable.';
html += '
';
}
html += '
';
container.innerHTML = html;
container.classList.remove('hidden');
}
/**
* Summarize text
*/
async summarizeText() {
const text = document.getElementById('summary-input').value.trim();
const maxSentences = parseInt(document.getElementById('max-sentences').value);
const style = document.getElementById('summary-style').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 = ' 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, style })
});
const data = await response.json();
if (!response.ok || !data.ok) {
throw new Error(data.error || 'Summarization failed');
}
this.displaySummaryResult(resultDiv, data, style);
this.addToHistory('summarize', { text, maxSentences, result: data });
this.updateStats();
} catch (error) {
this.showError(resultDiv, error.message);
} finally {
btn.disabled = false;
btn.innerHTML = ' Summarize';
}
}
/**
* Display summary result
*/
displaySummaryResult(container, data, style = 'detailed') {
if (!container) return;
let html = '';
html += '
Summary
';
if (data.summary) {
if (style === 'bullet') {
html += '
';
data.summary.split('.').filter(s => s.trim()).forEach(sentence => {
html += `- ${this.escapeHtml(sentence.trim())}.
`;
});
html += '
';
} else {
html += `
${this.escapeHtml(data.summary)}
`;
}
}
if (data.sentences && data.sentences.length > 0 && style !== 'bullet') {
html += '
Key Sentences
';
html += '
';
data.sentences.forEach(sentence => {
html += `- ${this.escapeHtml(sentence)}
`;
});
html += '
';
}
html += '
';
container.innerHTML = html;
container.classList.remove('hidden');
}
/**
* Analyze news article
*/
async analyzeNews() {
const text = document.getElementById('news-input').value.trim();
const symbol = document.getElementById('news-symbol').value.trim().toUpperCase();
const analysisType = document.getElementById('analysis-type').value;
const btn = document.getElementById('analyze-news-btn');
const resultDiv = document.getElementById('news-result');
if (!text) {
this.showError(resultDiv, 'Please enter news text to analyze');
return;
}
btn.disabled = true;
btn.innerHTML = ' Analyzing...';
resultDiv?.classList.add('hidden');
try {
const results = {};
if (analysisType === 'full' || analysisType === 'sentiment') {
const sentimentRes = await fetch('/api/sentiment/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, mode: 'news', symbol })
});
if (sentimentRes.ok) {
results.sentiment = await sentimentRes.json();
}
}
if (analysisType === 'full' || analysisType === 'summary') {
const summaryRes = await fetch('/api/ai/summarize', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, max_sentences: 3 })
});
if (summaryRes.ok) {
results.summary = await summaryRes.json();
}
}
this.displayNewsResult(resultDiv, results);
this.addToHistory('news', { text, symbol, result: results });
this.updateStats();
} catch (error) {
this.showError(resultDiv, error.message);
} finally {
btn.disabled = false;
btn.innerHTML = ' Analyze News';
}
}
/**
* Display news analysis result
*/
displayNewsResult(container, results) {
if (!container) return;
let html = '';
html += '
News Analysis Result
';
if (results.sentiment && results.sentiment.ok) {
const sent = results.sentiment;
const label = sent.label || 'unknown';
const score = (sent.score * 100).toFixed(1);
html += '
';
html += '
Sentiment
';
html += `${label.toUpperCase()}`;
html += `${score}%`;
html += '';
}
if (results.summary && results.summary.ok) {
html += '
';
html += '
Summary
';
html += `
${this.escapeHtml(results.summary.summary || '')}
`;
html += '
';
}
html += '
';
container.innerHTML = html;
container.classList.remove('hidden');
}
/**
* Get trading decision
*/
async getTradingDecision() {
const symbol = document.getElementById('trading-symbol').value.trim().toUpperCase();
const timeframe = document.getElementById('trading-timeframe').value;
const context = document.getElementById('trading-context').value.trim();
const btn = document.getElementById('get-trading-decision-btn');
const resultDiv = document.getElementById('trading-result');
if (!symbol) {
this.showError(resultDiv, 'Please enter an asset symbol');
return;
}
btn.disabled = true;
btn.innerHTML = ' Analyzing...';
resultDiv?.classList.add('hidden');
try {
const response = await fetch('/api/ai/decision', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ symbol, timeframe, context })
});
const data = await response.json();
if (!response.ok || !data.ok) {
throw new Error(data.error || 'Trading decision failed');
}
this.displayTradingResult(resultDiv, data);
this.addToHistory('trading', { symbol, timeframe, result: data });
this.updateStats();
} catch (error) {
this.showError(resultDiv, error.message);
} finally {
btn.disabled = false;
btn.innerHTML = ' Get Trading Decision';
}
}
/**
* Display trading decision result
*/
displayTradingResult(container, data) {
if (!container) return;
const decision = data.decision || data.action || 'HOLD';
const confidence = data.confidence || data.score || 0;
const reasoning = data.reasoning || data.reason || 'No reasoning provided';
// Sanitize all dynamic content
const safeDecision = this.escapeHtml(decision);
const safeConfidence = this.escapeHtml((confidence * 100).toFixed(1));
const safeReasoning = this.escapeHtml(reasoning);
let html = '';
html += '
Trading Decision
';
html += `
`;
html += `${safeDecision}`;
html += `${safeConfidence}% Confidence`;
html += `
`;
html += `
${safeReasoning}
`;
html += '
';
container.innerHTML = html;
container.classList.remove('hidden');
}
/**
* Process batch of texts
*/
async processBatch() {
const text = document.getElementById('batch-input').value.trim();
const operation = document.getElementById('batch-operation').value;
const format = document.getElementById('batch-format').value;
const btn = document.getElementById('process-batch-btn');
const resultDiv = document.getElementById('batch-result');
if (!text) {
this.showError(resultDiv, 'Please enter texts to process');
return;
}
const texts = text.split('\n').filter(t => t.trim());
if (texts.length === 0) {
this.showError(resultDiv, 'Please enter at least one text');
return;
}
btn.disabled = true;
const safeCount = this.escapeHtml(String(texts.length));
btn.innerHTML = ` Processing ${safeCount} items...`;
resultDiv?.classList.add('hidden');
try {
const results = [];
for (let i = 0; i < texts.length; i++) {
const item = { text: texts[i], index: i + 1 };
if (operation === 'sentiment' || operation === 'both') {
const res = await fetch('/api/sentiment/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: texts[i], mode: 'auto' })
});
if (res.ok) {
item.sentiment = await res.json();
}
}
if (operation === 'summarize' || operation === 'both') {
const res = await fetch('/api/ai/summarize', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: texts[i], max_sentences: 2 })
});
if (res.ok) {
item.summary = await res.json();
}
}
results.push(item);
}
this.displayBatchResult(resultDiv, results, format);
this.addToHistory('batch', { count: texts.length, operation, results });
this.updateStats();
} catch (error) {
this.showError(resultDiv, error.message);
} finally {
btn.disabled = false;
btn.innerHTML = ' Process Batch';
}
}
/**
* Display batch processing result
*/
displayBatchResult(container, results, format) {
if (!container) return;
let html = '';
html += '
';
html += `
Batch Results (${results.length} items)
`;
html += ``;
html += '';
if (format === 'table') {
html += '
| # | Text Preview | ';
if (results[0].sentiment) html += 'Sentiment | ';
if (results[0].summary) html += 'Summary | ';
html += '
';
results.forEach(item => {
html += '';
html += `| ${item.index} | `;
html += `${this.escapeHtml(item.text.substring(0, 100))}... | `;
if (item.sentiment && item.sentiment.ok) {
const sentimentLabel = this.escapeHtml(item.sentiment.label || 'N/A');
const sentimentClass = this.escapeHtml((item.sentiment.label?.toLowerCase() || 'neutral'));
html += `${sentimentLabel} | `;
}
if (item.summary && item.summary.ok) {
html += `${this.escapeHtml(item.summary.summary?.substring(0, 80) || '')}... | `;
}
html += '
';
});
html += '
';
} else {
html += '
';
html += this.escapeHtml(JSON.stringify(results, null, 2));
html += '';
}
html += '
';
container.innerHTML = html;
container.classList.remove('hidden');
}
/**
* Download batch results
*/
downloadBatchResults(results) {
const dataStr = JSON.stringify(results, null, 2);
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
const link = document.createElement('a');
link.setAttribute('href', dataUri);
link.setAttribute('download', `batch-results-${Date.now()}.json`);
link.click();
}
/**
* Load model status
*/
async loadModelStatus() {
const statusDiv = document.getElementById('registry-status');
const tableDiv = document.getElementById('models-table');
const btn = document.getElementById('refresh-status-btn');
if (btn) {
btn.disabled = true;
btn.innerHTML = ' 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);
this.updateStats();
} catch (error) {
this.showError(statusDiv, 'Failed to load model status: ' + error.message);
} finally {
if (btn) {
btn.disabled = false;
btn.innerHTML = ' Refresh';
}
}
}
/**
* Display registry status
*/
displayRegistryStatus(container, data) {
if (!container) return;
let html = '';
html += '
';
html += '
HF Mode
';
html += `
${data.hf_mode || 'unknown'}
`;
html += '
';
html += '
';
html += '
Overall Status
';
html += `
${data.status || 'unknown'}
`;
html += '
';
html += '
';
html += '
Models Loaded
';
html += `
${data.models_loaded || 0}
`;
html += '
';
html += '
';
html += '
Models Failed
';
html += `
${data.models_failed || 0}
`;
html += '
';
html += '
';
if (data.status === 'disabled' || data.hf_mode === 'off') {
html += '';
html += 'Note: HF models are disabled. To enable them, set HF_MODE=public or HF_MODE=auth in the environment.';
html += '
';
} else if (data.models_loaded === 0 && data.status !== 'disabled') {
html += '';
html += 'Warning: No models could be loaded. Check model IDs or HF credentials.';
html += '
';
}
if (data.error) {
html += '';
html += `Error: ${this.escapeHtml(data.error)}`;
html += '
';
}
if (data.failed && data.failed.length > 0) {
html += '';
html += '
Failed Models
';
html += '
';
data.failed.forEach(([key, error]) => {
html += `
`;
html += `${key}: `;
html += `${this.escapeHtml(error)}`;
html += `
`;
});
html += '
';
html += '
';
}
container.innerHTML = html;
}
/**
* Display models table
*/
displayModelsTable(container, data) {
if (!container) return;
if (!data.models || data.models.length === 0) {
container.innerHTML = 'No models configured
';
return;
}
let html = '';
html += '
';
html += '';
html += '| Key | ';
html += 'Task | ';
html += 'Model ID | ';
html += 'Loaded | ';
html += 'Error | ';
html += '
';
html += '';
data.models.forEach(model => {
html += '';
html += `| ${model.key || 'N/A'} | `;
html += `${model.task || 'N/A'} | `;
html += `${model.model_id || 'N/A'} | `;
html += '';
if (model.loaded) {
html += 'Yes';
} else {
html += 'No';
}
html += ' | ';
html += `${model.error ? this.escapeHtml(model.error) : '-'} | `;
html += '
';
});
html += '';
html += '
';
html += '
';
container.innerHTML = html;
}
/**
* Add to history
*/
addToHistory(type, data) {
const entry = {
type,
timestamp: new Date().toISOString(),
data
};
this.history.unshift(entry);
if (this.history.length > 100) {
this.history = this.history.slice(0, 100);
}
this.saveHistory();
this.renderHistory();
}
/**
* Load history from localStorage
*/
loadHistory() {
try {
const stored = localStorage.getItem('ai-tools-history');
return stored ? JSON.parse(stored) : [];
} catch {
return [];
}
}
/**
* Save history to localStorage
*/
saveHistory() {
try {
localStorage.setItem('ai-tools-history', JSON.stringify(this.history));
} catch (error) {
console.error('Failed to save history:', error);
}
}
/**
* Render history list
*/
renderHistory() {
const container = document.getElementById('history-list');
if (!container) return;
if (this.history.length === 0) {
container.innerHTML = 'No analysis history yet. Start analyzing to see your history here.
';
return;
}
let html = '';
this.history.slice(0, 50).forEach((entry, index) => {
const date = new Date(entry.timestamp);
html += ``;
html += ``;
html += `
${this.escapeHtml(JSON.stringify(entry.data).substring(0, 150))}...
`;
html += `
`;
html += `
`;
});
container.innerHTML = html;
}
/**
* View history item
*/
viewHistoryItem(index) {
const entry = this.history[index];
if (!entry) return;
alert(JSON.stringify(entry, null, 2));
}
/**
* Clear history
*/
clearHistory() {
if (confirm('Are you sure you want to clear all history?')) {
this.history = [];
this.saveHistory();
this.renderHistory();
this.updateStats();
}
}
/**
* Export history
*/
exportHistory() {
const dataStr = JSON.stringify(this.history, null, 2);
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
const link = document.createElement('a');
link.setAttribute('href', dataUri);
link.setAttribute('download', `ai-tools-history-${Date.now()}.json`);
link.click();
}
/**
* Show error message
*/
showError(container, message) {
if (!container) return;
container.innerHTML = `Error: ${this.escapeHtml(message)}
`;
container.classList.remove('hidden');
}
/**
* Escape HTML
*/
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
export default AIToolsPage;