// API Configuration - Use relative URL for HuggingFace
const API_BASE_URL = window.location.origin;
// Session Management
let currentSessionId = null;
let conversationHistory = []; // Store all Q&A pairs
let queryHistory = [];
// DOM Elements
const queryInput = document.getElementById('queryInput');
const submitBtn = document.getElementById('submitBtn');
const btnText = submitBtn.querySelector('.btn-text');
const loader = submitBtn.querySelector('.loader');
const responseSection = document.getElementById('responseSection');
const initialState = document.getElementById('initialState');
const loadingState = document.getElementById('loadingState');
const answerDiv = document.getElementById('answer');
const sourcesDiv = document.getElementById('sources');
const errorToast = document.getElementById('errorToast');
const errorMessage = document.getElementById('errorMessage');
const expandedQueriesDiv = document.getElementById('expandedQueries');
const queriesList = document.getElementById('queriesList');
const exportBtn = document.getElementById('exportBtn');
const githubBtn = document.getElementById('githubBtn');
const historySelect = document.getElementById('historySelect');
// Initialize
function init() {
currentSessionId = sessionStorage.getItem('sessionId') || generateSessionId();
sessionStorage.setItem('sessionId', currentSessionId);
// Load history
const saved = sessionStorage.getItem('queryHistory');
if (saved) {
try {
queryHistory = JSON.parse(saved);
updateHistoryDropdown();
} catch (e) {
queryHistory = [];
}
}
setupEventListeners();
checkHealth();
}
function generateSessionId() {
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
function setupEventListeners() {
// Submit button
if (submitBtn) {
submitBtn.addEventListener('click', handleSubmit);
}
// Enter key (Ctrl+Enter)
if (queryInput) {
queryInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && e.ctrlKey) {
handleSubmit();
}
});
}
// Example queries
document.querySelectorAll('.example-item').forEach(item => {
item.addEventListener('click', () => {
const query = item.getAttribute('data-query');
if (query && queryInput) {
queryInput.value = query;
handleSubmit();
}
});
});
// Export button
if (exportBtn) {
exportBtn.addEventListener('click', exportToPDF);
}
// GitHub button
if (githubBtn) {
githubBtn.addEventListener('click', () => {
window.open('https://github.com/Adeyemi0/FinSight-RAG-Application-', '_blank');
});
}
// History dropdown
if (historySelect) {
historySelect.addEventListener('change', (e) => {
const query = e.target.value;
if (query && queryInput) {
queryInput.value = query;
}
});
}
}
// Update history dropdown
function updateHistoryDropdown() {
if (!historySelect) return;
historySelect.innerHTML = '';
queryHistory.slice().reverse().forEach((item, index) => {
const option = document.createElement('option');
option.value = item.query;
option.textContent = item.query.substring(0, 60) + (item.query.length > 60 ? '...' : '');
historySelect.appendChild(option);
});
}
// Save to history
function saveToHistory(query, answer) {
queryHistory.push({
query,
answer: answer.substring(0, 500),
timestamp: new Date().toISOString()
});
// Keep last 20
if (queryHistory.length > 20) {
queryHistory = queryHistory.slice(-20);
}
try {
sessionStorage.setItem('queryHistory', JSON.stringify(queryHistory));
updateHistoryDropdown();
} catch (e) {
console.error('Failed to save history:', e);
}
}
// Main submit handler
async function handleSubmit() {
if (!queryInput) return;
const query = queryInput.value.trim();
if (!query) {
showError('Please enter a question');
return;
}
// Show loading
setLoading(true);
hideError();
// Show loading indicator
if (loadingState) loadingState.style.display = 'flex';
// Hide initial state on first query
if (initialState && conversationHistory.length === 0) {
initialState.style.display = 'none';
}
// Show response section
if (responseSection) responseSection.style.display = 'block';
try {
// Extract ticker from query if mentioned
const tickerMatch = query.match(/\b([A-Z]{1,5})\b/);
const detectedTicker = tickerMatch ? tickerMatch[1] : null;
const requestData = {
query,
ticker: detectedTicker,
doc_types: null,
top_k: 10,
session_id: currentSessionId
};
console.log('Request data:', requestData);
const response = await fetch(`${API_BASE_URL}/query`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestData)
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `HTTP ${response.status}`);
}
const data = await response.json();
if (!data) {
throw new Error('Empty response from server');
}
// Add to conversation history
conversationHistory.push({
query,
answer: data.answer,
sources: data.sources,
expanded_queries: data.expanded_queries,
timestamp: new Date()
});
// Save to history
saveToHistory(query, data.answer || '');
// Display all conversation history
displayConversationHistory();
// Clear input
queryInput.value = '';
// Scroll to latest answer
setTimeout(() => {
const rightPanel = document.querySelector('.right-panel');
if (rightPanel) {
rightPanel.scrollTop = rightPanel.scrollHeight;
}
}, 100);
} catch (error) {
console.error('Error:', error);
showError(error.message || 'Failed to process query');
// Show initial state again if no history
if (conversationHistory.length === 0) {
if (loadingState) loadingState.style.display = 'none';
if (initialState) initialState.style.display = 'flex';
}
} finally {
setLoading(false);
if (loadingState) loadingState.style.display = 'none';
}
}
// Display entire conversation history
function displayConversationHistory() {
if (!answerDiv || !sourcesDiv) return;
// Clear existing content
answerDiv.innerHTML = '';
sourcesDiv.innerHTML = '';
// Render each Q&A pair
conversationHistory.forEach((item, index) => {
// Create conversation item container
const conversationItem = document.createElement('div');
conversationItem.className = 'conversation-item';
conversationItem.style.cssText = `
margin-bottom: 2rem;
padding-bottom: 2rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
`;
// Add question
const questionDiv = document.createElement('div');
questionDiv.className = 'question-block';
questionDiv.style.cssText = `
background: rgba(59, 130, 246, 0.1);
border-left: 3px solid #3b82f6;
padding: 1rem 1.5rem;
border-radius: 0.5rem;
margin-bottom: 1.5rem;
`;
questionDiv.innerHTML = `
Question ${index + 1}
${escapeHtml(item.query)}
`;
conversationItem.appendChild(questionDiv);
// Add answer
const answerBlock = document.createElement('div');
answerBlock.className = 'answer-block';
answerBlock.style.cssText = `
background: rgba(16, 185, 129, 0.05);
border-left: 3px solid #10b981;
padding: 1rem 1.5rem;
border-radius: 0.5rem;
margin-bottom: 1.5rem;
`;
answerBlock.innerHTML = `
Answer
${formatAnswer(item.answer || 'No answer available')}
`;
conversationItem.appendChild(answerBlock);
// Add expanded queries if available
if (item.expanded_queries && item.expanded_queries.length > 1) {
const expandedDiv = document.createElement('div');
expandedDiv.className = 'expanded-queries-block';
expandedDiv.style.cssText = `
background: rgba(139, 92, 246, 0.05);
border-left: 3px solid #8b5cf6;
padding: 1rem 1.5rem;
border-radius: 0.5rem;
margin-bottom: 1.5rem;
`;
expandedDiv.innerHTML = `
Expanded Queries
${item.expanded_queries.map(q => `- ${escapeHtml(q)}
`).join('')}
`;
conversationItem.appendChild(expandedDiv);
}
// Add sources
if (item.sources && item.sources.length > 0) {
const sourcesHeader = document.createElement('div');
sourcesHeader.style.cssText = `
color: #f59e0b;
font-size: 0.875rem;
margin-bottom: 1rem;
font-weight: 600;
`;
sourcesHeader.textContent = `Sources (${item.sources.length})`;
conversationItem.appendChild(sourcesHeader);
const sourcesContainer = document.createElement('div');
sourcesContainer.innerHTML = item.sources.map((source, sourceIndex) => {
if (!source) return '';
const docTypeLabel = source.doc_type === '10k' ? '10-K Report' :
source.doc_type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase());
return `
${source.ticker ? `Ticker: ${escapeHtml(source.ticker)}` : ''}
${source.chunk_id ? `Chunk: ${escapeHtml(source.chunk_id)}` : ''}
"${escapeHtml(source.text_preview || 'No preview available')}"
`;
}).filter(Boolean).join('');
conversationItem.appendChild(sourcesContainer);
}
// Add timestamp
const timestamp = document.createElement('div');
timestamp.style.cssText = `
color: #6b7280;
font-size: 0.75rem;
text-align: right;
margin-top: 1rem;
`;
timestamp.textContent = item.timestamp.toLocaleTimeString();
conversationItem.appendChild(timestamp);
answerDiv.appendChild(conversationItem);
});
// Show export button if there's content
if (exportBtn && conversationHistory.length > 0) {
exportBtn.style.display = 'flex';
}
}
// Toggle source expansion
function toggleSource(conversationIndex, sourceIndex) {
const card = document.getElementById(`source-${conversationIndex}-${sourceIndex}`);
if (!card) return;
const content = card.querySelector('.source-content');
if (!content) return;
if (content.style.maxHeight && content.style.maxHeight !== '0px') {
content.style.maxHeight = '0px';
content.style.padding = '0 1rem';
} else {
content.style.maxHeight = content.scrollHeight + 'px';
content.style.padding = '0 1rem';
}
}
// Format answer with enhanced styling - IMPROVED VERSION
function formatAnswer(answer) {
if (!answer) return '';
let formatted = escapeHtml(answer);
// ============================================
// 1. CONVERT LATEX MATH TO READABLE FORMAT
// ============================================
// Block math: \[ ... \] → Display as formatted equation
formatted = formatted.replace(/\\\[(.*?)\\\]/gs, (match, equation) => {
// Clean up the equation
let cleanEq = equation.trim()
.replace(/\\text\{([^}]+)\}/g, '$1') // Remove \text{}
.replace(/\\frac\{([^}]+)\}\{([^}]+)\}/g, '($1) ÷ ($2)') // Convert fractions
.replace(/\\_/g, '_') // Fix escaped underscores
.replace(/\\\\/g, '') // Remove double backslashes
.replace(/\\times/g, '×') // Convert \times to ×
.replace(/\\div/g, '÷') // Convert \div to ÷
.replace(/\\approx/g, '≈') // Convert \approx to ≈
.replace(/\\cdot/g, '·'); // Convert \cdot to ·
return `${cleanEq}
`;
});
// Inline math: \( ... \) → Convert to readable format
formatted = formatted.replace(/\\\((.*?)\\\)/g, (match, equation) => {
let cleanEq = equation.trim()
.replace(/\\text\{([^}]+)\}/g, '$1')
.replace(/\\frac\{([^}]+)\}\{([^}]+)\}/g, '($1) ÷ ($2)')
.replace(/\\times/g, '×')
.replace(/\\div/g, '÷')
.replace(/\\approx/g, '≈');
return `${cleanEq}`;
});
// ============================================
// 2. CLEAN UP ASTERISKS AND FORMATTING
// ============================================
// Remove asterisks used for emphasis (markdown bold/italic)
formatted = formatted.replace(/\*\*\*([^*]+)\*\*\*/g, '$1'); // ***bold italic***
formatted = formatted.replace(/\*\*([^*]+)\*\*/g, '$1'); // **bold**
formatted = formatted.replace(/\*([^*\n]+)\*/g, '$1'); // *italic*
// Replace any remaining asterisks with multiplication symbol
formatted = formatted.replace(/\s\*\s/g, ' × ');
// ============================================
// 3. FORMAT SECTIONS AND HEADERS
// ============================================
// Line breaks
formatted = formatted.replace(/\n\n/g, '
');
formatted = formatted.replace(/\n/g, '
');
// Headers with ###
formatted = formatted.replace(/###\s*([^<]+?)(
|$)/g, '');
// Section headers that end with colon (e.g., "Analysis:", "Sources:")
formatted = formatted.replace(/([^<]+?):<\/strong>/g, '');
// ============================================
// 4. HIGHLIGHT FINANCIAL DATA
// ============================================
// Citations [Source N]
formatted = formatted.replace(/\[Source (\d+)\]/g,
'[Source $1]');
// Currency values (e.g., $2.54B, $1.26M, $1,234.56)
formatted = formatted.replace(/\$[\d,]+\.?\d*[BMK]?/g,
match => `${match}`);
// Percentages
formatted = formatted.replace(/(\d+\.?\d*)%/g,
'$1%');
// Standalone ratios (e.g., 2.29, 1.5x, 3:1)
formatted = formatted.replace(/\b(\d+\.?\d+)(x|:1)(\s|<|$)/g,
'$1$2$3');
// ============================================
// 5. FORMAT LISTS
// ============================================
// Numbered lists with bold headers
formatted = formatted.replace(/(\d+)\.\s+([^<]+)<\/strong>/g,
'$1. $2
');
// Bullet points
formatted = formatted.replace(/(
|^)[\s]*[-•]\s+/g, '$1• ');
// ============================================
// 6. CLEAN UP DOUBLE SPACES
// ============================================
formatted = formatted.replace(/\s{2,}/g, ' ');
return formatted;
}
// Export to PDF function
function exportToPDF() {
if (conversationHistory.length === 0) {
showError('No conversation to export. Please ask a question first.');
return;
}
try {
if (typeof window.jspdf === 'undefined') {
showError('PDF library not loaded. Please refresh the page.');
return;
}
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
// Set title
doc.setFontSize(18);
doc.setFont(undefined, 'bold');
doc.text('FinSage Analytics Report', 20, 20);
doc.setFontSize(10);
doc.setFont(undefined, 'normal');
doc.setTextColor(100);
doc.text(`Generated on ${new Date().toLocaleString()}`, 20, 28);
doc.text(`Total Questions: ${conversationHistory.length}`, 20, 34);
let yPos = 45;
conversationHistory.forEach((item, index) => {
// Check for page break
if (yPos > 250) {
doc.addPage();
yPos = 20;
}
// Question
doc.setFontSize(12);
doc.setFont(undefined, 'bold');
doc.setTextColor(0);
doc.text(`Question ${index + 1}:`, 20, yPos);
yPos += 8;
doc.setFont(undefined, 'normal');
doc.setFontSize(10);
const queryLines = doc.splitTextToSize(item.query, 170);
doc.text(queryLines, 20, yPos);
yPos += queryLines.length * 7 + 5;
// Answer
if (yPos > 250) {
doc.addPage();
yPos = 20;
}
doc.setFontSize(11);
doc.setFont(undefined, 'bold');
doc.text('Answer:', 20, yPos);
yPos += 8;
doc.setFont(undefined, 'normal');
doc.setFontSize(10);
let cleanAnswer = item.answer
.replace(/<[^>]*>/g, '')
.replace(/\[Source \d+\]/g, '')
.replace(/ /g, ' ')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/&/g, '&')
.replace(/\\/g, '');
const answerLines = doc.splitTextToSize(cleanAnswer, 170);
answerLines.forEach(line => {
if (yPos > 270) {
doc.addPage();
yPos = 20;
}
doc.text(line, 20, yPos);
yPos += 7;
});
yPos += 10;
});
const filename = `FinSage_Conversation_${Date.now()}.pdf`;
doc.save(filename);
} catch (error) {
console.error('PDF Export Error:', error);
showError('Failed to export PDF. Please try again.');
}
}
// Loading state
function setLoading(isLoading) {
if (!submitBtn) return;
submitBtn.disabled = isLoading;
if (btnText && loader) {
btnText.style.display = isLoading ? 'none' : 'inline';
loader.style.display = isLoading ? 'inline-block' : 'none';
}
}
// Error handling
function showError(message) {
if (!errorToast || !errorMessage) {
alert(message);
return;
}
errorMessage.textContent = message;
errorToast.style.display = 'block';
setTimeout(() => {
errorToast.style.display = 'none';
}, 5000);
}
function hideError() {
if (errorToast) {
errorToast.style.display = 'none';
}
}
// Utility: Escape HTML
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Health check
async function checkHealth() {
try {
const response = await fetch(`${API_BASE_URL}/health`, {
signal: AbortSignal.timeout(5000)
});
if (!response.ok) {
console.warn('API health check failed');
}
} catch (error) {
console.error('Cannot connect to API:', error);
showError('Backend API is not responding. Please start the server.');
}
}
// Make toggleSource available globally
window.toggleSource = toggleSource;
// Initialize on load
init();