import { useEffect, useState, useRef, useCallback } from 'react' import { Link, useNavigate, useSearchParams } from 'react-router-dom' import { PageMeta } from '@/components/common/PageMeta' import { sessionsApi } from '@/api/sessions' import { aiSessionsApi } from '@/api/aiSessions' import { treesApi } from '@/api/trees' import type { Session, TreeListItem, SessionOutcome, AISessionSummary } from '@/types' import type { DateRange } from 'react-day-picker' import { SessionFilters } from '@/components/session/SessionFilters' import type { SessionFilterState } from '@/components/session/SessionFilters' import { Spinner } from '@/components/common/Spinner' import { EmptyState } from '@/components/common/EmptyState' import { SessionIllustration } from '@/components/common/EmptyStateIllustrations' import { AISessionListItem } from '@/components/flowpilot/AISessionListItem' import { cn } from '@/lib/utils' import { toast } from '@/lib/toast' import { getSessionResumePath } from '@/lib/routing' export function SessionHistoryPage() { const navigate = useNavigate() const [searchParams, setSearchParams] = useSearchParams() // Top-level tab: flow sessions vs AI sessions const [sessionType, setSessionType] = useState<'flow' | 'ai'>('flow') const [aiSessions, setAiSessions] = useState([]) const [aiLoading, setAiLoading] = useState(false) const [sessions, setSessions] = useState([]) const [hasMore, setHasMore] = useState(false) const [trees, setTrees] = useState([]) const [isLoading, setIsLoading] = useState(true) const [filter, setFilter] = useState<'all' | 'completed' | 'active' | 'prepared'>('active') // Close session popover state const [closingSessionId, setClosingSessionId] = useState(null) const [closeOutcome, setCloseOutcome] = useState('') const [closeNotes, setCloseNotes] = useState('') const [closeLoading, setCloseLoading] = useState(false) const closePopoverRef = useRef(null) // Initialize filters from URL params const [filters, setFilters] = useState(() => { const ticketNumber = searchParams.get('ticket') || '' const clientName = searchParams.get('client') || '' const treeName = searchParams.get('tree') || '' const dateType = (searchParams.get('dateType') || 'started') as 'started' | 'completed' const from = searchParams.get('from') const to = searchParams.get('to') const dateRange: DateRange | undefined = from && to ? { from: new Date(from), to: new Date(to) } : undefined return { ticketNumber, clientName, treeName, dateRange, dateType, } }) // Load trees for filter dropdown useEffect(() => { const loadTrees = async () => { try { const treesData = await treesApi.list({}) setTrees(treesData) } catch (err) { console.error('Failed to load trees:', err) } } loadTrees() }, []) // Load sessions when filters change useEffect(() => { let cancelled = false const loadSessions = async () => { setIsLoading(true) try { const params: Record = {} // Tab filter (all/active/completed/prepared) if (filter === 'prepared') { params.status = 'prepared' } else if (filter !== 'all') { params.completed = filter === 'completed' } // Search/filter params if (filters.ticketNumber) { params.ticket_number = filters.ticketNumber } if (filters.clientName) { params.client_name = filters.clientName } if (filters.treeName) { params.tree_name = filters.treeName } // Date range params if (filters.dateRange?.from) { const fromDate = filters.dateRange.from const toDate = filters.dateRange.to || filters.dateRange.from if (filters.dateType === 'started') { params.started_after = fromDate.toISOString() params.started_before = toDate.toISOString() } else { params.completed_after = fromDate.toISOString() params.completed_before = toDate.toISOString() } } const sessionsData = await sessionsApi.list({ ...params, size: 51 }) if (cancelled) return const truncated = sessionsData.length > 50 setHasMore(truncated) setSessions(truncated ? sessionsData.slice(0, 50) : sessionsData) } catch (err) { if (cancelled) return toast.error('Failed to load sessions') console.error(err) } finally { if (!cancelled) setIsLoading(false) } } loadSessions() return () => { cancelled = true } }, [filter, filters]) // Update URL params when filters change useEffect(() => { const params = new URLSearchParams() if (filters.ticketNumber) params.set('ticket', filters.ticketNumber) if (filters.clientName) params.set('client', filters.clientName) if (filters.treeName) params.set('tree', filters.treeName) if (filters.dateRange?.from) { params.set('from', filters.dateRange.from.toISOString()) params.set('to', (filters.dateRange.to || filters.dateRange.from).toISOString()) params.set('dateType', filters.dateType) } setSearchParams(params, { replace: true }) }, [filters, setSearchParams]) // Load AI sessions when tab is active useEffect(() => { if (sessionType !== 'ai') return let cancelled = false const loadAiSessions = async () => { setAiLoading(true) try { const data = await aiSessionsApi.listSessions({ limit: 50 }) if (!cancelled) setAiSessions(data) } catch { if (!cancelled) toast.error('Failed to load AI sessions') } finally { if (!cancelled) setAiLoading(false) } } loadAiSessions() return () => { cancelled = true } }, [sessionType]) const handleFilterChange = (newFilters: SessionFilterState) => { setFilters(newFilters) } const handleClearFilters = () => { setFilters({ ticketNumber: '', clientName: '', treeName: '', dateRange: undefined, dateType: 'started', }) } const handleCloseSession = useCallback(async () => { if (!closingSessionId || !closeOutcome) return setCloseLoading(true) try { await sessionsApi.complete(closingSessionId, { outcome: closeOutcome, outcome_notes: closeNotes || undefined, }) setSessions(prev => prev.map(s => s.id === closingSessionId ? { ...s, completed_at: new Date().toISOString(), outcome: closeOutcome, outcome_notes: closeNotes || null } : s ) ) toast.success('Session closed') setClosingSessionId(null) setCloseOutcome('') setCloseNotes('') } catch { toast.error('Failed to close session') } finally { setCloseLoading(false) } }, [closingSessionId, closeOutcome, closeNotes]) // Close popover on click outside useEffect(() => { if (!closingSessionId) return const handleClickOutside = (e: MouseEvent) => { if (closePopoverRef.current && !closePopoverRef.current.contains(e.target as Node)) { setClosingSessionId(null) setCloseOutcome('') setCloseNotes('') } } document.addEventListener('mousedown', handleClickOutside) return () => document.removeEventListener('mousedown', handleClickOutside) }, [closingSessionId]) const formatDate = (dateString: string) => { return new Date(dateString).toLocaleString() } const getTreeName = (session: Session): string => { return session.tree_snapshot?.name || 'Unknown Tree' } const formatOutcomeLabel = (outcome: Session['outcome']): string => { if (!outcome) return 'Not set' const labels: Record = { resolved: 'Resolved', escalated: 'Escalated', workaround: 'Workaround', unresolved: 'Unresolved', cancelled: 'Cancelled', resolved_externally: 'Resolved Externally', } return labels[outcome] ?? outcome } return (

Session History

Search and filter your troubleshooting sessions

{/* Session type toggle */}
{/* Filter Tabs (flow sessions only) */} {sessionType === 'flow' && (
{(['active', 'prepared', 'completed', 'all'] as const).map((tab) => ( ))}
)} {/* AI Sessions view */} {sessionType === 'ai' && ( aiLoading ? (
) : aiSessions.length === 0 ? ( Start AI Session } /> ) : (
{aiSessions.map((s) => ( ))}
) )} {/* Flow Sessions Content */} {sessionType === 'flow' && ( <>
{/* Loading State */} {isLoading ? (
) : sessions.length === 0 ? ( (filters.ticketNumber || filters.clientName || filters.treeName || filters.dateRange?.from) ? ( Clear all filters } /> ) : ( } title="Your session history will appear here" description="Every troubleshooting session is recorded with decisions, timing, and outcomes — ready for export or review." action={ Start a Session } learnMoreLink="/guides/sessions" /> ) ) : ( <>
{sessions.map((session, i) => (
{/* Status and Ticket/Client */}
{session.ticket_number || 'No ticket'} {session.client_name && ( {session.client_name} )} {session.completed_at && ( {formatOutcomeLabel(session.outcome)} )}
{/* Tree Name */}

Tree: {getTreeName(session)}

{/* Timestamps */}

Started: {session.started_at ? formatDate(session.started_at) : 'Not started'} {session.completed_at && ( <> · Completed: {formatDate(session.completed_at)} )}

{/* Stats */}

{session.decisions.length} decision{session.decisions.length !== 1 ? 's' : ''} recorded {session.scratchpad && session.scratchpad.trim() && ( · Has notes )}

{/* Actions */}
{!session.completed_at && session.started_at && ( <> )} {/* Close Session Popover */} {closingSessionId === session.id && (

Close Session