memoryos / src /pages /SessionView.tsx
Deeraj
initial deploy
ebab9b7
Raw
History Blame Contribute Delete
7.82 kB
/**
* SessionView — List and inspect conversation sessions
*/
import { useState } from 'react';
import { getSession } from '../api/client';
import type { SessionWithMemories, MemoryItem } from '../api/client';
export default function SessionView() {
const [sessionId, setSessionId] = useState('');
const [session, setSession] = useState<SessionWithMemories | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const handleFetch = async () => {
if (!sessionId) return;
setLoading(true);
setError(null);
try {
const data = await getSession(sessionId);
setSession(data);
} catch (e) {
setError(e instanceof Error ? e.message : 'Failed to fetch session');
setSession(null);
} finally {
setLoading(false);
}
};
const episodic = session?.memories.filter((m) => m.memory_type === 'episodic') || [];
const hasSummary = episodic.some((m) => m.summary);
return (
<div style={{ padding: '32px', maxWidth: '900px', margin: '0 auto' }}>
<h1 style={{ fontSize: '28px', fontWeight: 700, marginBottom: '8px', color: 'var(--color-text-primary)' }}>
Session View
</h1>
<p style={{ color: 'var(--color-text-muted)', marginBottom: '32px', fontSize: '14px' }}>
Inspect conversation sessions and their episodic timeline
</p>
{/* Session lookup */}
<div className="glass-card" style={{ padding: '20px', marginBottom: '24px' }}>
<div style={{ display: 'flex', gap: '12px' }}>
<input
type="text"
placeholder="Session ID (UUID)"
value={sessionId}
onChange={(e) => setSessionId(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleFetch()}
style={{
flex: 1,
padding: '10px 16px',
borderRadius: '10px',
border: '1px solid var(--color-border)',
background: 'var(--color-surface)',
color: 'var(--color-text-primary)',
fontSize: '14px',
outline: 'none',
}}
/>
<button
onClick={handleFetch}
disabled={loading}
style={{
padding: '10px 24px',
borderRadius: '10px',
border: 'none',
background: 'var(--color-accent)',
color: '#fff',
fontSize: '14px',
fontWeight: 600,
cursor: loading ? 'not-allowed' : 'pointer',
}}
>
{loading ? 'Loading...' : 'Load Session'}
</button>
</div>
</div>
{error && (
<div style={{ padding: '16px', borderRadius: '10px', background: 'rgba(239, 68, 68, 0.1)', border: '1px solid rgba(239, 68, 68, 0.3)', color: '#ef4444', marginBottom: '24px', fontSize: '14px' }}>
{error}
</div>
)}
{session && (
<>
{/* Session info */}
<div className="glass-card" style={{ padding: '20px', marginBottom: '24px' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<InfoItem label="Session ID" value={session.id} />
<InfoItem label="User ID" value={session.user_id} />
<InfoItem label="Started" value={new Date(session.started_at).toLocaleString()} />
<InfoItem label="Ended" value={session.ended_at ? new Date(session.ended_at).toLocaleString() : 'Active'} />
</div>
<div style={{ marginTop: '12px', display: 'flex', gap: '8px' }}>
<span
style={{
padding: '4px 12px',
borderRadius: '8px',
fontSize: '12px',
fontWeight: 600,
background: session.ended_at ? 'rgba(34, 197, 94, 0.15)' : 'rgba(99, 102, 241, 0.15)',
color: session.ended_at ? '#22c55e' : '#818cf8',
}}
>
{session.ended_at ? 'Completed' : '🔴 Active'}
</span>
{hasSummary && (
<span
style={{
padding: '4px 12px',
borderRadius: '8px',
fontSize: '12px',
fontWeight: 600,
background: 'rgba(127, 119, 221, 0.15)',
color: '#7F77DD',
}}
>
✓ Summarised
</span>
)}
<span style={{ padding: '4px 12px', borderRadius: '8px', fontSize: '12px', background: 'var(--color-surface)', border: '1px solid var(--color-border)', color: 'var(--color-text-secondary)' }}>
{session.memories.length} memories
</span>
</div>
</div>
{/* Summary */}
{hasSummary && (
<div className="glass-card" style={{ padding: '20px', marginBottom: '24px', borderLeft: '3px solid #7F77DD' }}>
<h3 style={{ fontSize: '14px', fontWeight: 600, color: '#7F77DD', marginBottom: '8px' }}>Session Summary</h3>
<p style={{ fontSize: '14px', color: 'var(--color-text-primary)', lineHeight: 1.6 }}>
{episodic.find((m) => m.summary)?.summary}
</p>
</div>
)}
{/* Episodic Timeline */}
<h3 style={{ fontSize: '18px', fontWeight: 600, marginBottom: '16px', color: 'var(--color-text-primary)' }}>
Conversation Timeline
</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}>
{episodic.sort((a, b) => (a.metadata as Record<string, number>).turn_index - (b.metadata as Record<string, number>).turn_index).map((mem) => (
<TimelineItem key={mem.id} memory={mem} />
))}
</div>
</>
)}
</div>
);
}
function InfoItem({ label, value }: { label: string; value: string }) {
return (
<div>
<div style={{ fontSize: '11px', color: 'var(--color-text-muted)', marginBottom: '2px', textTransform: 'uppercase', letterSpacing: '0.05em' }}>{label}</div>
<div style={{ fontSize: '13px', color: 'var(--color-text-primary)', fontFamily: 'monospace', wordBreak: 'break-all' }}>{value}</div>
</div>
);
}
function TimelineItem({ memory }: { memory: MemoryItem }) {
const meta = memory.metadata as Record<string, unknown>;
const userMsg = meta.user_message as string || '';
const asstMsg = meta.assistant_message as string || '';
const turnIdx = meta.turn_index as number ?? 0;
return (
<div style={{ display: 'flex', gap: '16px', padding: '16px 0' }}>
{/* Timeline dot */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '24px' }}>
<div style={{ width: '10px', height: '10px', borderRadius: '50%', background: '#378ADD', border: '2px solid var(--color-surface-alt)', flexShrink: 0 }} />
<div style={{ flex: 1, width: '2px', background: 'var(--color-border)', marginTop: '4px' }} />
</div>
{/* Content */}
<div style={{ flex: 1, paddingBottom: '8px' }}>
<div style={{ fontSize: '11px', color: 'var(--color-text-muted)', marginBottom: '8px' }}>Turn {turnIdx}</div>
<div className="chat-bubble-user" style={{ padding: '10px 14px', marginBottom: '8px', maxWidth: '80%' }}>
<span style={{ fontSize: '13px', color: '#fff' }}>{userMsg}</span>
</div>
<div className="chat-bubble-assistant" style={{ padding: '10px 14px', maxWidth: '80%' }}>
<span style={{ fontSize: '13px', color: 'var(--color-text-primary)' }}>{asstMsg}</span>
</div>
</div>
</div>
);
}