Active Sessions
@@ -74,11 +74,15 @@ export function ActiveFlowPilotSessions() {
{sessions.map((session) => (
))}
diff --git a/frontend/src/components/dashboard/RecentFlowPilotSessions.tsx b/frontend/src/components/dashboard/RecentFlowPilotSessions.tsx
index 4ab12e51..5d7da215 100644
--- a/frontend/src/components/dashboard/RecentFlowPilotSessions.tsx
+++ b/frontend/src/components/dashboard/RecentFlowPilotSessions.tsx
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { Link, useNavigate } from 'react-router-dom'
-import { CheckCircle, AlertTriangle, XCircle, ArrowRight } from 'lucide-react'
+import { CheckCircle, AlertTriangle, XCircle, ArrowRight, MessageCircle } from 'lucide-react'
import { aiSessionsApi } from '@/api/aiSessions'
import type { AISessionSummary } from '@/types/ai-session'
@@ -44,7 +44,7 @@ export function RecentFlowPilotSessions() {
Recent Sessions
navigate(`/pilot/${session.id}`)}
+ onClick={() => navigate(session.session_type === 'chat' ? `/assistant/${session.id}` : `/pilot/${session.id}`)}
className="flex w-full items-center gap-3 px-5 py-3 text-left hover:bg-[rgba(255,255,255,0.02)] transition-colors"
style={{
- borderBottom: i < sessions.length - 1 ? '1px solid var(--glass-border)' : undefined,
+ borderBottom: i < sessions.length - 1 ? '1px solid var(--color-border-default)' : undefined,
}}
>
-
+ {session.session_type === 'chat' ? (
+
+ ) : (
+
+ )}
- {session.problem_summary || 'Session'}
+ {session.session_type === 'chat'
+ ? (session.title || session.problem_summary || 'Chat')
+ : (session.problem_summary || 'Session')}
diff --git a/frontend/src/components/flowpilot/AISessionListItem.tsx b/frontend/src/components/flowpilot/AISessionListItem.tsx
index 8186c10e..c936c844 100644
--- a/frontend/src/components/flowpilot/AISessionListItem.tsx
+++ b/frontend/src/components/flowpilot/AISessionListItem.tsx
@@ -1,5 +1,5 @@
import { Link } from 'react-router-dom'
-import { Clock, CheckCircle2, ArrowUpRight, AlertCircle, Pause } from 'lucide-react'
+import { Clock, CheckCircle2, ArrowUpRight, AlertCircle, Pause, Route, MessageCircle } from 'lucide-react'
import { cn } from '@/lib/utils'
import type { AISessionSummary } from '@/types/ai-session'
@@ -18,17 +18,31 @@ const STATUS_CONFIG = {
export function AISessionListItem({ session }: AISessionListItemProps) {
const config = STATUS_CONFIG[session.status as keyof typeof STATUS_CONFIG] ?? STATUS_CONFIG.active
const StatusIcon = config.icon
+ const isChat = session.session_type === 'chat'
+ const TypeIcon = isChat ? MessageCircle : Route
+ const linkTo = isChat ? `/assistant/${session.id}` : `/pilot/${session.id}`
+ const displayTitle = isChat
+ ? (session.title || session.problem_summary || 'Untitled chat')
+ : (session.problem_summary || 'Untitled session')
return (
-
- {session.problem_summary || 'Untitled session'}
-
+
+
+
+
+
+ {displayTitle}
+
+
{session.problem_domain && (
@@ -40,7 +54,7 @@ export function AISessionListItem({ session }: AISessionListItemProps) {
{config.label}
- {session.step_count} steps
+ {session.step_count} {isChat ? 'messages' : 'steps'}
{new Date(session.created_at).toLocaleDateString(undefined, {
diff --git a/frontend/src/hooks/useFlowPilotSession.ts b/frontend/src/hooks/useFlowPilotSession.ts
index 66ae535c..3875109b 100644
--- a/frontend/src/hooks/useFlowPilotSession.ts
+++ b/frontend/src/hooks/useFlowPilotSession.ts
@@ -69,6 +69,8 @@ export function useFlowPilotSession(): UseFlowPilotSession {
setSession({
id: result.session_id,
+ session_type: 'guided',
+ title: null,
status: result.status,
intake_type: intake.intake_type,
intake_content: intake.intake_content,
@@ -89,6 +91,7 @@ export function useFlowPilotSession(): UseFlowPilotSession {
psa_connection_id: intake.psa_connection_id ?? null,
ticket_data: null,
steps: [firstStep],
+ conversation_messages: [],
})
setAllSteps([firstStep])
setCurrentStep(firstStep)
diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx
index 157ebab2..a1f69b3c 100644
--- a/frontend/src/pages/AssistantChatPage.tsx
+++ b/frontend/src/pages/AssistantChatPage.tsx
@@ -1,28 +1,31 @@
import { useState, useEffect, useRef, useCallback } from 'react'
-import { useLocation, useNavigate } from 'react-router-dom'
+import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { Sparkles, Send, Loader2, Flag, MessageSquare, Paperclip, Terminal, X, RotateCcw, ImagePlus } from 'lucide-react'
import { cn } from '@/lib/utils'
import { uploadsApi } from '@/api/uploads'
import type { PendingUpload } from '@/types/upload'
import { PageMeta } from '@/components/common/PageMeta'
-import { assistantChatApi } from '@/api/assistantChat'
+import { aiSessionsApi } from '@/api/aiSessions'
import { analytics } from '@/lib/analytics'
import { toast } from '@/lib/toast'
import { ChatSidebar } from '@/components/assistant/ChatSidebar'
import { ChatMessage } from '@/components/assistant/ChatMessage'
import { ConcludeSessionModal } from '@/components/assistant/ConcludeSessionModal'
-import type { ChatListItem, AssistantChatMessage as ChatMessageType, ConclusionOutcome } from '@/types/assistant-chat'
+import type { ChatListItem, ConclusionOutcome } from '@/types/assistant-chat'
import type { SuggestedFlow } from '@/types/copilot'
-interface MessageWithMeta extends ChatMessageType {
+interface MessageWithMeta {
+ role: 'user' | 'assistant'
+ content: string
suggestedFlows?: SuggestedFlow[]
}
export default function AssistantChatPage() {
const location = useLocation()
const navigate = useNavigate()
+ const { sessionId: urlSessionId } = useParams<{ sessionId?: string }>()
const [chats, setChats] = useState([])
- const [activeChatId, setActiveChatId] = useState(null)
+ const [activeChatId, setActiveChatId] = useState(urlSessionId || null)
const [messages, setMessages] = useState([])
const [input, setInput] = useState('')
const [loading, setLoading] = useState(false)
@@ -38,39 +41,53 @@ export default function AssistantChatPage() {
const dragCounterRef = useRef(0)
const prefillHandledRef = useRef(false)
- // Load chat list
+ // Load chat list from ai_sessions
useEffect(() => {
loadChats()
}, [])
- // Handle prefill from command palette handoff
+ // If URL has a session ID, load it
+ useEffect(() => {
+ if (urlSessionId && urlSessionId !== activeChatId) {
+ selectChat(urlSessionId)
+ }
+ }, [urlSessionId]) // eslint-disable-line react-hooks/exhaustive-deps
+
+ // Handle prefill from command palette / dashboard handoff
useEffect(() => {
const prefill = (location.state as { prefill?: string } | null)?.prefill
if (!prefill || prefillHandledRef.current) return
prefillHandledRef.current = true
- // Clear the location state so back-navigation doesn't retrigger
navigate(location.pathname, { replace: true, state: {} })
const sendPrefill = async () => {
try {
- const chat = await assistantChatApi.createChat()
- setChats(prev => [
- { id: chat.id, title: chat.title, message_count: 0, pinned: false, created_at: chat.created_at, updated_at: chat.updated_at },
- ...prev,
- ])
- setActiveChatId(chat.id)
+ const session = await aiSessionsApi.createChatSession({
+ intake_type: 'free_text',
+ intake_content: { text: prefill },
+ })
+ const chatItem: ChatListItem = {
+ id: session.session_id,
+ title: session.title,
+ message_count: 0,
+ pinned: false,
+ created_at: new Date().toISOString(),
+ updated_at: new Date().toISOString(),
+ }
+ setChats(prev => [chatItem, ...prev])
+ setActiveChatId(session.session_id)
setMessages([{ role: 'user', content: prefill }])
setLoading(true)
- const response = await assistantChatApi.sendMessage(chat.id, prefill)
+ const response = await aiSessionsApi.sendChatMessage(session.session_id, { message: prefill })
setMessages(prev => [
...prev,
{ role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows },
])
setChats(prev =>
prev.map(c =>
- c.id === chat.id
+ c.id === session.session_id
? { ...c, message_count: 2, title: prefill.slice(0, 100), updated_at: new Date().toISOString() }
: c
)
@@ -93,8 +110,15 @@ export default function AssistantChatPage() {
const loadChats = async () => {
try {
- const list = await assistantChatApi.listChats(1, 100)
- setChats(list)
+ const sessions = await aiSessionsApi.listSessions({ session_type: 'chat', limit: 100 })
+ setChats(sessions.map(s => ({
+ id: s.id,
+ title: s.title || s.problem_summary || 'New Chat',
+ message_count: s.step_count,
+ pinned: false,
+ created_at: s.created_at,
+ updated_at: s.created_at,
+ })))
} catch {
// silently handle
}
@@ -103,8 +127,13 @@ export default function AssistantChatPage() {
const selectChat = useCallback(async (chatId: string) => {
setActiveChatId(chatId)
try {
- const chat = await assistantChatApi.getChat(chatId)
- setMessages(chat.messages.map(m => ({ ...m })))
+ const detail = await aiSessionsApi.getSession(chatId)
+ setMessages(
+ (detail.conversation_messages || []).map(m => ({
+ role: m.role as 'user' | 'assistant',
+ content: m.content,
+ }))
+ )
} catch {
setMessages([])
}
@@ -112,12 +141,20 @@ export default function AssistantChatPage() {
const handleNewChat = async () => {
try {
- const chat = await assistantChatApi.createChat()
- setChats(prev => [
- { id: chat.id, title: chat.title, message_count: 0, pinned: false, created_at: chat.created_at, updated_at: chat.updated_at },
- ...prev,
- ])
- setActiveChatId(chat.id)
+ const session = await aiSessionsApi.createChatSession({
+ intake_type: 'free_text',
+ intake_content: { text: '' },
+ })
+ const chatItem: ChatListItem = {
+ id: session.session_id,
+ title: session.title,
+ message_count: 0,
+ pinned: false,
+ created_at: new Date().toISOString(),
+ updated_at: new Date().toISOString(),
+ }
+ setChats(prev => [chatItem, ...prev])
+ setActiveChatId(session.session_id)
setMessages([])
} catch {
toast.error('Failed to create chat')
@@ -126,7 +163,7 @@ export default function AssistantChatPage() {
const handleDeleteChat = async (chatId: string) => {
try {
- await assistantChatApi.deleteChat(chatId)
+ await aiSessionsApi.deleteSession(chatId)
setChats(prev => prev.filter(c => c.id !== chatId))
if (activeChatId === chatId) {
setActiveChatId(null)
@@ -137,15 +174,9 @@ export default function AssistantChatPage() {
}
}
- const handleTogglePin = async (chatId: string, pinned: boolean) => {
- try {
- await assistantChatApi.updateChat(chatId, { pinned })
- setChats(prev =>
- prev.map(c => c.id === chatId ? { ...c, pinned } : c)
- )
- } catch {
- toast.error('Failed to update chat')
- }
+ const handleTogglePin = async (_chatId: string, _pinned: boolean) => {
+ // Pin/unpin not yet supported on unified sessions — no-op for now
+ toast.info('Pin feature coming soon')
}
const handleSend = async () => {
@@ -157,13 +188,12 @@ export default function AssistantChatPage() {
setLoading(true)
try {
- const response = await assistantChatApi.sendMessage(activeChatId, userMessage)
+ const response = await aiSessionsApi.sendChatMessage(activeChatId, { message: userMessage })
analytics.aiFeatureUsed({ feature: 'assistant_chat' })
setMessages(prev => [
...prev,
{ role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows },
])
- // Update chat list title if it was the first message
setChats(prev =>
prev.map(c =>
c.id === activeChatId
@@ -182,44 +212,55 @@ export default function AssistantChatPage() {
}
}
- const handleConclude = async (outcome: ConclusionOutcome, notes: string): Promise => {
+ const handleConclude = async (outcome: ConclusionOutcome, _notes: string): Promise => {
if (!activeChatId) throw new Error('No active chat')
- const response = await assistantChatApi.concludeChat(activeChatId, { outcome, notes: notes || undefined })
- // Update chat in sidebar to show concluded status
- setChats(prev =>
- prev.map(c =>
- c.id === activeChatId
- ? { ...c, concluded_at: response.concluded_at, conclusion_outcome: outcome }
- : c
- )
- )
- return response.summary
+
+ // Map conclusion outcomes to ai_sessions actions
+ if (outcome === 'resolved') {
+ const result = await aiSessionsApi.resolveSession(activeChatId, {
+ resolution_summary: _notes || 'Resolved via assistant chat',
+ })
+ return result.documentation?.problem_summary || 'Session resolved'
+ } else if (outcome === 'escalated') {
+ const result = await aiSessionsApi.escalateSession(activeChatId, {
+ escalation_reason: _notes || 'Escalated from assistant chat',
+ })
+ return result.documentation?.problem_summary || 'Session escalated'
+ } else {
+ // paused
+ await aiSessionsApi.pauseSession(activeChatId)
+ return 'Session paused'
+ }
}
const handleResumeNew = async (summary: string) => {
try {
- const chat = await assistantChatApi.createChat()
- setChats(prev => [
- { id: chat.id, title: chat.title, message_count: 0, pinned: false, created_at: chat.created_at, updated_at: chat.updated_at },
- ...prev,
- ])
- setActiveChatId(chat.id)
- setMessages([])
-
- // Send the summary as the first message to prime the new chat
const resumePrompt = `I'm continuing a previous troubleshooting session. Here's where we left off:\n\n${summary}\n\nPlease review this context and help me continue from where we stopped.`
- setInput('')
+ const session = await aiSessionsApi.createChatSession({
+ intake_type: 'free_text',
+ intake_content: { text: resumePrompt },
+ })
+ const chatItem: ChatListItem = {
+ id: session.session_id,
+ title: session.title,
+ message_count: 0,
+ pinned: false,
+ created_at: new Date().toISOString(),
+ updated_at: new Date().toISOString(),
+ }
+ setChats(prev => [chatItem, ...prev])
+ setActiveChatId(session.session_id)
setMessages([{ role: 'user', content: resumePrompt }])
setLoading(true)
- const response = await assistantChatApi.sendMessage(chat.id, resumePrompt)
+ const response = await aiSessionsApi.sendChatMessage(session.session_id, { message: resumePrompt })
setMessages(prev => [
...prev,
{ role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows },
])
setChats(prev =>
prev.map(c =>
- c.id === chat.id
+ c.id === session.session_id
? { ...c, message_count: 2, title: resumePrompt.slice(0, 100), updated_at: new Date().toISOString() }
: c
)
diff --git a/frontend/src/pages/SessionHistoryPage.tsx b/frontend/src/pages/SessionHistoryPage.tsx
index 88694d76..2a7d2e6c 100644
--- a/frontend/src/pages/SessionHistoryPage.tsx
+++ b/frontend/src/pages/SessionHistoryPage.tsx
@@ -27,6 +27,7 @@ export function SessionHistoryPage() {
const aiSearchTimeout = useRef | undefined>(undefined)
const [aiFilters, setAiFilters] = useState({
q: '',
+ session_type: '',
problem_domain: '',
confidence_tier: '',
date_from: '',
@@ -176,6 +177,7 @@ export function SessionHistoryPage() {
const data = await aiSessionsApi.listSessions({
limit: 50,
q: aiFilters.q || undefined,
+ session_type: aiFilters.session_type || undefined,
problem_domain: aiFilters.problem_domain || undefined,
confidence_tier: aiFilters.confidence_tier || undefined,
date_from: aiFilters.date_from || undefined,
@@ -267,7 +269,7 @@ export function SessionHistoryPage() {
return labels[outcome] ?? outcome
}
- const hasAiFiltersActive = !!(aiSearchInput || aiFilters.q || aiFilters.problem_domain || aiFilters.confidence_tier || aiFilters.date_from || aiFilters.date_to)
+ const hasAiFiltersActive = !!(aiSearchInput || aiFilters.q || aiFilters.session_type || aiFilters.problem_domain || aiFilters.confidence_tier || aiFilters.date_from || aiFilters.date_to)
const hasFlowFiltersActive = !!(filters.ticketNumber || filters.clientName || filters.treeName || filters.dateRange?.from)
// Determine section visibility
@@ -314,7 +316,7 @@ export function SessionHistoryPage() {
{/* FlowPilot Sessions Section */}
{showAiSection && (
<>
- FlowPilot Sessions
+ AI Sessions
{/* AI Session Filter Bar */}
@@ -331,6 +333,24 @@ export function SessionHistoryPage() {
/>
+ {/* Session type pills */}
+
+ {(['', 'guided', 'chat'] as const).map((t) => (
+
+ ))}
+
+
{/* Problem domain dropdown */}