You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

589 lines
20 KiB

/**
* BRAIN API Integration Module
* Handles authentication, operators, and data fields from WorldQuant BRAIN
* Now uses a local Python proxy server to bypass CORS restrictions
*/
// BRAIN session and data storage
let brainSession = null;
let brainOperators = null;
let brainDataFields = null;
let brainSessionId = localStorage.getItem('brain_session_id');
// Flask app API endpoints
const PROXY_BASE = '';
async function readJsonOrText(response) {
const contentType = (response.headers && response.headers.get('content-type')) || '';
const rawText = await response.text();
const looksJson = contentType.includes('application/json') || rawText.trim().startsWith('{') || rawText.trim().startsWith('[');
if (looksJson) {
try {
return { ok: true, data: JSON.parse(rawText), rawText };
} catch (e) {
return { ok: false, data: null, rawText };
}
}
return { ok: false, data: null, rawText };
}
// Open BRAIN login modal
function openBrainLoginModal() {
const modal = document.getElementById('brainLoginModal');
const statusDiv = document.getElementById('brainLoginStatus');
statusDiv.innerHTML = '';
statusDiv.className = 'login-status';
// Clear previous inputs
document.getElementById('brainUsername').value = '';
document.getElementById('brainPassword').value = '';
modal.style.display = 'block';
document.getElementById('brainUsername').focus();
}
// Close BRAIN login modal
function closeBrainLoginModal() {
const modal = document.getElementById('brainLoginModal');
const loginBtn = document.getElementById('loginBtn');
// Don't allow closing if login is in progress
if (loginBtn.disabled) {
return;
}
// Clean up any biometric authentication buttons
const statusDiv = document.getElementById('brainLoginStatus');
const completeBtn = statusDiv.querySelector('button');
if (completeBtn) {
completeBtn.remove();
}
modal.style.display = 'none';
}
// Authenticate with BRAIN via proxy server
async function authenticateBrain() {
const username = document.getElementById('brainUsername').value.trim();
const password = document.getElementById('brainPassword').value;
const statusDiv = document.getElementById('brainLoginStatus');
const loginBtn = document.getElementById('loginBtn');
const spinner = document.getElementById('loginSpinner');
const modal = document.getElementById('brainLoginModal');
if (!username || !password) {
showLoginStatus('Please enter both username and password.', 'error');
return;
}
// Disable all inputs and buttons
document.getElementById('brainUsername').disabled = true;
document.getElementById('brainPassword').disabled = true;
document.getElementById('cancelBtn').disabled = true;
loginBtn.disabled = true;
loginBtn.textContent = 'Connecting...';
// Show spinner
spinner.style.display = 'block';
// Disable modal closing
modal.querySelector('.close').style.display = 'none';
// Show loading state
showLoginStatus('Connecting to proxy server...', 'loading');
try {
showLoginStatus('Authenticating with BRAIN...', 'loading');
// Authenticate via proxy server
const authResponse = await fetch(`${PROXY_BASE}/api/authenticate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: username,
password: password
})
});
const parsed = await readJsonOrText(authResponse);
const authData = parsed.data || {};
// If backend returned HTML (or otherwise non-JSON), show a helpful message
if (!parsed.ok) {
const status = authResponse.status;
if (status === 404) {
throw new Error('登录接口不存在(/api/authenticate 404)。请确认后端服务正在运行且页面没有被反向代理到别处。');
}
if (status === 401) {
throw new Error('用户名或密码错误(401)。');
}
// Common symptom: Flask 500 HTML error page
throw new Error(`登录失败(HTTP ${status})。后端返回了非JSON内容,通常表示后端异常或被重定向到HTML页面。请查看后端控制台日志。`);
}
// Check if biometric authentication is required
if (authData.requires_biometric) {
showLoginStatus('Biometric authentication required. Opening BRAIN website...', 'loading');
// Open biometric URL in new tab
window.open(authData.biometric_url, '_blank');
// Store the session ID for biometric completion
brainSessionId = authData.session_id;
localStorage.setItem('brain_session_id', brainSessionId);
showLoginStatus('Please complete biometric authentication in the new tab, then click "Complete Authentication" below.', 'info');
// Show complete authentication button
const completeBtn = document.createElement('button');
completeBtn.textContent = 'Complete Authentication';
completeBtn.className = 'btn btn-secondary';
completeBtn.style.marginTop = '10px';
completeBtn.onclick = completeBiometricAuth;
const statusDiv = document.getElementById('brainLoginStatus');
statusDiv.appendChild(completeBtn);
return;
}
if (!authResponse.ok) {
// Provide clearer message on credential errors
if (authResponse.status === 401) {
throw new Error(authData.error || '用户名或密码错误(401)。');
}
throw new Error(authData.error || 'Authentication failed');
}
brainSessionId = authData.session_id;
brainSession = { authenticated: true, username: username };
// Store simulation options if available
if (authData.options && typeof simulationOptions !== 'undefined') {
simulationOptions = authData.options;
console.log("Loaded simulation options:", simulationOptions);
}
// Store session ID in localStorage for other pages
localStorage.setItem('brain_session_id', brainSessionId);
// Fetch operators immediately for "Op" button functionality
showLoginStatus('Loading operators...', 'loading');
brainOperators = await getUserOperators();
// Store operators in sessionStorage for other pages (like Inspiration House)
sessionStorage.setItem('brainOperators', JSON.stringify(brainOperators));
// Update UI to show connected state
updateConnectedState();
showLoginStatus(`Successfully connected! Loaded ${brainOperators.length} operators.`, 'success');
// Update Inspiration Button State
if (window.updateInspirationButtonState) {
window.updateInspirationButtonState();
}
// Disable buttons to prevent further clicks
loginBtn.disabled = true;
document.getElementById('brainUsername').disabled = true;
document.getElementById('brainPassword').disabled = true;
// Close modal after a short delay
setTimeout(() => {
// Re-enable everything before closing
document.getElementById('brainUsername').disabled = false;
document.getElementById('brainPassword').disabled = false;
document.getElementById('cancelBtn').disabled = false;
loginBtn.disabled = false;
loginBtn.textContent = 'Connect';
spinner.style.display = 'none';
modal.querySelector('.close').style.display = 'block';
closeBrainLoginModal();
}, 1500);
} catch (error) {
console.error('BRAIN authentication failed:', error);
showLoginStatus(`Connection failed: ${error.message}`, 'error');
brainSession = null;
brainSessionId = null;
} finally {
// Re-enable everything
document.getElementById('brainUsername').disabled = false;
document.getElementById('brainPassword').disabled = false;
document.getElementById('cancelBtn').disabled = false;
loginBtn.disabled = false;
loginBtn.textContent = 'Connect';
spinner.style.display = 'none';
modal.querySelector('.close').style.display = 'block';
}
}
// Complete biometric authentication
async function completeBiometricAuth() {
const statusDiv = document.getElementById('brainLoginStatus');
try {
showLoginStatus('Verifying biometric authentication...', 'loading');
const response = await fetch(`${PROXY_BASE}/api/complete-biometric`, {
method: 'POST',
headers: {
'Session-ID': brainSessionId
}
});
const data = await response.json();
if (data.success) {
brainSessionId = data.session_id;
brainSession = { authenticated: true };
localStorage.setItem('brain_session_id', brainSessionId);
// Fetch operators
showLoginStatus('Loading operators...', 'loading');
brainOperators = await getUserOperators();
// Store operators in sessionStorage for other pages (like Inspiration House)
sessionStorage.setItem('brainOperators', JSON.stringify(brainOperators));
// Update UI
updateConnectedState();
showLoginStatus(`Successfully connected! Loaded ${brainOperators.length} operators.`, 'success');
// Close modal after delay
setTimeout(() => {
closeBrainLoginModal();
}, 1500);
} else {
showLoginStatus(`Biometric verification failed: ${data.error}`, 'error');
}
} catch (error) {
console.error('Biometric completion failed:', error);
showLoginStatus(`Biometric verification failed: ${error.message}`, 'error');
}
}
// Get user operators via proxy server
async function getUserOperators() {
if (!brainSession || !brainSessionId) {
throw new Error('Not authenticated with BRAIN');
}
try {
const response = await fetch(`${PROXY_BASE}/api/operators`, {
method: 'GET',
headers: {
'Session-ID': brainSessionId
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to fetch operators');
}
const operators = await response.json();
console.log(`Received ${operators.length} operators from BRAIN API`);
// Log the categories to verify we have all operator types
const categories = [...new Set(operators.map(op => op.category))].sort();
console.log(`Operator categories: ${categories.join(', ')}`);
return operators;
} catch (error) {
console.error('Failed to fetch operators:', error);
throw error;
}
}
// Get data fields via proxy server
async function getDatasets(region = 'USA', delay = 1, universe = 'TOP3000') {
if (!brainSession || !brainSessionId) {
throw new Error('Not authenticated with BRAIN');
}
try {
const params = new URLSearchParams({
region: region,
delay: delay.toString(),
universe: universe
});
const response = await fetch(`${PROXY_BASE}/api/datasets?${params}`, {
method: 'GET',
headers: {
'Session-ID': brainSessionId
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to fetch datasets');
}
const data = await response.json();
return data.results || [];
} catch (error) {
console.error('Failed to fetch datasets:', error);
throw error;
}
}
async function getSimulationOptions() {
if (!brainSession || !brainSessionId) {
throw new Error('Not authenticated with BRAIN');
}
try {
const response = await fetch(`${PROXY_BASE}/api/simulation-options`, {
method: 'GET',
headers: {
'Session-ID': brainSessionId
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to fetch simulation options');
}
return await response.json();
} catch (error) {
console.error('Failed to fetch simulation options:', error);
throw error;
}
}
async function getDataFields(region = 'USA', delay = 1, universe = 'TOP3000', datasetId = 'fundamental6') {
if (!brainSession || !brainSessionId) {
throw new Error('Not authenticated with BRAIN');
}
try {
const params = new URLSearchParams({
region: region,
delay: delay.toString(),
universe: universe,
dataset_id: datasetId
});
const response = await fetch(`${PROXY_BASE}/api/datafields?${params}`, {
method: 'GET',
headers: {
'Session-ID': brainSessionId
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to fetch data fields');
}
return await response.json();
} catch (error) {
console.error('Failed to fetch data fields:', error);
throw error;
}
}
// Update UI to show connected state
function updateConnectedState() {
// Update connect button if it exists (main page)
const connectBtn = document.getElementById('connectToBrain');
if (connectBtn) {
connectBtn.textContent = 'Connected to BRAIN';
connectBtn.className = 'btn btn-brain connected';
}
// Show connection info in the grammar errors area if it exists (main page)
const errorsDiv = document.getElementById('grammarErrors');
if (errorsDiv) {
errorsDiv.innerHTML = `<div class="success-message">
✓ Successfully connected to WorldQuant BRAIN<br>
<strong>Username:</strong> ${brainSession.username}<br>
<strong>Operators loaded:</strong> ${brainOperators ? brainOperators.length : 0}<br>
<em>Data fields will be loaded when needed.</em>
</div>`;
// Auto-hide the message after 5 seconds
setTimeout(() => {
if (errorsDiv.innerHTML.includes('Successfully connected')) {
errorsDiv.innerHTML = '';
}
}, 5000);
}
}
// Show login status message
function showLoginStatus(message, type) {
const statusDiv = document.getElementById('brainLoginStatus');
statusDiv.textContent = message;
statusDiv.className = `login-status ${type}`;
}
// Check if connected to BRAIN
function isConnectedToBrain() {
return brainSession !== null && brainSessionId !== null;
}
// Get all available operators (fetch on-demand)
async function getAllOperators() {
if (!brainOperators && isConnectedToBrain()) {
try {
brainOperators = await getUserOperators();
// Store operators in sessionStorage for other pages (like Inspiration House)
sessionStorage.setItem('brainOperators', JSON.stringify(brainOperators));
} catch (error) {
console.error('Failed to fetch operators on-demand:', error);
return [];
}
}
return brainOperators || [];
}
// Get loaded operators synchronously (for UI components)
function getLoadedOperators() {
return brainOperators || [];
}
// Get all available data fields (fetch on-demand)
async function getAllDataFields() {
if (!brainDataFields && isConnectedToBrain()) {
try {
brainDataFields = await getDataFields();
} catch (error) {
console.error('Failed to fetch data fields on-demand:', error);
return [];
}
}
return brainDataFields || [];
}
// Get operators by category (with on-demand loading)
async function getOperatorsByCategory(category) {
const operators = await getAllOperators();
return operators.filter(op => op.category === category);
}
// Search operators (with on-demand loading)
async function searchOperators(searchTerm) {
const operators = await getAllOperators();
const term = searchTerm.toLowerCase();
return operators.filter(op =>
op.name.toLowerCase().includes(term) ||
op.category.toLowerCase().includes(term)
);
}
// Search data fields (with on-demand loading)
async function searchDataFields(searchTerm) {
const dataFields = await getAllDataFields();
const term = searchTerm.toLowerCase();
return dataFields.filter(field =>
field.id.toLowerCase().includes(term) ||
field.description.toLowerCase().includes(term)
);
}
// Logout from BRAIN
async function logoutFromBrain() {
if (brainSessionId) {
try {
await fetch(`${PROXY_BASE}/api/logout`, {
method: 'POST',
headers: {
'Session-ID': brainSessionId
}
});
} catch (error) {
console.warn('Failed to logout from proxy server:', error);
}
}
// Clear local session data
brainSession = null;
brainSessionId = null;
brainOperators = null;
brainDataFields = null;
// Clear localStorage and sessionStorage
localStorage.removeItem('brain_session_id');
sessionStorage.removeItem('brainOperators');
// Update UI
const connectBtn = document.getElementById('connectToBrain');
if (connectBtn) {
connectBtn.textContent = 'Connect to BRAIN';
connectBtn.className = 'btn btn-brain';
}
}
// Check session validity on page load; optionally prompt to login immediately
async function checkSessionValidity({ promptOnMissing = false } = {}) {
if (brainSessionId) {
try {
const response = await fetch(`${PROXY_BASE}/api/status`, {
method: 'GET',
headers: {
'Session-ID': brainSessionId
}
});
if (response.ok) {
const data = await response.json();
if (data.valid) {
brainSession = { authenticated: true, username: data.username };
// Update UI to show connected state
updateConnectedState();
} else {
// Session expired, clear it
localStorage.removeItem('brain_session_id');
brainSessionId = null;
}
}
} catch (error) {
console.warn('Failed to check session validity:', error);
}
}
// If requested, prompt the login modal when no valid session is present
if (promptOnMissing && !isConnectedToBrain()) {
openBrainLoginModal();
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', () => checkSessionValidity({ promptOnMissing: true }));
// Export functions for use in other modules
window.brainAPI = {
openBrainLoginModal,
closeBrainLoginModal,
authenticateBrain,
isConnectedToBrain,
getAllOperators,
getAllDataFields,
getDatasets,
getSimulationOptions,
getDataFields,
getOperatorsByCategory,
searchOperators,
searchDataFields,
logoutFromBrain,
getLoadedOperators
};
// Also make key functions globally available for HTML onclick handlers
window.openBrainLoginModal = openBrainLoginModal;
window.closeBrainLoginModal = closeBrainLoginModal;
window.authenticateBrain = authenticateBrain;