Soma / frontend /src /App.jsx
Komalpreet Kaur
feat: implement responsive, theme-adaptive visitor analytics modal dialog box
df43d43 unverified
// Soma Cognitive Console: Refined Responsive Shell
import { useEffect, useState, useCallback } from 'react';
import ChatPanel from './components/ChatPanel';
import CognitiveBrainImageScene from './components/CognitiveBrainImageScene';
import CognitiveTimeline from './components/CognitiveTimeline';
import MemoryExplorer from './components/MemoryExplorer';
import KnowledgeGraph from './components/KnowledgeGraph';
import CognitiveDashboard from './components/CognitiveDashboard';
import KnowledgeInput from './components/KnowledgeInput';
import { SleepProgress } from './components/DreamSequence';
import AuthScreen from './components/AuthScreen';
import VisitorAnalytics from './components/VisitorAnalytics';
import { apiFetch } from './api';
import './App.css';
const NAV_ITEMS = [
{ id: 'console', label: 'Console', icon: 'chat_bubble_outline' },
{ id: 'memory', label: 'Memory', icon: 'layers' },
{ id: 'graph', label: 'Graph', icon: 'share' },
{ id: 'knowledge', label: 'Inscription', icon: 'auto_awesome' },
{ id: 'sleep', label: 'Sleep', icon: 'hotel' },
];
const COGNITIVE_PHASES = {
PERCEPTION: 'perception',
ATTENTION: 'attention',
RECALL: 'recall',
REASONING: 'reasoning',
RESPONDING: 'responding',
IDLE: 'idle',
LISTENING: 'listening'
};
function App() {
const [activePage, setActivePage] = useState('console');
const [username, setUsername] = useState(localStorage.getItem('soma_username'));
const [messages, setMessages] = useState([]);
const [vitals, setVitals] = useState(null);
const [trace, setTrace] = useState([]);
const [cognitiveState, setCognitiveState] = useState(COGNITIVE_PHASES.IDLE);
const [refreshTick, setRefreshTick] = useState(0);
const [knowledgeStatus, setKnowledgeStatus] = useState('');
const [sleepPhaseIndex, setSleepPhaseIndex] = useState(0);
const [sleepSummary, setSleepSummary] = useState(null);
const [showStatus, setShowStatus] = useState(false);
const [darkMode, setDarkMode] = useState(localStorage.getItem('soma_dark') === 'true');
const [showAnalyticsModal, setShowAnalyticsModal] = useState(false);
useEffect(() => {
if (darkMode) {
document.body.classList.add('dark-theme');
localStorage.setItem('soma_dark', 'true');
} else {
document.body.classList.remove('dark-theme');
localStorage.setItem('soma_dark', 'false');
}
}, [darkMode]);
useEffect(() => {
if (!username) return;
fetchHistory();
fetchVitals();
const interval = setInterval(fetchVitals, 10000);
return () => clearInterval(interval);
}, [username]);
useEffect(() => {
if (!username) return;
// Verify if session hit is already registered to protect Upstash limits
if (sessionStorage.getItem('soma_hit_registered') === 'true') return;
// Get or create unique persistent visitor ID
let visitorId = localStorage.getItem('soma_visitor_id');
if (!visitorId) {
visitorId = 'visitor_' + Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
localStorage.setItem('soma_visitor_id', visitorId);
}
const registerHit = async () => {
try {
const res = await apiFetch('/api/v1/analytics/hit', {
method: 'POST',
body: JSON.stringify({ visitor_id: visitorId })
});
if (res.ok) {
sessionStorage.setItem('soma_hit_registered', 'true');
}
} catch (error) {
console.error("Failed to register visitor telemetry hit:", error);
}
};
registerHit();
}, [username]);
const fetchHistory = async () => {
try {
const res = await apiFetch('/api/v1/history');
if (res.ok) {
const data = await res.json();
setMessages(data.messages || []);
}
} catch (error) { console.error('History fetch failed', error); }
};
const fetchVitals = async () => {
try {
const res = await apiFetch('/api/v1/brain/vitals');
if (res.ok) {
const data = await res.json();
setVitals(data);
}
} catch (error) { console.error('Vitals fetch failed', error); }
};
const handleSendMessage = async (text) => {
if (!text.trim()) return;
const timestamp = new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
setMessages(prev => [...prev, { role: 'user', content: text, timestamp }]);
setTrace([]);
// Remove artificial frontend phases since backend now streams them.
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 15000); // 15s timeout
const token = localStorage.getItem('soma_token');
const response = await fetch('/api/v1/query/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
},
body: JSON.stringify({ text }),
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok || !response.body) throw new Error('Query failed');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const dataStr = line.slice(6).trim();
if (!dataStr) continue;
try {
const data = JSON.parse(dataStr);
if (data.phase) {
// Live trace and state updates from backend
setCognitiveState(data.phase);
if (data.message) {
setTrace(prev => [{ time: new Date().toLocaleTimeString([], { hour12: false }), ...data }, ...prev]);
}
} else if (data.response) {
// Phase E: Response Generation
setCognitiveState(COGNITIVE_PHASES.RESPONDING);
setMessages(prev => [...prev, {
role: 'soma',
content: data.response,
timestamp: new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
}]);
fetchVitals();
setRefreshTick(prev => prev + 1);
}
} catch (e) {
console.error('JSON parse error', e);
}
}
}
} catch (error) {
console.error('Query failed', error);
setTrace(prev => [{ phase: 'Error', content: 'Neural connection interrupted.', time: 'ERROR' }, ...prev]);
} finally {
// Ensure we always return to idle
setCognitiveState(COGNITIVE_PHASES.IDLE);
}
};
const handleKnowledgeSubmit = async (text) => {
setKnowledgeStatus('Integrating knowledge into neural layers...');
try {
const res = await apiFetch('/api/v1/ingest', {
method: 'POST',
body: JSON.stringify({ text })
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || 'Ingestion failed');
setKnowledgeStatus(data.message || 'Knowledge stored successfully.');
setTimeout(() => setKnowledgeStatus(''), 10000);
fetchVitals();
} catch (error) {
setKnowledgeStatus('Neural integration failed: ' + error.message);
}
};
const handleSleepCycle = async () => {
setCognitiveState('consolidating');
setActivePage('sleep');
setSleepPhaseIndex(0);
setSleepSummary(null);
// Animate checklist steps gracefully during the pending API request
const stepInterval = setInterval(() => {
setSleepPhaseIndex(prev => {
if (prev < 2) return prev + 1;
return prev;
});
}, 1000);
try {
const res = await apiFetch('/api/v1/sleep', { method: 'POST' });
const data = await res.json();
clearInterval(stepInterval);
setSleepPhaseIndex(2); // Mark all checklists as complete
setSleepSummary({
linked: data.graph_relations_extracted || 0,
consolidated: data.summaries_created || 0,
pruned: data.messages_pruned || 0
});
} catch (error) {
clearInterval(stepInterval);
console.error("Sleep cycle failed:", error);
setSleepSummary({ linked: 0, consolidated: 0, pruned: 0 });
} finally {
setCognitiveState(COGNITIVE_PHASES.IDLE);
fetchVitals(); // Instantly update vitals to reflect pruned/cleared working queue
}
};
const handleLogout = () => {
localStorage.clear();
setUsername(null);
setMessages([]);
setActivePage('console');
};
if (!username) return <AuthScreen onAuth={setUsername} darkMode={darkMode} setDarkMode={setDarkMode} />;
const stats = [
{ label: 'Working Memory', value: vitals?.working || '0', icon: 'psychology' },
{ label: 'Sensory Memory', value: vitals?.sensory || '0', icon: 'cloud' },
{ label: 'Semantic Memory', value: vitals?.semantic?.nodes || '0', icon: 'account_tree' },
{ label: 'Neural Sparks', value: vitals?.semantic?.edges || '0', icon: 'auto_awesome' },
];
return (
<div className="soma-shell">
<aside className="soma-sidebar">
<div className="brand-block">
<div className="brand-mark">
<span className="material-icons">lens_blur</span>
</div>
<div className="brand-copy">
<h1>SOMA</h1>
<p>Cognitive Console</p>
</div>
</div>
<nav className="sidebar-nav">
{NAV_ITEMS.map(item => (
<button
key={item.id}
className={`sidebar-link ${activePage === item.id || (activePage === 'activity' && item.id === 'console') ? 'active' : ''}`}
onClick={() => setActivePage(item.id)}
>
<span className="material-icons">{item.icon}</span>
<span>{item.label}</span>
</button>
))}
<button
className={`sidebar-link visitors-btn ${showAnalyticsModal ? 'active' : ''}`}
onClick={() => setShowAnalyticsModal(true)}
>
<span className="material-icons">analytics</span>
<span>Visitors</span>
</button>
<button
className="sidebar-link theme-toggle-btn"
onClick={() => setDarkMode(!darkMode)}
style={{marginTop: '8px'}}
title="Toggle Dark/Light Mode"
>
<span className="material-icons">{darkMode ? 'light_mode' : 'dark_mode'}</span>
<span>{darkMode ? 'Light UI' : 'Dark UI'}</span>
</button>
</nav>
<div className="sidebar-footer">
<div className="session-card">
<div className="session-avatar">
<img src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${username}`} alt="Avatar" />
</div>
<div className="session-copy">
<strong>{username}</strong>
<span>GUEST-7F3A</span>
</div>
<button style={{marginLeft: 'auto', color: '#999', background: 'transparent'}} onClick={handleLogout}>
<span className="material-icons" style={{fontSize: '18px'}}>logout</span>
</button>
</div>
</div>
</aside>
<main className="soma-main-panel">
{activePage === 'console' && (
<section className="page-canvas fade-in">
<div className="page-header" style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
<h2>Cognitive Console</h2>
<button
className={`telemetry-trigger ${showStatus ? 'active' : ''}`}
onClick={() => setShowStatus(!showStatus)}
title="Toggle System Telemetry"
>
<span className="material-icons">analytics</span>
</button>
</div>
<div className="canvas-body" style={{display: 'flex', flex: 1, gap: '40px', minHeight: 0}}>
{/* Interaction Layer (The Chat) */}
<div className="chat-container" style={{flex: 0.7, display: 'flex', flexDirection: 'column'}}>
<ChatPanel
messages={messages}
onSendMessage={handleSendMessage}
onNewChat={() => {
setMessages([]);
setTrace([]);
}}
userAvatar={`https://api.dicebear.com/7.x/avataaars/svg?seed=${username}`}
isTyping={cognitiveState !== COGNITIVE_PHASES.IDLE && cognitiveState !== COGNITIVE_PHASES.LISTENING}
onInputStateChange={(isTyping) => {
if (cognitiveState === COGNITIVE_PHASES.IDLE && isTyping) setCognitiveState(COGNITIVE_PHASES.LISTENING);
if (cognitiveState === COGNITIVE_PHASES.LISTENING && !isTyping) setCognitiveState(COGNITIVE_PHASES.IDLE);
}}
/>
</div>
{/* Cognitive Layer (The Brain) */}
<div className="cognitive-container" style={{flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', position: 'relative'}}>
<CognitiveBrainImageScene state={cognitiveState} />
<div className="status-pill" style={{marginTop: '32px'}}>
<span className="label">Status</span>
<div className="value">
<div className={`status-dot ${cognitiveState !== COGNITIVE_PHASES.IDLE ? 'pulse' : ''}`} />
<span style={{textTransform: 'capitalize'}}>{cognitiveState}</span>
</div>
</div>
</div>
{/* Activity Layer (The Timeline) */}
<div className="activity-feed-wrapper" style={{flex: 0.6, maxWidth: '280px', display: 'flex', flexDirection: 'column', padding: '0', margin: '-50px 0 0 0', height: 'calc(100% + 50px)'}}>
<h3 style={{fontSize: '0.7rem', textTransform: 'uppercase', color: '#999', marginTop: 0, marginBottom: '8px', letterSpacing: '0.1em', fontWeight: 700}}>Activity Feed</h3>
<div style={{flex: 1, overflowY: 'auto', overflowX: 'hidden', paddingRight: '8px'}}>
<CognitiveTimeline trace={trace} />
</div>
</div>
</div>
</section>
)}
{activePage === 'activity' && (
<section className="page-canvas fade-in">
<div className="page-header">
<h2>Neural Activity</h2>
<button className="sidebar-link" style={{background: 'white'}} onClick={() => setActivePage('console')}>
Back to Console
</button>
</div>
<div style={{display: 'grid', gridTemplateColumns: '1.2fr 1fr', gap: '48px', height: '100%'}}>
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
<CognitiveBrainImageScene state={cognitiveState} />
</div>
<div className="status-card" style={{display: 'flex', flexDirection: 'column', height: '90%', padding: '0'}}>
<h3 style={{fontSize: '0.7rem', textTransform: 'uppercase', color: '#999', marginBottom: '32px', letterSpacing: '0.1em', fontWeight: 700}}>Full Activity Log</h3>
<div style={{flex: 1, overflowY: 'auto'}}><CognitiveTimeline trace={trace} /></div>
</div>
</div>
</section>
)}
{showStatus && (
<div className="telemetry-overlay" onClick={() => setShowStatus(false)}>
<div className="telemetry-modal" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h3>System Telemetry</h3>
<button className="modal-close" onClick={() => setShowStatus(false)}>
<span className="material-icons">close</span>
</button>
</div>
<div className="modal-scroll-area">
<CognitiveDashboard statusText={cognitiveState} stats={stats} />
</div>
</div>
</div>
)}
{activePage === 'memory' && (
<section className="page-canvas fade-in">
<div className="page-header"><h2>Neural Memory</h2></div>
<MemoryExplorer />
</section>
)}
{activePage === 'graph' && (
<section className="page-canvas fade-in">
<div className="page-header"><h2>Knowledge Graph</h2></div>
<KnowledgeGraph refreshTick={refreshTick} />
</section>
)}
{activePage === 'knowledge' && (
<section className="page-canvas fade-in">
<div className="page-header">
<h2>Neural Inscription</h2>
</div>
<KnowledgeInput onKnowledgeSubmit={handleKnowledgeSubmit} isBusy={knowledgeStatus.includes('Adding')} status={knowledgeStatus} />
</section>
)}
{activePage === 'sleep' && (
<SleepProgress
phaseIndex={sleepPhaseIndex}
isConsolidating={cognitiveState === 'consolidating'}
onStart={handleSleepCycle}
summary={sleepSummary}
vitals={vitals}
onClose={() => {
setCognitiveState(COGNITIVE_PHASES.IDLE);
setSleepPhaseIndex(0);
setSleepSummary(null);
setActivePage('console');
}}
/>
)}
</main>
{showAnalyticsModal && (
<VisitorAnalytics onClose={() => setShowAnalyticsModal(false)} />
)}
</div>
);
}
export default App;