feat: unified sessions — merge assistant chat into ai_sessions table
Add session_type ('guided'|'chat') and title columns to ai_sessions,
enabling both FlowPilot guided sessions and assistant chat sessions to
live in a single table. This is the foundation for a unified session
history and consistent UX across both interaction modes.
Backend:
- Migration 066: session_type + title columns
- unified_chat_service: chat sessions on ai_sessions with same AI/RAG
- POST /ai-sessions supports session_type='chat' creation
- POST /ai-sessions/{id}/chat for chat messages
- DELETE /ai-sessions/{id} for session deletion
- session_type filter on GET /ai-sessions
Frontend:
- AssistantChatPage rewired to aiSessionsApi (no more assistantChatApi)
- /assistant/:sessionId route for deep-linking
- Session history: type filter pills (All/Guided/Chat), type icons
- Dashboard: both types shown with correct routing and icons
- Fixed glass-border → border-default in dashboard components
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,9 @@ import type {
|
||||
PickupSessionRequest,
|
||||
StatusUpdateRequest,
|
||||
StatusUpdateResponse,
|
||||
ChatSessionCreateResponse,
|
||||
ChatMessageRequest,
|
||||
ChatMessageResponse,
|
||||
} from '@/types/ai-session'
|
||||
|
||||
export const aiSessionsApi = {
|
||||
@@ -23,6 +26,22 @@ export const aiSessionsApi = {
|
||||
return response.data
|
||||
},
|
||||
|
||||
async createChatSession(data: AISessionCreateRequest): Promise<ChatSessionCreateResponse> {
|
||||
const response = await apiClient.post<ChatSessionCreateResponse>('/ai-sessions', {
|
||||
...data,
|
||||
session_type: 'chat',
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
async sendChatMessage(sessionId: string, data: ChatMessageRequest): Promise<ChatMessageResponse> {
|
||||
const response = await apiClient.post<ChatMessageResponse>(
|
||||
`/ai-sessions/${sessionId}/chat`,
|
||||
data
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async respondToStep(sessionId: string, data: StepResponseRequest): Promise<StepResponseResponse> {
|
||||
const response = await apiClient.post<StepResponseResponse>(
|
||||
`/ai-sessions/${sessionId}/respond`,
|
||||
@@ -49,6 +68,7 @@ export const aiSessionsApi = {
|
||||
|
||||
async listSessions(params?: {
|
||||
status?: string
|
||||
session_type?: string
|
||||
skip?: number
|
||||
limit?: number
|
||||
problem_domain?: string
|
||||
@@ -104,6 +124,10 @@ export const aiSessionsApi = {
|
||||
})
|
||||
},
|
||||
|
||||
async deleteSession(sessionId: string): Promise<void> {
|
||||
await apiClient.delete(`/ai-sessions/${sessionId}`)
|
||||
},
|
||||
|
||||
async pickupSession(sessionId: string, data: PickupSessionRequest): Promise<StepResponseResponse> {
|
||||
const response = await apiClient.post<StepResponseResponse>(
|
||||
`/ai-sessions/${sessionId}/pickup`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { Sparkles, Clock, ArrowRight } from 'lucide-react'
|
||||
import { Clock, ArrowRight, Route, MessageCircle } from 'lucide-react'
|
||||
import { aiSessionsApi } from '@/api/aiSessions'
|
||||
import type { AISessionSummary } from '@/types/ai-session'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -30,7 +30,7 @@ export function ActiveFlowPilotSessions() {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="card-flat">
|
||||
<div className="px-5 py-3" style={{ borderBottom: '1px solid var(--glass-border)' }}>
|
||||
<div className="px-5 py-3" style={{ borderBottom: '1px solid var(--color-border-default)' }}>
|
||||
<h3 className="font-heading text-sm font-bold text-foreground">Active Sessions</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 p-4">
|
||||
@@ -46,7 +46,7 @@ export function ActiveFlowPilotSessions() {
|
||||
<div className="card-flat">
|
||||
<div
|
||||
className="flex items-center justify-between px-5 py-3"
|
||||
style={{ borderBottom: '1px solid var(--glass-border)' }}
|
||||
style={{ borderBottom: '1px solid var(--color-border-default)' }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-heading text-sm font-bold text-foreground">Active Sessions</h3>
|
||||
@@ -74,11 +74,15 @@ export function ActiveFlowPilotSessions() {
|
||||
{sessions.map((session) => (
|
||||
<button
|
||||
key={session.id}
|
||||
onClick={() => navigate(`/pilot/${session.id}`)}
|
||||
onClick={() => navigate(session.session_type === 'chat' ? `/assistant/${session.id}` : `/pilot/${session.id}`)}
|
||||
className="card-interactive p-4 text-left"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2 mb-2">
|
||||
<Sparkles size={14} className="shrink-0 text-primary mt-0.5" />
|
||||
{session.session_type === 'chat' ? (
|
||||
<MessageCircle size={14} className="shrink-0 text-violet-400 mt-0.5" />
|
||||
) : (
|
||||
<Route size={14} className="shrink-0 text-primary mt-0.5" />
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
'font-sans text-xs text-[0.5625rem] uppercase px-1.5 py-0.5 rounded',
|
||||
@@ -92,7 +96,9 @@ export function ActiveFlowPilotSessions() {
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-foreground truncate">
|
||||
{session.problem_summary || 'Session in progress'}
|
||||
{session.session_type === 'chat'
|
||||
? (session.title || session.problem_summary || 'Chat in progress')
|
||||
: (session.problem_summary || 'Session in progress')}
|
||||
</p>
|
||||
<div className="mt-2 flex items-center gap-2 text-[0.625rem] text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
@@ -100,7 +106,7 @@ export function ActiveFlowPilotSessions() {
|
||||
{timeAgo(session.created_at)}
|
||||
</span>
|
||||
<span>·</span>
|
||||
<span>{session.step_count} steps</span>
|
||||
<span>{session.step_count} {session.session_type === 'chat' ? 'messages' : 'steps'}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -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() {
|
||||
<div className="card-flat">
|
||||
<div
|
||||
className="flex items-center justify-between px-5 py-3"
|
||||
style={{ borderBottom: '1px solid var(--glass-border)' }}
|
||||
style={{ borderBottom: '1px solid var(--color-border-default)' }}
|
||||
>
|
||||
<h3 className="font-heading text-sm font-bold text-foreground">Recent Sessions</h3>
|
||||
<Link
|
||||
@@ -61,16 +61,22 @@ export function RecentFlowPilotSessions() {
|
||||
return (
|
||||
<button
|
||||
key={session.id}
|
||||
onClick={() => 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,
|
||||
}}
|
||||
>
|
||||
<StatusIcon size={14} style={{ color: config.color }} className="shrink-0" />
|
||||
{session.session_type === 'chat' ? (
|
||||
<MessageCircle size={14} className="shrink-0 text-violet-400" />
|
||||
) : (
|
||||
<StatusIcon size={14} style={{ color: config.color }} className="shrink-0" />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-foreground truncate">
|
||||
{session.problem_summary || 'Session'}
|
||||
{session.session_type === 'chat'
|
||||
? (session.title || session.problem_summary || 'Chat')
|
||||
: (session.problem_summary || 'Session')}
|
||||
</p>
|
||||
</div>
|
||||
<span className="shrink-0 font-sans text-xs text-muted-foreground">
|
||||
|
||||
@@ -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 (
|
||||
<Link
|
||||
to={`/pilot/${session.id}`}
|
||||
to={linkTo}
|
||||
className="card-interactive block p-4 transition-all"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-foreground truncate">
|
||||
{session.problem_summary || 'Untitled session'}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn(
|
||||
'flex items-center justify-center w-5 h-5 rounded',
|
||||
isChat ? 'text-violet-400' : 'text-primary'
|
||||
)}>
|
||||
<TypeIcon size={14} />
|
||||
</span>
|
||||
<p className="text-sm font-medium text-foreground truncate">
|
||||
{displayTitle}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-1.5 flex items-center gap-3 flex-wrap">
|
||||
{session.problem_domain && (
|
||||
<span className="font-sans text-xs rounded-md bg-accent-dim px-2 py-0.5 text-[0.625rem] uppercase tracking-wider text-primary">
|
||||
@@ -40,7 +54,7 @@ export function AISessionListItem({ session }: AISessionListItemProps) {
|
||||
{config.label}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{session.step_count} steps
|
||||
{session.step_count} {isChat ? 'messages' : 'steps'}
|
||||
</span>
|
||||
<span className="text-xs text-text-muted">
|
||||
{new Date(session.created_at).toLocaleDateString(undefined, {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<ChatListItem[]>([])
|
||||
const [activeChatId, setActiveChatId] = useState<string | null>(null)
|
||||
const [activeChatId, setActiveChatId] = useState<string | null>(urlSessionId || null)
|
||||
const [messages, setMessages] = useState<MessageWithMeta[]>([])
|
||||
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<string> => {
|
||||
const handleConclude = async (outcome: ConclusionOutcome, _notes: string): Promise<string> => {
|
||||
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
|
||||
)
|
||||
|
||||
@@ -27,6 +27,7 @@ export function SessionHistoryPage() {
|
||||
const aiSearchTimeout = useRef<ReturnType<typeof setTimeout> | 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 && (
|
||||
<>
|
||||
<h2 className="font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground mb-3">FlowPilot Sessions</h2>
|
||||
<h2 className="font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground mb-3">AI Sessions</h2>
|
||||
|
||||
{/* AI Session Filter Bar */}
|
||||
<div className="card-flat p-3 mb-4">
|
||||
@@ -331,6 +333,24 @@ export function SessionHistoryPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Session type pills */}
|
||||
<div className="flex gap-1">
|
||||
{(['', 'guided', 'chat'] as const).map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => setAiFilters((f) => ({ ...f, session_type: t }))}
|
||||
className={cn(
|
||||
'rounded-full border px-3 py-1 text-xs font-sans transition-colors',
|
||||
aiFilters.session_type === t
|
||||
? 'bg-accent-dim text-foreground border-primary/30'
|
||||
: 'bg-card text-muted-foreground border-border hover:text-foreground hover:border-[rgba(255,255,255,0.12)]'
|
||||
)}
|
||||
>
|
||||
{t === '' ? 'All' : t === 'guided' ? 'Guided' : 'Chat'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Problem domain dropdown */}
|
||||
<select
|
||||
value={aiFilters.problem_domain}
|
||||
@@ -393,7 +413,7 @@ export function SessionHistoryPage() {
|
||||
<button
|
||||
onClick={() => {
|
||||
setAiSearchInput('')
|
||||
setAiFilters({ q: '', problem_domain: '', confidence_tier: '', date_from: '', date_to: '' })
|
||||
setAiFilters({ q: '', session_type: '', problem_domain: '', confidence_tier: '', date_from: '', date_to: '' })
|
||||
}}
|
||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
@@ -415,7 +435,7 @@ export function SessionHistoryPage() {
|
||||
<button
|
||||
onClick={() => {
|
||||
setAiSearchInput('')
|
||||
setAiFilters({ q: '', problem_domain: '', confidence_tier: '', date_from: '', date_to: '' })
|
||||
setAiFilters({ q: '', session_type: '', problem_domain: '', confidence_tier: '', date_from: '', date_to: '' })
|
||||
}}
|
||||
className="text-foreground hover:underline text-sm"
|
||||
>
|
||||
|
||||
@@ -194,6 +194,7 @@ export const router = sentryCreateBrowserRouter([
|
||||
{ path: 'script-builder', element: page(ScriptBuilderPage) },
|
||||
{ path: 'kb-accelerator', element: page(KBAcceleratorPage) },
|
||||
{ path: 'assistant', element: page(AssistantChatPage) },
|
||||
{ path: 'assistant/:sessionId', element: page(AssistantChatPage) },
|
||||
{ path: 'flow-assist', element: page(FlowAssistPage) },
|
||||
{ path: 'pilot', element: page(FlowPilotSessionPage) },
|
||||
{ path: 'pilot/:sessionId', element: page(FlowPilotSessionPage) },
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// ── Intake ──
|
||||
|
||||
export type SessionType = 'guided' | 'chat'
|
||||
|
||||
export interface AISessionCreateRequest {
|
||||
session_type?: SessionType
|
||||
intake_type: 'free_text' | 'psa_ticket' | 'screenshot' | 'log_paste' | 'combined'
|
||||
intake_content: Record<string, unknown>
|
||||
psa_ticket_id?: string
|
||||
@@ -159,6 +162,8 @@ export interface RateSessionRequest {
|
||||
|
||||
export interface AISessionSummary {
|
||||
id: string
|
||||
session_type: SessionType
|
||||
title: string | null
|
||||
status: string
|
||||
intake_type: string
|
||||
problem_summary: string | null
|
||||
@@ -189,6 +194,7 @@ export interface AISessionDetail extends AISessionSummary {
|
||||
psa_connection_id: string | null
|
||||
ticket_data: Record<string, unknown> | null
|
||||
steps: AISessionStepResponse[]
|
||||
conversation_messages: Array<{ role: string; content: string }>
|
||||
}
|
||||
|
||||
export interface AISessionSearchResult {
|
||||
@@ -199,6 +205,24 @@ export interface AISessionSearchResult {
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// ── Chat session ──
|
||||
|
||||
export interface ChatSessionCreateResponse {
|
||||
session_id: string
|
||||
session_type: 'chat'
|
||||
title: string
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface ChatMessageRequest {
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface ChatMessageResponse {
|
||||
content: string
|
||||
suggested_flows: Array<{ tree_id: string; tree_name: string; tree_type: string; relevance_snippet: string }>
|
||||
}
|
||||
|
||||
export interface SimilarSession {
|
||||
id: string
|
||||
problem_summary: string | null
|
||||
|
||||
Reference in New Issue
Block a user