Files
resolutionflow/frontend/src/hooks/useFlowPilotSession.ts
Michael Chihlas b7f8e70be2 fix(lint): replace explicit-any types + unused-expressions ternaries
Five files, all stylistic:

- useFlowPilotSession.ts: typed the axios error shape with a narrow
  inline type instead of \`as any\`.
- FlowPilotSessionPage.tsx: same — typed location.state once, then
  destructured.
- ScriptBuilderTab.tsx: handleViewScript was a placeholder no-op;
  declared the args properly with \`void script; void filename\` so the
  signature matches ScriptBuilderChatProps without no-unused-vars
  firing.
- TicketsPage.tsx: replaced 8 ternaries-as-statements (\`x ? f() : g()\`)
  with proper if/else blocks. Same control flow, satisfies
  no-unused-expressions, and reads better in the URL-param update paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 02:32:57 -04:00

301 lines
10 KiB
TypeScript

import { useState, useCallback } from 'react'
import { aiSessionsApi } from '@/api'
import type {
AISessionCreateRequest,
AISessionCreateResponse,
AISessionDetail,
AISessionStepResponse,
StepResponseRequest,
StepResponseResponse,
ResolveSessionRequest,
EscalateSessionRequest,
SessionDocumentation,
StatusUpdateRequest,
StatusUpdateResponse,
} from '@/types/ai-session'
import { toast } from '@/lib/toast'
export interface UseFlowPilotSession {
// State
session: AISessionDetail | null
currentStep: AISessionStepResponse | null
allSteps: AISessionStepResponse[]
isLoading: boolean
isProcessing: boolean
error: string | null
// Actions
startSession: (intake: AISessionCreateRequest) => Promise<void>
respondToStep: (response: StepResponseRequest) => Promise<void>
resolveSession: (data: ResolveSessionRequest) => Promise<SessionDocumentation | null>
escalateSession: (data: EscalateSessionRequest) => Promise<SessionDocumentation | null>
pauseSession: () => Promise<void>
resumeOwnSession: () => Promise<void>
abandonSession: () => Promise<void>
rateSession: (rating: number, feedback?: string) => Promise<void>
generateStatusUpdate: (data: StatusUpdateRequest) => Promise<StatusUpdateResponse>
loadSession: (sessionId: string) => Promise<void>
// Derived
isActive: boolean
canResolve: boolean
canEscalate: boolean
// Post-close
documentation: SessionDocumentation | null
psaPushStatus: string | null
psaPushError: string | null
memberMappingWarning: string | null
}
export function useFlowPilotSession(): UseFlowPilotSession {
const [session, setSession] = useState<AISessionDetail | null>(null)
const [currentStep, setCurrentStep] = useState<AISessionStepResponse | null>(null)
const [allSteps, setAllSteps] = useState<AISessionStepResponse[]>([])
const [isLoading, setIsLoading] = useState(false)
const [isProcessing, setIsProcessing] = useState(false)
const [error, setError] = useState<string | null>(null)
const [documentation, setDocumentation] = useState<SessionDocumentation | null>(null)
const [psaPushStatus, setPsaPushStatus] = useState<string | null>(null)
const [psaPushError, setPsaPushError] = useState<string | null>(null)
const [memberMappingWarning, setMemberMappingWarning] = useState<string | null>(null)
const startSession = useCallback(async (intake: AISessionCreateRequest) => {
setIsLoading(true)
setError(null)
try {
const result: AISessionCreateResponse = await aiSessionsApi.createSession(intake)
const firstStep = result.first_step
setSession({
id: result.session_id,
session_type: 'guided',
title: null,
status: result.status,
intake_type: intake.intake_type,
intake_content: intake.intake_content,
problem_summary: result.problem_summary,
problem_domain: result.problem_domain,
confidence_tier: result.confidence_tier,
step_count: 1,
session_rating: null,
created_at: new Date().toISOString(),
resolved_at: null,
matched_flow_id: result.matched_flow_id,
match_score: result.match_score,
resolution_summary: null,
resolution_action: null,
escalation_reason: null,
session_feedback: null,
psa_ticket_id: intake.psa_ticket_id ?? null,
psa_connection_id: intake.psa_connection_id ?? null,
ticket_data: null,
steps: [firstStep],
conversation_messages: [],
pending_task_lane: null,
is_branching: false,
active_branch_id: null,
})
setAllSteps([firstStep])
setCurrentStep(firstStep)
} catch (e: unknown) {
// Prefer the backend's detail message over the generic axios status string
const axiosErr = e as { response?: { status?: number; data?: { detail?: unknown } } }
const detail = axiosErr?.response?.data?.detail
const message = typeof detail === 'string' ? detail : (e instanceof Error ? e.message : 'Failed to start session')
setError(message)
// Global axios interceptor already shows a toast for 5xx — skip duplicate
const status = axiosErr?.response?.status
if (!status || status < 500) {
toast.error(message)
}
} finally {
setIsLoading(false)
}
}, [])
const respondToStep = useCallback(async (response: StepResponseRequest) => {
if (!session) return
setIsProcessing(true)
setError(null)
try {
const result: StepResponseResponse = await aiSessionsApi.respondToStep(session.id, response)
setSession(prev => prev ? {
...prev,
status: result.status,
confidence_tier: result.confidence_tier,
step_count: prev.step_count + 1,
} : null)
if (result.next_step) {
setAllSteps(prev => [...prev, result.next_step!])
setCurrentStep(result.next_step)
}
} catch (e: unknown) {
const message = e instanceof Error ? e.message : 'Failed to process response'
setError(message)
toast.error(message)
} finally {
setIsProcessing(false)
}
}, [session])
const resolveSession = useCallback(async (data: ResolveSessionRequest): Promise<SessionDocumentation | null> => {
if (!session) throw new Error('No active session')
setIsProcessing(true)
try {
const result = await aiSessionsApi.resolveSession(session.id, data)
setSession(prev => prev ? { ...prev, status: 'resolved' } : null)
setDocumentation(result.documentation)
setPsaPushStatus(result.psa_push_status)
setPsaPushError(result.psa_push_error)
setMemberMappingWarning(result.member_mapping_warning)
setCurrentStep(null)
toast.success('Session resolved')
return result.documentation
} catch (e: unknown) {
const message = e instanceof Error ? e.message : 'Failed to resolve session'
toast.error(message)
throw e
} finally {
setIsProcessing(false)
}
}, [session])
const escalateSession = useCallback(async (data: EscalateSessionRequest): Promise<SessionDocumentation | null> => {
if (!session) throw new Error('No active session')
setIsProcessing(true)
try {
const result = await aiSessionsApi.escalateSession(session.id, data)
setSession(prev => prev ? { ...prev, status: 'requesting_escalation' } : null)
setDocumentation(result.documentation)
setPsaPushStatus(result.psa_push_status)
setPsaPushError(result.psa_push_error)
setMemberMappingWarning(result.member_mapping_warning)
setCurrentStep(null)
toast.success('Session escalated')
return result.documentation
} catch (e: unknown) {
const message = e instanceof Error ? e.message : 'Failed to escalate session'
toast.error(message)
throw e
} finally {
setIsProcessing(false)
}
}, [session])
const pauseSession = useCallback(async () => {
if (!session) return
try {
await aiSessionsApi.pauseSession(session.id)
setSession(prev => prev ? { ...prev, status: 'paused' } : null)
toast.success('Session paused')
} catch {
toast.error('Failed to pause session')
}
}, [session])
const resumeOwnSession = useCallback(async () => {
if (!session) return
try {
await aiSessionsApi.resumeSession(session.id)
setSession(prev => prev ? { ...prev, status: 'active' } : null)
toast.success('Session resumed')
} catch {
toast.error('Failed to resume session')
}
}, [session])
const abandonSession = useCallback(async () => {
if (!session) return
try {
await aiSessionsApi.abandonSession(session.id)
setSession(prev => prev ? { ...prev, status: 'abandoned' } : null)
setCurrentStep(null)
toast.success('Session closed')
} catch {
toast.error('Failed to close session')
}
}, [session])
const rateSession = useCallback(async (rating: number, feedback?: string) => {
if (!session) return
try {
await aiSessionsApi.rateSession(session.id, { rating, feedback })
setSession(prev => prev ? { ...prev, session_rating: rating } : null)
toast.success('Thanks for your feedback!')
} catch {
toast.error('Failed to submit rating')
}
}, [session])
const generateStatusUpdate = useCallback(async (data: StatusUpdateRequest): Promise<StatusUpdateResponse> => {
if (!session) throw new Error('No active session')
try {
return await aiSessionsApi.generateStatusUpdate(session.id, data)
} catch (e: unknown) {
const message = e instanceof Error ? e.message : 'Failed to generate status update'
toast.error(message)
throw e
}
}, [session])
const loadSession = useCallback(async (sessionId: string) => {
setIsLoading(true)
setError(null)
try {
const detail = await aiSessionsApi.getSession(sessionId)
setSession(detail)
setAllSteps(detail.steps)
// Set current step for active and paused sessions (paused can be resumed)
setCurrentStep(
detail.status === 'active' || detail.status === 'paused'
? detail.steps[detail.steps.length - 1] ?? null
: null
)
if (detail.status === 'resolved' || detail.status === 'escalated') {
const doc = await aiSessionsApi.getDocumentation(sessionId)
setDocumentation(doc)
}
} catch (e: unknown) {
const message = e instanceof Error ? e.message : 'Failed to load session'
setError(message)
toast.error(message)
} finally {
setIsLoading(false)
}
}, [])
const isActive = session?.status === 'active'
const canResolve = isActive && allSteps.length >= 1
const canEscalate = isActive && allSteps.length >= 1
return {
session,
currentStep,
allSteps,
isLoading,
isProcessing,
error,
startSession,
respondToStep,
resolveSession,
escalateSession,
pauseSession,
resumeOwnSession,
abandonSession,
rateSession,
generateStatusUpdate,
loadSession,
isActive,
canResolve,
canEscalate,
documentation,
psaPushStatus,
psaPushError,
memberMappingWarning,
}
}