From 813b5981018c9d7e9ff30e8383a953ca782d2a81 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Fri, 3 Apr 2026 05:02:42 +0000 Subject: [PATCH] fix: cockpit/flowpilot bugs and redesign view toggle placement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- backend/app/services/unified_chat_service.py | 2 +- .../components/assistant/FlowPilotAsks.tsx | 45 +++++++++-- .../components/assistant/IncidentHeader.tsx | 8 +- .../src/components/assistant/ViewToggle.tsx | 81 ++++++++++++------- .../dashboard/StartSessionInput.tsx | 31 ------- frontend/src/hooks/useAssistantSession.ts | 2 + frontend/src/pages/CockpitPage.tsx | 29 ++++--- frontend/src/pages/FlowPilotPage.tsx | 2 +- frontend/src/pages/QuickStartPage.tsx | 9 +++ 9 files changed, 121 insertions(+), 88 deletions(-) diff --git a/backend/app/services/unified_chat_service.py b/backend/app/services/unified_chat_service.py index dcb9f670..7f966e5c 100644 --- a/backend/app/services/unified_chat_service.py +++ b/backend/app/services/unified_chat_service.py @@ -223,7 +223,7 @@ async def send_chat_message( message: str, db: AsyncSession, images: list[dict[str, Any]] | None = None, -) -> tuple[str, list[dict[str, Any]], AISession, dict[str, Any] | None, list[dict[str, Any]] | None, list[dict[str, Any]] | None]: +) -> tuple[str, list[dict[str, Any]], AISession, dict[str, Any] | None, list[dict[str, Any]] | None, list[dict[str, Any]] | None, dict[str, Any] | None]: """Send a message in a chat session and get AI response. Args: diff --git a/frontend/src/components/assistant/FlowPilotAsks.tsx b/frontend/src/components/assistant/FlowPilotAsks.tsx index 7042f4dd..c28af110 100644 --- a/frontend/src/components/assistant/FlowPilotAsks.tsx +++ b/frontend/src/components/assistant/FlowPilotAsks.tsx @@ -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 (
-
- - FlowPilot Asks +
+
+ + FlowPilot Asks +
+ {questions.length > 1 && ( +
+ + + {currentIdx + 1}/{questions.length} + + +
+ )}

{question.text} diff --git a/frontend/src/components/assistant/IncidentHeader.tsx b/frontend/src/components/assistant/IncidentHeader.tsx index aca4e1d7..4a84d2be 100644 --- a/frontend/src/components/assistant/IncidentHeader.tsx +++ b/frontend/src/components/assistant/IncidentHeader.tsx @@ -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(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 - +

) diff --git a/frontend/src/components/assistant/ViewToggle.tsx b/frontend/src/components/assistant/ViewToggle.tsx index a0395547..f497891d 100644 --- a/frontend/src/components/assistant/ViewToggle.tsx +++ b/frontend/src/components/assistant/ViewToggle.tsx @@ -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 ( -
- - +
+
+ {VIEW_OPTIONS.map(({ key, label, icon: Icon }) => ( + + ))} +
+ {showSubtitle && ( + + {activeOption.subtitle} + + )}
) } diff --git a/frontend/src/components/dashboard/StartSessionInput.tsx b/frontend/src/components/dashboard/StartSessionInput.tsx index f93175ac..919e3e7b 100644 --- a/frontend/src/components/dashboard/StartSessionInput.tsx +++ b/frontend/src/components/dashboard/StartSessionInput.tsx @@ -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(null) const fileInputRef = useRef(null) const dragCounterRef = useRef(0) @@ -326,36 +325,6 @@ export function StartSessionInput() { )}
- {/* View preference toggle */} - {hasCockpit && ( -
- - -
- )} - {/* Send button */}
+ {session.activeChatId && ( + + )}
{session.activeChatId && ( - + )}
+ {/* View preference — standalone row above input */} +
+ Launch in + +
+ {/* Chat-style input */}