diff --git a/config/datacortex.yaml b/config/datacortex.yaml index 0bc9276..4f2c6ee 100644 --- a/config/datacortex.yaml +++ b/config/datacortex.yaml @@ -16,7 +16,7 @@ server: # Pulse settings pulse: - directory: pulses + directory: .datacore/pulses schedule: manual # daily, weekly, manual # Graph generation settings @@ -31,3 +31,24 @@ graph: visualization: node_size_metric: degree # degree, centrality color_by: type # type, space, cluster + +# AI settings +ai: + # === Embeddings === + # Local embedding model (sentence-transformers) + embedding_model: sentence-transformers/all-mpnet-base-v2 + + # Content to embed: title + first N chars of content + content_length: 500 + + # Batch size for embedding computation + batch_size: 32 + + # === Q&A Synthesis === + # LLM for answering questions (when running standalone, not in Claude Code) + # Options: claude-3-haiku-20240307, claude-3-5-sonnet-20241022, claude-sonnet-4-20250514 + qa_model: claude-3-haiku-20240307 + + # API key from env: ANTHROPIC_API_KEY or .datacore/env/anthropic.env + # Max tokens for answer generation + qa_max_tokens: 1024 diff --git a/frontend/js/app.js b/frontend/js/app.js index b5623f8..2bfc687 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -10,22 +10,38 @@ class Datacortex { this.graphView = null; this.selectedNode = null; this.pulses = []; - this.filters = { - spaces: new Set(), - types: new Set(), - minDegree: 1, - searchQuery: '' - }; + + // Simple filter model: sets contain what's INCLUDED (checked) + this.includedSpaces = new Set(); // Empty = show nothing, check to add + this.includedTypes = new Set(); // Empty = show nothing, check to add + this.minDegree = 1; + this.searchQuery = ''; + + // Track all available options (populated from first API call) + this.allSpaces = new Set(); + this.allTypes = new Set(); + this.initialLoadDone = false; + + // Debounced graph loader + this.debouncedLoadGraph = this.debounce(() => this.loadGraph(), 300); this.init(); } + debounce(fn, ms) { + let timeout; + return (...args) => { + clearTimeout(timeout); + timeout = setTimeout(() => fn.apply(this, args), ms); + }; + } + async init() { // Initialize graph visualization this.graphView = new GraphView('#graph', this); - // Load initial data - await this.loadGraph(); + // First load - get all data to discover available spaces/types + await this.loadGraph({ initial: true }); await this.loadPulses(); await this.loadTags(); @@ -39,32 +55,69 @@ class Datacortex { try { const queryParams = new URLSearchParams(); - if (this.filters.spaces.size > 0) { - queryParams.set('spaces', Array.from(this.filters.spaces).join(',')); - } - if (this.filters.types.size > 0) { - queryParams.set('types', Array.from(this.filters.types).join(',')); + // Only apply filters after initial load + if (this.initialLoadDone) { + // Send included spaces to API (if not all) + if (this.includedSpaces.size > 0 && this.includedSpaces.size < this.allSpaces.size) { + queryParams.set('spaces', [...this.includedSpaces].join(',')); + } else if (this.includedSpaces.size === 0) { + // Nothing selected - show empty graph + this.graph = { nodes: [], links: [], stats: { node_count: 0, edge_count: 0, avg_degree: 0, cluster_count: 0, orphan_count: 0, nodes_by_space: {}, nodes_by_type: {} }}; + this.graphView.render({ nodes: [], links: [] }); + this.updateStats(); + return; + } + // Types are filtered client-side after fetch } - if (this.filters.minDegree > 0) { - queryParams.set('min_degree', this.filters.minDegree); + + if (this.minDegree > 0) { + queryParams.set('min_degree', this.minDegree); } const url = `${API_BASE}/graph?${queryParams}`; + console.log('Fetching:', url); const response = await fetch(url); this.graph = await response.json(); - // Apply search filter client-side + // On initial load, discover all spaces/types and include all by default + if (params.initial || !this.initialLoadDone) { + if (this.graph.stats) { + this.allSpaces = new Set(Object.keys(this.graph.stats.nodes_by_space || {})); + this.allTypes = new Set(Object.keys(this.graph.stats.nodes_by_type || {})); + // Start with NOTHING selected for testing + this.includedSpaces = new Set(); // Empty - user opts in + this.includedTypes = new Set([...this.allTypes]); // All types by default + } + this.initialLoadDone = true; + + // Show empty graph initially + this.graphView.render({ nodes: [], links: [] }); + this.updateStats(); + return; + } + + // Apply type filter client-side let nodes = this.graph.nodes; let links = this.graph.links; - if (this.filters.searchQuery) { - const query = this.filters.searchQuery.toLowerCase(); + if (this.includedTypes.size > 0 && this.includedTypes.size < this.allTypes.size) { + nodes = nodes.filter(n => this.includedTypes.has(n.type)); + const nodeIds = new Set(nodes.map(n => n.id)); + links = links.filter(l => { + const sourceId = typeof l.source === 'object' ? l.source.id : l.source; + const targetId = typeof l.target === 'object' ? l.target.id : l.target; + return nodeIds.has(sourceId) && nodeIds.has(targetId); + }); + } + + // Apply search filter client-side + if (this.searchQuery) { + const query = this.searchQuery.toLowerCase(); nodes = nodes.filter(n => n.title.toLowerCase().includes(query) || (n.tags && n.tags.some(t => t.toLowerCase().includes(query))) ); const nodeIds = new Set(nodes.map(n => n.id)); - // Handle both string IDs (from API) and object refs (after D3 render) links = links.filter(l => { const sourceId = typeof l.source === 'object' ? l.source.id : l.source; const targetId = typeof l.target === 'object' ? l.target.id : l.target; @@ -174,15 +227,15 @@ class Datacortex { content.innerHTML = `