From 6ee6faa712fa2dc6036a6db0f984f64479676a54 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Wed, 1 Apr 2026 22:51:29 +0000 Subject: [PATCH] feat: refactor AssistantChatPage into cockpit layout (Phase 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Stacked zone layout: incident header → work zone → drag handle → conversation log → compose - IncidentHeader wired with triageMeta state and field save handlers - Work zone: StepsPanel (left) + FlowPilotAsks + WhatWeKnow (right) - Drag-resizable split with localStorage persistence - Compact conversation log with you:/fp: prefixes - Triage state populated on session load/resume - AI triage_update merged into header via mergeTriageUpdate() - MSP-native language: FlowPilot, New Case, Close Case Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/pages/AssistantChatPage.tsx | 271 +++++++++++++++++------ 1 file changed, 201 insertions(+), 70 deletions(-) diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx index 9b6169cb..40668d69 100644 --- a/frontend/src/pages/AssistantChatPage.tsx +++ b/frontend/src/pages/AssistantChatPage.tsx @@ -1,10 +1,10 @@ import { useState, useEffect, useRef, useCallback } from 'react' import { useLocation, useNavigate, useParams } from 'react-router-dom' -import { Sparkles, Send, Loader2, Flag, MessageSquare, Paperclip, Terminal, X, RotateCcw, ImagePlus, ListChecks, FileText } from 'lucide-react' +import { Sparkles, Send, Loader2, Flag, MessageSquare, Paperclip, Terminal, X, RotateCcw, ImagePlus, ListChecks, FileText, GripHorizontal } from 'lucide-react' import { cn } from '@/lib/utils' import { uploadsApi } from '@/api/uploads' import type { PendingUpload } from '@/types/upload' -import type { ForkMetadata, ActionItem, QuestionItem } from '@/types/ai-session' +import type { ForkMetadata, ActionItem, QuestionItem, TriageMeta, EvidenceItem, TriageUpdate } from '@/types/ai-session' import { PageMeta } from '@/components/common/PageMeta' import { aiSessionsApi } from '@/api/aiSessions' import { useBranching } from '@/hooks/useBranching' @@ -15,6 +15,10 @@ import { ChatMessage } from '@/components/assistant/ChatMessage' import { TaskLane } from '@/components/assistant/TaskLane' import { ConcludeSessionModal } from '@/components/assistant/ConcludeSessionModal' import { StatusUpdateModal } from '@/components/flowpilot/StatusUpdateModal' +import { IncidentHeader } from '@/components/assistant/IncidentHeader' +import { StepsPanel } from '@/components/assistant/StepsPanel' +import { FlowPilotAsks } from '@/components/assistant/FlowPilotAsks' +import { WhatWeKnow } from '@/components/assistant/WhatWeKnow' import type { ChatListItem, ConclusionOutcome } from '@/types/assistant-chat' import type { SuggestedFlow } from '@/types/copilot' @@ -71,6 +75,16 @@ export default function AssistantChatPage() { const [sidebarCollapsed, setSidebarCollapsed] = useState(() => localStorage.getItem('rf-chat-sidebar-collapsed') === 'true' ) + const [triageMeta, setTriageMeta] = useState({ + client_name: null, asset_name: null, issue_category: null, + triage_hypothesis: null, evidence_items: [], + }) + const [workZonePct, setWorkZonePct] = useState(() => { + const saved = localStorage.getItem('rf-assistant-work-zone-height') + return saved ? parseFloat(saved) : 55 + }) + const [activeStepIndex, setActiveStepIndex] = useState(0) + const splitContainerRef = useRef(null) const toggleSidebarCollapse = () => { const next = !sidebarCollapsed setSidebarCollapsed(next) @@ -219,10 +233,11 @@ export default function AssistantChatPage() { const selectChat = useCallback(async (chatId: string) => { currentChatRef.current = chatId setActiveChatId(chatId) - // Clear TaskLane when switching chats — will restore from backend if available + // Clear TaskLane and triage when switching chats — will restore from backend setShowTaskLane(false) setActiveQuestions([]) setActiveActions([]) + setTriageMeta({ client_name: null, asset_name: null, issue_category: null, triage_hypothesis: null, evidence_items: [] }) try { const detail = await aiSessionsApi.getSession(chatId) // Guard: if the user switched to a different chat while this API call was @@ -235,6 +250,14 @@ export default function AssistantChatPage() { content: m.content, })) ) + // Restore triage metadata from session + setTriageMeta({ + client_name: detail.client_name ?? null, + asset_name: detail.asset_name ?? null, + issue_category: detail.issue_category ?? null, + triage_hypothesis: detail.triage_hypothesis ?? null, + evidence_items: detail.evidence_items ?? [], + }) // Restore task lane from persisted state if (detail.pending_task_lane) { const q = detail.pending_task_lane.questions || [] @@ -344,6 +367,8 @@ export default function AssistantChatPage() { setActiveActions(response.actions || []) setShowTaskLane(true) } + // Merge triage update from AI + if (response.triage_update) mergeTriageUpdate(response.triage_update) } catch { setMessages(prev => [ ...prev, @@ -475,6 +500,81 @@ export default function AssistantChatPage() { } } + // ── Triage handlers ── + + const handleTriageFieldSave = useCallback(async (field: keyof TriageMeta, value: string) => { + if (!activeChatId) return + try { + await aiSessionsApi.updateTriage(activeChatId, { [field]: value }) + setTriageMeta(prev => ({ ...prev, [field]: value })) + } catch { + toast.error('Failed to save field') + } + }, [activeChatId]) + + const handleEvidenceAdd = useCallback(async (text: string, status: EvidenceItem['status']) => { + const newItem: EvidenceItem = { text, status } + const updated = [...triageMeta.evidence_items, newItem] + setTriageMeta(prev => ({ ...prev, evidence_items: updated })) + if (activeChatId) { + try { await aiSessionsApi.updateTriage(activeChatId, { evidence_items: updated }) } catch { /* best-effort */ } + } + }, [activeChatId, triageMeta.evidence_items]) + + const handleEvidenceEdit = useCallback(async (index: number, text: string, status: EvidenceItem['status']) => { + const updated = triageMeta.evidence_items.map((item, i) => i === index ? { text, status } : item) + setTriageMeta(prev => ({ ...prev, evidence_items: updated })) + if (activeChatId) { + try { await aiSessionsApi.updateTriage(activeChatId, { evidence_items: updated }) } catch { /* best-effort */ } + } + }, [activeChatId, triageMeta.evidence_items]) + + const handleFlowPilotAnswer = useCallback((answer: string) => { + setInput(answer) + // Trigger send after a tick so the input state is set + setTimeout(() => { + const event = new KeyboardEvent('keydown', { key: 'Enter' }) + inputRef.current?.dispatchEvent(event) + }, 0) + }, []) + + // Merge triage_update from AI response into local state + const mergeTriageUpdate = useCallback((update: TriageUpdate) => { + setTriageMeta(prev => { + const merged = { ...prev } + // AI only fills null fields (manual edits win) + if (update.client_name && !prev.client_name) merged.client_name = update.client_name + if (update.asset_name && !prev.asset_name) merged.asset_name = update.asset_name + if (update.issue_category && !prev.issue_category) merged.issue_category = update.issue_category + if (update.triage_hypothesis && !prev.triage_hypothesis) merged.triage_hypothesis = update.triage_hypothesis + // Append new evidence items + if (update.evidence_items && update.evidence_items.length > 0) { + merged.evidence_items = [...prev.evidence_items, ...update.evidence_items] + } + return merged + }) + }, []) + + // Drag handle for work zone / chat split + const handleDragStart = useCallback((e: React.MouseEvent) => { + e.preventDefault() + const container = splitContainerRef.current + if (!container) return + const rect = container.getBoundingClientRect() + const onMove = (ev: MouseEvent) => { + const pct = ((ev.clientY - rect.top) / rect.height) * 100 + const clamped = Math.max(25, Math.min(75, pct)) + setWorkZonePct(clamped) + } + const onUp = () => { + document.removeEventListener('mousemove', onMove) + document.removeEventListener('mouseup', onUp) + setWorkZonePct(prev => { localStorage.setItem('rf-assistant-work-zone-height', String(prev)); return prev }) + } + document.addEventListener('mousemove', onMove) + document.addEventListener('mouseup', onUp) + }, []) + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() @@ -557,7 +657,7 @@ export default function AssistantChatPage() { return ( <> - +
{/* Sidebar — hidden on mobile, collapsed to top bar or full sidebar on desktop */} {!sidebarCollapsed && ( @@ -601,64 +701,113 @@ export default function AssistantChatPage() {
)} - {/* Chat content row: chat column + TaskLane side by side */} -
-
- {/* Mobile header with chat history toggle */} + {/* Cockpit content area */} +
+ {/* Mobile header with case history toggle */}
{activeChatId ? ( <> - {/* Messages */} -
- {messages.length === 0 && !loading && ( -
-
- -
-

- AI Assistant -

-

- Ask me anything about IT infrastructure, networking, Active Directory, - cloud platforms, or troubleshooting. I'll also suggest relevant flows from your team's library. -

+ {/* Incident Header */} + 0 ? null : null} + sessionId={activeChatId} + onFieldSave={handleTriageFieldSave} + onResolve={() => setShowConclude(true)} + onOverflow={() => {}} + /> + + {/* Resizable work zone + conversation log split */} +
+ {/* Work zone */} +
+ {/* Left: Steps panel */} +
+
- )} - {messages.map((msg, i) => ( - - ))} - {loading && ( -
-
- -
-
- -
+ {/* Right: FlowPilot Asks + What We Know */} +
+ { + setInput(answer) + setTimeout(() => handleSend(), 10) + }} + loading={loading} + /> +
- )} -
+
+ + {/* Drag handle */} +
+ +
+ + {/* Conversation log */} +
+
+ Conversation Log +
+ {messages.length === 0 && !loading && ( +
+ +

+ Start a new case to begin troubleshooting +

+
+ )} +
+ {messages.map((msg, i) => ( +
+ + {msg.role === 'user' ? 'you:' : 'fp:'} + + + {msg.content.length > 300 ? msg.content.slice(0, 300) + '…' : msg.content} + +
+ ))} + {loading && ( +
+ fp: + +
+ )} +
+
+
{/* Rich Input */} @@ -693,7 +842,7 @@ export default function AssistantChatPage() { onChange={e => setInput(e.target.value)} onKeyDown={handleKeyDown} onPaste={handlePaste} - placeholder={loading ? 'AI is thinking...' : 'Type a message, paste a screenshot, or drag a file...'} + placeholder={loading ? 'FlowPilot is working...' : 'Describe finding, paste log output, or ask FlowPilot...'} disabled={loading} rows={1} className="w-full resize-none bg-transparent px-4 pt-3 pb-1 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none disabled:cursor-not-allowed" @@ -769,9 +918,9 @@ export default function AssistantChatPage() { Update - )} @@ -804,43 +953,25 @@ export default function AssistantChatPage() {

- AI Assistant + FlowPilot

- Your Senior Systems & Network Engineer. Ask anything about IT infrastructure, - or start a new chat to get personalized help with your team's flows. + Your MSP troubleshooting copilot. Start a new case to begin + diagnosing with structured steps, evidence tracking, and AI guidance.

)}
- {/* Task lane — slides in when AI sends questions or actions */} - {showTaskLane && (activeQuestions.length > 0 || activeActions.length > 0) && ( - { - setShowTaskLane(false) - }} - loading={loading} - /> - )} +
{/* close cockpit content area */} - {/* Branch map hidden — branching is now silent/background only. - Branches are tracked in the DB but not shown to the user. - The AI manages branch context internally. */} -
{/* close chat content row */} -
{/* close outer flex-col */} - - {/* Conclude Session Modal */} + {/* Close Case Session Modal */} setShowConclude(false)}