diff --git a/frontend/src/components/session/BranchRevivalCard.tsx b/frontend/src/components/session/BranchRevivalCard.tsx new file mode 100644 index 00000000..4c3175d7 --- /dev/null +++ b/frontend/src/components/session/BranchRevivalCard.tsx @@ -0,0 +1,26 @@ +import { RotateCcw } from 'lucide-react' +import type { BranchResponse } from '@/types/branching' + +interface BranchRevivalCardProps { + branch: BranchResponse + evidenceSource: BranchResponse | null +} + +export function BranchRevivalCard({ branch, evidenceSource }: BranchRevivalCardProps) { + if (branch.status !== 'revived') return null + + return ( +
+
+ + Branch Revived +
+ {branch.evidence_description && ( +

{branch.evidence_description}

+ )} + {evidenceSource && ( +

Evidence from: {evidenceSource.label}

+ )} +
+ ) +} diff --git a/frontend/src/components/session/BranchTransitionBar.tsx b/frontend/src/components/session/BranchTransitionBar.tsx new file mode 100644 index 00000000..8ddb33c3 --- /dev/null +++ b/frontend/src/components/session/BranchTransitionBar.tsx @@ -0,0 +1,22 @@ +import { ArrowRight } from 'lucide-react' +import type { BranchResponse } from '@/types/branching' + +interface BranchTransitionBarProps { + fromBranch: BranchResponse | null + toBranch: BranchResponse +} + +export function BranchTransitionBar({ fromBranch, toBranch }: BranchTransitionBarProps) { + return ( +
+ Switched to + {toBranch.label} + {fromBranch && ( + <> + + from {fromBranch.label} + + )} +
+ ) +} diff --git a/frontend/src/hooks/useFlowPilotSession.ts b/frontend/src/hooks/useFlowPilotSession.ts index 3875109b..8b0f661c 100644 --- a/frontend/src/hooks/useFlowPilotSession.ts +++ b/frontend/src/hooks/useFlowPilotSession.ts @@ -92,6 +92,8 @@ export function useFlowPilotSession(): UseFlowPilotSession { ticket_data: null, steps: [firstStep], conversation_messages: [], + is_branching: false, + active_branch_id: null, }) setAllSteps([firstStep]) setCurrentStep(firstStep) diff --git a/frontend/src/hooks/useHandoff.ts b/frontend/src/hooks/useHandoff.ts new file mode 100644 index 00000000..38c8cc77 --- /dev/null +++ b/frontend/src/hooks/useHandoff.ts @@ -0,0 +1,49 @@ +import { useState, useCallback } from 'react' +import { handoffsApi } from '@/api' +import type { HandoffCreateRequest, HandoffResponse, QueueItemResponse } from '@/types/branching' +import { toast } from '@/lib/toast' + +export function useHandoff() { + const [isSubmitting, setIsSubmitting] = useState(false) + const [queue, setQueue] = useState([]) + const [isLoadingQueue, setIsLoadingQueue] = useState(false) + + const createHandoff = useCallback(async (sessionId: string, data: HandoffCreateRequest): Promise => { + setIsSubmitting(true) + try { + const result = await handoffsApi.createHandoff(sessionId, data) + toast.success(data.intent === 'park' ? 'Session parked' : 'Session escalated') + return result + } catch { + toast.error('Failed to hand off session') + return null + } finally { + setIsSubmitting(false) + } + }, []) + + const claimHandoff = useCallback(async (sessionId: string, handoffId: string): Promise => { + try { + const result = await handoffsApi.claimHandoff(sessionId, handoffId) + toast.success('Session claimed') + return result + } catch { + toast.error('Failed to claim session') + return null + } + }, []) + + const loadQueue = useCallback(async () => { + setIsLoadingQueue(true) + try { + const items = await handoffsApi.getQueue() + setQueue(items) + } catch { + toast.error('Failed to load queue') + } finally { + setIsLoadingQueue(false) + } + }, []) + + return { isSubmitting, queue, isLoadingQueue, createHandoff, claimHandoff, loadQueue } +} diff --git a/frontend/src/hooks/useResolutionOutputs.ts b/frontend/src/hooks/useResolutionOutputs.ts new file mode 100644 index 00000000..70cd1430 --- /dev/null +++ b/frontend/src/hooks/useResolutionOutputs.ts @@ -0,0 +1,43 @@ +import { useState, useCallback } from 'react' +import { resolutionsApi } from '@/api' +import type { ResolutionOutputResponse, PushDestination } from '@/types/branching' +import { toast } from '@/lib/toast' + +export function useResolutionOutputs(sessionId: string) { + const [outputs, setOutputs] = useState([]) + const [isLoading, setIsLoading] = useState(false) + + const loadOutputs = useCallback(async () => { + setIsLoading(true) + try { + const data = await resolutionsApi.getOutputs(sessionId) + setOutputs(data.outputs) + } catch { + toast.error('Failed to load resolution outputs') + } finally { + setIsLoading(false) + } + }, [sessionId]) + + const editOutput = useCallback(async (outputId: string, content: string) => { + try { + const updated = await resolutionsApi.editOutput(sessionId, outputId, { edited_content: content }) + setOutputs(prev => prev.map(o => o.id === updated.id ? updated : o)) + toast.success('Changes saved') + } catch { + toast.error('Failed to save changes') + } + }, [sessionId]) + + const pushOutput = useCallback(async (outputId: string, destination: PushDestination) => { + try { + await resolutionsApi.pushOutput(sessionId, outputId, { destination }) + toast.success(`Pushed to ${destination}`) + await loadOutputs() + } catch { + toast.error('Failed to push output') + } + }, [sessionId, loadOutputs]) + + return { outputs, isLoading, loadOutputs, editOutput, pushOutput } +} diff --git a/frontend/src/pages/SessionQueuePage.tsx b/frontend/src/pages/SessionQueuePage.tsx new file mode 100644 index 00000000..6a4973d6 --- /dev/null +++ b/frontend/src/pages/SessionQueuePage.tsx @@ -0,0 +1,54 @@ +import { useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { Inbox, ArrowUpRight, Pause, Clock } from 'lucide-react' +import { useHandoff } from '@/hooks/useHandoff' + +export default function SessionQueuePage() { + const navigate = useNavigate() + const { queue, isLoadingQueue, loadQueue, claimHandoff } = useHandoff() + + useEffect(() => { loadQueue() }, [loadQueue]) + + const handleClaim = async (item: typeof queue[0]) => { + const result = await claimHandoff(item.session_id, item.handoff_id) + if (result) navigate(`/pilot?sessionId=${item.session_id}`) + } + + return ( +
+
+ +

Session Queue

+
+ {isLoadingQueue ? ( +

Loading queue...

+ ) : queue.length === 0 ? ( +
+ +

No sessions waiting

+
+ ) : ( +
+ {queue.map(item => ( +
+
+
+ {item.intent === 'escalate' ? : } + {item.problem_summary || 'Untitled session'} + {item.priority === 'elevated' && Elevated} +
+ {item.engineer_notes &&

{item.engineer_notes}

} +
+ + {new Date(item.created_at).toLocaleString()} + {item.problem_domain && ยท {item.problem_domain}} +
+
+ +
+ ))} +
+ )} +
+ ) +}