fix: deduplicate actions, promote ViewToggle tab bar, standardize naming

Remove duplicate Update/Close actions from chat toolbars (FlowPilotPage,
CockpitPage) — session lifecycle actions now live only in headers. Redesign
ViewToggle as a persistent tab bar with bottom-border active indicator and
ARIA attributes. Standardize all action naming: Resolve (emerald), Update
(blue), Close (rose), Pause (muted). Fix IncidentHeader Resolve from orange
to emerald. Delete unused FlowPilotActionBar component (227 lines). Update
ConcludeSessionModal copy to use forward-facing action verbs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-04-04 04:40:06 +00:00
parent aca976a49a
commit 165e402284
9 changed files with 109 additions and 328 deletions

View File

@@ -33,8 +33,8 @@ interface ConcludeSessionModalProps {
const OUTCOMES: { value: ConclusionOutcome; label: string; description: string; icon: typeof CheckCircle2; color: string; bg: string; border: string }[] = [ const OUTCOMES: { value: ConclusionOutcome; label: string; description: string; icon: typeof CheckCircle2; color: string; bg: string; border: string }[] = [
{ {
value: 'resolved', value: 'resolved',
label: 'Resolved', label: 'Resolve',
description: 'Issue has been fixed or answered', description: 'Mark the issue as fixed',
icon: CheckCircle2, icon: CheckCircle2,
color: 'text-emerald-400', color: 'text-emerald-400',
bg: 'bg-emerald-400/10', bg: 'bg-emerald-400/10',
@@ -43,7 +43,7 @@ const OUTCOMES: { value: ConclusionOutcome; label: string; description: string;
{ {
value: 'escalated', value: 'escalated',
label: 'Escalate', label: 'Escalate',
description: 'Needs to be handed off or escalated', description: 'Hand off to another engineer or team',
icon: ArrowUpRight, icon: ArrowUpRight,
color: 'text-amber-400', color: 'text-amber-400',
bg: 'bg-amber-400/10', bg: 'bg-amber-400/10',
@@ -51,8 +51,8 @@ const OUTCOMES: { value: ConclusionOutcome; label: string; description: string;
}, },
{ {
value: 'paused', value: 'paused',
label: 'Paused', label: 'Pause',
description: 'Continuing later — saving progress', description: 'Save progress and come back later',
icon: Pause, icon: Pause,
color: 'text-blue-400', color: 'text-blue-400',
bg: 'bg-blue-400/10', bg: 'bg-blue-400/10',
@@ -155,7 +155,7 @@ export function ConcludeSessionModal({
setSummary('') setSummary('')
} }
} catch { } catch {
setError('Failed to conclude session. Please try again.') setError('Failed to close case. Please try again.')
setGenerating(false) setGenerating(false)
} }
} }
@@ -296,7 +296,7 @@ export function ConcludeSessionModal({
{step === 'select-outcome' && ( {step === 'select-outcome' && (
<div className="space-y-3"> <div className="space-y-3">
<p className="text-sm text-muted-foreground mb-4"> <p className="text-sm text-muted-foreground mb-4">
How did this session end? What would you like to do?
</p> </p>
{OUTCOMES.map(o => { {OUTCOMES.map(o => {
const Icon = o.icon const Icon = o.icon
@@ -526,12 +526,12 @@ export function ConcludeSessionModal({
{generating ? ( {generating ? (
<> <>
<Loader2 size={15} className="animate-spin" /> <Loader2 size={15} className="animate-spin" />
Generating... Closing...
</> </>
) : ( ) : (
<> <>
<Sparkles size={15} /> <Sparkles size={15} />
Generate Summary Close &amp; Generate
</> </>
)} )}
</button> </button>

View File

@@ -1,5 +1,5 @@
import { useState, useRef, useEffect } from 'react' import { useState, useRef, useEffect } from 'react'
import { Pencil, X, Check, ExternalLink, Pause, XCircle, Link2, MoreHorizontal, FileText } from 'lucide-react' import { Pencil, X, Check, CheckCircle2, ExternalLink, Pause, XCircle, Link2, MoreHorizontal, FileText } from 'lucide-react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { toast } from '@/lib/toast' import { toast } from '@/lib/toast'
import type { TriageMeta } from '@/types/ai-session' import type { TriageMeta } from '@/types/ai-session'
@@ -12,8 +12,6 @@ interface IncidentHeaderProps {
onStatusUpdate?: () => void onStatusUpdate?: () => void
onPause?: () => void onPause?: () => void
onClose?: () => void onClose?: () => void
/** Extra elements rendered in the action group (e.g. ViewToggle) */
extraActions?: React.ReactNode
} }
interface EditPopoverProps { interface EditPopoverProps {
@@ -129,14 +127,14 @@ function OverflowMenu({ onPause, onClose }: { onPause?: () => void; onClose?: ()
{open && ( {open && (
<> <>
<div className="fixed inset-0 z-40" onClick={() => setOpen(false)} /> <div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
<div className="absolute right-0 top-full mt-1 z-50 w-44 rounded-lg border border-border bg-card py-1 shadow-xl"> <div className="absolute right-0 top-full mt-1 z-50 w-40 rounded-lg border border-border bg-card py-1 shadow-xl">
{onPause && ( {onPause && (
<button <button
onClick={() => { onPause(); setOpen(false) }} onClick={() => { onPause(); setOpen(false) }}
className="w-full flex 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 text-left" className="w-full flex 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 text-left"
> >
<Pause size={13} /> <Pause size={13} />
Pause Case Pause
</button> </button>
)} )}
<button <button
@@ -152,7 +150,7 @@ function OverflowMenu({ onPause, onClose }: { onPause?: () => void; onClose?: ()
className="w-full flex items-center gap-2 px-3 py-2 text-xs text-muted-foreground hover:text-rose-400 hover:bg-rose-500/10 transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-2 text-xs text-muted-foreground hover:text-rose-400 hover:bg-rose-500/10 transition-colors text-left"
> >
<XCircle size={13} /> <XCircle size={13} />
Close Case Close
</button> </button>
)} )}
</div> </div>
@@ -170,7 +168,6 @@ export function IncidentHeader({
onStatusUpdate, onStatusUpdate,
onPause, onPause,
onClose, onClose,
extraActions,
}: IncidentHeaderProps) { }: IncidentHeaderProps) {
return ( return (
<div className="bg-card border-b border-default px-4 py-2 flex items-center gap-4 flex-wrap"> <div className="bg-card border-b border-default px-4 py-2 flex items-center gap-4 flex-wrap">
@@ -212,21 +209,21 @@ export function IncidentHeader({
)} )}
<button <button
onClick={onResolve} 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" className="flex items-center gap-1.5 bg-emerald-500/10 border border-emerald-500/20 rounded-lg px-3 py-1.5 text-xs font-medium text-emerald-400 hover:bg-emerald-500/20 transition-colors"
> >
<CheckCircle2 size={13} />
Resolve Resolve
</button> </button>
{onStatusUpdate && ( {onStatusUpdate && (
<button <button
onClick={onStatusUpdate} onClick={onStatusUpdate}
className="flex items-center gap-1.5 bg-blue-500/10 border border-blue-500/20 rounded px-3 py-1 text-xs font-medium text-blue-400 hover:bg-blue-500/20 transition-colors" className="flex items-center gap-1.5 bg-blue-500/10 border border-blue-500/20 rounded-lg px-3 py-1.5 text-xs font-medium text-blue-400 hover:bg-blue-500/20 transition-colors"
> >
<FileText size={13} /> <FileText size={13} />
Update Update
</button> </button>
)} )}
<OverflowMenu onPause={onPause} onClose={onClose} /> <OverflowMenu onPause={onPause} onClose={onClose} />
{extraActions}
</div> </div>
</div> </div>
) )

View File

@@ -18,6 +18,13 @@ interface ViewToggleProps {
sessionId?: string sessionId?: string
} }
/**
* Persistent tab bar for switching between FlowPilot (chat) and Cockpit (triage) views.
* Renders as a horizontal tab strip with an active bottom-border indicator.
*
* NOTE: If the tab bar proves too tall or prominent in certain layouts,
* consider pivoting to a compact segmented control (Option A from the critique).
*/
export function ViewToggle({ currentView, sessionId }: ViewToggleProps) { export function ViewToggle({ currentView, sessionId }: ViewToggleProps) {
const navigate = useNavigate() const navigate = useNavigate()
const hasCockpit = useFeatureFlag('flowpilot_cockpit') const hasCockpit = useFeatureFlag('flowpilot_cockpit')
@@ -37,19 +44,24 @@ export function ViewToggle({ currentView, sessionId }: ViewToggleProps) {
} }
return ( return (
<div className="flex items-center rounded-lg border border-border bg-card p-0.5 text-xs"> <div className="flex items-center border-b border-border px-3 shrink-0" role="tablist">
{VIEW_OPTIONS.map(({ key, label, icon: Icon }) => ( {VIEW_OPTIONS.map(({ key, label, icon: Icon }) => (
<button <button
key={key} key={key}
role="tab"
aria-selected={currentView === key}
aria-current={currentView === key ? 'page' : undefined}
onClick={() => handleSwitch(key)} onClick={() => handleSwitch(key)}
className={cn( className={cn(
'flex items-center gap-1.5 rounded-md px-2.5 py-1 font-medium transition-colors', 'flex items-center gap-1.5 px-3 py-2 text-xs font-medium border-b-2 -mb-px',
'transition-[color,border-color] duration-150',
'active:opacity-80',
currentView === key currentView === key
? 'bg-elevated text-foreground' ? 'text-foreground border-primary'
: 'text-muted-foreground hover:text-foreground' : 'text-muted-foreground hover:text-foreground border-transparent'
)} )}
> >
<Icon size={12} /> <Icon size={14} />
{label} {label}
</button> </button>
))} ))}

View File

@@ -1,227 +0,0 @@
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-emerald-500/10 border border-emerald-500/20 px-2.5 sm:px-4 py-2 min-h-[40px] sm:min-h-[44px] text-xs sm:text-sm font-medium text-emerald-400 hover:bg-emerald-500/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-amber-500/10 border border-amber-500/20 px-2.5 sm:px-4 py-2 min-h-[40px] sm:min-h-[44px] text-xs sm:text-sm font-medium text-amber-400 hover:bg-amber-500/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-blue-500/10 border border-blue-500/20 px-2.5 sm:px-4 py-2 min-h-[40px] sm:min-h-[44px] text-xs sm:text-sm font-medium text-blue-400 hover:bg-blue-500/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-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={handleAbandon}
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={onEscalate}
isProcessing={isProcessing || submitting}
hasPsaTicket={hasPsaTicket}
sessionId={sessionId}
/>
{/* Status Update modal */}
{onGenerateStatusUpdate && (
<StatusUpdateModal
open={showStatusUpdate}
onClose={() => setShowStatusUpdate(false)}
onGenerate={onGenerateStatusUpdate}
context="status"
hasPsaTicket={hasPsaTicket}
/>
)}
</>
)
}

View File

@@ -2,7 +2,6 @@ export { FlowPilotIntake } from './FlowPilotIntake'
export { FlowPilotSession } from './FlowPilotSession' export { FlowPilotSession } from './FlowPilotSession'
export { FlowPilotStepCard } from './FlowPilotStepCard' export { FlowPilotStepCard } from './FlowPilotStepCard'
export { FlowPilotOptions } from './FlowPilotOptions' export { FlowPilotOptions } from './FlowPilotOptions'
export { FlowPilotActionBar } from './FlowPilotActionBar'
export { ConfidenceIndicator } from './ConfidenceIndicator' export { ConfidenceIndicator } from './ConfidenceIndicator'
export { SessionDocView } from './SessionDocView' export { SessionDocView } from './SessionDocView'
export { AISessionListItem } from './AISessionListItem' export { AISessionListItem } from './AISessionListItem'

View File

@@ -1,6 +1,6 @@
import { useState, useRef, useCallback, useEffect } from 'react' import { useState, useRef, useCallback, useEffect } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { Sparkles, Send, Loader2, Flag, MessageSquare, Paperclip, Terminal, X, RotateCcw, ImagePlus, ListChecks, FileText, GripHorizontal } from 'lucide-react' import { Sparkles, Send, Loader2, MessageSquare, Paperclip, Terminal, X, RotateCcw, ImagePlus, ListChecks, GripHorizontal } from 'lucide-react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { PageMeta } from '@/components/common/PageMeta' import { PageMeta } from '@/components/common/PageMeta'
import { aiSessionsApi } from '@/api/aiSessions' import { aiSessionsApi } from '@/api/aiSessions'
@@ -254,9 +254,6 @@ export default function CockpitPage() {
Cases Cases
</button> </button>
<div className="flex-1" /> <div className="flex-1" />
{session.activeChatId && (
<ViewToggle currentView="cockpit" sessionId={session.activeChatId} />
)}
<button <button
onClick={session.handleNewChat} onClick={session.handleNewChat}
className="rounded-lg px-3 py-2 text-sm font-medium text-primary hover:bg-primary/10 transition-colors" className="rounded-lg px-3 py-2 text-sm font-medium text-primary hover:bg-primary/10 transition-colors"
@@ -265,6 +262,11 @@ export default function CockpitPage() {
</button> </button>
</div> </div>
{/* View tab bar — persistent when a session is active */}
{session.activeChatId && (
<ViewToggle currentView="cockpit" sessionId={session.activeChatId} />
)}
{session.activeChatId ? ( {session.activeChatId ? (
<> <>
{/* Incident Header */} {/* Incident Header */}
@@ -275,11 +277,6 @@ export default function CockpitPage() {
onResolve={() => session.setShowConclude(true)} onResolve={() => session.setShowConclude(true)}
onStatusUpdate={session.messages.length >= 2 ? () => session.setShowStatusUpdate(true) : undefined} onStatusUpdate={session.messages.length >= 2 ? () => session.setShowStatusUpdate(true) : undefined}
onClose={() => session.setShowConclude(true)} onClose={() => session.setShowConclude(true)}
extraActions={
<div className="hidden sm:block">
<ViewToggle currentView="cockpit" sessionId={session.activeChatId} />
</div>
}
/> />
{/* Resizable work zone + conversation log split */} {/* Resizable work zone + conversation log split */}
@@ -520,18 +517,6 @@ export default function CockpitPage() {
<span className="hidden sm:inline">Paste Logs</span> <span className="hidden sm:inline">Paste Logs</span>
</button> </button>
)} )}
{session.messages.length >= 2 && (
<>
<button type="button" onClick={() => session.setShowStatusUpdate(true)} disabled={session.loading} className="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs text-muted-foreground hover:text-blue-400 hover:bg-blue-500/10 transition-colors disabled:opacity-40" title="Share status update">
<FileText size={14} />
<span className="hidden sm:inline">Update</span>
</button>
<button type="button" onClick={() => session.setShowConclude(true)} disabled={session.loading} className="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs text-muted-foreground hover:text-amber-400 hover:bg-amber-400/10 transition-colors disabled:opacity-40" title="Close case">
<Flag size={14} />
<span className="hidden sm:inline">Close Case</span>
</button>
</>
)}
{!session.showTaskLane && (session.activeQuestions.length > 0 || session.activeActions.length > 0) && ( {!session.showTaskLane && (session.activeQuestions.length > 0 || session.activeActions.length > 0) && (
<button <button
type="button" type="button"

View File

@@ -1,5 +1,5 @@
import { useEffect } from 'react' import { useEffect, useState } from 'react'
import { Sparkles, Send, Loader2, Flag, MessageSquare, Paperclip, Terminal, X, RotateCcw, ImagePlus, ListChecks, FileText } from 'lucide-react' import { Sparkles, Send, Loader2, MessageSquare, Paperclip, Terminal, X, RotateCcw, ImagePlus, ListChecks, CheckCircle2, FileText, MoreHorizontal, Pause } from 'lucide-react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { PageMeta } from '@/components/common/PageMeta' import { PageMeta } from '@/components/common/PageMeta'
import { aiSessionsApi } from '@/api/aiSessions' import { aiSessionsApi } from '@/api/aiSessions'
@@ -13,6 +13,7 @@ import { useAssistantSession } from '@/hooks/useAssistantSession'
export default function FlowPilotPage() { export default function FlowPilotPage() {
const session = useAssistantSession() const session = useAssistantSession()
const [showOverflow, setShowOverflow] = useState(false)
// Handle prefill from dashboard / command palette // Handle prefill from dashboard / command palette
useEffect(() => { useEffect(() => {
@@ -112,12 +113,9 @@ export default function FlowPilotPage() {
className="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-[var(--color-bg-elevated)] transition-colors" className="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-[var(--color-bg-elevated)] transition-colors"
> >
<MessageSquare size={16} /> <MessageSquare size={16} />
Chats Cases
</button> </button>
<div className="flex-1" /> <div className="flex-1" />
{session.activeChatId && (
<ViewToggle currentView="flowpilot" sessionId={session.activeChatId} />
)}
<button <button
onClick={session.handleNewChat} onClick={session.handleNewChat}
className="rounded-lg px-3 py-2 text-sm font-medium text-primary hover:bg-primary/10 transition-colors" className="rounded-lg px-3 py-2 text-sm font-medium text-primary hover:bg-primary/10 transition-colors"
@@ -126,36 +124,65 @@ export default function FlowPilotPage() {
</button> </button>
</div> </div>
{/* View tab bar — persistent when a session is active */}
{session.activeChatId && (
<ViewToggle currentView="flowpilot" sessionId={session.activeChatId} />
)}
{session.activeChatId ? ( {session.activeChatId ? (
<> <>
{/* Desktop view toggle + action bar */} {/* Action bar — Resolve, Update, overflow (Pause/Close) */}
<div className="hidden sm:flex items-center justify-between px-4 py-1.5 border-b border-border/50"> {session.messages.length >= 2 && (
<div className="flex items-center gap-1.5"> <div className="hidden sm:flex items-center gap-1.5 px-4 py-1.5 border-b border-border/50">
{session.messages.length >= 2 && ( <button
<> type="button"
<button onClick={() => session.setShowConclude(true)}
type="button" disabled={session.loading}
onClick={() => session.setShowStatusUpdate(true)} 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"
disabled={session.loading} >
className="flex items-center gap-1.5 bg-blue-500/10 border border-blue-500/20 rounded-lg px-3 py-1 text-xs font-medium text-blue-400 hover:bg-blue-500/20 disabled:opacity-40 disabled:pointer-events-none transition-colors" <CheckCircle2 size={13} />
> Resolve
<FileText size={13} /> </button>
Update <button
</button> type="button"
<button onClick={() => session.setShowStatusUpdate(true)}
type="button" disabled={session.loading}
onClick={() => session.setShowConclude(true)} className="flex items-center gap-1.5 bg-blue-500/10 border border-blue-500/20 rounded-lg px-3 py-1.5 text-xs font-medium text-blue-400 hover:bg-blue-500/20 disabled:opacity-40 disabled:pointer-events-none transition-colors"
disabled={session.loading} >
className="flex items-center gap-1.5 bg-amber-500/10 border border-amber-500/20 rounded-lg px-3 py-1 text-xs font-medium text-amber-400 hover:bg-amber-500/20 disabled:opacity-40 disabled:pointer-events-none transition-colors" <FileText size={13} />
> Update
<Flag size={13} /> </button>
Conclude <div className="relative">
</button> <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-40 rounded-lg border border-border bg-card py-1 shadow-lg">
<button
onClick={() => { setShowOverflow(false); /* pause not wired on chat sessions yet */ }}
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); session.setShowConclude(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
</button>
</div>
</>
)}
</div>
</div> </div>
<ViewToggle currentView="flowpilot" sessionId={session.activeChatId} /> )}
</div>
{/* Messages */} {/* Messages */}
<div className="flex-1 overflow-y-auto px-4 sm:px-6 py-4 space-y-4"> <div className="flex-1 overflow-y-auto px-4 sm:px-6 py-4 space-y-4">
@@ -297,18 +324,6 @@ export default function FlowPilotPage() {
<span className="hidden sm:inline">Paste Logs</span> <span className="hidden sm:inline">Paste Logs</span>
</button> </button>
)} )}
{session.messages.length >= 2 && (
<>
<button type="button" onClick={() => session.setShowStatusUpdate(true)} disabled={session.loading} className="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs text-muted-foreground hover:text-blue-400 hover:bg-blue-500/10 transition-colors disabled:opacity-40" title="Share status update">
<FileText size={14} />
<span className="hidden sm:inline">Update</span>
</button>
<button type="button" onClick={() => session.setShowConclude(true)} disabled={session.loading} className="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs text-muted-foreground hover:text-amber-400 hover:bg-amber-400/10 transition-colors disabled:opacity-40" title="Conclude session">
<Flag size={14} />
<span className="hidden sm:inline">Conclude</span>
</button>
</>
)}
{!session.showTaskLane && (session.activeQuestions.length > 0 || session.activeActions.length > 0) && ( {!session.showTaskLane && (session.activeQuestions.length > 0 || session.activeActions.length > 0) && (
<button <button
type="button" type="button"
@@ -368,7 +383,7 @@ export default function FlowPilotPage() {
</div> </div>
</div> </div>
{/* Conclude Session Modal */} {/* Close Case Modal */}
<ConcludeSessionModal <ConcludeSessionModal
isOpen={session.showConclude} isOpen={session.showConclude}
onClose={() => session.setShowConclude(false)} onClose={() => session.setShowConclude(false)}

View File

@@ -264,7 +264,7 @@ export default function FlowPilotSessionPage() {
onClick={() => setShowStatusUpdate(true)} onClick={() => setShowStatusUpdate(true)}
disabled={fp.isProcessing} disabled={fp.isProcessing}
className="flex items-center gap-1.5 rounded-lg bg-blue-500/10 border border-blue-500/20 px-3 py-1.5 text-xs font-medium text-blue-400 hover:bg-blue-500/20 disabled:opacity-40 disabled:pointer-events-none transition-colors" className="flex items-center gap-1.5 rounded-lg bg-blue-500/10 border border-blue-500/20 px-3 py-1.5 text-xs font-medium text-blue-400 hover:bg-blue-500/20 disabled:opacity-40 disabled:pointer-events-none transition-colors"
title="Share Update" title="Update"
> >
<FileText size={13} /> <FileText size={13} />
Update Update
@@ -281,7 +281,7 @@ export default function FlowPilotSessionPage() {
{showOverflow && ( {showOverflow && (
<> <>
<div className="fixed inset-0 z-40" onClick={() => setShowOverflow(false)} /> <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"> <div className="absolute right-0 top-full mt-1 z-50 w-40 rounded-lg border border-border bg-card py-1 shadow-lg">
<button <button
onClick={() => { setShowOverflow(false); fp.pauseSession() }} 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" 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"
@@ -294,7 +294,7 @@ export default function FlowPilotSessionPage() {
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" 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} /> <X size={13} />
Close Session Close
</button> </button>
</div> </div>
</> </>
@@ -313,7 +313,7 @@ export default function FlowPilotSessionPage() {
{showOverflow && ( {showOverflow && (
<> <>
<div className="fixed inset-0 z-40" onClick={() => setShowOverflow(false)} /> <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"> <div className="absolute right-0 top-full mt-1 z-50 w-40 rounded-lg border border-border bg-card py-1 shadow-lg">
<button <button
onClick={() => { setShowOverflow(false); setShowResolve(true) }} onClick={() => { setShowOverflow(false); setShowResolve(true) }}
disabled={!fp.canResolve} disabled={!fp.canResolve}
@@ -336,7 +336,7 @@ export default function FlowPilotSessionPage() {
className="flex w-full items-center gap-2 px-3 py-2.5 text-xs text-blue-400 hover:bg-blue-500/10 transition-colors" className="flex w-full items-center gap-2 px-3 py-2.5 text-xs text-blue-400 hover:bg-blue-500/10 transition-colors"
> >
<FileText size={14} /> <FileText size={14} />
Share Update Update
</button> </button>
)} )}
<div className="my-1 border-t border-border" /> <div className="my-1 border-t border-border" />
@@ -352,7 +352,7 @@ export default function FlowPilotSessionPage() {
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" 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} /> <X size={14} />
Close Session Close
</button> </button>
</div> </div>
</> </>
@@ -450,7 +450,7 @@ export default function FlowPilotSessionPage() {
{showAbandon && ( {showAbandon && (
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center bg-black/60 backdrop-blur-sm"> <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"> <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> <h3 className="font-heading text-lg font-semibold text-foreground mb-1">Close</h3>
<p className="text-sm text-muted-foreground mb-4"> <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. Are you sure you want to close this session? The session history will be kept but it won't count as resolved.
</p> </p>
@@ -475,7 +475,7 @@ export default function FlowPilotSessionPage() {
disabled={submitting} 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" 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'} {submitting ? 'Closing...' : 'Close'}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -624,7 +624,7 @@ export default function SessionHistoryPage() {
ref={closePopoverRef} ref={closePopoverRef}
className="absolute right-0 top-full z-20 mt-2 w-72 rounded-xl border border-border bg-card p-4 shadow-xl" className="absolute right-0 top-full z-20 mt-2 w-72 rounded-xl border border-border bg-card p-4 shadow-xl"
> >
<p className="text-sm font-heading font-medium text-foreground mb-3">Close Session</p> <p className="text-sm font-heading font-medium text-foreground mb-3">Close</p>
<label className="block text-[0.625rem] font-sans uppercase tracking-[0.1em] text-muted-foreground mb-1">Outcome</label> <label className="block text-[0.625rem] font-sans uppercase tracking-[0.1em] text-muted-foreground mb-1">Outcome</label>
<select <select