import { useEffect, useState } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { Copy, Check, Eye, Save, Share2, CheckCircle2, AlertTriangle, ArrowUpRight, HelpCircle, Flag, Sparkles, Loader2 } from 'lucide-react' import { Button } from '@/components/ui/Button' import { sessionsApi } from '@/api/sessions' import { stepsApi } from '@/api/steps' import { treesApi } from '@/api/trees' import { sessionToFlowApi } from '@/api/sessionToFlow' import { ExportPreviewModal } from '@/components/session/ExportPreviewModal' import { SaveSessionAsTreeModal } from '@/components/session/SaveSessionAsTreeModal' import { analytics } from '@/lib/analytics' import { ShareSessionModal } from '@/components/session/ShareSessionModal' import { SessionOutcomeModal } from '@/components/session/SessionOutcomeModal' import { SessionTimeline } from '@/components/session/SessionTimeline' import { StepRatingModal } from '@/components/session/StepRatingModal' import { ActionMenu } from '@/components/common/ActionMenu' import type { MenuAction } from '@/components/common/ActionMenu' import type { SessionOutcome } from '@/types' import { useUserPreferencesStore } from '@/store/userPreferencesStore' import type { Session, SessionExport, SaveAsTreeRequest, Step, RedactionSummary } from '@/types' import { hasRatedSession, markSessionRated } from '@/lib/sessionRatings' import { Spinner } from '@/components/common/Spinner' import { cn } from '@/lib/utils' import { toast } from '@/lib/toast' import { getTreeEditorPath } from '@/lib/routing' export function SessionDetailPage() { const { id } = useParams<{ id: string }>() const navigate = useNavigate() const { defaultExportFormat } = useUserPreferencesStore() const [session, setSession] = useState(null) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const [isExporting, setIsExporting] = useState(false) const [exportFormat, setExportFormat] = useState<'markdown' | 'text' | 'html' | 'psa' | 'pdf'>(defaultExportFormat) const [exportContent, setExportContent] = useState(null) const [showPreview, setShowPreview] = useState(false) const [copied, setCopied] = useState(false) const [copiedPsa, setCopiedPsa] = useState(false) const [showSaveAsTreeModal, setShowSaveAsTreeModal] = useState(false) const [isSavingTree, setIsSavingTree] = useState(false) const [showRatingModal, setShowRatingModal] = useState(false) const [isSavingRatings, setIsSavingRatings] = useState(false) const [librarySteps, setLibrarySteps] = useState([]) const [showShareModal, setShowShareModal] = useState(false) const [showOutcomeModal, setShowOutcomeModal] = useState(false) const [isCompleting, setIsCompleting] = useState(false) const [maxStepIndex, setMaxStepIndex] = useState(null) const [detailLevel, setDetailLevel] = useState<'standard' | 'full'>('standard') const [includeSummary, setIncludeSummary] = useState(false) const [redactionMode, setRedactionMode] = useState<'none' | 'mask'>('none') const [redactionSummary, setRedactionSummary] = useState(null) const [isGeneratingFlow, setIsGeneratingFlow] = useState(false) const [pdfLoading, setPdfLoading] = useState(false) useEffect(() => { if (id) { loadSession() } }, [id]) // Auto-show rating modal for completed sessions with library steps useEffect(() => { if (!session || !session.completed_at) return // Check if already rated if (hasRatedSession(session.id)) return // Extract library steps from custom_steps const stepsFromLibrary = session.custom_steps?.filter( (customStep) => { // Check if step_data is a Step (from library) by checking if it has an id const stepData = customStep.step_data return 'id' in stepData && stepData.id } ) || [] if (stepsFromLibrary.length === 0) return // Extract the Step objects const steps = stepsFromLibrary.map((cs) => cs.step_data as Step) setLibrarySteps(steps) // Show modal after 1 second delay const timer = setTimeout(() => { setShowRatingModal(true) }, 1000) return () => clearTimeout(timer) }, [session]) const loadSession = async () => { setIsLoading(true) setError(null) try { const data = await sessionsApi.get(id!) setSession(data) } catch (err) { setError('Failed to load session') console.error(err) } finally { setIsLoading(false) } } const getFilename = () => { if (!session) return 'export.txt' const ext = exportFormat === 'markdown' ? 'md' : exportFormat === 'html' ? 'html' : 'txt' // psa and text both use .txt return `session-${session.ticket_number || session.id}.${ext}` } const buildExportOptions = (overrides?: Partial): SessionExport => ({ format: exportFormat, include_timestamps: true, include_tree_info: true, ...(maxStepIndex !== null && { max_step_index: maxStepIndex }), detail_level: detailLevel, include_summary: includeSummary, redaction_mode: redactionMode, ...overrides, }) const fetchExportContent = async () => { if (!session) return null const result = await sessionsApi.exportWithMeta(session.id, buildExportOptions()) setRedactionSummary(result.redactionSummary) return result.content } const handlePreview = async () => { setIsExporting(true) try { const content = await fetchExportContent() if (content) { setExportContent(content) setShowPreview(true) } } catch (err) { console.error('Export failed:', err) toast.error('Failed to generate export preview') } finally { setIsExporting(false) } } const handleCopy = async () => { setIsExporting(true) try { const content = await fetchExportContent() if (content) { await navigator.clipboard.writeText(content) setCopied(true) setTimeout(() => setCopied(false), 2000) toast.success('Copied to clipboard') analytics.exportGenerated({ session_id: session?.id || '', format: exportFormat }) } } catch (err) { console.error('Copy failed:', err) toast.error('Failed to copy to clipboard') } finally { setIsExporting(false) } } const handleCopyForTicket = async () => { if (!session) return try { const content = await sessionsApi.export(session.id, buildExportOptions({ format: 'psa' })) if (content) { await navigator.clipboard.writeText(content) setCopiedPsa(true) setTimeout(() => setCopiedPsa(false), 2000) toast.success('Copied ticket notes to clipboard') analytics.exportGenerated({ session_id: session.id, format: 'psa' }) } } catch (err) { console.error('Copy for ticket failed:', err) toast.error('Failed to copy ticket notes') } } const handleDownload = (content: string) => { if (!session) return analytics.exportGenerated({ session_id: session.id, format: exportFormat }) const blob = new Blob([content], { type: 'text/plain' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = getFilename() document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) } const handleToggleSummary = async (include: boolean) => { setIncludeSummary(include) if (!session) return try { const result = await sessionsApi.exportWithMeta(session.id, buildExportOptions({ include_summary: include })) setExportContent(result.content) setRedactionSummary(result.redactionSummary) toast.success(include ? 'Summary added' : 'Summary removed') } catch (err) { console.error('Failed to re-fetch export:', err) toast.error('Failed to update export') setIncludeSummary(!include) } } const handleToggleRedaction = async (enabled: boolean) => { const mode = enabled ? 'mask' as const : 'none' as const setRedactionMode(mode) if (!session) return try { const result = await sessionsApi.exportWithMeta(session.id, buildExportOptions({ redaction_mode: mode })) setExportContent(result.content) setRedactionSummary(result.redactionSummary) toast.success(enabled ? 'Sensitive data masked' : 'Redaction removed') } catch (err) { console.error('Failed to re-fetch export:', err) toast.error('Failed to update export') setRedactionMode(enabled ? 'none' : 'mask') } } const handleDownloadPdf = async () => { if (!session) return setPdfLoading(true) try { const { apiClient } = await import('@/api/client') const response = await apiClient.post( `/sessions/${session.id}/export`, { format: 'pdf' }, { responseType: 'blob' } ) const url = URL.createObjectURL(response.data) const a = document.createElement('a') a.href = url a.download = `session-export-${session.id}.pdf` a.click() URL.revokeObjectURL(url) analytics.exportGenerated({ session_id: session.id, format: 'pdf' }) } catch (error) { console.error('PDF export failed:', error) toast.error('Failed to generate PDF export') } finally { setPdfLoading(false) } } const handleSaveAsTree = async (data: SaveAsTreeRequest) => { if (!session) return setIsSavingTree(true) try { const result = await sessionsApi.saveAsTree(session.id, data) toast.success(result.message) setShowSaveAsTreeModal(false) // Navigate to tree editor with the new tree navigate(`/trees/${result.tree_id}/edit`) } catch (err) { console.error('Failed to save session as tree:', err) toast.error('Failed to save session as tree') } finally { setIsSavingTree(false) } } const handleCompleteSession = async (data: { outcome: SessionOutcome; outcome_notes?: string; next_steps?: string }) => { if (!session) return setIsCompleting(true) try { const updated = await sessionsApi.complete(session.id, data) setSession(updated) setShowOutcomeModal(false) toast.success('Session completed') } catch { toast.error('Failed to complete session') } finally { setIsCompleting(false) } } const handleCreateFlowFromSession = async () => { if (!session) return setIsGeneratingFlow(true) try { const flowData = await sessionToFlowApi.generate(session.id) analytics.aiFeatureUsed({ feature: 'session_to_flow' }) const tree = await treesApi.create({ name: flowData.name, description: flowData.description, tree_type: flowData.tree_type as import('@/types').TreeType, tree_structure: flowData.tree_structure, tags: flowData.tags, }) toast.success('Flow generated! Opening editor...') analytics.flowCreated({ flow_type: 'procedural', method: 'session_to_flow' }) navigate(getTreeEditorPath(tree.id, 'procedural')) } catch (err) { console.error('Failed to generate flow from session:', err) toast.error('Failed to generate flow. Please try again.') } finally { setIsGeneratingFlow(false) } } const getDefaultTreeName = () => { if (!session) return '' const treeName = session.tree_snapshot?.name || 'Tree' const ticket = session.ticket_number ? ` - ${session.ticket_number}` : '' return `${treeName}${ticket}` } const handleSubmitRatings = async (ratings: Map) => { if (!session) return setIsSavingRatings(true) try { // Submit each rating individually const ratingPromises = Array.from(ratings.entries()).map(([stepId, data]) => stepsApi.rate(stepId, { rating: data.rating, review_text: data.review || undefined, was_helpful: data.helpful !== null ? data.helpful : undefined, session_id: session.id }) ) await Promise.all(ratingPromises) toast.success(`Submitted ${ratings.size} rating${ratings.size > 1 ? 's' : ''}!`) markSessionRated(session.id) setShowRatingModal(false) } catch (err) { console.error('Failed to submit ratings:', err) toast.error('Failed to submit ratings') } finally { setIsSavingRatings(false) } } const formatDuration = (durationSeconds: number | null | undefined) => { if (durationSeconds == null || durationSeconds < 0) return null if (durationSeconds < 60) return `${durationSeconds}s` const hours = Math.floor(durationSeconds / 3600) const minutes = Math.floor((durationSeconds % 3600) / 60) const seconds = durationSeconds % 60 if (hours > 0) return seconds > 0 ? `${hours}h ${minutes}m ${seconds}s` : `${hours}h ${minutes}m` return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m` } const getTotalDuration = () => { if (!session?.completed_at) return 'In progress' const startedAtMs = new Date(session.started_at || Date.now()).getTime() const completedAtMs = new Date(session.completed_at).getTime() if (Number.isNaN(startedAtMs) || Number.isNaN(completedAtMs)) return 'Unknown' const seconds = Math.max(0, Math.floor((completedAtMs - startedAtMs) / 1000)) return formatDuration(seconds) || '0s' } const outcomeLabel = session?.outcome ? session.outcome === 'workaround' ? 'Workaround' : session.outcome.charAt(0).toUpperCase() + session.outcome.slice(1) : null if (isLoading) { return (
) } if (error || !session) { return (
{error || 'Session not found'}
) } // Outcome display config const OUTCOME_CONFIG: Record = { resolved: { icon: , color: 'text-emerald-400', bg: 'bg-emerald-500/10', border: 'border-emerald-500/20' }, workaround: { icon: , color: 'text-amber-400', bg: 'bg-amber-500/10', border: 'border-amber-500/20' }, escalated: { icon: , color: 'text-red-400', bg: 'bg-red-500/10', border: 'border-red-500/20' }, unresolved: { icon: , color: 'text-muted-foreground', bg: 'bg-muted', border: 'border-border' }, } const outcomeConfig = session.outcome ? OUTCOME_CONFIG[session.outcome] : null return (
{/* Back nav */} {/* Page title row */}

