diff --git a/frontend/src/components/assistant/FlowPilotAsks.tsx b/frontend/src/components/assistant/FlowPilotAsks.tsx new file mode 100644 index 00000000..7042f4dd --- /dev/null +++ b/frontend/src/components/assistant/FlowPilotAsks.tsx @@ -0,0 +1,74 @@ +import { useState } from 'react' +import { Send, HelpCircle } from 'lucide-react' +import type { QuestionItem } from '@/types/ai-session' + +interface FlowPilotAsksProps { + questions: QuestionItem[] + onAnswer: (answer: string) => void + loading?: boolean +} + +export function FlowPilotAsks({ questions, onAnswer, loading }: FlowPilotAsksProps) { + const [freeText, setFreeText] = useState('') + + // Show first unanswered question + const question = questions.length > 0 ? questions[0] : null + + if (!question) return null + + const handleFreeTextSubmit = () => { + if (!freeText.trim()) return + onAnswer(freeText.trim()) + setFreeText('') + } + + return ( +
+
+ + FlowPilot Asks +
+

+ {question.text} +

+ {question.context && ( +

+ {question.context} +

+ )} + {question.options ? ( +
+ {question.options.map((option, idx) => ( + + ))} +
+ ) : ( +
+ setFreeText(e.target.value)} + onKeyDown={e => e.key === 'Enter' && handleFreeTextSubmit()} + placeholder="Type your answer..." + disabled={loading} + className="flex-1 bg-input border border-default rounded px-2.5 py-1.5 text-sm text-primary outline-none focus:border-accent placeholder:text-muted-foreground/50" + /> + +
+ )} +
+ ) +} diff --git a/frontend/src/components/assistant/IncidentHeader.tsx b/frontend/src/components/assistant/IncidentHeader.tsx new file mode 100644 index 00000000..38d09220 --- /dev/null +++ b/frontend/src/components/assistant/IncidentHeader.tsx @@ -0,0 +1,158 @@ +import { useState, useRef, useEffect } from 'react' +import { Pencil, X, Check, ExternalLink } from 'lucide-react' +import { cn } from '@/lib/utils' +import type { TriageMeta } from '@/types/ai-session' + +interface IncidentHeaderProps { + triageMeta: TriageMeta + psaTicketId: string | null + sessionId: string + onFieldSave: (field: keyof TriageMeta, value: string) => void + onResolve: () => void + onOverflow: () => void +} + +interface EditPopoverProps { + value: string + onSave: (value: string) => void + onCancel: () => void +} + +function EditPopover({ value, onSave, onCancel }: EditPopoverProps) { + const [editValue, setEditValue] = useState(value) + const inputRef = useRef(null) + + useEffect(() => { + inputRef.current?.focus() + inputRef.current?.select() + }, []) + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') onSave(editValue) + if (e.key === 'Escape') onCancel() + } + + return ( +
+ setEditValue(e.target.value)} + onKeyDown={handleKeyDown} + className="flex-1 bg-input border border-default rounded px-2 py-1 text-sm text-primary outline-none focus:border-accent" + /> + + +
+ ) +} + +interface HeaderFieldProps { + label: string + value: string | null + placeholder: string + onSave: (value: string) => void + isHypothesis?: boolean +} + +function HeaderField({ label, value, placeholder, onSave, isHypothesis }: HeaderFieldProps) { + const [editing, setEditing] = useState(false) + + return ( +
+ + {label} + +
+ + {value || placeholder} + + +
+ {editing && ( + { onSave(v); setEditing(false) }} + onCancel={() => setEditing(false)} + /> + )} +
+ ) +} + +export function IncidentHeader({ + triageMeta, + psaTicketId, + onFieldSave, + onResolve, + onOverflow, +}: IncidentHeaderProps) { + return ( +
+ onFieldSave('client_name', v)} + /> +
+ onFieldSave('asset_name', v)} + /> +
+ onFieldSave('issue_category', v)} + /> +
+ onFieldSave('triage_hypothesis', v)} + isHypothesis + /> + +
+ {psaTicketId && ( + + + CW #{psaTicketId} + + )} + + +
+
+ ) +} diff --git a/frontend/src/components/assistant/StepsPanel.tsx b/frontend/src/components/assistant/StepsPanel.tsx new file mode 100644 index 00000000..e059b904 --- /dev/null +++ b/frontend/src/components/assistant/StepsPanel.tsx @@ -0,0 +1,70 @@ +import { Check, ArrowRight, Circle, Terminal } from 'lucide-react' +import { cn } from '@/lib/utils' +import type { ActionItem } from '@/types/ai-session' + +interface StepsPanelProps { + actions: ActionItem[] + activeIndex: number + onGenerateScript?: () => void +} + +export function StepsPanel({ actions, activeIndex, onGenerateScript }: StepsPanelProps) { + if (actions.length === 0) { + return ( +
+ No steps yet — start troubleshooting +
+ ) + } + + const showScriptCta = actions[activeIndex]?.command?.toLowerCase().includes('script') || + actions[activeIndex]?.description?.toLowerCase().includes('script') + + return ( +
+
+ Steps +
+
+ {actions.map((action, idx) => { + const isCompleted = idx < activeIndex + const isActive = idx === activeIndex + const isPending = idx > activeIndex + + return ( +
+ + {isCompleted && } + {isActive && } + {isPending && } + +
+ {action.label} + {isActive && action.description && ( +

{action.description}

+ )} +
+
+ ) + })} +
+ {showScriptCta && onGenerateScript && ( + + )} +
+ ) +} diff --git a/frontend/src/components/assistant/WhatWeKnow.tsx b/frontend/src/components/assistant/WhatWeKnow.tsx new file mode 100644 index 00000000..cb881d01 --- /dev/null +++ b/frontend/src/components/assistant/WhatWeKnow.tsx @@ -0,0 +1,139 @@ +import { useState } from 'react' +import { Check, X, HelpCircle, Plus } from 'lucide-react' +import { cn } from '@/lib/utils' +import type { EvidenceItem } from '@/types/ai-session' + +interface WhatWeKnowProps { + items: EvidenceItem[] + onAdd: (text: string, status: EvidenceItem['status']) => void + onEdit: (index: number, text: string, status: EvidenceItem['status']) => void +} + +const STATUS_CONFIG = { + confirmed: { icon: Check, color: 'text-success', label: '✓' }, + ruled_out: { icon: X, color: 'text-danger', label: '✗' }, + pending: { icon: HelpCircle, color: 'text-muted-foreground', label: '?' }, +} as const + +const STATUS_CYCLE: EvidenceItem['status'][] = ['confirmed', 'ruled_out', 'pending'] + +export function WhatWeKnow({ items, onAdd, onEdit }: WhatWeKnowProps) { + const [addingText, setAddingText] = useState('') + const [showAddInput, setShowAddInput] = useState(false) + const [editingIdx, setEditingIdx] = useState(null) + const [editText, setEditText] = useState('') + + const handleAdd = () => { + if (!addingText.trim()) return + onAdd(addingText.trim(), 'pending') + setAddingText('') + setShowAddInput(false) + } + + const handleStatusToggle = (idx: number) => { + const item = items[idx] + const currentIdx = STATUS_CYCLE.indexOf(item.status) + const nextStatus = STATUS_CYCLE[(currentIdx + 1) % STATUS_CYCLE.length] + onEdit(idx, item.text, nextStatus) + } + + const handleEditStart = (idx: number) => { + setEditingIdx(idx) + setEditText(items[idx].text) + } + + const handleEditSave = (idx: number) => { + if (editText.trim()) { + onEdit(idx, editText.trim(), items[idx].status) + } + setEditingIdx(null) + } + + return ( +
+
+ What We Know +
+ {items.length === 0 ? ( +

No evidence collected yet

+ ) : ( +
+ {items.map((item, idx) => { + const config = STATUS_CONFIG[item.status] + const Icon = config.icon + + return ( +
+ + {editingIdx === idx ? ( + setEditText(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') handleEditSave(idx); if (e.key === 'Escape') setEditingIdx(null) }} + onBlur={() => handleEditSave(idx)} + autoFocus + className="flex-1 bg-input border border-default rounded px-1.5 py-0.5 text-xs text-primary outline-none focus:border-accent" + /> + ) : ( + handleEditStart(idx)} + className={cn( + 'text-xs leading-relaxed cursor-pointer hover:text-foreground transition-colors', + item.status === 'confirmed' && 'text-success/80', + item.status === 'ruled_out' && 'text-danger/80', + item.status === 'pending' && 'text-muted-foreground', + )} + > + {item.text} + + )} +
+ ) + })} +
+ )} + + {showAddInput ? ( +
+ setAddingText(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') handleAdd(); if (e.key === 'Escape') setShowAddInput(false) }} + placeholder="New finding..." + autoFocus + className="flex-1 bg-input border border-default rounded px-2 py-1 text-xs text-primary outline-none focus:border-accent placeholder:text-muted-foreground/50" + /> + + +
+ ) : ( + + )} +
+ ) +}