Files
resolutionflow/frontend/src/components/assistant/IncidentHeader.tsx
chihlasm 165e402284 fix: deduplicate actions, promote ViewToggle tab bar, standardize naming
Remove duplicate Update/Close actions from chat toolbars (FlowPilotPage,
CockpitPage) — session lifecycle actions now live only in headers. Redesign
ViewToggle as a persistent tab bar with bottom-border active indicator and
ARIA attributes. Standardize all action naming: Resolve (emerald), Update
(blue), Close (rose), Pause (muted). Fix IncidentHeader Resolve from orange
to emerald. Delete unused FlowPilotActionBar component (227 lines). Update
ConcludeSessionModal copy to use forward-facing action verbs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 04:40:06 +00:00

231 lines
7.7 KiB
TypeScript

import { useState, useRef, useEffect } from 'react'
import { Pencil, X, Check, CheckCircle2, ExternalLink, Pause, XCircle, Link2, MoreHorizontal, FileText } from 'lucide-react'
import { cn } from '@/lib/utils'
import { toast } from '@/lib/toast'
import type { TriageMeta } from '@/types/ai-session'
interface IncidentHeaderProps {
triageMeta: TriageMeta
psaTicketId: string | null
onFieldSave: (field: keyof TriageMeta, value: string) => void
onResolve: () => void
onStatusUpdate?: () => void
onPause?: () => void
onClose?: () => 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<HTMLInputElement>(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 (
<div className="absolute top-full left-0 mt-1 z-50 bg-elevated border border-hover rounded-md p-2 shadow-lg flex gap-1.5 items-center min-w-[200px]">
<input
ref={inputRef}
type="text"
value={editValue}
onChange={e => 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"
/>
<button onClick={() => onSave(editValue)} className="p-1 text-success hover:text-success/80">
<Check size={14} />
</button>
<button onClick={onCancel} className="p-1 text-muted-foreground hover:text-foreground">
<X size={14} />
</button>
</div>
)
}
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 (
<div className="relative group flex flex-col gap-0.5 min-w-0">
<span className="text-[10px] uppercase tracking-wider text-muted font-semibold leading-none">
{label}
</span>
<div className="flex items-center gap-1 min-w-0">
<span
className={cn(
'text-sm truncate',
value ? (isHypothesis ? 'text-warning' : 'text-primary') : 'text-muted-foreground italic',
)}
>
{value || placeholder}
</span>
<button
onClick={() => setEditing(true)}
className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 text-muted-foreground hover:text-foreground flex-shrink-0"
>
<Pencil size={11} />
</button>
</div>
{editing && (
<EditPopover
value={value || ''}
onSave={(v) => { onSave(v); setEditing(false) }}
onCancel={() => setEditing(false)}
/>
)}
</div>
)
}
function OverflowMenu({ onPause, onClose }: { onPause?: () => void; onClose?: () => void }) {
const [open, setOpen] = useState(false)
useEffect(() => {
if (!open) return
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') setOpen(false)
}
document.addEventListener('keydown', handleEsc)
return () => { document.removeEventListener('keydown', handleEsc) }
}, [open])
const handleCopyLink = () => {
navigator.clipboard.writeText(`${window.location.origin}${window.location.pathname}`)
toast.success('Session link copied')
setOpen(false)
}
return (
<div className="relative">
<button
onClick={() => setOpen(!open)}
aria-label="More actions"
className="flex items-center justify-center rounded-lg px-2 py-1.5 text-muted-foreground hover:text-foreground hover:bg-[rgba(255,255,255,0.06)] transition-colors"
>
<MoreHorizontal size={16} />
</button>
{open && (
<>
<div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
<div className="absolute right-0 top-full mt-1 z-50 w-40 rounded-lg border border-border bg-card py-1 shadow-xl">
{onPause && (
<button
onClick={() => { onPause(); setOpen(false) }}
className="w-full flex items-center gap-2 px-3 py-2 text-xs text-muted-foreground hover:text-foreground hover:bg-[rgba(255,255,255,0.06)] transition-colors text-left"
>
<Pause size={13} />
Pause
</button>
)}
<button
onClick={handleCopyLink}
className="w-full flex items-center gap-2 px-3 py-2 text-xs text-muted-foreground hover:text-foreground hover:bg-[rgba(255,255,255,0.06)] transition-colors text-left"
>
<Link2 size={13} />
Copy Link
</button>
{onClose && (
<button
onClick={() => { onClose(); setOpen(false) }}
className="w-full flex items-center gap-2 px-3 py-2 text-xs text-muted-foreground hover:text-rose-400 hover:bg-rose-500/10 transition-colors text-left"
>
<XCircle size={13} />
Close
</button>
)}
</div>
</>
)}
</div>
)
}
export function IncidentHeader({
triageMeta,
psaTicketId,
onFieldSave,
onResolve,
onStatusUpdate,
onPause,
onClose,
}: IncidentHeaderProps) {
return (
<div className="bg-card border-b border-default px-4 py-2 flex items-center gap-4 flex-wrap">
<HeaderField
label="Client"
value={triageMeta.client_name}
placeholder="Unknown client"
onSave={v => onFieldSave('client_name', v)}
/>
<div className="w-px h-7 bg-elevated flex-shrink-0 hidden sm:block" />
<HeaderField
label="Device"
value={triageMeta.asset_name}
placeholder="No device"
onSave={v => onFieldSave('asset_name', v)}
/>
<div className="w-px h-7 bg-elevated flex-shrink-0 hidden sm:block" />
<HeaderField
label="Category"
value={triageMeta.issue_category}
placeholder="Uncategorized"
onSave={v => onFieldSave('issue_category', v)}
/>
<div className="w-px h-7 bg-elevated flex-shrink-0 hidden sm:block" />
<HeaderField
label="Hypothesis"
value={triageMeta.triage_hypothesis}
placeholder="No hypothesis yet"
onSave={v => onFieldSave('triage_hypothesis', v)}
isHypothesis
/>
<div className="flex items-center gap-2 ml-auto flex-shrink-0">
{psaTicketId && (
<span className="bg-elevated border border-default rounded px-2 py-0.5 text-xs text-muted-foreground flex items-center gap-1">
<ExternalLink size={10} />
CW #{psaTicketId}
</span>
)}
<button
onClick={onResolve}
className="flex items-center gap-1.5 bg-emerald-500/10 border border-emerald-500/20 rounded-lg px-3 py-1.5 text-xs font-medium text-emerald-400 hover:bg-emerald-500/20 transition-colors"
>
<CheckCircle2 size={13} />
Resolve
</button>
{onStatusUpdate && (
<button
onClick={onStatusUpdate}
className="flex items-center gap-1.5 bg-blue-500/10 border border-blue-500/20 rounded-lg px-3 py-1.5 text-xs font-medium text-blue-400 hover:bg-blue-500/20 transition-colors"
>
<FileText size={13} />
Update
</button>
)}
<OverflowMenu onPause={onPause} onClose={onClose} />
</div>
</div>
)
}