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:
chihlasm
2026-04-01 22:43:15 +00:00
parent 036198b224
commit 23a7cee1f5
4 changed files with 441 additions and 0 deletions

View 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>
)
}