fix: cockpit/flowpilot bugs and redesign view toggle placement

- Fix return type annotation on unified_chat_service.send_chat_message (6→7 tuple)
- Fix stale closure in CockpitPage handleStepComplete auto-advance logic
- Fix IncidentHeader copy link hardcoding /assistant/ path (now uses current URL)
- Wire psaTicketId from session data through to CockpitPage incident header
- Fix FlowPilotAsks showing only first question — add chevron navigation for all
- Redesign ViewToggle: add icons (MessageSquare/LayoutDashboard) and subtitles
- Move view toggle from chatbar toolbar to standalone row above input on dashboard
- Standardize toggle placement across dashboard, FlowPilot, and Cockpit pages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-04-03 05:02:42 +00:00
parent ed6e6cd1ed
commit 813b598101
9 changed files with 121 additions and 88 deletions

View File

@@ -1,5 +1,5 @@
import { useState } from 'react'
import { Send, HelpCircle } from 'lucide-react'
import { useState, useEffect } from 'react'
import { Send, HelpCircle, ChevronLeft, ChevronRight } from 'lucide-react'
import type { QuestionItem } from '@/types/ai-session'
interface FlowPilotAsksProps {
@@ -10,11 +10,17 @@ interface FlowPilotAsksProps {
export function FlowPilotAsks({ questions, onAnswer, loading }: FlowPilotAsksProps) {
const [freeText, setFreeText] = useState('')
const [currentIdx, setCurrentIdx] = useState(0)
// Show first unanswered question
const question = questions.length > 0 ? questions[0] : null
// Reset index when questions change
useEffect(() => {
setCurrentIdx(0)
setFreeText('')
}, [questions])
if (!question) return null
if (questions.length === 0) return null
const question = questions[Math.min(currentIdx, questions.length - 1)]
const handleFreeTextSubmit = () => {
if (!freeText.trim()) return
@@ -24,9 +30,32 @@ export function FlowPilotAsks({ questions, onAnswer, loading }: FlowPilotAsksPro
return (
<div className="bg-card border border-default rounded-lg p-3">
<div className="text-[10px] uppercase tracking-wider text-warning font-semibold mb-2 flex items-center gap-1.5">
<HelpCircle size={11} />
FlowPilot Asks
<div className="flex items-center justify-between mb-2">
<div className="text-[10px] uppercase tracking-wider text-warning font-semibold flex items-center gap-1.5">
<HelpCircle size={11} />
FlowPilot Asks
</div>
{questions.length > 1 && (
<div className="flex items-center gap-1.5">
<button
onClick={() => setCurrentIdx(i => Math.max(0, i - 1))}
disabled={currentIdx === 0}
className="p-0.5 text-muted-foreground hover:text-foreground disabled:opacity-30 transition-colors"
>
<ChevronLeft size={14} />
</button>
<span className="text-[10px] text-muted-foreground tabular-nums">
{currentIdx + 1}/{questions.length}
</span>
<button
onClick={() => setCurrentIdx(i => Math.min(questions.length - 1, i + 1))}
disabled={currentIdx >= questions.length - 1}
className="p-0.5 text-muted-foreground hover:text-foreground disabled:opacity-30 transition-colors"
>
<ChevronRight size={14} />
</button>
</div>
)}
</div>
<p className="text-sm text-primary leading-relaxed mb-2.5">
{question.text}

View File

@@ -7,7 +7,6 @@ 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
onPause?: () => void
@@ -97,7 +96,7 @@ function HeaderField({ label, value, placeholder, onSave, isHypothesis }: Header
)
}
function OverflowMenu({ onPause, onClose, sessionId }: { onPause?: () => void; onClose?: () => void; sessionId: string }) {
function OverflowMenu({ onPause, onClose }: { onPause?: () => void; onClose?: () => void }) {
const [open, setOpen] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
@@ -118,7 +117,7 @@ function OverflowMenu({ onPause, onClose, sessionId }: { onPause?: () => void; o
}, [open])
const handleCopyLink = () => {
navigator.clipboard.writeText(`${window.location.origin}/assistant/${sessionId}`)
navigator.clipboard.writeText(`${window.location.origin}${window.location.pathname}`)
toast.success('Session link copied')
setOpen(false)
}
@@ -167,7 +166,6 @@ function OverflowMenu({ onPause, onClose, sessionId }: { onPause?: () => void; o
export function IncidentHeader({
triageMeta,
psaTicketId,
sessionId,
onFieldSave,
onResolve,
onPause,
@@ -217,7 +215,7 @@ export function IncidentHeader({
>
Resolve
</button>
<OverflowMenu onPause={onPause} onClose={onClose} sessionId={sessionId} />
<OverflowMenu onPause={onPause} onClose={onClose} />
</div>
</div>
)

View File

@@ -1,50 +1,69 @@
import { useNavigate } from 'react-router-dom'
import { MessageSquare, LayoutDashboard } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useFeatureFlag } from '@/hooks/useFeatureFlag'
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
type FlowPilotView = 'flowpilot' | 'cockpit'
const VIEW_OPTIONS: { key: FlowPilotView; label: string; icon: typeof MessageSquare; subtitle: string }[] = [
{ key: 'flowpilot', label: 'FlowPilot', icon: MessageSquare, subtitle: 'Chat-first AI troubleshooting' },
{ key: 'cockpit', label: 'Cockpit', icon: LayoutDashboard, subtitle: 'Steps, evidence & triage board' },
]
interface ViewToggleProps {
currentView: 'flowpilot' | 'cockpit'
sessionId: string
/** Which view is currently active — drives highlight state */
currentView: FlowPilotView
/** Session ID for navigation (session pages only). Omit for preference-only mode (dashboard). */
sessionId?: string
/** Show the subtitle below the toggle. Default true for standalone, false for inline. */
showSubtitle?: boolean
}
export function ViewToggle({ currentView, sessionId }: ViewToggleProps) {
export function ViewToggle({ currentView, sessionId, showSubtitle = true }: ViewToggleProps) {
const navigate = useNavigate()
const hasCockpit = useFeatureFlag('flowpilot_cockpit')
const setPreferredView = useUserPreferencesStore(s => s.setPreferredFlowPilotView)
if (!hasCockpit) return null
const handleSwitch = (view: 'flowpilot' | 'cockpit') => {
const activeOption = VIEW_OPTIONS.find(o => o.key === currentView) ?? VIEW_OPTIONS[0]
const handleSwitch = (view: FlowPilotView) => {
if (view === currentView) return
const path = view === 'cockpit'
? `/cockpit/${sessionId}`
: `/assistant/${sessionId}`
navigate(path)
setPreferredView(view)
if (sessionId) {
const path = view === 'cockpit'
? `/cockpit/${sessionId}`
: `/assistant/${sessionId}`
navigate(path)
}
}
return (
<div className="flex items-center rounded-lg border border-border bg-card p-0.5 text-xs">
<button
onClick={() => handleSwitch('flowpilot')}
className={cn(
'rounded-md px-2.5 py-1 font-medium transition-colors',
currentView === 'flowpilot'
? 'bg-elevated text-foreground'
: 'text-muted-foreground hover:text-foreground'
)}
>
FlowPilot
</button>
<button
onClick={() => handleSwitch('cockpit')}
className={cn(
'rounded-md px-2.5 py-1 font-medium transition-colors',
currentView === 'cockpit'
? 'bg-elevated text-foreground'
: 'text-muted-foreground hover:text-foreground'
)}
>
Cockpit
</button>
<div className="flex flex-col items-start gap-1">
<div className="flex items-center rounded-lg border border-border bg-card p-0.5 text-xs">
{VIEW_OPTIONS.map(({ key, label, icon: Icon }) => (
<button
key={key}
onClick={() => handleSwitch(key)}
className={cn(
'flex items-center gap-1.5 rounded-md px-2.5 py-1 font-medium transition-colors',
currentView === key
? 'bg-elevated text-foreground'
: 'text-muted-foreground hover:text-foreground'
)}
>
<Icon size={12} />
{label}
</button>
))}
</div>
{showSubtitle && (
<span className="text-[10px] text-muted-foreground leading-none pl-0.5">
{activeOption.subtitle}
</span>
)}
</div>
)
}

View File

@@ -29,7 +29,6 @@ export function StartSessionInput() {
const navigate = useNavigate()
const hasCockpit = useFeatureFlag('flowpilot_cockpit')
const preferredView = useUserPreferencesStore(s => s.preferredFlowPilotView)
const setPreferredView = useUserPreferencesStore(s => s.setPreferredFlowPilotView)
const textareaRef = useRef<HTMLTextAreaElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const dragCounterRef = useRef(0)
@@ -326,36 +325,6 @@ export function StartSessionInput() {
)}
</div>
{/* View preference toggle */}
{hasCockpit && (
<div className="flex items-center rounded-lg border border-border bg-card p-0.5 text-xs mr-2">
<button
type="button"
onClick={() => setPreferredView('flowpilot')}
className={cn(
'rounded-md px-2 py-1 font-medium transition-colors',
preferredView === 'flowpilot'
? 'bg-elevated text-foreground'
: 'text-muted-foreground hover:text-foreground'
)}
>
FlowPilot
</button>
<button
type="button"
onClick={() => setPreferredView('cockpit')}
className={cn(
'rounded-md px-2 py-1 font-medium transition-colors',
preferredView === 'cockpit'
? 'bg-elevated text-foreground'
: 'text-muted-foreground hover:text-foreground'
)}
>
Cockpit
</button>
</div>
)}
{/* Send button */}
<button
type="button"