Files
resolutionflow/frontend/src/components/flowpilot/FlowPilotSession.tsx
chihlasm 483653f5fc fix: remove unused props from FlowPilotSession after action bar removal
canResolve, canEscalate, onResolve, onEscalate, onPause, onAbandon are
now handled at the page level in FlowPilotSessionPage header. Remove
from component interface to fix TS6133 build errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:45:20 +00:00

358 lines
14 KiB
TypeScript

import { useEffect, useRef, useState } from 'react'
import { Network, Clock, Hash, Play, Ticket, ChevronDown, ChevronUp, FileText } from 'lucide-react'
import type {
AISessionDetail,
AISessionStepResponse,
StepResponseRequest,
SessionDocumentation,
StatusUpdateAudience,
StatusUpdateLength,
StatusUpdateContext,
StatusUpdateResponse,
} from '@/types/ai-session'
import { ConfidenceIndicator } from './ConfidenceIndicator'
import { FlowPilotStepCard } from './FlowPilotStepCard'
import { FlowPilotMessageBar } from './FlowPilotMessageBar'
import { SessionDocView } from './SessionDocView'
import { StatusUpdateModal } from './StatusUpdateModal'
import { SessionTicketCard } from './SessionTicketCard'
import { SimilarSessions } from './SimilarSessions'
import { TicketPickerModal } from '@/components/session/TicketPickerModal'
import { aiSessionsApi } from '@/api'
import { toast } from '@/lib/toast'
import type { PSATicketInfo } from '@/types/integrations'
interface FlowPilotSessionProps {
session: AISessionDetail
allSteps: AISessionStepResponse[]
currentStep: AISessionStepResponse | null
isProcessing: boolean
documentation: SessionDocumentation | null
psaPushStatus?: string | null
psaPushError?: string | null
memberMappingWarning?: string | null
onRespond: (response: StepResponseRequest) => void
onResume?: () => Promise<void>
onRate: (rating: number) => void
onReloadSession?: () => Promise<void>
onGenerateStatusUpdate?: (audience: StatusUpdateAudience, length: StatusUpdateLength, context: StatusUpdateContext) => Promise<StatusUpdateResponse>
}
export function FlowPilotSession({
session,
allSteps,
currentStep,
isProcessing,
documentation,
psaPushStatus,
psaPushError,
memberMappingWarning,
onRespond,
onResume,
onRate,
onReloadSession,
onGenerateStatusUpdate,
}: FlowPilotSessionProps) {
const scrollRef = useRef<HTMLDivElement>(null)
const [showTicketPicker, setShowTicketPicker] = useState(false)
const [linkingTicket, setLinkingTicket] = useState(false)
const [showShareCommunication, setShowShareCommunication] = useState(false)
const [showMobileSidebar, setShowMobileSidebar] = useState(false)
const handleLinkTicket = async (ticketId: string, _ticket: PSATicketInfo) => {
if (!session.psa_connection_id && !session.ticket_data) {
// Need a connection ID — try to get it from the integrations API
// For now, we'll need it passed in. This will work when ticket_data has it.
toast.error('No PSA connection available')
return
}
setLinkingTicket(true)
setShowTicketPicker(false)
try {
// We need the psa_connection_id. If the session doesn't have one,
// fetch it from the integrations API
let connectionId = session.psa_connection_id
if (!connectionId) {
const { integrationsApi } = await import('@/api/integrations')
const conn = await integrationsApi.getConnection()
if (!conn?.id) {
toast.error('No PSA connection configured')
return
}
connectionId = conn.id
}
await aiSessionsApi.linkTicket(session.id, {
psa_ticket_id: ticketId,
psa_connection_id: connectionId,
})
toast.success(`Linked to ticket #${ticketId}`)
// Reload session to get updated ticket_data
if (onReloadSession) {
await onReloadSession()
}
} catch {
toast.error('Failed to link ticket')
} finally {
setLinkingTicket(false)
}
}
// Auto-scroll to latest step
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
}
}, [allSteps.length])
const isCompleted = session.status === 'resolved' || session.status === 'escalated'
// Show documentation view for completed sessions
if (isCompleted && documentation) {
const shareContext = session.status === 'resolved' ? 'resolution' as const : 'escalation' as const
const shareLabel = session.status === 'resolved' ? 'Share Resolution' : 'Share Escalation'
return (
<div className="flex h-full flex-col">
<div className="flex-1 overflow-y-auto p-3 sm:p-4 lg:p-6">
{/* Share Resolution/Escalation button */}
{onGenerateStatusUpdate && (
<div className="mb-4">
<button
onClick={() => setShowShareCommunication(true)}
className="flex items-center gap-2 rounded-lg bg-cyan-500/10 border border-cyan-500/20 px-4 py-2.5 text-sm font-medium text-cyan-400 hover:bg-cyan-500/20 transition-colors"
>
<FileText size={16} />
{shareLabel}
</button>
</div>
)}
<SessionDocView
documentation={documentation}
onRate={onRate}
currentRating={session.session_rating}
psaPushStatus={psaPushStatus}
psaPushError={psaPushError}
memberMappingWarning={memberMappingWarning}
sessionId={session.id}
ticketId={session.psa_ticket_id}
/>
</div>
{/* Share communication modal for resolved/escalated sessions */}
{onGenerateStatusUpdate && (
<StatusUpdateModal
open={showShareCommunication}
onClose={() => setShowShareCommunication(false)}
onGenerate={onGenerateStatusUpdate}
context={shareContext}
hasPsaTicket={!!session.psa_ticket_id}
/>
)}
</div>
)
}
return (
<div className="flex h-full flex-col">
{/* Mobile sidebar summary (collapsible) */}
<div className="lg:hidden border-b border-border">
<button
onClick={() => setShowMobileSidebar(!showMobileSidebar)}
className="flex w-full items-center justify-between px-3 py-2 sm:px-4"
>
<div className="flex items-center gap-3 text-xs text-muted-foreground overflow-x-auto">
{session.problem_domain && (
<span className="font-sans text-xs rounded-md bg-accent-dim px-1.5 py-0.5 text-[0.5625rem] uppercase tracking-wider text-primary shrink-0">
{session.problem_domain}
</span>
)}
<span className="flex items-center gap-1 shrink-0">
<Hash size={10} />
{session.step_count} steps
</span>
<ConfidenceIndicator
tier={session.confidence_tier}
score={currentStep?.confidence_score ?? 0}
/>
</div>
{showMobileSidebar ? <ChevronUp size={14} className="text-muted-foreground shrink-0" /> : <ChevronDown size={14} className="text-muted-foreground shrink-0" />}
</button>
{showMobileSidebar && (
<div className="px-3 pb-3 sm:px-4 space-y-3">
{session.psa_ticket_id ? (
<SessionTicketCard
ticketId={session.psa_ticket_id}
ticketData={session.ticket_data as Record<string, unknown> | null}
/>
) : session.status === 'active' ? (
<button
onClick={() => setShowTicketPicker(true)}
disabled={linkingTicket}
className="w-full flex items-center gap-2 rounded-xl border border-dashed border-border px-3 py-2.5 text-xs text-muted-foreground hover:text-foreground hover:border-primary/30 transition-colors disabled:opacity-50 min-h-[44px]"
>
<Ticket size={14} />
{linkingTicket ? 'Linking...' : 'Link Ticket'}
</button>
) : null}
{session.problem_summary && (
<div>
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted mb-1">Problem</h4>
<p className="text-sm text-foreground">{session.problem_summary}</p>
</div>
)}
{session.matched_flow_id && (
<div className="flex items-center gap-2">
<Network size={14} className="text-muted-foreground" />
<span className="text-xs text-foreground">
{session.match_score ? `${Math.round(session.match_score * 100)}% match` : 'Match found'}
</span>
</div>
)}
<SimilarSessions sessionId={session.id} />
</div>
)}
</div>
{/* Main content area: conversation + sidebar */}
<div className="flex flex-1 min-h-0">
{/* Conversation column — pb-24 provides clearance for the fixed message bar */}
<div ref={scrollRef} className="flex-1 overflow-y-auto p-3 pb-24 sm:p-4 sm:pb-24 lg:p-6 lg:pb-24">
<div className="mx-auto max-w-2xl space-y-3">
{allSteps.map((step) => (
<FlowPilotStepCard
key={step.step_id}
step={step}
isCurrentStep={currentStep?.step_id === step.step_id}
isProcessing={isProcessing && currentStep?.step_id === step.step_id}
sessionId={session.id}
onRespond={onRespond}
/>
))}
</div>
</div>
{/* Sidebar — desktop only */}
<div
className="hidden w-72 shrink-0 overflow-y-auto border-l border-border p-4 lg:block"
>
<div className="space-y-4">
{/* Ticket context */}
{session.psa_ticket_id ? (
<SessionTicketCard
ticketId={session.psa_ticket_id}
ticketData={session.ticket_data as Record<string, unknown> | null}
/>
) : session.status === 'active' ? (
<button
onClick={() => setShowTicketPicker(true)}
disabled={linkingTicket}
className="w-full flex items-center gap-2 rounded-xl border border-dashed border-border px-3 py-2.5 text-xs text-muted-foreground hover:text-foreground hover:border-primary/30 transition-colors disabled:opacity-50"
>
<Ticket size={14} />
{linkingTicket ? 'Linking...' : 'Link Ticket'}
</button>
) : null}
{/* Problem summary */}
{session.problem_summary && (
<div>
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted mb-1">
Problem
</h4>
<p className="text-sm text-foreground">{session.problem_summary}</p>
</div>
)}
{/* Domain */}
{session.problem_domain && (
<div>
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted mb-1">
Domain
</h4>
<span className="font-sans text-xs rounded-md bg-accent-dim px-2 py-0.5 text-[0.625rem] uppercase tracking-wider text-primary">
{session.problem_domain}
</span>
</div>
)}
{/* Confidence */}
<div>
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted mb-1">
Confidence
</h4>
<ConfidenceIndicator
tier={session.confidence_tier}
score={currentStep?.confidence_score ?? 0}
/>
</div>
{/* Matched flow */}
{session.matched_flow_id && (
<div>
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-wider text-text-muted mb-1">
Matched flow
</h4>
<div className="flex items-center gap-2">
<Network size={14} className="text-muted-foreground" />
<span className="text-xs text-foreground">
{session.match_score ? `${Math.round(session.match_score * 100)}% match` : 'Match found'}
</span>
</div>
</div>
)}
{/* Steps */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-1.5">
<Hash size={12} className="text-muted-foreground" />
<span className="text-xs text-muted-foreground">{session.step_count} steps</span>
</div>
<div className="flex items-center gap-1.5">
<Clock size={12} className="text-muted-foreground" />
<span className="text-xs text-muted-foreground">
{new Date(session.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
</div>
{/* Similar sessions */}
<SimilarSessions sessionId={session.id} />
</div>
</div>
</div>
{/* Message bar — now the only fixed bottom element */}
{session.status === 'active' && (
<FlowPilotMessageBar
onRespond={onRespond}
isProcessing={isProcessing}
disabled={currentStep?.allow_free_text === false}
/>
)}
{/* Paused banner */}
{session.status === 'paused' && onResume && (
<div
className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between border-t border-border bg-card px-3 py-3 sm:px-5"
>
<span className="text-sm text-muted-foreground">Session paused</span>
<button
onClick={onResume}
className="flex items-center gap-2 rounded-lg bg-primary text-white px-4 py-2 text-sm font-semibold hover:brightness-110 active:scale-[0.98] transition-all"
>
<Play size={14} />
Resume Session
</button>
</div>
)}
{/* Ticket picker modal for mid-session linking */}
<TicketPickerModal
open={showTicketPicker}
onClose={() => setShowTicketPicker(false)}
onSelect={handleLinkTicket}
/>
</div>
)
}