diff --git a/frontend/src/components/assistant/IncidentHeader.tsx b/frontend/src/components/assistant/IncidentHeader.tsx index 38d09220..aca4e1d7 100644 --- a/frontend/src/components/assistant/IncidentHeader.tsx +++ b/frontend/src/components/assistant/IncidentHeader.tsx @@ -1,6 +1,7 @@ import { useState, useRef, useEffect } from 'react' -import { Pencil, X, Check, ExternalLink } from 'lucide-react' +import { Pencil, X, Check, ExternalLink, Pause, XCircle, Link2 } from 'lucide-react' import { cn } from '@/lib/utils' +import { toast } from '@/lib/toast' import type { TriageMeta } from '@/types/ai-session' interface IncidentHeaderProps { @@ -9,7 +10,8 @@ interface IncidentHeaderProps { sessionId: string onFieldSave: (field: keyof TriageMeta, value: string) => void onResolve: () => void - onOverflow: () => void + onPause?: () => void + onClose?: () => void } interface EditPopoverProps { @@ -95,12 +97,81 @@ function HeaderField({ label, value, placeholder, onSave, isHypothesis }: Header ) } +function OverflowMenu({ onPause, onClose, sessionId }: { onPause?: () => void; onClose?: () => void; sessionId: string }) { + const [open, setOpen] = useState(false) + const menuRef = useRef(null) + + useEffect(() => { + if (!open) return + const handleClickOutside = (e: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(e.target as Node)) setOpen(false) + } + const handleEsc = (e: KeyboardEvent) => { + if (e.key === 'Escape') setOpen(false) + } + document.addEventListener('mousedown', handleClickOutside) + document.addEventListener('keydown', handleEsc) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + document.removeEventListener('keydown', handleEsc) + } + }, [open]) + + const handleCopyLink = () => { + navigator.clipboard.writeText(`${window.location.origin}/assistant/${sessionId}`) + toast.success('Session link copied') + setOpen(false) + } + + return ( +
+ + {open && ( +
+ {onPause && ( + + )} + + {onClose && ( + + )} +
+ )} +
+ ) +} + export function IncidentHeader({ triageMeta, psaTicketId, + sessionId, onFieldSave, onResolve, - onOverflow, + onPause, + onClose, }: IncidentHeaderProps) { return (
@@ -146,12 +217,7 @@ export function IncidentHeader({ > Resolve - +
) diff --git a/frontend/src/components/assistant/StepsPanel.tsx b/frontend/src/components/assistant/StepsPanel.tsx index e059b904..e3560e94 100644 --- a/frontend/src/components/assistant/StepsPanel.tsx +++ b/frontend/src/components/assistant/StepsPanel.tsx @@ -1,14 +1,24 @@ -import { Check, ArrowRight, Circle, Terminal } from 'lucide-react' +import { Check, ArrowRight, Circle, Terminal, ChevronRight } from 'lucide-react' import { cn } from '@/lib/utils' import type { ActionItem } from '@/types/ai-session' interface StepsPanelProps { actions: ActionItem[] activeIndex: number + completedSteps?: Set + onStepComplete?: (index: number) => void + onStepSelect?: (index: number) => void onGenerateScript?: () => void } -export function StepsPanel({ actions, activeIndex, onGenerateScript }: StepsPanelProps) { +export function StepsPanel({ + actions, + activeIndex, + completedSteps = new Set(), + onStepComplete, + onStepSelect, + onGenerateScript, +}: StepsPanelProps) { if (actions.length === 0) { return (
@@ -17,45 +27,100 @@ export function StepsPanel({ actions, activeIndex, onGenerateScript }: StepsPane ) } + const completedCount = completedSteps.size + const totalCount = actions.length + const progressPct = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0 + const showScriptCta = actions[activeIndex]?.command?.toLowerCase().includes('script') || actions[activeIndex]?.description?.toLowerCase().includes('script') return (
-
- Steps + {/* Header with progress */} +
+
+ Steps +
+ + {completedCount}/{totalCount} complete +
+ + {/* Progress bar */} +
+
+
+ + {/* Steps list */}
{actions.map((action, idx) => { - const isCompleted = idx < activeIndex + const isCompleted = completedSteps.has(idx) const isActive = idx === activeIndex - const isPending = idx > activeIndex + const isPending = !isCompleted && !isActive return (
onStepSelect?.(idx)} className={cn( - 'rounded px-3 py-2 text-sm flex items-start gap-2.5 transition-colors', - isCompleted && 'bg-elevated/50 text-muted-foreground', + 'rounded-md px-3 py-2 text-sm flex items-start gap-2.5 transition-all duration-200 cursor-pointer group', + isCompleted && 'bg-success/[0.06] text-muted-foreground hover:bg-success/[0.1]', isActive && 'bg-elevated border border-accent/30 text-primary', - isPending && 'bg-card text-muted-foreground/60', + isPending && 'bg-card text-muted-foreground/60 hover:bg-elevated/50', )} > - - {isCompleted && } - {isActive && } - {isPending && } - -
- {action.label} - {isActive && action.description && ( -

{action.description}

+ {/* Status icon / complete button */} + + +
+ {action.label} + {/* Show description for active step and on hover for others */} + {action.description && ( +

+ {action.description} +

)}
+ + {/* Active indicator */} + {isActive && !isCompleted && ( + + )}
) })}
+ {showScriptCta && onGenerateScript && ( diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx index 57a4aa85..819b524a 100644 --- a/frontend/src/pages/AssistantChatPage.tsx +++ b/frontend/src/pages/AssistantChatPage.tsx @@ -84,7 +84,15 @@ export default function AssistantChatPage() { const saved = localStorage.getItem('rf-assistant-work-zone-height') return saved ? parseFloat(saved) : 55 }) - const [activeStepIndex] = useState(0) + const [activeStepIndex, setActiveStepIndex] = useState(0) + const [completedSteps, setCompletedSteps] = useState>(new Set()) + const [showOnboarding, setShowOnboarding] = useState(() => + !localStorage.getItem('rf-cockpit-onboarded') + ) + const dismissOnboarding = () => { + setShowOnboarding(false) + localStorage.setItem('rf-cockpit-onboarded', '1') + } const splitContainerRef = useRef(null) const toggleSidebarCollapse = () => { const next = !sidebarCollapsed @@ -336,6 +344,7 @@ export default function AssistantChatPage() { .map((u) => u.result!.id) setInput('') setPendingUploads([]) + if (showOnboarding) dismissOnboarding() setMessages(prev => [...prev, { role: 'user', content: userMessage }]) setLoading(true) @@ -480,7 +489,24 @@ export default function AssistantChatPage() { } }, [activeChatId, triageMeta.evidence_items]) + const handleStepComplete = useCallback((index: number) => { + setCompletedSteps(prev => { + const next = new Set(prev) + next.add(index) + return next + }) + // Auto-advance to the next incomplete step + const nextIncomplete = activeActions.findIndex((_, i) => i > index && !completedSteps.has(i)) + if (nextIncomplete !== -1) { + setActiveStepIndex(nextIncomplete) + } else if (index + 1 < activeActions.length) { + setActiveStepIndex(index + 1) + } + }, [activeActions, completedSteps]) + const handleStepSelect = useCallback((index: number) => { + setActiveStepIndex(index) + }, []) // Merge triage_update from AI response into local state const mergeTriageUpdate = useCallback((update: TriageUpdate) => { @@ -674,11 +700,43 @@ export default function AssistantChatPage() { sessionId={activeChatId} onFieldSave={handleTriageFieldSave} onResolve={() => setShowConclude(true)} - onOverflow={() => {}} + onClose={() => setShowConclude(true)} /> {/* Resizable work zone + conversation log split */} -
+
+ {/* First-run onboarding overlay */} + {showOnboarding && messages.length === 0 && ( +
+ {/* Steps zone label */} +
+
+

Steps Panel

+

Troubleshooting steps appear here as FlowPilot identifies them. Click to mark done.

+
+
+ {/* FlowPilot Asks zone label */} +
+
+

AI Questions & Evidence

+

FlowPilot asks clarifying questions here. Evidence you confirm or rule out is tracked below.

+
+
+ {/* Conversation log label */} +
+
+

Conversation Log

+

Full chat history lives here. Drag the handle above to resize.

+ +
+
+
+ )} {/* Work zone */}
{/* Left: Steps panel */} @@ -686,6 +744,9 @@ export default function AssistantChatPage() {
{/* Right: FlowPilot Asks + What We Know */} @@ -709,13 +770,14 @@ export default function AssistantChatPage() { {/* Drag handle */}
- +
{/* Conversation log */} -
+
Conversation Log
@@ -727,26 +789,34 @@ export default function AssistantChatPage() {

)} -
+
{messages.map((msg, i) => ( -
+
- {msg.role === 'user' ? 'you:' : 'fp:'} + {msg.role === 'user' ? 'You' : 'FlowPilot'} - {msg.content.length > 300 ? msg.content.slice(0, 300) + '…' : msg.content} + {msg.content}
))} {loading && ( -
- fp: - +
+ FlowPilot +
)}
@@ -786,7 +856,7 @@ export default function AssistantChatPage() { onChange={e => setInput(e.target.value)} onKeyDown={handleKeyDown} onPaste={handlePaste} - placeholder={loading ? 'FlowPilot is working...' : 'Describe finding, paste log output, or ask FlowPilot...'} + placeholder={loading ? 'FlowPilot is working...' : 'Describe the issue or ask FlowPilot...'} disabled={loading} rows={1} className="w-full resize-none bg-transparent px-4 pt-3 pb-1 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none disabled:cursor-not-allowed"