GodSpeed / frontend /src /components /analytics /AnalyticsExport.tsx
Samyuktha24's picture
feat: add Zustand stores for filters and UI state management
9dfccd9
import { useState } from 'react'
import { apiFetch } from '@/lib/http'
import { cn } from '@/lib/utils'
type ExportFormat = 'csv' | 'pdf'
type ExportScope = 'queries' | 'topics' | 'health' | 'escalations' | 'full'
const SCOPES: Array<{ id: ExportScope; label: string; desc: string }> = [
{ id: 'queries', label: 'Query analytics', desc: 'Volume, success rate, response times' },
{ id: 'topics', label: 'Top topics', desc: 'Most-queried subjects' },
{ id: 'health', label: 'Knowledge health', desc: 'Coverage, freshness, accuracy scores' },
{ id: 'escalations', label: 'Escalations', desc: 'Unresolved queries by frequency' },
{ id: 'full', label: 'Full report', desc: 'All of the above combined' },
]
export function AnalyticsExport() {
const [scope, setScope] = useState<ExportScope>('full')
const [format, setFormat] = useState<ExportFormat>('csv')
const [dateRange, setDateRange] = useState('30d')
const [exporting, setExporting] = useState(false)
async function handleExport() {
setExporting(true)
try {
const res = await apiFetch(
`/api/analytics/export?scope=${scope}&format=${format}&date_range=${dateRange}`,
)
const blob = await res.blob()
const ext = format === 'pdf' ? 'pdf' : 'csv'
const filename = `godspeed-analytics-${scope}-${dateRange}.${ext}`
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
} finally {
setExporting(false)
}
}
return (
<div className="rounded-xl border border-surface-subtle p-5">
<p className="mb-4 text-sm font-medium text-stone-500">Export Analytics</p>
<div className="flex flex-col gap-5 sm:flex-row sm:items-end">
{/* Scope */}
<div className="flex-1">
<label className="mb-1.5 block text-xs font-medium text-stone-500">Report scope</label>
<div className="flex flex-col gap-1.5">
{SCOPES.map((s) => (
<label
key={s.id}
className={cn(
'flex cursor-pointer items-start gap-3 rounded-lg border p-3 transition-colors',
scope === s.id
? 'border-brand bg-amber-50 dark:bg-amber-950/20'
: 'border-surface-subtle hover:border-stone-300',
)}
>
<input
type="radio"
name="scope"
value={s.id}
checked={scope === s.id}
onChange={() => setScope(s.id)}
className="mt-0.5 accent-amber-700"
/>
<div>
<p className="text-sm font-medium">{s.label}</p>
<p className="text-xs text-stone-400">{s.desc}</p>
</div>
</label>
))}
</div>
</div>
{/* Format + date range + button */}
<div className="flex flex-col gap-4 sm:w-48">
<div>
<label className="mb-1.5 block text-xs font-medium text-stone-500">Format</label>
<div className="flex rounded-lg border border-surface-subtle text-sm">
{(['csv', 'pdf'] as const).map((f) => (
<button
key={f}
onClick={() => setFormat(f)}
className={cn(
'flex-1 py-2 transition-colors first:rounded-l-lg last:rounded-r-lg font-medium uppercase tracking-wide',
format === f
? 'bg-brand text-white'
: 'text-stone-500 hover:text-stone-700',
)}
>
{f}
</button>
))}
</div>
</div>
<div>
<label className="mb-1.5 block text-xs font-medium text-stone-500">Date range</label>
<select
value={dateRange}
onChange={(e) => setDateRange(e.target.value)}
className="w-full rounded-lg border border-surface-subtle bg-white px-3 py-2 text-sm dark:bg-stone-900"
>
<option value="7d">Last 7 days</option>
<option value="30d">Last 30 days</option>
<option value="90d">Last 90 days</option>
<option value="all">All time</option>
</select>
</div>
<button
onClick={handleExport}
disabled={exporting}
className={cn(
'flex items-center justify-center gap-2 rounded-lg py-2.5 text-sm font-semibold transition-colors',
exporting
? 'cursor-not-allowed bg-stone-200 text-stone-400 dark:bg-stone-700'
: 'bg-brand text-white hover:bg-brand/90',
)}
>
{exporting ? (
<>
<svg className="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Exporting…
</>
) : (
<>
<svg className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
Download {format.toUpperCase()}
</>
)}
</button>
</div>
</div>
</div>
)
}