Skip to main content
Glama
orneryd

M.I.M.I.R - Multi-agent Intelligent Memory & Insight Repository

by orneryd
Browser.tsx36.6 kB
import { useEffect, useState } from 'react'; import { Database, Search, Play, History, Terminal, Network, HardDrive, Clock, Activity, ChevronRight, Sparkles, X, Zap, Loader2, MessageCircle } from 'lucide-react'; import { useAppStore } from '../store/appStore'; import { Bifrost } from '../../Bifrost'; interface EmbedStats { running: boolean; processed: number; failed: number; } interface EmbedData { stats: EmbedStats | null; totalEmbeddings: number; enabled: boolean; } export function Browser() { const { stats, connected, fetchStats, cypherQuery, setCypherQuery, cypherResult, executeCypher, queryLoading, queryError, queryHistory, searchQuery, setSearchQuery, searchResults, executeSearch, searchLoading, selectedNode, setSelectedNode, findSimilar, expandedSimilar, collapseSimilar, } = useAppStore(); const [activeTab, setActiveTab] = useState<'query' | 'search'>('query'); const [showHistory, setShowHistory] = useState(false); const [embedData, setEmbedData] = useState<EmbedData>({ stats: null, totalEmbeddings: 0, enabled: false }); const [embedTriggering, setEmbedTriggering] = useState(false); const [embedMessage, setEmbedMessage] = useState<string | null>(null); const [showAIChat, setShowAIChat] = useState(false); // Fetch embed stats periodically useEffect(() => { const fetchEmbedStats = async () => { try { const res = await fetch('/nornicdb/embed/stats'); if (res.ok) { const data = await res.json(); setEmbedData({ stats: data.stats || null, totalEmbeddings: data.total_embeddings || 0, enabled: data.enabled || false, }); } } catch { // Ignore errors } }; fetchEmbedStats(); const interval = setInterval(fetchEmbedStats, 3000); return () => clearInterval(interval); }, []); const handleTriggerEmbed = async () => { setEmbedTriggering(true); setEmbedMessage(null); try { // Use regenerate=true to clear existing embeddings and regenerate all const res = await fetch('/nornicdb/embed/trigger?regenerate=true', { method: 'POST' }); const data = await res.json(); if (res.ok) { setEmbedMessage(data.message); if (data.stats) { setEmbedData(prev => ({ ...prev, stats: data.stats })); } } else { setEmbedMessage(data.message || 'Failed to trigger embeddings'); } } catch (err) { setEmbedMessage('Error triggering embeddings'); } finally { setEmbedTriggering(false); // Clear message after 5 seconds (longer for regenerate) setTimeout(() => setEmbedMessage(null), 5000); } }; useEffect(() => { fetchStats(); const interval = setInterval(fetchStats, 5000); return () => clearInterval(interval); }, [fetchStats]); const handleQuerySubmit = (e: React.FormEvent) => { e.preventDefault(); executeCypher(); }; const handleSearchSubmit = (e: React.FormEvent) => { e.preventDefault(); executeSearch(); }; const formatUptime = (seconds: number) => { const hours = Math.floor(seconds / 3600); const mins = Math.floor((seconds % 3600) / 60); return `${hours}h ${mins}m`; }; return ( <div className="min-h-screen bg-norse-night flex flex-col"> {/* Header */} <header className="bg-norse-shadow border-b border-norse-rune px-4 py-3"> <div className="flex items-center justify-between"> <div className="flex items-center gap-3"> {/* NornicDB Logo - Interwoven threads with gold nexus */} <svg viewBox="0 0 200 180" width="44" height="40" className="flex-shrink-0" role="img" aria-hidden="true"> {/* Three interwoven threads */} <path d="M 40 140 Q 30 100 50 70 Q 70 40 100 35 Q 130 30 145 55 Q 155 75 140 90" fill="none" stroke="#4a9eff" strokeWidth="12" strokeLinecap="round" opacity="0.9"/> <path d="M 100 25 Q 100 50 85 75 Q 70 100 85 120 Q 100 140 100 165" fill="none" stroke="#4a9eff" strokeWidth="12" strokeLinecap="round"/> <path d="M 160 140 Q 170 100 150 70 Q 130 40 100 35 Q 70 30 55 55 Q 45 75 60 90" fill="none" stroke="#4a9eff" strokeWidth="12" strokeLinecap="round" opacity="0.9"/> {/* Central nexus - solid gold colors without gradient for simplicity */} <circle cx="100" cy="85" r="12" fill="#d4af37"/> <circle cx="100" cy="85" r="8" fill="#141824"/> <circle cx="100" cy="85" r="5" fill="#d4af37"/> {/* Destiny nodes */} <circle cx="55" cy="65" r="5" fill="#d4af37" opacity="0.8"/> <circle cx="145" cy="65" r="5" fill="#d4af37" opacity="0.8"/> <circle cx="100" cy="140" r="5" fill="#d4af37" opacity="0.8"/> {/* Connecting lines */} <line x1="60" y1="67" x2="93" y2="82" stroke="#d4af37" strokeWidth="1.5" opacity="0.4"/> <line x1="140" y1="67" x2="107" y2="82" stroke="#d4af37" strokeWidth="1.5" opacity="0.4"/> <line x1="100" y1="135" x2="100" y2="92" stroke="#d4af37" strokeWidth="1.5" opacity="0.4"/> </svg> <div> <h1 className="text-lg font-semibold text-white">NornicDB</h1> <p className="text-xs text-norse-silver">The Graph Database That Learns</p> </div> </div> {/* Connection Status */} <div className="flex items-center gap-6"> {stats?.database && ( <> <div className="flex items-center gap-2 text-sm"> <Network className="w-4 h-4 text-norse-silver" /> <span className="text-norse-silver">{stats.database.nodes?.toLocaleString() ?? '?'} nodes</span> </div> <div className="flex items-center gap-2 text-sm"> <HardDrive className="w-4 h-4 text-norse-silver" /> <span className="text-norse-silver">{stats.database.edges?.toLocaleString() ?? '?'} edges</span> </div> <div className="flex items-center gap-2 text-sm"> <Clock className="w-4 h-4 text-norse-silver" /> <span className="text-norse-silver">{formatUptime(stats.server?.uptime_seconds ?? 0)}</span> </div> </> )} {/* Embed Button */} <button type="button" onClick={handleTriggerEmbed} disabled={embedTriggering} className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium transition-all ${ embedData.stats?.running ? 'bg-amber-500/20 text-amber-400 border border-amber-500/30' : 'bg-norse-shadow hover:bg-norse-rune text-norse-silver hover:text-white border border-norse-rune' }`} title={`Total embeddings: ${embedData.totalEmbeddings}${embedData.stats ? `, Session: ${embedData.stats.processed} processed, ${embedData.stats.failed} failed` : ''}`} > {embedTriggering || embedData.stats?.running ? ( <Loader2 className="w-4 h-4 animate-spin" /> ) : ( <Zap className="w-4 h-4" /> )} <span> {embedData.stats?.running ? 'Embedding...' : 'Embeddings'} </span> <span className="text-xs text-valhalla-gold">({embedData.totalEmbeddings.toLocaleString()})</span> </button> <button type="button" onClick={() => setShowAIChat(true)} className="flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium transition-all bg-valhalla-gold/20 hover:bg-valhalla-gold/30 text-valhalla-gold border border-valhalla-gold/30" title="Open AI Assistant" > <MessageCircle className="w-4 h-4" /> <span>AI Assistant</span> </button> <div className={`flex items-center gap-2 px-3 py-1 rounded-full ${connected ? 'bg-nornic-primary/20 status-connected' : 'bg-red-500/20'}`}> <Activity className={`w-4 h-4 ${connected ? 'text-nornic-primary' : 'text-red-400'}`} /> <span className={`text-sm ${connected ? 'text-nornic-primary' : 'text-red-400'}`}> {connected ? 'Connected' : 'Disconnected'} </span> </div> </div> </div> {/* Embed Message Toast */} {embedMessage && ( <div className="absolute top-16 right-4 bg-norse-shadow border border-norse-rune rounded-lg px-4 py-2 text-sm text-norse-silver shadow-lg"> {embedMessage} </div> )} </header> {/* Main Content */} <div className="flex-1 flex"> {/* Left Panel - Query/Search */} <div className="w-1/2 border-r border-norse-rune flex flex-col"> {/* Tabs */} <div className="flex border-b border-norse-rune"> <button type="button" onClick={() => setActiveTab('query')} className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${ activeTab === 'query' ? 'text-nornic-primary border-b-2 border-nornic-primary bg-norse-shadow/50' : 'text-norse-silver hover:text-white' }`} > <Terminal className="w-4 h-4" /> Cypher Query </button> <button type="button" onClick={() => setActiveTab('search')} className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${ activeTab === 'search' ? 'text-nornic-primary border-b-2 border-nornic-primary bg-norse-shadow/50' : 'text-norse-silver hover:text-white' }`} > <Sparkles className="w-4 h-4" /> Semantic Search </button> </div> {/* Query Panel */} {activeTab === 'query' && ( <div className="flex-1 flex flex-col p-4 gap-4"> <form onSubmit={handleQuerySubmit} className="flex flex-col gap-3"> <div className="relative"> <textarea value={cypherQuery} onChange={(e) => setCypherQuery(e.target.value)} className="cypher-editor w-full h-32 p-3 resize-none" placeholder="MATCH (n) RETURN n LIMIT 25" spellCheck={false} /> <button type="button" onClick={() => setShowHistory(!showHistory)} className="absolute top-2 right-2 p-1.5 rounded hover:bg-norse-rune transition-colors" title="Query History" > <History className="w-4 h-4 text-norse-silver" /> </button> </div> {showHistory && queryHistory.length > 0 && ( <div className="bg-norse-stone border border-norse-rune rounded-lg p-2 max-h-40 overflow-y-auto"> {queryHistory.map((q, i) => ( <button key={i} type="button" onClick={() => { setCypherQuery(q); setShowHistory(false); }} className="w-full text-left px-2 py-1 text-sm text-norse-silver hover:bg-norse-rune rounded truncate" > {q} </button> ))} </div> )} <button type="submit" disabled={queryLoading} className="flex items-center justify-center gap-2 px-4 py-2 bg-nornic-primary text-white rounded-lg hover:bg-nornic-secondary disabled:opacity-50 transition-colors" > <Play className="w-4 h-4" /> {queryLoading ? 'Executing...' : 'Run Query'} </button> </form> {queryError && ( <div className="p-3 bg-red-500/10 border border-red-500/30 rounded-lg"> <p className="text-sm text-red-400 font-mono">{queryError}</p> </div> )} {/* Query Results */} {cypherResult && cypherResult.results[0] && ( <div className="flex-1 overflow-auto"> <table className="result-table"> <thead> <tr> {cypherResult.results[0].columns.map((col, i) => ( <th key={i}>{col}</th> ))} </tr> </thead> <tbody> {cypherResult.results[0].data.map((row, i) => ( <tr key={i} onClick={() => { // Find first node-like object in row and select it for (const cell of row.row) { if (cell && typeof cell === 'object') { const cellObj = cell as Record<string, unknown>; if (cellObj.id || cellObj._nodeId) { const nodeData = extractNodeFromResult(cellObj); if (nodeData) { setSelectedNode({ node: { ...nodeData, created_at: '' }, score: 0 }); break; } } } } }} className="cursor-pointer hover:bg-nornic-primary/10" > {row.row.map((cell, j) => ( <td key={j} className="font-mono text-xs"> {typeof cell === 'object' ? <NodePreview data={cell} /> : String(cell)} </td> ))} </tr> ))} </tbody> </table> <p className="text-xs text-norse-silver mt-2 px-2"> {cypherResult.results[0].data.length} row(s) returned </p> </div> )} </div> )} {/* Search Panel */} {activeTab === 'search' && ( <div className="flex-1 flex flex-col p-4 gap-4"> <form onSubmit={handleSearchSubmit} className="flex gap-2"> <div className="relative flex-1"> <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-norse-fog" /> <input type="text" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2 bg-norse-stone border border-norse-rune rounded-lg text-white placeholder-norse-fog focus:outline-none focus:ring-2 focus:ring-nornic-primary" placeholder="Search nodes semantically..." /> </div> <button type="submit" disabled={searchLoading} className="px-4 py-2 bg-nornic-primary text-white rounded-lg hover:bg-nornic-secondary disabled:opacity-50 transition-colors" > {searchLoading ? '...' : 'Search'} </button> </form> {/* Search Results */} <div className="flex-1 overflow-auto space-y-2"> {searchResults.map((result) => ( <div key={result.node.id}> {/* Main result card */} <button type="button" onClick={() => setSelectedNode(result)} className={`w-full text-left p-3 rounded-lg border transition-colors ${ selectedNode?.node.id === result.node.id ? 'bg-nornic-primary/20 border-nornic-primary' : 'bg-norse-stone border-norse-rune hover:border-norse-fog' }`} > <div className="flex items-center justify-between mb-1"> <div className="flex items-center gap-2"> {result.node.labels.map((label) => ( <span key={label} className="px-2 py-0.5 text-xs bg-frost-ice/20 text-frost-ice rounded"> {label} </span> ))} </div> <span className="text-xs text-valhalla-gold"> Score: {result.score.toFixed(2)} </span> </div> <p className="text-sm text-norse-silver truncate"> {getNodePreview(result.node.properties)} </p> </button> {/* Inline Similar Expansion */} {expandedSimilar?.nodeId === result.node.id && ( <div className="ml-4 mt-2 mb-3 border-l-2 border-frost-ice/30 pl-3 animate-in slide-in-from-top-2 duration-200"> <div className="flex items-center justify-between mb-2"> <span className="text-xs font-medium text-frost-ice flex items-center gap-1"> <Sparkles className="w-3 h-3" /> Similar Items ({expandedSimilar.results.length}) </span> <button type="button" onClick={() => collapseSimilar()} className="text-xs text-norse-fog hover:text-white transition-colors" > Close </button> </div> {expandedSimilar.loading ? ( <div className="flex items-center gap-2 text-norse-fog text-sm py-2"> <Loader2 className="w-4 h-4 animate-spin" /> Finding similar... </div> ) : expandedSimilar.results.length === 0 ? ( <p className="text-xs text-norse-fog py-2">No similar items found</p> ) : ( <div className="space-y-1"> {expandedSimilar.results.map((similar) => ( <button type="button" key={similar.node.id} onClick={() => setSelectedNode(similar)} className="w-full text-left p-2 rounded bg-norse-shadow/50 hover:bg-norse-shadow border border-transparent hover:border-frost-ice/20 transition-colors" > <div className="flex items-center justify-between"> <div className="flex items-center gap-1"> {similar.node.labels.slice(0, 2).map((label) => ( <span key={label} className="px-1.5 py-0.5 text-xs bg-frost-ice/10 text-frost-ice/80 rounded"> {label} </span> ))} </div> <span className="text-xs text-valhalla-gold/70"> {similar.score.toFixed(2)} </span> </div> <p className="text-xs text-norse-silver/80 truncate mt-1"> {getNodePreview(similar.node.properties)} </p> </button> ))} </div> )} </div> )} </div> ))} {searchResults.length === 0 && searchQuery && !searchLoading && ( <p className="text-center text-norse-silver py-8">No results found</p> )} </div> </div> )} </div> {/* Right Panel - Node Details */} <div className="w-1/2 flex flex-col bg-norse-shadow/30"> {selectedNode ? ( <> <div className="flex items-center justify-between p-4 border-b border-norse-rune"> <h2 className="font-medium text-white">Node Details</h2> <div className="flex items-center gap-2"> <button type="button" onClick={() => { // Toggle: if already expanded for this node, collapse if (expandedSimilar?.nodeId === selectedNode.node.id) { collapseSimilar(); } else { findSimilar(selectedNode.node.id); } }} className={`flex items-center gap-1 px-3 py-1 text-sm rounded transition-colors ${ expandedSimilar?.nodeId === selectedNode.node.id ? 'bg-frost-ice text-norse-night hover:bg-frost-ice/90' : 'bg-frost-ice/20 text-frost-ice hover:bg-frost-ice/30' }`} > <Sparkles className="w-3 h-3" /> {expandedSimilar?.nodeId === selectedNode.node.id ? 'Hide Similar' : 'Find Similar'} </button> <button type="button" onClick={() => setSelectedNode(null)} className="p-1 hover:bg-norse-rune rounded transition-colors" > <X className="w-4 h-4 text-norse-silver" /> </button> </div> </div> <div className="flex-1 overflow-auto p-4"> {/* Labels */} <div className="mb-4"> <h3 className="text-xs font-medium text-norse-silver mb-2">LABELS</h3> <div className="flex flex-wrap gap-2"> {(selectedNode.node.labels as string[]).map((label, i) => ( <span key={`label-${i}`} className="px-3 py-1 bg-frost-ice/20 text-frost-ice rounded-full text-sm"> {String(label)} </span> ))} </div> </div> {/* ID */} <div className="mb-4"> <h3 className="text-xs font-medium text-norse-silver mb-2">ID</h3> <code className="text-sm text-valhalla-gold font-mono">{selectedNode.node.id}</code> </div> {/* Embedding Status - Always show at top */} {'embedding' in selectedNode.node.properties && selectedNode.node.properties.embedding != null && ( <div className="mb-4"> <h3 className="text-xs font-medium text-norse-silver mb-2">EMBEDDING</h3> <EmbeddingStatus embedding={selectedNode.node.properties.embedding} /> </div> )} {/* Scores */} {(selectedNode.rrf_score || selectedNode.vector_rank || selectedNode.bm25_rank) && ( <div className="mb-4 flex gap-4"> {selectedNode.rrf_score && ( <div> <h3 className="text-xs font-medium text-norse-silver mb-1">RRF Score</h3> <span className="text-nornic-accent">{selectedNode.rrf_score.toFixed(4)}</span> </div> )} {selectedNode.vector_rank && ( <div> <h3 className="text-xs font-medium text-norse-silver mb-1">Vector Rank</h3> <span className="text-frost-ice">#{selectedNode.vector_rank}</span> </div> )} {selectedNode.bm25_rank && ( <div> <h3 className="text-xs font-medium text-norse-silver mb-1">BM25 Rank</h3> <span className="text-valhalla-gold">#{selectedNode.bm25_rank}</span> </div> )} </div> )} {/* Properties (excluding embedding - shown above) */} <div className="mb-4"> <h3 className="text-xs font-medium text-norse-silver mb-2">PROPERTIES</h3> <div className="space-y-2"> {Object.entries(selectedNode.node.properties) .filter(([key]) => key !== 'embedding') .map(([key, value]) => ( <div key={key} className="bg-norse-stone rounded-lg p-3"> <div className="flex items-center gap-2 mb-1"> <ChevronRight className="w-3 h-3 text-norse-fog" /> <span className="text-sm text-frost-ice font-medium">{key}</span> </div> <div className="pl-5"> <JsonPreview data={value} expanded /> </div> </div> ))} </div> </div> {/* Similar Items Section - Shows when Find Similar is clicked */} {expandedSimilar?.nodeId === selectedNode.node.id && ( <div className="border-t border-norse-rune pt-4 animate-in slide-in-from-bottom-2 duration-200"> <div className="flex items-center gap-2 mb-3"> <Sparkles className="w-4 h-4 text-frost-ice" /> <h3 className="text-sm font-medium text-frost-ice">Similar Items</h3> {!expandedSimilar.loading && ( <span className="text-xs text-norse-fog">({expandedSimilar.results.length} found)</span> )} </div> {expandedSimilar.loading ? ( <div className="flex items-center gap-2 text-norse-fog py-4"> <Loader2 className="w-5 h-5 animate-spin" /> <span>Finding similar nodes...</span> </div> ) : expandedSimilar.results.length === 0 ? ( <div className="text-center py-4 text-norse-fog"> <p>No similar items found</p> <p className="text-xs mt-1">This node may not have an embedding yet</p> </div> ) : ( <div className="space-y-2"> {expandedSimilar.results.map((similar) => ( <button type="button" key={similar.node.id} onClick={() => setSelectedNode(similar)} className="w-full text-left p-3 rounded-lg bg-norse-stone hover:bg-norse-shadow border border-transparent hover:border-frost-ice/30 transition-colors" > <div className="flex items-center justify-between mb-2"> <div className="flex items-center gap-2 flex-wrap"> {similar.node.labels.slice(0, 3).map((label) => ( <span key={label} className="px-2 py-0.5 text-xs bg-frost-ice/20 text-frost-ice rounded"> {label} </span> ))} </div> <span className="text-xs text-valhalla-gold font-medium"> {(similar.score * 100).toFixed(1)}% similar </span> </div> <p className="text-sm text-norse-silver line-clamp-2"> {getNodePreview(similar.node.properties)} </p> <p className="text-xs text-norse-fog mt-1 font-mono"> {similar.node.id} </p> </button> ))} </div> )} </div> )} </div> </> ) : ( <div className="flex-1 flex items-center justify-center"> <div className="text-center text-norse-silver"> <Database className="w-12 h-12 mx-auto mb-3 opacity-30" /> <p>Select a node to view details</p> <p className="text-sm text-norse-fog mt-1"> Run a query or search to get started </p> </div> </div> )} </div> </div> {/* AI Assistant Chat */} <Bifrost isOpen={showAIChat} onClose={() => setShowAIChat(false)} /> </div> ); } // Helper components function JsonPreview({ data, expanded = false }: { data: unknown; expanded?: boolean }) { if (data === null) return <span className="json-null">null</span>; if (typeof data === 'string') { const displayValue = expanded ? data : data.slice(0, 100) + (data.length > 100 ? '...' : ''); return <span className="json-string whitespace-pre-wrap">"{displayValue}"</span>; } if (typeof data === 'number') return <span className="json-number">{data}</span>; if (typeof data === 'boolean') return <span className="json-boolean">{String(data)}</span>; if (Array.isArray(data)) { if (!expanded && data.length > 3) { return <span className="text-norse-silver">[{data.length} items]</span>; } return <span className="text-norse-silver">[...]</span>; } if (typeof data === 'object') { const keys = Object.keys(data); if (!expanded && keys.length > 3) { return <span className="text-norse-silver">{'{'}...{keys.length} props{'}'}</span>; } return <span className="text-norse-silver">{'{'}...{'}'}</span>; } return <span>{String(data)}</span>; } function getNodePreview(properties: Record<string, unknown>): string { const previewFields = ['title', 'name', 'text', 'content', 'description', 'path']; for (const field of previewFields) { if (properties[field] && typeof properties[field] === 'string') { return properties[field] as string; } } return JSON.stringify(properties).slice(0, 100); } // Extract node data from Cypher result cell function extractNodeFromResult(cell: Record<string, unknown>): { id: string; labels: string[]; properties: Record<string, unknown> } | null { if (!cell || typeof cell !== 'object') return null; // Get ID (could be _nodeId, id, or elementId) const id = (cell._nodeId || cell.id || cell.elementId) as string; if (!id) return null; // Get labels let labels: string[] = []; if (Array.isArray(cell.labels)) { labels = cell.labels as string[]; } else if (cell.type && typeof cell.type === 'string') { labels = [cell.type]; } // Properties are the rest of the fields (excluding metadata) const excludeKeys = new Set(['_nodeId', 'id', 'elementId', 'labels', 'meta']); const properties: Record<string, unknown> = {}; for (const [key, value] of Object.entries(cell)) { if (!excludeKeys.has(key)) { properties[key] = value; } } return { id, labels, properties }; } // Show node preview with ID and title function NodePreview({ data }: { data: unknown }) { if (data === null) return <span className="json-null">null</span>; if (typeof data !== 'object') return <span>{String(data)}</span>; const obj = data as Record<string, unknown>; // Check if it's a node-like object const id = obj._nodeId || obj.id || obj.elementId; const title = obj.title || obj.name || obj.text; const type = obj.type; const labels = obj.labels as string[] | undefined; if (id) { const idStr = String(id); const titleStr = title ? String(title) : ''; return ( <div className="flex items-center gap-2"> {labels && labels.length > 0 ? ( <span className="px-1.5 py-0.5 text-xs bg-frost-ice/20 text-frost-ice rounded"> {labels[0]} </span> ) : typeof type === 'string' ? ( <span className="px-1.5 py-0.5 text-xs bg-nornic-primary/20 text-nornic-primary rounded"> {type} </span> ) : null} <span className="text-valhalla-gold text-xs">{idStr.slice(0, 20)}</span> {titleStr && ( <span className="text-norse-silver truncate max-w-[200px]"> {titleStr.slice(0, 50)}{titleStr.length > 50 ? '...' : ''} </span> )} </div> ); } // Not a node - show key count const keys = Object.keys(obj); return <span className="text-norse-silver">{'{'}...{keys.length} props{'}'}</span>; } // Display embedding status nicely function EmbeddingStatus({ embedding }: { embedding: unknown }) { if (!embedding || typeof embedding !== 'object') { return <span className="text-norse-silver">No embedding data</span>; } const emb = embedding as Record<string, unknown>; const status = emb.status as string || 'unknown'; const dimensions = emb.dimensions as number || 0; const model = emb.model as string | undefined; const isReady = status === 'ready'; const isPending = status === 'pending'; return ( <div className="bg-norse-stone rounded-lg p-3"> <div className="flex items-center gap-3"> {/* Status indicator */} <div className={`flex items-center gap-2 px-3 py-1.5 rounded-full ${ isReady ? 'bg-nornic-primary/20' : isPending ? 'bg-valhalla-gold/20' : 'bg-red-500/20' }`}> <div className={`w-2 h-2 rounded-full ${ isReady ? 'bg-nornic-primary animate-pulse' : isPending ? 'bg-valhalla-gold animate-pulse' : 'bg-red-400' }`} /> <span className={`text-sm font-medium ${ isReady ? 'text-nornic-primary' : isPending ? 'text-valhalla-gold' : 'text-red-400' }`}> {isReady ? 'Ready' : isPending ? 'Generating...' : status} </span> </div> {/* Dimensions */} {dimensions > 0 && ( <div className="flex items-center gap-1"> <span className="text-xs text-norse-silver">Dimensions:</span> <span className="text-sm text-frost-ice font-mono">{dimensions}</span> </div> )} {/* Model */} {model && ( <div className="flex items-center gap-1"> <span className="text-xs text-norse-silver">Model:</span> <span className="text-sm text-nornic-accent">{model}</span> </div> )} </div> {isPending && ( <p className="text-xs text-norse-fog mt-2"> Embedding will be generated automatically by the background queue. </p> )} </div> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/orneryd/Mimir'

If you have feedback or need assistance with the MCP directory API, please join our Discord server