feat: add cockpit work zone components (Phase 4)
- IncidentHeader: labelled fields with per-field edit popovers - StepsPanel: ordered step checklist (✓/→/○) with script CTA - FlowPilotAsks: quick-reply buttons or free-text input - WhatWeKnow: evidence list with status toggle and inline editing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
158
frontend/src/components/assistant/IncidentHeader.tsx
Normal file
158
frontend/src/components/assistant/IncidentHeader.tsx
Normal file
@@ -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<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>
|
||||
)
|
||||
}
|
||||
|
||||
export function IncidentHeader({
|
||||
triageMeta,
|
||||
psaTicketId,
|
||||
onFieldSave,
|
||||
onResolve,
|
||||
onOverflow,
|
||||
}: 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="bg-accent/15 border border-accent rounded px-3 py-1 text-xs font-medium text-accent hover:bg-accent/25 transition-colors"
|
||||
>
|
||||
Resolve
|
||||
</button>
|
||||
<button
|
||||
onClick={onOverflow}
|
||||
className="bg-elevated border border-default rounded px-2 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
⋯
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user