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>
231 lines
7.7 KiB
TypeScript
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>
|
|
)
|
|
}
|