| |
| |
| |
|
|
| 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> |
| ); |
| } |
|
|