import { useEffect, useState } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { Copy, Check, Eye, Save, Share2 } from 'lucide-react' import { sessionsApi } from '@/api/sessions' import { stepsApi } from '@/api/steps' import { ExportPreviewModal } from '@/components/session/ExportPreviewModal' import { SaveSessionAsTreeModal } from '@/components/session/SaveSessionAsTreeModal' import { ShareSessionModal } from '@/components/session/ShareSessionModal' 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 { useUserPreferencesStore } from '@/store/userPreferencesStore' import type { Session, SessionExport, SaveAsTreeRequest, Step, RedactionSummary } from '@/types' import { hasRatedSession, markSessionRated } from '@/lib/sessionRatings' import { cn } from '@/lib/utils' import { toast } from '@/lib/toast' 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'>(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 [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) 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') } } 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') } } catch (err) { console.error('Copy for ticket failed:', err) toast.error('Failed to copy ticket notes') } } const handleDownload = (content: string) => { if (!session) return 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 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 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, is_verified_use: true }) ) 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).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'}
) } return (
{/* Header */}

{session.ticket_number || 'Session Details'}

{session.completed_at ? 'Completed' : 'In Progress'} {session.client_name && Client: {session.client_name}} {session.completed_at && ( Duration: {getTotalDuration()} )} {outcomeLabel && ( Outcome: {outcomeLabel} )}
{session.outcome_notes && (

Outcome Notes: {session.outcome_notes}

)} {session.next_steps && (
Next Steps:

{session.next_steps}

)}
{/* Actions */}
setShowShareModal(true), }, ...(session.completed_at ? [{ label: 'Save as Tree', icon: Save, onClick: () => setShowSaveAsTreeModal(true), }] as MenuAction[] : []), ]} /> {/* Copy for Ticket */} {/* Export Controls */}
{session.decisions.length > 1 && ( )}
{/* 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} 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)} />
) } export default SessionDetailPage