import { useEffect, useState, useRef, useCallback } from 'react' import { Search } from 'lucide-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' const PAGE_SIZE = 25 const TABS = [ { id: 'ai', label: 'AI Sessions' }, { id: 'flows', label: 'Flow Sessions' }, ] as const type TabId = typeof TABS[number]['id'] export default function SessionHistoryPage() { const navigate = useNavigate() const [searchParams, setSearchParams] = useSearchParams() const [activeTab, setActiveTab] = useState(() => { // If URL params target flow session filters, start on flows tab const hasFlowParams = searchParams.get('ticket') || searchParams.get('client') || searchParams.get('tree') return hasFlowParams ? 'flows' : 'ai' }) // ── AI Session state ── const [aiSessions, setAiSessions] = useState([]) const [aiLoading, setAiLoading] = useState(false) const [aiLoadingMore, setAiLoadingMore] = useState(false) const [aiHasMore, setAiHasMore] = useState(false) const [aiSearchInput, setAiSearchInput] = useState('') const aiSearchTimeout = useRef | undefined>(undefined) const aiFilterGenRef = useRef(0) const [aiFilters, setAiFilters] = useState({ q: '', session_type: '', problem_domain: '', confidence_tier: '', date_from: '', date_to: '', }) // ── Flow Session state ── const [sessions, setSessions] = useState([]) const [flowLoading, setFlowLoading] = useState(false) const [flowHasMore, setFlowHasMore] = useState(false) const [trees, setTrees] = useState([]) const [flowTab, setFlowTab] = 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) 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 } }) // ── AI Sessions: debounce search ── useEffect(() => { if (aiSearchTimeout.current) clearTimeout(aiSearchTimeout.current) aiSearchTimeout.current = setTimeout(() => { setAiFilters(prev => ({ ...prev, q: aiSearchInput })) }, 400) return () => { if (aiSearchTimeout.current) clearTimeout(aiSearchTimeout.current) } }, [aiSearchInput]) // ── AI Sessions: fetch ── useEffect(() => { let cancelled = false const gen = ++aiFilterGenRef.current setAiSessions([]) setAiHasMore(false) const load = async () => { setAiLoading(true) try { const data = await aiSessionsApi.listSessions({ limit: PAGE_SIZE, q: aiFilters.q || undefined, session_type: aiFilters.session_type || undefined, problem_domain: aiFilters.problem_domain || undefined, confidence_tier: aiFilters.confidence_tier || undefined, date_from: aiFilters.date_from || undefined, date_to: aiFilters.date_to ? `${aiFilters.date_to}T23:59:59.999Z` : undefined, }) if (!cancelled && gen === aiFilterGenRef.current) { setAiSessions(data) setAiHasMore(data.length >= PAGE_SIZE) } } catch { if (!cancelled) toast.error('Failed to load AI sessions') } finally { if (!cancelled) setAiLoading(false) } } load() return () => { cancelled = true } }, [aiFilters]) const loadMoreAiSessions = async () => { const gen = aiFilterGenRef.current setAiLoadingMore(true) try { const data = await aiSessionsApi.listSessions({ skip: aiSessions.length, limit: PAGE_SIZE, q: aiFilters.q || undefined, session_type: aiFilters.session_type || undefined, problem_domain: aiFilters.problem_domain || undefined, confidence_tier: aiFilters.confidence_tier || undefined, date_from: aiFilters.date_from || undefined, date_to: aiFilters.date_to ? `${aiFilters.date_to}T23:59:59.999Z` : undefined, }) if (gen === aiFilterGenRef.current) { setAiSessions(prev => [...prev, ...data]) setAiHasMore(data.length >= PAGE_SIZE) } } catch { toast.error('Failed to load more sessions') } finally { setAiLoadingMore(false) } } // ── Dynamic problem domains derived from loaded sessions ── const problemDomains = [...new Set(aiSessions.map(s => s.problem_domain).filter(Boolean))] as string[] // ── Flow Sessions: load trees ── useEffect(() => { treesApi.list({}).then(setTrees).catch(() => {}) }, []) // ── Flow Sessions: fetch ── useEffect(() => { if (activeTab !== 'flows') return let cancelled = false const load = async () => { setFlowLoading(true) try { const params: Record = {} if (flowTab === 'prepared') { params.status = 'prepared' } else if (flowTab !== 'all') { params.completed = flowTab === 'completed' } if (filters.ticketNumber) params.ticket_number = filters.ticketNumber if (filters.clientName) params.client_name = filters.clientName if (filters.treeName) params.tree_name = filters.treeName if (filters.dateRange?.from) { const fromDate = filters.dateRange.from const toDate = filters.dateRange.to || filters.dateRange.from const toDateEnd = new Date(toDate) toDateEnd.setHours(23, 59, 59, 999) if (filters.dateType === 'started') { params.started_after = fromDate.toISOString() params.started_before = toDateEnd.toISOString() } else { params.completed_after = fromDate.toISOString() params.completed_before = toDateEnd.toISOString() } } const data = await sessionsApi.list({ ...params, size: PAGE_SIZE + 1 }) if (cancelled) return const truncated = data.length > PAGE_SIZE setFlowHasMore(truncated) setSessions(truncated ? data.slice(0, PAGE_SIZE) : data) } catch { if (!cancelled) toast.error('Failed to load sessions') } finally { if (!cancelled) setFlowLoading(false) } } load() return () => { cancelled = true } }, [activeTab, flowTab, filters]) // ── Flow Sessions: URL param sync ── 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]) // ── Close session handlers ── 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]) 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 handleFilterChange = (newFilters: SessionFilterState) => setFilters(newFilters) const handleClearFilters = () => setFilters({ ticketNumber: '', clientName: '', treeName: '', dateRange: undefined, dateType: 'started' }) const formatDate = (dateString: string) => new Date(dateString).toLocaleString() const getTreeName = (session: Session): string => 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 } const hasAiFiltersActive = !!(aiSearchInput || aiFilters.q || aiFilters.session_type || aiFilters.problem_domain || aiFilters.confidence_tier || aiFilters.date_from || aiFilters.date_to) const hasFlowFiltersActive = !!(filters.ticketNumber || filters.clientName || filters.treeName || filters.dateRange?.from) return (
{/* Page heading */}

Session History

View and manage your sessions

{/* Tab bar */}
{TABS.map((tab) => ( ))}
{/* ════════ AI Sessions Tab ════════ */} {activeTab === 'ai' && ( <> {/* AI Session Filter Bar */}
{/* Search input */}
setAiSearchInput(e.target.value)} placeholder="Search sessions..." className="w-full rounded-lg border border-border bg-card pl-8 pr-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(249,115,22,0.25)] focus:ring-1 focus:ring-[rgba(249,115,22,0.1)] focus:outline-none" />
{/* Session type pills */}
{(['', 'guided', 'chat'] as const).map((t) => ( ))}
{/* Problem domain dropdown — dynamic */} {/* Confidence tier pills */}
{(['', 'guided', 'exploring', 'discovery'] as const).map((tier) => ( ))}
{/* Date range inputs */}
setAiFilters((f) => ({ ...f, date_from: e.target.value }))} title="From date" className="rounded-lg border border-border bg-card px-3 py-1.5 text-sm text-foreground focus:border-[rgba(249,115,22,0.25)] focus:ring-1 focus:ring-[rgba(249,115,22,0.1)] focus:outline-none [color-scheme:dark]" /> to setAiFilters((f) => ({ ...f, date_to: e.target.value }))} title="To date" className="rounded-lg border border-border bg-card px-3 py-1.5 text-sm text-foreground focus:border-[rgba(249,115,22,0.25)] focus:ring-1 focus:ring-[rgba(249,115,22,0.1)] focus:outline-none [color-scheme:dark]" />
{/* Clear filters */} {hasAiFiltersActive && ( )}
{/* AI Session list */} {aiLoading ? (
) : aiSessions.length === 0 ? ( hasAiFiltersActive ? ( { setAiSearchInput('') setAiFilters({ q: '', session_type: '', problem_domain: '', confidence_tier: '', date_from: '', date_to: '' }) }} className="text-foreground hover:underline text-sm" > Clear all filters } /> ) : ( } title="No AI sessions yet" description="Start a FlowPilot or chat session to begin. All your sessions will appear here." action={ Start a Session } /> ) ) : ( <>
{aiSessions.map((s) => ( ))}
{/* Load more / count */}
{aiHasMore ? ( ) : (

Showing all {aiSessions.length} session{aiSessions.length !== 1 ? 's' : ''}

)}
)} )} {/* ════════ Flow Sessions Tab ════════ */} {activeTab === 'flows' && ( <> {/* Flow tab sub-filters */}
{(['active', 'prepared', 'completed', 'all'] as const).map((tab) => ( ))}
{/* Flow session list */} {flowLoading ? (
) : sessions.length === 0 ? ( hasFlowFiltersActive ? ( Clear all filters } /> ) : ( } title="Your flow sessions will appear here" description="Every troubleshooting session is recorded with decisions, timing, and outcomes." action={ Start a Flow } /> ) ) : ( <>
{sessions.map((session, i) => (
{session.ticket_number || 'No ticket'} {session.client_name && ( {session.client_name} )} {session.completed_at && ( {formatOutcomeLabel(session.outcome)} )}

Tree: {getTreeName(session)}

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

{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