{session.ticket_number || 'Session Details'}

{session.tree_snapshot?.name} {session.client_name && <> · Client: {session.client_name}} {session.started_at && <>{' · '}{new Date(session.started_at).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}}

setShowShareModal(true) }, ...(session.completed_at ? [{ label: 'Save as Tree', icon: Save, onClick: () => setShowSaveAsTreeModal(true) }] as MenuAction[] : []), ]} />
{/* Session summary card */} {session.completed_at && outcomeConfig ? (
{outcomeConfig.icon}
{outcomeLabel} · {getTotalDuration()}
{session.outcome_notes && (

{session.outcome_notes}

)} {session.next_steps && (
Next Steps

{session.next_steps}

)}
{/* Primary action: Copy for Ticket */}
) : null} {/* Create Flow from Session — only for completed sessions */} {session.completed_at && (
)} {!session.completed_at ? ( /* In-progress banner */

Session in progress

Set an outcome to finalize this session and generate documentation.

) : null} {/* Export toolbar (secondary) */}
{session.decisions.length > 1 && ( )} {/* Copy for ticket (secondary position when session is complete) */} {session.completed_at && ( )}
{/* Timeline / Step Checklist */} ).tree_type as string} startedAt={session.started_at || ''} completedAt={session.completed_at} /> {/* Export Preview Modal */} setShowPreview(false)} content={exportContent || ''} filename={getFilename()} format={exportFormat} onDownload={handleDownload} onDownloadPdf={handleDownloadPdf} pdfLoading={pdfLoading} includeSummary={includeSummary} onToggleSummary={handleToggleSummary} redactionEnabled={redactionMode === 'mask'} onToggleRedaction={handleToggleRedaction} redactionSummary={redactionSummary} /> {/* Save as Tree Modal */} setShowSaveAsTreeModal(false)} onSave={handleSaveAsTree} defaultTreeName={getDefaultTreeName()} isSaving={isSavingTree} /> {/* Step Rating Modal */} setShowRatingModal(false)} onSubmit={handleSubmitRatings} librarySteps={librarySteps} isSaving={isSavingRatings} /> {/* Share Session Modal */} setShowShareModal(false)} /> {/* Complete Session Modal (in-progress sessions) */} setShowOutcomeModal(false)} onSubmit={handleCompleteSession} isSubmitting={isCompleting} />
) } export default SessionDetailPage