feat(search): add structured filters to AI session list endpoint and frontend
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -43,8 +43,23 @@ export const aiSessionsApi = {
|
||||
return response.data
|
||||
},
|
||||
|
||||
async listSessions(params?: { status?: string; skip?: number; limit?: number }): Promise<AISessionSummary[]> {
|
||||
const response = await apiClient.get<AISessionSummary[]>('/ai-sessions', { params })
|
||||
async listSessions(params?: {
|
||||
status?: string
|
||||
skip?: number
|
||||
limit?: number
|
||||
problem_domain?: string
|
||||
matched_flow_id?: string
|
||||
confidence_tier?: string
|
||||
ticket_id?: string
|
||||
date_from?: string
|
||||
date_to?: string
|
||||
q?: string
|
||||
}): Promise<AISessionSummary[]> {
|
||||
// Strip empty string values so they aren't sent as empty query params
|
||||
const cleanParams = params
|
||||
? Object.fromEntries(Object.entries(params).filter(([, v]) => v !== '' && v !== undefined))
|
||||
: undefined
|
||||
const response = await apiClient.get<AISessionSummary[]>('/ai-sessions', { params: cleanParams })
|
||||
return response.data
|
||||
},
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState, useRef, useCallback } from 'react'
|
||||
import { Search } from 'lucide-react'
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { sessionsApi } from '@/api/sessions'
|
||||
@@ -24,6 +25,13 @@ export function SessionHistoryPage() {
|
||||
const [sessionType, setSessionType] = useState<'flow' | 'ai'>('flow')
|
||||
const [aiSessions, setAiSessions] = useState<AISessionSummary[]>([])
|
||||
const [aiLoading, setAiLoading] = useState(false)
|
||||
const [aiFilters, setAiFilters] = useState({
|
||||
q: '',
|
||||
problem_domain: '',
|
||||
confidence_tier: '',
|
||||
date_from: '',
|
||||
date_to: '',
|
||||
})
|
||||
|
||||
const [sessions, setSessions] = useState<Session[]>([])
|
||||
const [hasMore, setHasMore] = useState(false)
|
||||
@@ -147,14 +155,21 @@ export function SessionHistoryPage() {
|
||||
setSearchParams(params, { replace: true })
|
||||
}, [filters, setSearchParams])
|
||||
|
||||
// Load AI sessions when tab is active
|
||||
// Load AI sessions when tab is active or filters change
|
||||
useEffect(() => {
|
||||
if (sessionType !== 'ai') return
|
||||
let cancelled = false
|
||||
const loadAiSessions = async () => {
|
||||
setAiLoading(true)
|
||||
try {
|
||||
const data = await aiSessionsApi.listSessions({ limit: 50 })
|
||||
const data = await aiSessionsApi.listSessions({
|
||||
limit: 50,
|
||||
q: aiFilters.q || undefined,
|
||||
problem_domain: aiFilters.problem_domain || undefined,
|
||||
confidence_tier: aiFilters.confidence_tier || undefined,
|
||||
date_from: aiFilters.date_from || undefined,
|
||||
date_to: aiFilters.date_to || undefined,
|
||||
})
|
||||
if (!cancelled) setAiSessions(data)
|
||||
} catch {
|
||||
if (!cancelled) toast.error('Failed to load AI sessions')
|
||||
@@ -164,7 +179,7 @@ export function SessionHistoryPage() {
|
||||
}
|
||||
loadAiSessions()
|
||||
return () => { cancelled = true }
|
||||
}, [sessionType])
|
||||
}, [sessionType, aiFilters])
|
||||
|
||||
const handleFilterChange = (newFilters: SessionFilterState) => {
|
||||
setFilters(newFilters)
|
||||
@@ -300,30 +315,131 @@ export function SessionHistoryPage() {
|
||||
|
||||
{/* AI Sessions view */}
|
||||
{sessionType === 'ai' && (
|
||||
aiLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : aiSessions.length === 0 ? (
|
||||
<EmptyState
|
||||
title="No AI sessions yet"
|
||||
description="Start a FlowPilot session to get AI-guided troubleshooting. Sessions will appear here."
|
||||
action={
|
||||
<Link
|
||||
to="/pilot"
|
||||
className="inline-flex items-center gap-2 rounded-[10px] bg-gradient-brand px-5 py-2.5 text-sm font-semibold text-[#101114] shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97] transition-all"
|
||||
<>
|
||||
{/* AI Session Filter Bar */}
|
||||
<div className="glass-card-static p-3 mb-4">
|
||||
<div className="flex flex-wrap gap-3 items-center">
|
||||
{/* Search input */}
|
||||
<div className="relative flex-1 min-w-[180px]">
|
||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground pointer-events-none" />
|
||||
<input
|
||||
type="text"
|
||||
value={aiFilters.q}
|
||||
onChange={(e) => setAiFilters((f) => ({ ...f, q: e.target.value }))}
|
||||
placeholder="Search sessions..."
|
||||
className="w-full rounded-lg border border-border bg-card pl-8 pr-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Problem domain dropdown */}
|
||||
<select
|
||||
value={aiFilters.problem_domain}
|
||||
onChange={(e) => setAiFilters((f) => ({ ...f, problem_domain: e.target.value }))}
|
||||
title="Filter by problem domain"
|
||||
className="rounded-lg border border-border bg-card px-3 py-1.5 text-sm text-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-none [&>option]:bg-[#1a1c21] [&>option]:text-foreground"
|
||||
>
|
||||
Start AI Session
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{aiSessions.map((s) => (
|
||||
<AISessionListItem key={s.id} session={s} />
|
||||
))}
|
||||
<option value="">All domains</option>
|
||||
<option value="Active Directory">Active Directory</option>
|
||||
<option value="Networking">Networking</option>
|
||||
<option value="Microsoft 365">Microsoft 365</option>
|
||||
<option value="Hardware">Hardware</option>
|
||||
<option value="Security">Security</option>
|
||||
<option value="Email">Email</option>
|
||||
<option value="Printing">Printing</option>
|
||||
<option value="VPN / Remote Access">VPN / Remote Access</option>
|
||||
<option value="Cloud Services">Cloud Services</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
|
||||
{/* Confidence tier pills */}
|
||||
<div className="flex gap-1">
|
||||
{(['', 'guided', 'exploring', 'discovery'] as const).map((tier) => (
|
||||
<button
|
||||
key={tier}
|
||||
onClick={() => setAiFilters((f) => ({ ...f, confidence_tier: tier }))}
|
||||
className={cn(
|
||||
'rounded-full border px-3 py-1 text-xs font-label transition-colors',
|
||||
aiFilters.confidence_tier === tier
|
||||
? 'bg-primary/10 text-foreground border-primary/30'
|
||||
: 'bg-card text-muted-foreground border-border hover:text-foreground hover:border-[rgba(255,255,255,0.12)]'
|
||||
)}
|
||||
>
|
||||
{tier === '' ? 'All' : tier.charAt(0).toUpperCase() + tier.slice(1)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Date range inputs */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="date"
|
||||
value={aiFilters.date_from}
|
||||
onChange={(e) => setAiFilters((f) => ({ ...f, date_from: e.target.value }))}
|
||||
title="From date"
|
||||
className="rounded-lg border border-border bg-card px-3 py-1.5 text-sm text-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-none [color-scheme:dark]"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">to</span>
|
||||
<input
|
||||
type="date"
|
||||
value={aiFilters.date_to}
|
||||
onChange={(e) => setAiFilters((f) => ({ ...f, date_to: e.target.value }))}
|
||||
title="To date"
|
||||
className="rounded-lg border border-border bg-card px-3 py-1.5 text-sm text-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-none [color-scheme:dark]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Clear filters */}
|
||||
{(aiFilters.q || aiFilters.problem_domain || aiFilters.confidence_tier || aiFilters.date_from || aiFilters.date_to) && (
|
||||
<button
|
||||
onClick={() => setAiFilters({ q: '', problem_domain: '', confidence_tier: '', date_from: '', date_to: '' })}
|
||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
{aiLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : aiSessions.length === 0 ? (
|
||||
(aiFilters.q || aiFilters.problem_domain || aiFilters.confidence_tier || aiFilters.date_from || aiFilters.date_to) ? (
|
||||
<EmptyState
|
||||
title="No sessions match your filters"
|
||||
description="Try adjusting your search or filters."
|
||||
action={
|
||||
<button
|
||||
onClick={() => setAiFilters({ q: '', problem_domain: '', confidence_tier: '', date_from: '', date_to: '' })}
|
||||
className="text-foreground hover:underline text-sm"
|
||||
>
|
||||
Clear all filters
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
title="No AI sessions yet"
|
||||
description="Start a FlowPilot session to get AI-guided troubleshooting. Sessions will appear here."
|
||||
action={
|
||||
<Link
|
||||
to="/pilot"
|
||||
className="inline-flex items-center gap-2 rounded-[10px] bg-gradient-brand px-5 py-2.5 text-sm font-semibold text-[#101114] shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97] transition-all"
|
||||
>
|
||||
Start AI Session
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{aiSessions.map((s) => (
|
||||
<AISessionListItem key={s.id} session={s} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Flow Sessions Content */}
|
||||
|
||||
Reference in New Issue
Block a user