/**
* Main application initialization
*/
import { setupDebug } from './utils/debug.js';
import { initSettingsForm } from './components/settings.js';
import { checkAPIHealth } from './services/apiService.js';
import { SPARQLBrowser } from './components/sparqlBrowser.js';
import { initMCPClient } from './components/mcpClient.js';
import { initChatForms, loadChatProviders } from './components/chat.js';
import { initMemoryVisualization } from './components/memoryVisualization.js';
import { init as initVSOM } from './features/vsom/index.js';
import Console from './components/Console/Console.js';
import Help from './components/Help/Help.js';
import { logger, replaceConsole } from './utils/logger.js';
import tabManager from './utils/tabManager.js';
// Import Atuin and event bus for RDF visualization
let TurtleEditor, GraphVisualizer, LoggerService, eventBus, EVENTS;
/**
* Initialize Atuin components and real event bus
*/
async function initializeAtuin() {
try {
// Use the real event bus from evb package
console.log('Setting up real event bus for RDF visualization');
const { eventBus, EVENTS } = await import('evb');
// Make the real event bus globally available
window.eventBus = eventBus;
window.EVENTS = EVENTS;
console.log('Real event bus initialized for RDF visualization');
return true;
} catch (error) {
console.warn('Failed to initialize real event bus, falling back to mock:', error.message);
// Fallback to simple mock only if real event bus fails
window.eventBus = {
listeners: {},
emit(event, data) {
console.log(`EventBus: Emitting ${event}`, data);
if (this.listeners[event]) {
this.listeners[event].forEach(callback => callback(data));
}
},
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
console.log(`EventBus: Added listener for ${event}`);
}
};
window.EVENTS = {
MODEL_SYNCED: 'rdf:model:synced',
GRAPH_UPDATED: 'dataset:graph:updated'
};
return false;
}
}
/**
* Initialize the entire application
*/
export async function initializeApp() {
try {
// Setup debug functionality
setupDebug();
// Initialize Atuin components first
await initializeAtuin();
// Initialize TabManager first
tabManager.init();
// Initialize other components
initSettingsForm();
initRangeInputs();
initSearchForm();
initMemoryForms();
await loadChatProviders();
initChatForms();
initMemoryVisualization();
initVSOM();
await initMCPClient();
await initSPARQLBrowser();
// Initialize console and help after a short delay
setTimeout(() => {
initializeConsole();
initializeHelp();
}, 500);
// Hide loading indicator
const loadingIndicator = document.getElementById('loading-indicator');
if (loadingIndicator) {
loadingIndicator.style.display = 'none';
}
console.log('Application initialized successfully');
} catch (error) {
console.error('Error initializing application:', error);
const loadingIndicator = document.getElementById('loading-indicator');
if (loadingIndicator) {
loadingIndicator.innerHTML = `
<div class="error">
<h3>Error initializing application</h3>
<p>${error.message}</p>
</div>`;
}
}
}
/**
* Initialize range inputs with value display
*/
function initRangeInputs() {
// Search threshold
const searchThreshold = document.getElementById('search-threshold');
const thresholdValue = document.getElementById('threshold-value');
if (searchThreshold && thresholdValue) {
searchThreshold.addEventListener('input', (e) => {
thresholdValue.textContent = e.target.value;
});
}
// Memory threshold
const memoryThreshold = document.getElementById('memory-threshold');
const memoryThresholdValue = document.getElementById('memory-threshold-value');
if (memoryThreshold && memoryThresholdValue) {
memoryThreshold.addEventListener('input', (e) => {
memoryThresholdValue.textContent = e.target.value;
});
}
// Chat temperature
const chatTemperature = document.getElementById('chat-temperature');
const temperatureValue = document.getElementById('temperature-value');
if (chatTemperature && temperatureValue) {
chatTemperature.addEventListener('input', (e) => {
temperatureValue.textContent = e.target.value;
});
}
// Chat stream temperature
const chatStreamTemperature = document.getElementById('chat-stream-temperature');
const streamTemperatureValue = document.getElementById('stream-temperature-value');
if (chatStreamTemperature && streamTemperatureValue) {
chatStreamTemperature.addEventListener('input', (e) => {
streamTemperatureValue.textContent = e.target.value;
});
}
}
/**
* Setup failsafe timeout for loading indicator
*/
function setupFailsafeTimeout() {
let loadingTimeout = null;
const resetLoadingTimeout = () => {
clearTimeout(loadingTimeout);
loadingTimeout = setTimeout(() => {
const loadingIndicator = document.getElementById('loading-indicator');
if (loadingIndicator) {
loadingIndicator.style.display = 'none';
}
}, 10000);
};
// Expose reset function globally
window.resetLoadingTimeout = resetLoadingTimeout;
}
// Helper functions for search functionality
function escapeHtml(unsafe) {
if (!unsafe) return '';
return unsafe
.toString()
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
async function fetchWithTimeout(resource, options = {}) {
const { timeout = 30000 } = options;
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(resource, {
...options,
signal: controller.signal
});
clearTimeout(id);
return response;
} catch (error) {
clearTimeout(id);
throw error;
}
}
function showLoading(show) {
const loadingIndicator = document.getElementById('loading-indicator');
if (loadingIndicator) {
loadingIndicator.style.display = show ? 'flex' : 'none';
}
}
function displayError(message, container) {
console.error('Displaying error:', message);
// Use provided container or default to body
const targetContainer = container || document.body;
// Create error element if it doesn't exist
let errorElement = targetContainer.querySelector('.error-message');
if (!errorElement) {
errorElement = document.createElement('div');
errorElement.className = 'error-message';
errorElement.style.color = 'red';
errorElement.style.margin = '10px 0';
errorElement.style.padding = '10px';
errorElement.style.border = '1px solid #ff9999';
errorElement.style.borderRadius = '4px';
errorElement.style.backgroundColor = '#fff0f0';
targetContainer.prepend(errorElement);
}
// Set error message
errorElement.textContent = message;
// Auto-hide after 10 seconds
setTimeout(() => {
errorElement.remove();
}, 10000);
}
function displaySearchResults(results, container) {
console.log(`Displaying ${results.length} search results`);
if (!results || results.length === 0) {
container.innerHTML = `
<div class="results-placeholder">
<p>No results found. Try adjusting your search criteria.</p>
</div>`;
return;
}
// Create results HTML
const resultsHtml = results.map((result, index) => {
const score = typeof result.score === 'number' ? result.score.toFixed(2) : 'N/A';
const title = result.title || 'Untitled';
const content = result.content || result.text || '';
const url = result.url || '#';
return `
<div class="result-item">
<h3 class="result-title">
<a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer">
${escapeHtml(title)}
</a>
</h3>
<div class="result-content">
${escapeHtml(content.length > 200 ? content.substring(0, 200) + '...' : content)}
</div>
<div class="result-meta">
<div class="result-meta-item">
<span class="result-source">${escapeHtml(result.type || 'document')}</span>
<span class="result-score">Score: ${score}</span>
</div>
${result.uri ? `
<div class="result-uri" title="${escapeHtml(result.uri)}">
<span>URI: </span>
<a href="${escapeHtml(result.uri)}" target="_blank" rel="noopener noreferrer" class="uri-link">
${escapeHtml(result.uri.length > 50 ? result.uri.substring(0, 50) + '...' : result.uri)}
</a>
</div>` : ''}
</div>
</div>`;
}).join('');
// Update the container with results
container.innerHTML = `
<div class="result-stats">
<div class="stat-item">
<span class="stat-label">Results</span>
<span class="stat-value">${results.length}</span>
</div>
<div class="stat-item">
<span class="stat-label">Top Score</span>
<span class="stat-value">${results[0]?.score?.toFixed(2) || 'N/A'}</span>
</div>
</div>
<div class="results-list">
${resultsHtml}
</div>
<div class="debug-info" style="margin-top: 20px; font-size: 0.9em; color: #666;">
<details>
<summary>Debug Info</summary>
<pre>${escapeHtml(JSON.stringify(results, null, 2))}</pre>
</details>
</div>`;
}
// Search form initialization
function initSearchForm() {
console.log('Initializing search form');
const searchForm = document.getElementById('search-form');
const searchResults = document.getElementById('search-results');
if (!searchForm) {
console.warn('Search form not found, skipping initialization');
return;
}
searchForm.addEventListener('submit', async (e) => {
e.preventDefault(); // This prevents the page refresh!
const formData = new FormData(searchForm);
const query = formData.get('query');
const limit = formData.get('limit');
const threshold = formData.get('threshold');
const types = formData.get('types');
const graph = formData.get('graph');
if (!query) return;
try {
showLoading(true);
console.log('Search form submitted, showing loading indicator');
// Clear previous results first
if (searchResults) {
searchResults.innerHTML = `
<div class="results-placeholder">
<p>Searching for "${escapeHtml(query)}"...</p>
</div>
`;
}
// Build query params
const params = new URLSearchParams({
q: query, // Server expects 'q' parameter, not 'query'
limit: limit
});
if (threshold) params.append('threshold', threshold);
if (types) params.append('types', types);
if (graph) params.append('graph', graph);
console.log(`Searching with params: ${params.toString()}`);
// Perform search with longer timeout for search operations
const searchUrl = `/api/search?${params.toString()}`;
console.log(`Making search request to: ${searchUrl}`);
try {
const response = await fetchWithTimeout(searchUrl, {
timeout: 30000, // 30 second timeout for search
headers: {
'X-API-Key': 'semem-dev-key'
}
});
console.log(`Search response received: ${response.status}`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Search failed');
}
const data = await response.json();
console.log(`Search returned ${data.results?.length || 0} results`);
if (searchResults) {
displaySearchResults(data.results || [], searchResults);
}
} catch (fetchError) {
console.log(`Search fetch error: ${fetchError.message}`);
// Special handling for timeouts
if (fetchError.name === 'AbortError') {
if (searchResults) {
displayError('Search request timed out. This may happen if the server is busy or the search operation is complex. Please try again with a simpler query.', searchResults);
}
} else {
if (searchResults) {
displayError(fetchError.message || 'Search operation failed', searchResults);
}
}
}
} catch (error) {
console.log(`General search error: ${error.message}`);
if (searchResults) {
displayError(`Search error: ${error.message}`, searchResults);
}
} finally {
showLoading(false);
console.log('Search completed, hiding loading indicator');
}
});
// Initialize graph selector
initGraphSelector();
}
// Graph selector functionality
function initGraphSelector() {
const graphSelector = document.getElementById('graph-selector');
const addGraphBtn = document.getElementById('add-graph-btn');
const removeGraphBtn = document.getElementById('remove-graph-btn');
if (!graphSelector || !addGraphBtn || !removeGraphBtn) {
console.warn('Graph selector elements not found, skipping initialization');
return;
}
// Load default graph from config and saved graphs from localStorage
loadGraphList();
// Add event listeners
addGraphBtn.addEventListener('click', addNewGraph);
removeGraphBtn.addEventListener('click', removeSelectedGraph);
graphSelector.addEventListener('change', saveSelectedGraph);
}
function loadGraphList() {
const graphSelector = document.getElementById('graph-selector');
if (!graphSelector) return;
// Get saved graphs from localStorage
const savedGraphs = JSON.parse(localStorage.getItem('semem-graph-list') || '[]');
// Default graph (from config fallback)
const defaultGraph = 'http://hyperdata.it/content';
// Create Set to avoid duplicates
const allGraphs = new Set([defaultGraph, ...savedGraphs]);
// Clear existing options
graphSelector.innerHTML = '';
// Add all graphs as options
allGraphs.forEach(graph => {
const option = document.createElement('option');
option.value = graph;
option.textContent = graph;
graphSelector.appendChild(option);
});
// Set selected graph from localStorage or default
const selectedGraph = localStorage.getItem('semem-selected-graph') || defaultGraph;
if (graphSelector.querySelector(`option[value="${selectedGraph}"]`)) {
graphSelector.value = selectedGraph;
}
}
function saveGraphList() {
const graphSelector = document.getElementById('graph-selector');
if (!graphSelector) return;
const graphs = Array.from(graphSelector.options).map(option => option.value);
const defaultGraph = 'http://hyperdata.it/content';
// Save all graphs except the default one
const savedGraphs = graphs.filter(graph => graph !== defaultGraph);
localStorage.setItem('semem-graph-list', JSON.stringify(savedGraphs));
}
function saveSelectedGraph() {
const graphSelector = document.getElementById('graph-selector');
if (!graphSelector) return;
localStorage.setItem('semem-selected-graph', graphSelector.value);
}
function addNewGraph() {
const newGraph = prompt('Enter the graph name (URI):');
if (!newGraph || !newGraph.trim()) return;
const graphSelector = document.getElementById('graph-selector');
if (!graphSelector) return;
// Check if graph already exists
if (graphSelector.querySelector(`option[value="${newGraph.trim()}"]`)) {
alert('Graph already exists in the list');
return;
}
// Add new option
const option = document.createElement('option');
option.value = newGraph.trim();
option.textContent = newGraph.trim();
graphSelector.appendChild(option);
// Select the new graph
graphSelector.value = newGraph.trim();
// Save to localStorage
saveGraphList();
saveSelectedGraph();
}
function removeSelectedGraph() {
const graphSelector = document.getElementById('graph-selector');
if (!graphSelector) return;
const selectedValue = graphSelector.value;
const defaultGraph = 'http://hyperdata.it/content';
// Don't allow removing the default graph
if (selectedValue === defaultGraph) {
alert('Cannot remove the default graph');
return;
}
if (graphSelector.options.length <= 1) {
alert('Cannot remove the last graph');
return;
}
if (confirm(`Remove graph "${selectedValue}" from the list?`)) {
// Remove the selected option
const selectedOption = graphSelector.querySelector(`option[value="${selectedValue}"]`);
if (selectedOption) {
selectedOption.remove();
}
// Select the first remaining option
if (graphSelector.options.length > 0) {
graphSelector.selectedIndex = 0;
}
// Save to localStorage
saveGraphList();
saveSelectedGraph();
}
}
function initMemoryForms() {
console.log('TODO: Implement memory forms initialization');
}
// Chat functions now imported from ./components/chat.js
function initEmbeddingForm() {
console.log('TODO: Implement embedding form initialization');
}
function initConceptsForm() {
console.log('TODO: Implement concepts form initialization');
}
function initIndexForm() {
console.log('TODO: Implement index form initialization');
}
async function initSPARQLBrowser() {
// Initialize SPARQL browser when the tab becomes visible
const sparqlTab = document.querySelector('[data-tab="sparql-browser"]');
if (sparqlTab) {
const initializeSparqlBrowser = async () => {
// Check if we're on the SPARQL browser tab
const sparqlSection = document.getElementById('sparql-browser-tab');
if (sparqlSection && !sparqlSection.classList.contains('hidden')) {
if (!window.sparqlBrowser) {
console.log('Initializing SPARQL Browser...');
window.sparqlBrowser = new SPARQLBrowser();
await window.sparqlBrowser.init();
// Set up basic graph visualization listener
if (window.eventBus && window.EVENTS) {
window.eventBus.on(window.EVENTS.MODEL_SYNCED, (rdfContent) => {
console.log('Received RDF content for visualization');
// This will be handled by the displayGraphResult method
});
console.log('Event bus listeners set up for RDF visualization');
}
}
}
};
// Initialize when tab is clicked
sparqlTab.addEventListener('click', initializeSparqlBrowser);
// Also initialize if already active
setTimeout(initializeSparqlBrowser, 100);
}
}
// Track if console and help are already initialized
let consoleInstance = null;
let helpInstance = null;
// Initialize console after the app is loaded
async function initializeConsole() {
try {
// Return existing instance if already initialized
if (window.appConsole) {
console.log('[DEBUG] Console already initialized, returning existing instance');
return window.appConsole;
}
console.log('[DEBUG] Starting initializeConsole()');
// Import the Console component
const { default: Console } = await import('./components/Console/Console.js');
console.log('[DEBUG] Console component imported:', typeof Console);
// Create and initialize the console if not already in DOM
const consoleRoot = document.getElementById('console-root');
if (!consoleRoot) {
console.warn('[DEBUG] Console root element not found, creating one');
const newRoot = document.createElement('div');
newRoot.id = 'console-root';
document.body.appendChild(newRoot);
}
// Only create new instance if one doesn't exist
if (!consoleInstance) {
consoleInstance = new Console({
initialLogLevel: 'debug',
maxLogs: 1000
});
console.log('[DEBUG] New console instance created');
}
// Make console available globally for debugging
window.appConsole = consoleInstance;
// Replace console methods to capture logs
if (typeof replaceConsole === 'function') {
replaceConsole();
console.log('[DEBUG] replaceConsole() called');
} else {
console.warn('[DEBUG] replaceConsole is not a function');
}
// Log a test message
console.log('Console component initialized');
// Console is ready but remains hidden by default
// Users can open it with the hamburger menu or backtick (`) key
console.log('[DEBUG] Console initialized and ready (hidden by default)');
return consoleInstance;
} catch (error) {
console.error('[DEBUG] Failed to initialize console:', error);
throw error;
}
}
// Initialize help panel
async function initializeHelp() {
try {
// Return existing instance if already initialized
if (window.appHelp) {
console.log('[DEBUG] Help already initialized, returning existing instance');
return window.appHelp;
}
console.log('[DEBUG] Starting initializeHelp()');
// Only create new instance if one doesn't exist
if (!helpInstance) {
helpInstance = new Help();
console.log('[DEBUG] New help instance created');
}
// Make help available globally for debugging
window.appHelp = helpInstance;
// Help is ready but remains hidden by default
// Users can open it with the hamburger menu or ? key
console.log('[DEBUG] Help initialized and ready (hidden by default)');
return helpInstance;
} catch (error) {
console.error('[DEBUG] Failed to initialize help:', error);
throw error;
}
}
// Make sure initializeConsole and initializeHelp run after DOMContentLoaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
initializeConsole().catch(e => console.error('[DEBUG] Console init error after DOMContentLoaded:', e));
initializeHelp().catch(e => console.error('[DEBUG] Help init error after DOMContentLoaded:', e));
});
} else {
initializeConsole().catch(e => console.error('[DEBUG] Console init error:', e));
initializeHelp().catch(e => console.error('[DEBUG] Help init error:', e));
}
// Initialize the application when the DOM is fully loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
initializeApp().then(() => {
initializeConsole();
initializeHelp();
}).catch(console.error);
});
} else {
initializeApp().then(() => {
initializeConsole();
initializeHelp();
}).catch(console.error);
}