feat(escalations): magic-moment 3-option CTA + claim 500 fix
- HandoffContextScreen: 3-option layout (Continue/AI analysis/Own thing)
with hasTaskLane, activeOptionKey, spinner/disabled states
- AssistantChatPage: wire up handleContinue, handleAIAnalysis, handleOwnThing
handlers; chip detail expansion inline with copy-button fix; post-escalation
redirect to dashboard on ConcludeSessionModal close
- TaskLane: fix async copy button (await + execCommand fallback + copiedKey
visual feedback); whitespace-pre-wrap on command blocks
- Fix 500 on claim: Pydantic v2 model_validate() + model_copy(update={})
(was passing update= kwarg directly which v2 rejects)
- HandoffResponse schema: handed_off_by_name field
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,8 +6,10 @@ import {
|
||||
Clock,
|
||||
FileText,
|
||||
Hash,
|
||||
Loader2,
|
||||
Sparkles,
|
||||
Target,
|
||||
User,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
import type { HandoffResponse } from '@/types/branching'
|
||||
@@ -35,12 +37,21 @@ type ConfidenceTier = 'low' | 'medium' | 'high' | string
|
||||
|
||||
interface HandoffContextScreenProps {
|
||||
handoff: HandoffResponse
|
||||
onStartHere: () => Promise<void> | void
|
||||
// Pre-claim entry point: one of three choices is made before claiming.
|
||||
// Post-claim re-open (dismissible=true) keeps the legacy onStartHere path.
|
||||
onContinue?: () => Promise<void> | void
|
||||
onAIAnalysis?: () => Promise<void> | void
|
||||
onOwnThing?: () => Promise<void> | void
|
||||
// Legacy single-CTA — used when dismissible=true (post-claim toolbar re-open)
|
||||
onStartHere?: () => Promise<void> | void
|
||||
onDismiss?: () => void
|
||||
// When true, renders an "X" close affordance in the corner. Used when the
|
||||
// screen is re-opened from the FlowPilot toolbar (post-claim re-read).
|
||||
dismissible?: boolean
|
||||
isProcessing?: boolean
|
||||
// Whether the task lane has items — drives the 3-option vs 2-option layout
|
||||
hasTaskLane?: boolean
|
||||
activeOptionKey?: 'continue' | 'ai' | 'own' | null
|
||||
}
|
||||
|
||||
function ConfidenceBadge({ value }: { value: number | string | null | undefined }) {
|
||||
@@ -76,10 +87,15 @@ function ConfidenceBadge({ value }: { value: number | string | null | undefined
|
||||
|
||||
export function HandoffContextScreen({
|
||||
handoff,
|
||||
onContinue,
|
||||
onAIAnalysis,
|
||||
onOwnThing,
|
||||
onStartHere,
|
||||
onDismiss,
|
||||
dismissible = false,
|
||||
isProcessing = false,
|
||||
hasTaskLane = false,
|
||||
activeOptionKey = null,
|
||||
}: HandoffContextScreenProps) {
|
||||
const startBtnRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
@@ -114,6 +130,7 @@ export function HandoffContextScreen({
|
||||
|
||||
const assessment = handoff.ai_assessment_data
|
||||
const likelyCause = assessment?.likely_cause
|
||||
const whatWeKnow = assessment?.what_we_know ?? []
|
||||
const suggestedSteps = assessment?.suggested_steps ?? []
|
||||
const assessmentConfidence = assessment?.confidence
|
||||
const assessmentText = handoff.ai_assessment
|
||||
@@ -256,6 +273,21 @@ export function HandoffContextScreen({
|
||||
<p className="text-sm text-foreground">{likelyCause}</p>
|
||||
</div>
|
||||
)}
|
||||
{whatWeKnow.length > 0 && (
|
||||
<div>
|
||||
<p className="font-sans text-[0.625rem] uppercase tracking-wider text-muted-foreground mb-1.5">
|
||||
What we know
|
||||
</p>
|
||||
<ul className="space-y-1">
|
||||
{whatWeKnow.map((fact, i) => (
|
||||
<li key={i} className="text-sm text-foreground flex items-start gap-2">
|
||||
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-muted-foreground/50" />
|
||||
<span>{fact}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{assessmentText && !likelyCause && (
|
||||
<p className="text-sm text-foreground whitespace-pre-wrap">
|
||||
{assessmentText}
|
||||
@@ -287,22 +319,92 @@ export function HandoffContextScreen({
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Start here CTA */}
|
||||
{!dismissible && (
|
||||
<div className="mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Picking up assigns this session to you and reactivates it.
|
||||
</p>
|
||||
{/* CTA footer */}
|
||||
{dismissible ? (
|
||||
// Post-claim re-open from toolbar — single close action
|
||||
<div className="mt-6 flex justify-end">
|
||||
<button
|
||||
ref={startBtnRef}
|
||||
onClick={() => void onStartHere()}
|
||||
disabled={isProcessing}
|
||||
className="flex items-center justify-center gap-2 rounded-lg bg-accent px-5 py-3 min-h-[44px] text-sm font-semibold text-white hover:brightness-110 active:scale-[0.98] disabled:opacity-50 disabled:pointer-events-none transition-all"
|
||||
onClick={() => onDismiss?.()}
|
||||
className="px-4 py-2 rounded-lg text-sm text-muted-foreground hover:text-foreground bg-input border border-border hover:border-border-hover transition-all"
|
||||
>
|
||||
<ArrowRight size={14} />
|
||||
{isProcessing ? 'Picking up…' : 'Start here'}
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
// Pre-claim: 3 options (task lane exists) or 2 options (empty lane)
|
||||
<div className="mt-6 space-y-2">
|
||||
<p className="text-xs text-muted-foreground mb-3">
|
||||
How would you like to approach this session?
|
||||
</p>
|
||||
|
||||
{/* Continue — only when task lane has items */}
|
||||
{hasTaskLane && onContinue && (
|
||||
<button
|
||||
ref={startBtnRef}
|
||||
onClick={() => void onContinue()}
|
||||
disabled={isProcessing}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-3 rounded-lg px-4 py-3 min-h-[52px] text-sm font-semibold transition-all',
|
||||
'bg-accent text-white hover:brightness-110 active:scale-[0.98] disabled:opacity-50 disabled:pointer-events-none',
|
||||
)}
|
||||
>
|
||||
{activeOptionKey === 'continue' ? (
|
||||
<Loader2 size={16} className="shrink-0 animate-spin" />
|
||||
) : (
|
||||
<ArrowRight size={16} className="shrink-0" />
|
||||
)}
|
||||
<span className="flex-1 text-left">
|
||||
Continue where{' '}
|
||||
<span className="font-bold">
|
||||
{handoff.handed_off_by_name ?? 'the original engineer'}
|
||||
</span>{' '}
|
||||
left off
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* AI analysis */}
|
||||
{onAIAnalysis && (
|
||||
<button
|
||||
ref={!hasTaskLane ? startBtnRef : undefined}
|
||||
onClick={() => void onAIAnalysis()}
|
||||
disabled={isProcessing}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-3 rounded-lg border px-4 py-3 min-h-[52px] text-sm font-semibold transition-all disabled:opacity-50 disabled:pointer-events-none',
|
||||
hasTaskLane
|
||||
? 'border-border bg-card text-foreground hover:bg-elevated hover:border-border-hover active:scale-[0.98]'
|
||||
: 'bg-accent text-white border-transparent hover:brightness-110 active:scale-[0.98]',
|
||||
)}
|
||||
>
|
||||
{activeOptionKey === 'ai' ? (
|
||||
<Loader2 size={16} className="shrink-0 animate-spin" />
|
||||
) : (
|
||||
<Sparkles size={16} className="shrink-0" />
|
||||
)}
|
||||
<span className="flex-1 text-left">Get AI analysis</span>
|
||||
<span className="text-xs font-normal opacity-70">
|
||||
{hasTaskLane ? 'Fresh take on what\'s been tried' : 'Generate diagnostic steps'}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Own approach */}
|
||||
{onOwnThing && (
|
||||
<button
|
||||
onClick={() => void onOwnThing()}
|
||||
disabled={isProcessing}
|
||||
className="w-full flex items-center gap-3 rounded-lg border border-border bg-card px-4 py-3 min-h-[52px] text-sm text-foreground hover:bg-elevated hover:border-border-hover active:scale-[0.98] disabled:opacity-50 disabled:pointer-events-none transition-all"
|
||||
>
|
||||
{activeOptionKey === 'own' ? (
|
||||
<Loader2 size={16} className="shrink-0 animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
<User size={16} className="shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
<span className="flex-1 text-left">I'll take it from here</span>
|
||||
<span className="text-xs text-muted-foreground">I know what to try</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user