Files
resolutionflow/frontend/src/components/flowpilot/FlowPilotActionBar.tsx
Michael Chihlas cef853d7ea refactor: normalize FlowPilot/Assistant/ScriptBuilder to design system tokens
Replace hardcoded Tailwind color utilities with semantic CSS variable tokens
across 31 files in the FlowPilot, Assistant Chat, and Script Builder feature
communities — the areas graphify identified as design-system-free.

- text-blue-400 → text-accent, bg-blue-500/10 → bg-accent-dim, border-blue-500/20 → border-accent/20
- text-amber-400 → text-warning, bg-amber-400/10 → bg-warning-dim, border-l-amber-500 → border-l-warning
- text-rose-400/500 → text-danger, bg-rose-500/10 → bg-danger-dim
- text-emerald-400 → text-success, bg-emerald-500/10 → bg-success-dim, border-l-emerald-500 → border-l-success
- bg-white/[0.08] → bg-elevated (opacity hack → semantic surface token)
- bg-gradient-to-r from-blue-500 to-blue-400 → bg-accent (no gradient surfaces)
- bg-[#60a5fa] → bg-accent (hard-coded hex removed)

Also adds graphify-out/ to .gitignore.

Theme resilience: accent color has changed twice in 5 weeks. Semantic tokens
mean the next change is a 1-line edit in index.css, not 110 grep-and-replace.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 20:20:07 -04:00

228 lines
9.3 KiB
TypeScript

import { useState } from 'react'
import { CheckCircle2, ArrowUpRight, Pause, X, FileText } from 'lucide-react'
import { EscalateModal } from './EscalateModal'
import { StatusUpdateModal } from './StatusUpdateModal'
import type {
ResolveSessionRequest,
EscalateSessionRequest,
SessionDocumentation,
StatusUpdateAudience,
StatusUpdateLength,
StatusUpdateContext,
StatusUpdateResponse,
} from '@/types/ai-session'
interface FlowPilotActionBarProps {
canResolve: boolean
canEscalate: boolean
isProcessing: boolean
hasPsaTicket?: boolean
sessionId?: string
canShareUpdate?: boolean
onResolve: (data: ResolveSessionRequest) => Promise<SessionDocumentation>
onEscalate: (data: EscalateSessionRequest) => Promise<SessionDocumentation>
onPause?: () => Promise<void>
onAbandon?: () => Promise<void>
onGenerateStatusUpdate?: (audience: StatusUpdateAudience, length: StatusUpdateLength, context: StatusUpdateContext) => Promise<StatusUpdateResponse>
}
export function FlowPilotActionBar({
canResolve,
canEscalate,
isProcessing,
hasPsaTicket = false,
sessionId,
canShareUpdate = false,
onResolve,
onEscalate,
onPause,
onAbandon,
onGenerateStatusUpdate,
}: FlowPilotActionBarProps) {
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)
const handleResolve = async () => {
if (!resolutionSummary.trim() || resolutionSummary.length < 5) return
setSubmitting(true)
try {
await onResolve({ resolution_summary: resolutionSummary })
setShowResolve(false)
} finally {
setSubmitting(false)
}
}
const handlePause = async () => {
if (onPause) {
setSubmitting(true)
try {
await onPause()
} finally {
setSubmitting(false)
}
}
}
const handleAbandon = async () => {
if (onAbandon) {
setSubmitting(true)
try {
await onAbandon()
setShowAbandon(false)
} finally {
setSubmitting(false)
}
}
}
return (
<>
{/* Bottom bar — fixed to viewport bottom, single row on all screen sizes */}
<div
className="fixed bottom-0 right-0 z-40 flex items-center gap-1.5 sm:gap-3 border-t border-border bg-card px-2 sm:px-5 py-2 sm:py-3"
style={{ left: 'var(--sidebar-w, 0px)' }}
>
{/* Primary actions */}
<button
onClick={() => { setShowResolve(true); setShowEscalate(false) }}
disabled={!canResolve || isProcessing}
className="flex items-center justify-center gap-1.5 rounded-lg bg-success-dim border border-success/20 px-2.5 sm:px-4 py-2 min-h-[40px] sm:min-h-[44px] text-xs sm:text-sm font-medium text-success hover:bg-success/20 disabled:opacity-40 disabled:pointer-events-none transition-colors"
>
<CheckCircle2 size={15} />
Resolve
</button>
<button
onClick={() => setShowEscalate(true)}
disabled={!canEscalate || isProcessing}
className="flex items-center justify-center gap-1.5 rounded-lg bg-warning-dim border border-warning/20 px-2.5 sm:px-4 py-2 min-h-[40px] sm:min-h-[44px] text-xs sm:text-sm font-medium text-warning hover:bg-warning/20 disabled:opacity-40 disabled:pointer-events-none transition-colors"
>
<ArrowUpRight size={15} />
Escalate
</button>
{canShareUpdate && onGenerateStatusUpdate && (
<button
onClick={() => setShowStatusUpdate(true)}
disabled={isProcessing}
className="flex items-center justify-center gap-1.5 rounded-lg bg-accent-dim border border-accent/20 px-2.5 sm:px-4 py-2 min-h-[40px] sm:min-h-[44px] text-xs sm:text-sm font-medium text-accent hover:bg-accent/20 disabled:opacity-40 disabled:pointer-events-none transition-colors"
title="Share Update"
>
<FileText size={15} />
<span className="hidden sm:inline">Share Update</span>
</button>
)}
{/* Spacer */}
<div className="flex-1" />
{/* Secondary actions — right side */}
{onPause && (
<button
onClick={handlePause}
disabled={isProcessing || submitting}
className="flex items-center justify-center gap-1.5 rounded-lg bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] px-2.5 sm:px-4 py-2 min-h-[40px] sm:min-h-[44px] text-xs sm:text-sm font-medium text-muted-foreground hover:text-foreground hover:border-[rgba(255,255,255,0.12)] disabled:opacity-40 disabled:pointer-events-none transition-colors"
>
<Pause size={15} />
<span className="hidden sm:inline">Pause</span>
</button>
)}
{onAbandon && (
<button
onClick={() => setShowAbandon(true)}
disabled={isProcessing || submitting}
className="flex items-center justify-center gap-1.5 rounded-lg bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] px-2.5 sm:px-4 py-2 min-h-[40px] sm:min-h-[44px] text-xs sm:text-sm font-medium text-muted-foreground hover:text-foreground hover:border-[rgba(255,255,255,0.12)] disabled:opacity-40 disabled:pointer-events-none transition-colors"
>
<X size={15} />
<span className="hidden sm:inline">Close</span>
</button>
)}
</div>
{/* 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(96,165,250,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={handleResolve}
disabled={resolutionSummary.length < 5 || submitting}
className="rounded-lg bg-success/20 border border-success/30 px-4 py-2 min-h-[44px] text-sm font-medium text-success hover:bg-success/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={handleAbandon}
disabled={submitting}
className="rounded-lg bg-danger/20 border border-danger/30 px-4 py-2 min-h-[44px] text-sm font-medium text-danger hover:bg-danger/30 disabled:opacity-50 transition-colors"
>
{submitting ? 'Closing...' : 'Close Session'}
</button>
</div>
</div>
</div>
)}
{/* Escalate modal */}
<EscalateModal
open={showEscalate}
onClose={() => setShowEscalate(false)}
onEscalate={onEscalate}
isProcessing={isProcessing || submitting}
hasPsaTicket={hasPsaTicket}
sessionId={sessionId}
/>
{/* Status Update modal */}
{onGenerateStatusUpdate && (
<StatusUpdateModal
open={showStatusUpdate}
onClose={() => setShowStatusUpdate(false)}
onGenerate={onGenerateStatusUpdate}
context="status"
hasPsaTicket={hasPsaTicket}
/>
)}
</>
)
}