Skip to main content
Glama
orneryd

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

by orneryd
Bifrost.tsx16.9 kB
import React, { useState, useRef, useEffect, useCallback } from 'react'; import './Bifrost.css'; interface Message { id: string; role: 'user' | 'assistant' | 'system' | 'heimdall'; content: string; timestamp: Date; streaming?: boolean; } interface BifrostProps { isOpen: boolean; onClose: () => void; modelName?: string; apiEndpoint?: string; } /** * Bifrost - AI Chat Interface * * A terminal-style chat interface for interacting with NornicDB's * built-in AI assistant. Provides natural language access to * database management, diagnostics, and intelligent operations. * * Uses OpenAI-compatible SSE streaming API. */ // Session storage key for persisting chat history const BIFROST_MESSAGES_KEY = 'bifrost-messages'; const BIFROST_COMMAND_HISTORY_KEY = 'bifrost-command-history'; // Load messages from session storage const loadSessionMessages = (): Message[] => { try { const stored = sessionStorage.getItem(BIFROST_MESSAGES_KEY); if (stored) { const parsed = JSON.parse(stored); // Restore Date objects return parsed.map((m: any) => ({ ...m, timestamp: new Date(m.timestamp) })); } } catch (e) { // Ignore storage errors } return [{ id: '0', role: 'system', content: '✓ AI Assistant Connected\n\nReady for database operations.\nType /help for available commands.', timestamp: new Date() }]; }; const loadCommandHistory = (): string[] => { try { const stored = sessionStorage.getItem(BIFROST_COMMAND_HISTORY_KEY); if (stored) return JSON.parse(stored); } catch (e) { // Ignore storage errors } return []; }; export const Bifrost: React.FC<BifrostProps> = ({ isOpen, onClose, modelName = 'qwen2.5-0.5b-instruct', apiEndpoint = '/api/bifrost/chat/completions' }) => { const [messages, setMessages] = useState<Message[]>(loadSessionMessages); const [input, setInput] = useState(''); const [isStreaming, setIsStreaming] = useState(false); const [commandHistory, setCommandHistory] = useState<string[]>(loadCommandHistory); const [historyIndex, setHistoryIndex] = useState(-1); const [selectedModel, setSelectedModel] = useState(modelName); const [connectionStatus, setConnectionStatus] = useState<'ready' | 'streaming' | 'error'>('ready'); const scrollRef = useRef<HTMLDivElement>(null); const inputRef = useRef<HTMLTextAreaElement>(null); const abortControllerRef = useRef<AbortController | null>(null); // Auto-scroll to bottom when messages change const messagesLength = messages.length; useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [messagesLength]); // Persist messages to session storage (survives closing/reopening Bifrost panel) useEffect(() => { try { // Only save non-streaming messages const toSave = messages.filter(m => !m.streaming); sessionStorage.setItem(BIFROST_MESSAGES_KEY, JSON.stringify(toSave)); } catch (e) { // Ignore storage errors } }, [messages]); // Persist command history to session storage useEffect(() => { try { sessionStorage.setItem(BIFROST_COMMAND_HISTORY_KEY, JSON.stringify(commandHistory)); } catch (e) { // Ignore storage errors } }, [commandHistory]); // Focus input when opened useEffect(() => { if (isOpen && inputRef.current) { setTimeout(() => inputRef.current?.focus(), 100); } }, [isOpen]); // Check Heimdall status on open useEffect(() => { if (isOpen) { checkStatus(); } return () => { // Cancel any ongoing stream when closing abortControllerRef.current?.abort(); }; }, [isOpen]); // NOTE: Heimdall notifications are now sent inline with streaming responses // No separate EventSource needed - this simplifies the architecture and ensures // proper ordering of notifications with chat content const checkStatus = async () => { try { const res = await fetch('/api/bifrost/status'); if (res.ok) { const data = await res.json(); if (data.heimdall?.enabled) { setConnectionStatus('ready'); setSelectedModel(data.model || modelName); } else { setConnectionStatus('error'); setMessages(prev => [...prev, { id: crypto.randomUUID(), role: 'system', content: '⚠️ AI Assistant is not enabled. Set NORNICDB_HEIMDALL_ENABLED=true to activate.', timestamp: new Date() }]); } } } catch (err) { setConnectionStatus('error'); } }; const handleBuiltInCommand = (cmd: string): boolean => { const command = cmd.toLowerCase().trim(); if (command === '/help') { setMessages(prev => [...prev, { id: crypto.randomUUID(), role: 'system', content: `Available commands: /help - Show this help message /clear - Clear chat history /health - Check database health /stats - Get graph statistics /status - Show connection status /model - Show current model`, timestamp: new Date() }]); return true; } if (command === '/clear') { setMessages([{ id: crypto.randomUUID(), role: 'system', content: '✓ Chat cleared', timestamp: new Date() }]); return true; } if (command === '/status') { checkStatus(); setMessages(prev => [...prev, { id: crypto.randomUUID(), role: 'system', content: `Status: ${connectionStatus}\nModel: ${selectedModel}`, timestamp: new Date() }]); return true; } if (command === '/model') { setMessages(prev => [...prev, { id: crypto.randomUUID(), role: 'system', content: `Current model: ${selectedModel}`, timestamp: new Date() }]); return true; } return false; }; // Send message using OpenAI-compatible SSE streaming const sendMessage = useCallback(async () => { if (!input.trim() || isStreaming) return; const trimmedInput = input.trim(); // Check for built-in commands if (trimmedInput.startsWith('/')) { if (handleBuiltInCommand(trimmedInput)) { setCommandHistory(prev => [...prev, trimmedInput]); setHistoryIndex(-1); setInput(''); return; } } const userMessage: Message = { id: crypto.randomUUID(), role: 'user', content: trimmedInput, timestamp: new Date() }; // Add to history setCommandHistory(prev => [...prev, trimmedInput]); setHistoryIndex(-1); // Add user message setMessages(prev => [...prev, userMessage]); // Add placeholder for assistant response const assistantId = crypto.randomUUID(); const assistantMessage: Message = { id: assistantId, role: 'assistant', content: '', timestamp: new Date(), streaming: true }; setMessages(prev => [...prev, assistantMessage]); setInput(''); setIsStreaming(true); setConnectionStatus('streaming'); // Create abort controller for cancellation abortControllerRef.current = new AbortController(); try { // Single-shot messaging: only send current user message, no context // History is kept in UI for display purposes only const response = await fetch(apiEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ model: selectedModel, messages: [{ role: 'user', content: trimmedInput }], stream: true, max_tokens: 512, temperature: 0.1 }), signal: abortControllerRef.current.signal }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const reader = response.body?.getReader(); const decoder = new TextDecoder(); if (!reader) { throw new Error('No response body'); } let fullContent = ''; while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); const lines = chunk.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6); if (data === '[DONE]') { // Stream complete setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, streaming: false, content: fullContent } : m )); break; } try { const parsed = JSON.parse(data); const delta = parsed.choices?.[0]?.delta; // Check if this is a Heimdall notification (inline with stream) if (delta?.role === 'heimdall' && delta?.content) { // Insert Heimdall message before the current assistant message setMessages(prev => { const heimdallMsg: Message = { id: crypto.randomUUID(), role: 'heimdall', content: delta.content.trim(), timestamp: new Date() }; // Find the streaming assistant message and insert before it const idx = prev.findIndex(m => m.id === assistantId); if (idx > 0) { return [...prev.slice(0, idx), heimdallMsg, ...prev.slice(idx)]; } return [...prev, heimdallMsg]; }); } else if (delta?.content) { // Regular assistant content fullContent += delta.content; setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, content: fullContent } : m )); } } catch (e) { // Ignore parse errors for incomplete JSON } } } } setConnectionStatus('ready'); } catch (err: any) { if (err.name === 'AbortError') { // User cancelled setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, streaming: false, content: m.content || '(cancelled)' } : m )); } else { setMessages(prev => [ ...prev.filter(m => m.id !== assistantId), { id: crypto.randomUUID(), role: 'system', content: `❌ Error: ${err.message}`, timestamp: new Date() } ]); setConnectionStatus('error'); } } finally { setIsStreaming(false); abortControllerRef.current = null; } }, [input, isStreaming, messages, apiEndpoint, selectedModel]); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } else if (e.key === 'ArrowUp') { e.preventDefault(); if (commandHistory.length > 0 && historyIndex < commandHistory.length - 1) { const newIndex = historyIndex + 1; setHistoryIndex(newIndex); setInput(commandHistory[commandHistory.length - 1 - newIndex]); } } else if (e.key === 'ArrowDown') { e.preventDefault(); if (historyIndex > 0) { const newIndex = historyIndex - 1; setHistoryIndex(newIndex); setInput(commandHistory[commandHistory.length - 1 - newIndex]); } else if (historyIndex === 0) { setHistoryIndex(-1); setInput(''); } } else if (e.key === 'Escape') { if (isStreaming) { abortControllerRef.current?.abort(); } else { onClose(); } } }; const formatTimestamp = (date: Date) => { return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }); }; const getStatusColor = () => { switch (connectionStatus) { case 'ready': return 'ready'; case 'streaming': return 'streaming'; case 'error': return 'error'; default: return ''; } }; if (!isOpen) return null; return ( <div className="bifrost-portal"> <div className="bifrost-overlay" onClick={onClose} onKeyDown={(e) => e.key === 'Escape' && onClose()} role="button" tabIndex={0} aria-label="Close AI Assistant" /> <div className="bifrost-container"> {/* Header */} <div className="bifrost-header"> <div className="bifrost-title"> <svg className="bifrost-logo" viewBox="0 0 40 40" width="28" height="28"> {/* Heimdall - The Watchman */} {/* Helmet silhouette */} <path d="M 12 28 Q 10 20 12 14 Q 14 8 20 6 Q 26 8 28 14 Q 30 20 28 28 Q 25 31 20 32 Q 15 31 12 28" fill="var(--frost-ice)"/> {/* Helmet crest */} <path d="M 20 5 L 20 9" fill="none" stroke="var(--frost-ice)" strokeWidth="3" strokeLinecap="round"/> {/* Helmet wings */} <path d="M 10 13 Q 6 10 4 6" fill="none" stroke="var(--frost-ice)" strokeWidth="2" strokeLinecap="round"/> <path d="M 30 13 Q 34 10 36 6" fill="none" stroke="var(--frost-ice)" strokeWidth="2" strokeLinecap="round"/> {/* All-Seeing Eye */} <ellipse cx="20" cy="18" rx="6" ry="4" fill="none" stroke="var(--valhalla-gold)" strokeWidth="1.5"/> <circle cx="20" cy="18" r="3" fill="var(--valhalla-gold)"/> <circle cx="20" cy="18" r="1.5" fill="var(--norse-night)"/> {/* Sight rays */} <line x1="27" y1="17" x2="32" y2="16" stroke="var(--valhalla-gold)" strokeWidth="1" opacity="0.6"/> <line x1="13" y1="17" x2="8" y2="16" stroke="var(--valhalla-gold)" strokeWidth="1" opacity="0.6"/> {/* Gjallarhorn */} <path d="M 32 22 Q 35 19 36 14" fill="none" stroke="var(--frost-ice)" strokeWidth="2" strokeLinecap="round"/> <circle cx="36" cy="14" r="1.5" fill="var(--valhalla-gold)"/> </svg> <span>AI Assistant</span> <span className="bifrost-subtitle">NornicDB</span> <div className={`bifrost-status ${getStatusColor()}`} /> </div> <div className="bifrost-controls"> <span className="bifrost-model">{selectedModel}</span> <button type="button" className="bifrost-close" onClick={onClose} title="Close (Esc)" > ✕ </button> </div> </div> {/* Messages */} <div className="bifrost-messages" ref={scrollRef}> {messages.map((message) => ( <div key={message.id} className={`bifrost-message bifrost-message-${message.role}`} > <div className="bifrost-message-header"> <span className="bifrost-message-role"> {message.role === 'user' ? '>' : message.role === 'assistant' ? '◈' : message.role === 'heimdall' ? '⛊' : '⚙'} </span> <span className="bifrost-message-time"> {formatTimestamp(message.timestamp)} </span> </div> <div className="bifrost-message-content"> {message.content} {message.streaming && <span className="bifrost-cursor">▊</span>} </div> </div> ))} </div> {/* Input */} <div className="bifrost-input-container"> <span className="bifrost-prompt">{'>'}</span> <textarea ref={inputRef} className="bifrost-input" value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={handleKeyDown} placeholder={isStreaming ? 'Processing...' : 'Ask about database operations...'} disabled={isStreaming} rows={1} /> <button className="bifrost-send" onClick={sendMessage} disabled={isStreaming || !input.trim()} type="button" > {isStreaming ? '⏳' : '⏎'} </button> </div> </div> </div> ); }; export default Bifrost;

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