feat: refactor AssistantChatPage into cockpit layout (Phase 5)
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
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 { cn } from '@/lib/utils'
|
||||||
import { uploadsApi } from '@/api/uploads'
|
import { uploadsApi } from '@/api/uploads'
|
||||||
import type { PendingUpload } from '@/types/upload'
|
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 { PageMeta } from '@/components/common/PageMeta'
|
||||||
import { aiSessionsApi } from '@/api/aiSessions'
|
import { aiSessionsApi } from '@/api/aiSessions'
|
||||||
import { useBranching } from '@/hooks/useBranching'
|
import { useBranching } from '@/hooks/useBranching'
|
||||||
@@ -15,6 +15,10 @@ import { ChatMessage } from '@/components/assistant/ChatMessage'
|
|||||||
import { TaskLane } from '@/components/assistant/TaskLane'
|
import { TaskLane } from '@/components/assistant/TaskLane'
|
||||||
import { ConcludeSessionModal } from '@/components/assistant/ConcludeSessionModal'
|
import { ConcludeSessionModal } from '@/components/assistant/ConcludeSessionModal'
|
||||||
import { StatusUpdateModal } from '@/components/flowpilot/StatusUpdateModal'
|
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 { ChatListItem, ConclusionOutcome } from '@/types/assistant-chat'
|
||||||
import type { SuggestedFlow } from '@/types/copilot'
|
import type { SuggestedFlow } from '@/types/copilot'
|
||||||
|
|
||||||
@@ -71,6 +75,16 @@ export default function AssistantChatPage() {
|
|||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(() =>
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(() =>
|
||||||
localStorage.getItem('rf-chat-sidebar-collapsed') === 'true'
|
localStorage.getItem('rf-chat-sidebar-collapsed') === 'true'
|
||||||
)
|
)
|
||||||
|
const [triageMeta, setTriageMeta] = useState<TriageMeta>({
|
||||||
|
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<HTMLDivElement>(null)
|
||||||
const toggleSidebarCollapse = () => {
|
const toggleSidebarCollapse = () => {
|
||||||
const next = !sidebarCollapsed
|
const next = !sidebarCollapsed
|
||||||
setSidebarCollapsed(next)
|
setSidebarCollapsed(next)
|
||||||
@@ -219,10 +233,11 @@ export default function AssistantChatPage() {
|
|||||||
const selectChat = useCallback(async (chatId: string) => {
|
const selectChat = useCallback(async (chatId: string) => {
|
||||||
currentChatRef.current = chatId
|
currentChatRef.current = chatId
|
||||||
setActiveChatId(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)
|
setShowTaskLane(false)
|
||||||
setActiveQuestions([])
|
setActiveQuestions([])
|
||||||
setActiveActions([])
|
setActiveActions([])
|
||||||
|
setTriageMeta({ client_name: null, asset_name: null, issue_category: null, triage_hypothesis: null, evidence_items: [] })
|
||||||
try {
|
try {
|
||||||
const detail = await aiSessionsApi.getSession(chatId)
|
const detail = await aiSessionsApi.getSession(chatId)
|
||||||
// Guard: if the user switched to a different chat while this API call was
|
// 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,
|
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
|
// Restore task lane from persisted state
|
||||||
if (detail.pending_task_lane) {
|
if (detail.pending_task_lane) {
|
||||||
const q = detail.pending_task_lane.questions || []
|
const q = detail.pending_task_lane.questions || []
|
||||||
@@ -344,6 +367,8 @@ export default function AssistantChatPage() {
|
|||||||
setActiveActions(response.actions || [])
|
setActiveActions(response.actions || [])
|
||||||
setShowTaskLane(true)
|
setShowTaskLane(true)
|
||||||
}
|
}
|
||||||
|
// Merge triage update from AI
|
||||||
|
if (response.triage_update) mergeTriageUpdate(response.triage_update)
|
||||||
} catch {
|
} catch {
|
||||||
setMessages(prev => [
|
setMessages(prev => [
|
||||||
...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) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -557,7 +657,7 @@ export default function AssistantChatPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageMeta title="AI Assistant" />
|
<PageMeta title="FlowPilot" />
|
||||||
<div className="flex h-[calc(100vh-3.5rem)]">
|
<div className="flex h-[calc(100vh-3.5rem)]">
|
||||||
{/* Sidebar — hidden on mobile, collapsed to top bar or full sidebar on desktop */}
|
{/* Sidebar — hidden on mobile, collapsed to top bar or full sidebar on desktop */}
|
||||||
{!sidebarCollapsed && (
|
{!sidebarCollapsed && (
|
||||||
@@ -601,64 +701,113 @@ export default function AssistantChatPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Chat content row: chat column + TaskLane side by side */}
|
{/* Cockpit content area */}
|
||||||
<div className="flex-1 flex min-w-0 min-h-0">
|
<div className="flex-1 flex flex-col min-w-0 min-h-0">
|
||||||
<div className="flex-1 flex flex-col min-w-0">
|
{/* Mobile header with case history toggle */}
|
||||||
{/* Mobile header with chat history toggle */}
|
|
||||||
<div className="sm:hidden flex items-center gap-2 px-3 py-2 border-b border-border shrink-0">
|
<div className="sm:hidden flex items-center gap-2 px-3 py-2 border-b border-border shrink-0">
|
||||||
<button
|
<button
|
||||||
onClick={() => setMobileSidebarOpen(true)}
|
onClick={() => setMobileSidebarOpen(true)}
|
||||||
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" />
|
||||||
<button
|
<button
|
||||||
onClick={handleNewChat}
|
onClick={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"
|
||||||
>
|
>
|
||||||
+ New
|
+ New Case
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeChatId ? (
|
{activeChatId ? (
|
||||||
<>
|
<>
|
||||||
{/* Messages */}
|
{/* Incident Header */}
|
||||||
<div className="flex-1 overflow-y-auto px-4 sm:px-6 py-4 space-y-4">
|
<IncidentHeader
|
||||||
{messages.length === 0 && !loading && (
|
triageMeta={triageMeta}
|
||||||
<div className="flex flex-col items-center justify-center h-full text-center">
|
psaTicketId={messages.length > 0 ? null : null}
|
||||||
<div className="w-16 h-16 rounded-full bg-accent-dim flex items-center justify-center mb-4">
|
sessionId={activeChatId}
|
||||||
<Sparkles size={28} className="text-primary" />
|
onFieldSave={handleTriageFieldSave}
|
||||||
</div>
|
onResolve={() => setShowConclude(true)}
|
||||||
<h2 className="text-lg font-heading font-semibold text-foreground mb-2">
|
onOverflow={() => {}}
|
||||||
AI Assistant
|
/>
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-muted-foreground max-w-md">
|
{/* Resizable work zone + conversation log split */}
|
||||||
Ask me anything about IT infrastructure, networking, Active Directory,
|
<div ref={splitContainerRef} className="flex-1 flex flex-col min-h-0">
|
||||||
cloud platforms, or troubleshooting. I'll also suggest relevant flows from your team's library.
|
{/* Work zone */}
|
||||||
</p>
|
<div className="flex min-h-0 overflow-hidden" style={{ height: `${workZonePct}%` }}>
|
||||||
|
{/* Left: Steps panel */}
|
||||||
|
<div className="flex-[55] min-w-0 p-3 overflow-y-auto border-r border-default">
|
||||||
|
<StepsPanel
|
||||||
|
actions={activeActions}
|
||||||
|
activeIndex={activeStepIndex}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
{/* Right: FlowPilot Asks + What We Know */}
|
||||||
{messages.map((msg, i) => (
|
<div className="flex-[45] min-w-0 p-3 overflow-y-auto flex flex-col gap-3">
|
||||||
<ChatMessage
|
<FlowPilotAsks
|
||||||
key={i}
|
questions={activeQuestions}
|
||||||
role={msg.role}
|
onAnswer={(answer) => {
|
||||||
content={msg.content}
|
setInput(answer)
|
||||||
suggestedFlows={msg.suggestedFlows}
|
setTimeout(() => handleSend(), 10)
|
||||||
/>
|
}}
|
||||||
))}
|
loading={loading}
|
||||||
{loading && (
|
/>
|
||||||
<div className="flex gap-3">
|
<WhatWeKnow
|
||||||
<div className="w-8 h-8 rounded-full bg-primary/15 flex items-center justify-center">
|
items={triageMeta.evidence_items}
|
||||||
<Sparkles size={14} className="text-primary" />
|
onAdd={handleEvidenceAdd}
|
||||||
</div>
|
onEdit={handleEvidenceEdit}
|
||||||
<div className="bg-input border border-border rounded-2xl px-4 py-3">
|
/>
|
||||||
<Loader2 size={16} className="animate-spin text-primary" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
<div ref={messagesEndRef} />
|
|
||||||
|
{/* Drag handle */}
|
||||||
|
<div
|
||||||
|
onMouseDown={handleDragStart}
|
||||||
|
className="h-2 flex items-center justify-center cursor-row-resize hover:bg-elevated/50 transition-colors flex-shrink-0 border-y border-default/50"
|
||||||
|
>
|
||||||
|
<GripHorizontal size={14} className="text-muted" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Conversation log */}
|
||||||
|
<div className="flex-1 min-h-0 overflow-y-auto" style={{ background: '#13151c' }}>
|
||||||
|
<div className="px-4 pt-2 pb-1">
|
||||||
|
<span className="text-[10px] uppercase tracking-wider text-muted font-semibold">Conversation Log</span>
|
||||||
|
</div>
|
||||||
|
{messages.length === 0 && !loading && (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full text-center px-4 py-8">
|
||||||
|
<Sparkles size={24} className="text-muted mb-2" />
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Start a new case to begin troubleshooting
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="px-4 pb-2 space-y-1">
|
||||||
|
{messages.map((msg, i) => (
|
||||||
|
<div key={i} className="flex gap-2 text-xs leading-relaxed">
|
||||||
|
<span className={cn(
|
||||||
|
'font-mono flex-shrink-0 mt-px',
|
||||||
|
msg.role === 'user' ? 'text-muted' : 'text-accent/50'
|
||||||
|
)}>
|
||||||
|
{msg.role === 'user' ? 'you:' : 'fp:'}
|
||||||
|
</span>
|
||||||
|
<span className={cn(
|
||||||
|
msg.role === 'user' ? 'text-muted-foreground/70' : 'text-muted-foreground'
|
||||||
|
)}>
|
||||||
|
{msg.content.length > 300 ? msg.content.slice(0, 300) + '…' : msg.content}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{loading && (
|
||||||
|
<div className="flex gap-2 text-xs">
|
||||||
|
<span className="font-mono text-accent/50">fp:</span>
|
||||||
|
<Loader2 size={12} className="animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Rich Input */}
|
{/* Rich Input */}
|
||||||
@@ -693,7 +842,7 @@ export default function AssistantChatPage() {
|
|||||||
onChange={e => setInput(e.target.value)}
|
onChange={e => setInput(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onPaste={handlePaste}
|
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}
|
disabled={loading}
|
||||||
rows={1}
|
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"
|
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() {
|
|||||||
<FileText size={14} />
|
<FileText size={14} />
|
||||||
<span className="hidden sm:inline">Update</span>
|
<span className="hidden sm:inline">Update</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={() => setShowConclude(true)} disabled={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">
|
<button type="button" onClick={() => setShowConclude(true)} disabled={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} />
|
<Flag size={14} />
|
||||||
<span className="hidden sm:inline">Conclude</span>
|
<span className="hidden sm:inline">Close Case</span>
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -804,43 +953,25 @@ export default function AssistantChatPage() {
|
|||||||
<Sparkles size={32} className="text-primary" />
|
<Sparkles size={32} className="text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-xl font-heading font-semibold text-foreground mb-2">
|
<h2 className="text-xl font-heading font-semibold text-foreground mb-2">
|
||||||
AI Assistant
|
FlowPilot
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-muted-foreground max-w-md mb-6">
|
<p className="text-sm text-muted-foreground max-w-md mb-6">
|
||||||
Your Senior Systems & Network Engineer. Ask anything about IT infrastructure,
|
Your MSP troubleshooting copilot. Start a new case to begin
|
||||||
or start a new chat to get personalized help with your team's flows.
|
diagnosing with structured steps, evidence tracking, and AI guidance.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={handleNewChat}
|
onClick={handleNewChat}
|
||||||
className="bg-primary text-white font-semibold text-sm rounded-lg px-6 py-2.5 hover:brightness-110 active:scale-[0.98] transition-all"
|
className="bg-primary text-white font-semibold text-sm rounded-lg px-6 py-2.5 hover:brightness-110 active:scale-[0.98] transition-all"
|
||||||
>
|
>
|
||||||
Start a Conversation
|
New Case
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Task lane — slides in when AI sends questions or actions */}
|
</div>{/* close cockpit content area */}
|
||||||
{showTaskLane && (activeQuestions.length > 0 || activeActions.length > 0) && (
|
|
||||||
<TaskLane
|
|
||||||
questions={activeQuestions}
|
|
||||||
actions={activeActions}
|
|
||||||
sessionId={activeChatId}
|
|
||||||
onSubmit={handleTaskSubmit}
|
|
||||||
onClose={() => {
|
|
||||||
setShowTaskLane(false)
|
|
||||||
}}
|
|
||||||
loading={loading}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Branch map hidden — branching is now silent/background only.
|
{/* Close Case Session Modal */}
|
||||||
Branches are tracked in the DB but not shown to the user.
|
|
||||||
The AI manages branch context internally. */}
|
|
||||||
</div>{/* close chat content row */}
|
|
||||||
</div>{/* close outer flex-col */}
|
|
||||||
|
|
||||||
{/* Conclude Session Modal */}
|
|
||||||
<ConcludeSessionModal
|
<ConcludeSessionModal
|
||||||
isOpen={showConclude}
|
isOpen={showConclude}
|
||||||
onClose={() => setShowConclude(false)}
|
onClose={() => setShowConclude(false)}
|
||||||
|
|||||||
Reference in New Issue
Block a user