refactor: move FlowPilot actions to header bar, fix dividers
- Moved Resolve/Escalate/Share Update buttons from fixed bottom bar into the session header. Desktop: inline buttons + overflow menu (Pause/Close). Mobile: single overflow menu with all actions. - Removed FlowPilotActionBar from bottom — message input is now the only fixed element at bottom, giving more breathing room. - Replaced jarring white dividers (var(--glass-border)) with subtle border-border (#2e3240) throughout FlowPilot session. - Restyled status badge from button-looking element to dot + text indicator with semantic colors (green=resolved, amber=escalated). - Reduced content padding from pb-32 to pb-24. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -178,7 +178,7 @@ export function FlowPilotMessageBar({ onRespond, disabled = false, isProcessing
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed bottom-[52px] sm:bottom-[56px] right-0 lg:right-72 z-40 px-3 sm:px-6 lg:px-8 pb-2"
|
||||
className="fixed bottom-0 right-0 lg:right-72 z-40 px-3 sm:px-6 lg:px-8 pb-3"
|
||||
style={{ left: 'var(--sidebar-w, 0px)' }}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnter={handleDragEnter}
|
||||
|
||||
@@ -14,7 +14,6 @@ import type {
|
||||
} from '@/types/ai-session'
|
||||
import { ConfidenceIndicator } from './ConfidenceIndicator'
|
||||
import { FlowPilotStepCard } from './FlowPilotStepCard'
|
||||
import { FlowPilotActionBar } from './FlowPilotActionBar'
|
||||
import { FlowPilotMessageBar } from './FlowPilotMessageBar'
|
||||
import { SessionDocView } from './SessionDocView'
|
||||
import { StatusUpdateModal } from './StatusUpdateModal'
|
||||
@@ -170,7 +169,7 @@ export function FlowPilotSession({
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Mobile sidebar summary (collapsible) */}
|
||||
<div className="lg:hidden border-b" style={{ borderColor: 'var(--glass-border)' }}>
|
||||
<div className="lg:hidden border-b border-border">
|
||||
<button
|
||||
onClick={() => setShowMobileSidebar(!showMobileSidebar)}
|
||||
className="flex w-full items-center justify-between px-3 py-2 sm:px-4"
|
||||
@@ -230,8 +229,8 @@ export function FlowPilotSession({
|
||||
|
||||
{/* Main content area: conversation + sidebar */}
|
||||
<div className="flex flex-1 min-h-0">
|
||||
{/* Conversation column — pb-32 provides clearance for the fixed message bar + action bar */}
|
||||
<div ref={scrollRef} className="flex-1 overflow-y-auto p-3 pb-32 sm:p-4 sm:pb-32 lg:p-6 lg:pb-32">
|
||||
{/* Conversation column — pb-24 provides clearance for the fixed message bar */}
|
||||
<div ref={scrollRef} className="flex-1 overflow-y-auto p-3 pb-24 sm:p-4 sm:pb-24 lg:p-6 lg:pb-24">
|
||||
<div className="mx-auto max-w-2xl space-y-3">
|
||||
{allSteps.map((step) => (
|
||||
<FlowPilotStepCard
|
||||
@@ -248,8 +247,7 @@ export function FlowPilotSession({
|
||||
|
||||
{/* Sidebar — desktop only */}
|
||||
<div
|
||||
className="hidden w-72 shrink-0 overflow-y-auto border-l p-4 lg:block"
|
||||
style={{ borderColor: 'var(--glass-border)' }}
|
||||
className="hidden w-72 shrink-0 overflow-y-auto border-l border-border p-4 lg:block"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* Ticket context */}
|
||||
@@ -337,7 +335,7 @@ export function FlowPilotSession({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Message bar */}
|
||||
{/* Message bar — now the only fixed bottom element */}
|
||||
{session.status === 'active' && (
|
||||
<FlowPilotMessageBar
|
||||
onRespond={onRespond}
|
||||
@@ -346,23 +344,6 @@ export function FlowPilotSession({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Action bar */}
|
||||
{session.status === 'active' && (
|
||||
<FlowPilotActionBar
|
||||
canResolve={canResolve}
|
||||
canEscalate={canEscalate}
|
||||
isProcessing={isProcessing}
|
||||
hasPsaTicket={!!session.psa_ticket_id}
|
||||
sessionId={session.id}
|
||||
canShareUpdate={allSteps.length >= 2}
|
||||
onResolve={onResolve}
|
||||
onEscalate={onEscalate}
|
||||
onPause={onPause}
|
||||
onAbandon={onAbandon}
|
||||
onGenerateStatusUpdate={onGenerateStatusUpdate}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Paused banner */}
|
||||
{session.status === 'paused' && onResume && (
|
||||
<div
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useParams, useSearchParams, useLocation, useBlocker, useNavigate } from 'react-router-dom'
|
||||
import { Sparkles, Loader2, AlertTriangle } from 'lucide-react'
|
||||
import { Sparkles, Loader2, AlertTriangle, CheckCircle2, ArrowUpRight, FileText, MoreHorizontal, Pause, X } from 'lucide-react'
|
||||
import { useFlowPilotSession } from '@/hooks/useFlowPilotSession'
|
||||
import { FlowPilotIntake, FlowPilotSession, SessionBriefing } from '@/components/flowpilot'
|
||||
import { EscalateModal } from '@/components/flowpilot/EscalateModal'
|
||||
import { StatusUpdateModal } from '@/components/flowpilot/StatusUpdateModal'
|
||||
import { aiSessionsApi } from '@/api'
|
||||
import { toast } from '@/lib/toast'
|
||||
|
||||
@@ -15,6 +17,13 @@ export default function FlowPilotSessionPage() {
|
||||
const isPickup = searchParams.get('pickup') === 'true'
|
||||
const fp = useFlowPilotSession()
|
||||
const prefillHandledRef = useRef(false)
|
||||
const [showOverflow, setShowOverflow] = useState(false)
|
||||
const [showResolve, setShowResolve] = useState(false)
|
||||
const [showEscalate, setShowEscalate] = useState(false)
|
||||
const [showAbandon, setShowAbandon] = useState(false)
|
||||
const [showStatusUpdate, setShowStatusUpdate] = useState(false)
|
||||
const [resolutionSummary, setResolutionSummary] = useState('')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
// Block navigation when session is active
|
||||
const isActiveSession = fp.session?.status === 'active'
|
||||
@@ -195,12 +204,11 @@ export default function FlowPilotSessionPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Header */}
|
||||
{/* Header with actions */}
|
||||
<div
|
||||
className="flex items-center gap-3 border-b px-5 py-3 shrink-0"
|
||||
style={{ borderColor: 'var(--glass-border)' }}
|
||||
className="flex items-center gap-3 border-b border-border px-3 sm:px-5 py-2.5 shrink-0"
|
||||
>
|
||||
<span className="flex h-7 w-7 items-center justify-center rounded-lg bg-accent-dim">
|
||||
<span className="flex h-7 w-7 items-center justify-center rounded-lg bg-accent-dim shrink-0">
|
||||
<Sparkles size={14} className="text-primary" />
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -208,9 +216,147 @@ export default function FlowPilotSessionPage() {
|
||||
{fp.session.problem_summary || 'FlowPilot Session'}
|
||||
</h1>
|
||||
</div>
|
||||
<span className="font-sans text-xs rounded-md bg-card px-2 py-0.5 text-[0.625rem] uppercase tracking-wider text-muted-foreground border border-border">
|
||||
{fp.session.status}
|
||||
</span>
|
||||
|
||||
{/* Action buttons — desktop inline, mobile overflow menu */}
|
||||
{fp.session.status === 'active' && (
|
||||
<>
|
||||
{/* Desktop actions */}
|
||||
<div className="hidden sm:flex items-center gap-1.5">
|
||||
<button
|
||||
onClick={() => setShowResolve(true)}
|
||||
disabled={!fp.canResolve || fp.isProcessing}
|
||||
className="flex items-center gap-1.5 rounded-lg bg-emerald-500/10 border border-emerald-500/20 px-3 py-1.5 text-xs font-medium text-emerald-400 hover:bg-emerald-500/20 disabled:opacity-40 disabled:pointer-events-none transition-colors"
|
||||
>
|
||||
<CheckCircle2 size={13} />
|
||||
Resolve
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowEscalate(true)}
|
||||
disabled={!fp.canEscalate || fp.isProcessing}
|
||||
className="flex items-center gap-1.5 rounded-lg bg-amber-500/10 border border-amber-500/20 px-3 py-1.5 text-xs font-medium text-amber-400 hover:bg-amber-500/20 disabled:opacity-40 disabled:pointer-events-none transition-colors"
|
||||
>
|
||||
<ArrowUpRight size={13} />
|
||||
Escalate
|
||||
</button>
|
||||
{fp.allSteps.length >= 2 && (
|
||||
<button
|
||||
onClick={() => setShowStatusUpdate(true)}
|
||||
disabled={fp.isProcessing}
|
||||
className="flex items-center gap-1.5 rounded-lg bg-cyan-500/10 border border-cyan-500/20 px-3 py-1.5 text-xs font-medium text-cyan-400 hover:bg-cyan-500/20 disabled:opacity-40 disabled:pointer-events-none transition-colors"
|
||||
title="Share Update"
|
||||
>
|
||||
<FileText size={13} />
|
||||
Update
|
||||
</button>
|
||||
)}
|
||||
{/* Overflow: Pause / Close */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowOverflow(!showOverflow)}
|
||||
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>
|
||||
{showOverflow && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40" onClick={() => setShowOverflow(false)} />
|
||||
<div className="absolute right-0 top-full mt-1 z-50 w-36 rounded-lg border border-border bg-card py-1 shadow-lg">
|
||||
<button
|
||||
onClick={() => { setShowOverflow(false); fp.pauseSession() }}
|
||||
className="flex w-full 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"
|
||||
>
|
||||
<Pause size={13} />
|
||||
Pause
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setShowOverflow(false); setShowAbandon(true) }}
|
||||
className="flex w-full items-center gap-2 px-3 py-2 text-xs text-muted-foreground hover:text-rose-400 hover:bg-rose-500/10 transition-colors"
|
||||
>
|
||||
<X size={13} />
|
||||
Close Session
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile: single overflow menu */}
|
||||
<div className="sm:hidden relative">
|
||||
<button
|
||||
onClick={() => setShowOverflow(!showOverflow)}
|
||||
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={18} />
|
||||
</button>
|
||||
{showOverflow && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40" onClick={() => setShowOverflow(false)} />
|
||||
<div className="absolute right-0 top-full mt-1 z-50 w-44 rounded-lg border border-border bg-card py-1 shadow-lg">
|
||||
<button
|
||||
onClick={() => { setShowOverflow(false); setShowResolve(true) }}
|
||||
disabled={!fp.canResolve}
|
||||
className="flex w-full items-center gap-2 px-3 py-2.5 text-xs text-emerald-400 hover:bg-emerald-500/10 transition-colors disabled:opacity-40"
|
||||
>
|
||||
<CheckCircle2 size={14} />
|
||||
Resolve
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setShowOverflow(false); setShowEscalate(true) }}
|
||||
disabled={!fp.canEscalate}
|
||||
className="flex w-full items-center gap-2 px-3 py-2.5 text-xs text-amber-400 hover:bg-amber-500/10 transition-colors disabled:opacity-40"
|
||||
>
|
||||
<ArrowUpRight size={14} />
|
||||
Escalate
|
||||
</button>
|
||||
{fp.allSteps.length >= 2 && (
|
||||
<button
|
||||
onClick={() => { setShowOverflow(false); setShowStatusUpdate(true) }}
|
||||
className="flex w-full items-center gap-2 px-3 py-2.5 text-xs text-cyan-400 hover:bg-cyan-500/10 transition-colors"
|
||||
>
|
||||
<FileText size={14} />
|
||||
Share Update
|
||||
</button>
|
||||
)}
|
||||
<div className="my-1 border-t border-border" />
|
||||
<button
|
||||
onClick={() => { setShowOverflow(false); fp.pauseSession() }}
|
||||
className="flex w-full items-center gap-2 px-3 py-2.5 text-xs text-muted-foreground hover:text-foreground hover:bg-[rgba(255,255,255,0.06)] transition-colors"
|
||||
>
|
||||
<Pause size={14} />
|
||||
Pause
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setShowOverflow(false); setShowAbandon(true) }}
|
||||
className="flex w-full items-center gap-2 px-3 py-2.5 text-xs text-muted-foreground hover:text-rose-400 hover:bg-rose-500/10 transition-colors"
|
||||
>
|
||||
<X size={14} />
|
||||
Close Session
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Status badge — non-active states */}
|
||||
{fp.session.status !== 'active' && (
|
||||
<span className={`flex items-center gap-1.5 text-[0.625rem] uppercase tracking-wider font-sans ${
|
||||
fp.session.status === 'resolved' ? 'text-emerald-400' :
|
||||
fp.session.status === 'escalated' ? 'text-amber-400' :
|
||||
fp.session.status === 'paused' ? 'text-muted-foreground' :
|
||||
'text-muted-foreground'
|
||||
}`}>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${
|
||||
fp.session.status === 'resolved' ? 'bg-emerald-400' :
|
||||
fp.session.status === 'escalated' ? 'bg-amber-400' :
|
||||
fp.session.status === 'paused' ? 'bg-muted-foreground' :
|
||||
'bg-muted-foreground'
|
||||
}`} />
|
||||
{fp.session.status}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Session content */}
|
||||
@@ -240,6 +386,105 @@ export default function FlowPilotSessionPage() {
|
||||
onGenerateStatusUpdate={(audience, length, context) => fp.generateStatusUpdate({ audience, length, context })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ── Page-level modals (moved from action bar) ── */}
|
||||
|
||||
{/* Resolve modal */}
|
||||
{showResolve && (
|
||||
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="card-flat w-full max-w-full sm:max-w-lg mx-0 sm:mx-4 p-4 sm:p-6 rounded-t-2xl sm:rounded-2xl">
|
||||
<h3 className="font-heading text-lg font-semibold text-foreground mb-1">Resolve Session</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">Summarize what fixed the issue. This will be included in the auto-generated documentation.</p>
|
||||
<textarea
|
||||
value={resolutionSummary}
|
||||
onChange={(e) => setResolutionSummary(e.target.value)}
|
||||
placeholder="What resolved the issue?"
|
||||
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-none resize-none"
|
||||
rows={4}
|
||||
autoFocus
|
||||
/>
|
||||
<div className="mt-4 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
||||
<button
|
||||
onClick={() => setShowResolve(false)}
|
||||
className="rounded-lg px-4 py-2 min-h-[44px] text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (resolutionSummary.length < 5) return
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await fp.resolveSession({ resolution_summary: resolutionSummary })
|
||||
setShowResolve(false)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}}
|
||||
disabled={resolutionSummary.length < 5 || submitting}
|
||||
className="rounded-lg bg-emerald-500/20 border border-emerald-500/30 px-4 py-2 min-h-[44px] text-sm font-medium text-emerald-400 hover:bg-emerald-500/30 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{submitting ? 'Resolving...' : 'Resolve Session'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Close/Abandon confirmation */}
|
||||
{showAbandon && (
|
||||
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="card-flat w-full max-w-full sm:max-w-lg mx-0 sm:mx-4 p-4 sm:p-6 rounded-t-2xl sm:rounded-2xl">
|
||||
<h3 className="font-heading text-lg font-semibold text-foreground mb-1">Close Session</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Are you sure you want to close this session? The session history will be kept but it won't count as resolved.
|
||||
</p>
|
||||
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
||||
<button
|
||||
onClick={() => setShowAbandon(false)}
|
||||
className="rounded-lg px-4 py-2 min-h-[44px] text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await fp.abandonSession()
|
||||
setShowAbandon(false)
|
||||
navigate('/sessions')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}}
|
||||
disabled={submitting}
|
||||
className="rounded-lg bg-rose-500/20 border border-rose-500/30 px-4 py-2 min-h-[44px] text-sm font-medium text-rose-400 hover:bg-rose-500/30 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{submitting ? 'Closing...' : 'Close Session'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Escalate modal */}
|
||||
<EscalateModal
|
||||
open={showEscalate}
|
||||
onClose={() => setShowEscalate(false)}
|
||||
onEscalate={fp.escalateSession}
|
||||
isProcessing={fp.isProcessing || submitting}
|
||||
hasPsaTicket={!!fp.session.psa_ticket_id}
|
||||
sessionId={fp.session.id}
|
||||
/>
|
||||
|
||||
{/* Status Update modal */}
|
||||
<StatusUpdateModal
|
||||
open={showStatusUpdate}
|
||||
onClose={() => setShowStatusUpdate(false)}
|
||||
onGenerate={(audience, length, context) => fp.generateStatusUpdate({ audience, length, context })}
|
||||
context="status"
|
||||
hasPsaTicket={!!fp.session.psa_ticket_id}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user