Ukweli / web /src /api /client.ts
Benard John
feat: implement full-stack architecture with database models, authentication services, and web dashboard components
37b5223
Raw
History Blame Contribute Delete
4.13 kB
import axios, { AxiosError } from 'axios'
import type {
QueryResponse,
Report,
SearchFilters,
FeedbackPayload,
GraphData,
StreamEvent,
} from '@/types'
import { useAuthStore } from '@/stores/auth'
const API_BASE = import.meta.env.VITE_API_URL || '/api'
const client = axios.create({
baseURL: API_BASE,
timeout: 60000,
headers: {
'Content-Type': 'application/json',
},
withCredentials: true,
})
// Inject the JWT on every request
client.interceptors.request.use((config) => {
const auth = useAuthStore()
if (auth.accessToken) {
config.headers.Authorization = `Bearer ${auth.accessToken}`
}
return config
})
// Track rate-limit headers + show the auth modal on 429
client.interceptors.response.use(
(response) => {
const auth = useAuthStore()
auth.updateRateFromHeaders(response.headers as unknown as Headers)
return response
},
(error: AxiosError) => {
const auth = useAuthStore()
// Feed any rate headers we DID get back into the store
if (error.response?.headers) {
try {
auth.updateRateFromHeaders(error.response.headers as unknown as Headers)
} catch {
// axios headers are not always Headers — that's fine
}
}
if (error.response?.status === 429) {
// Open the signup modal so the user can convert
auth.openAuthModal()
throw new Error('Daily limit reached. Sign up for 25× more queries.')
}
if (error.response?.status === 401) {
// Token rejected — clear it and force re-auth
auth.$reset?.()
throw new Error('Your session has expired. Please sign in again.')
}
throw error
}
)
export const api = {
// Health
health: () => client.get('/health'),
// RAG query (non-streaming)
query: async (
query: string,
filters?: SearchFilters,
language: string = 'en',
mode: string = 'concise'
) => {
const response = await client.post<QueryResponse>('/query', {
query,
language,
mode,
filters,
})
return response.data
},
// Streaming query (SSE)
queryStream: (
query: string,
onEvent: (event: StreamEvent) => void,
filters?: SearchFilters
): EventSource => {
const auth = useAuthStore()
const url = new URL(`${API_BASE}/agent/stream`, window.location.origin)
// EventSource doesn't support custom headers, so we pass the JWT
// as a query param fallback (the server may ignore if it prefers headers).
if (auth.accessToken) {
url.searchParams.set('access_token', auth.accessToken)
}
const eventSource = new EventSource(url.toString())
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
onEvent(data)
} catch {
onEvent({ event: 'error', data: { message: 'Bad event payload' } })
}
}
eventSource.onerror = () => {
onEvent({ event: 'error', data: { message: 'Stream connection failed' } })
eventSource.close()
}
return eventSource
},
// Documents
getDocuments: async (params?: {
auditee?: string
fy?: string
limit?: number
offset?: number
}) => {
const response = await client.get<Report[]>('/documents', { params })
return response.data
},
getReport: async (id: string) => {
const response = await client.get<Report>(`/documents/${id}`)
return response.data
},
// Graph
graphQuery: async (cypher: string) => {
const response = await client.post<GraphData>('/graph/query', {
cypher,
read_only: true,
})
return response.data
},
getEntityGraph: async (entityId: string) => {
const response = await client.get<GraphData>(`/graph/entity/${entityId}`)
return response.data
},
// Feedback
submitFeedback: async (payload: FeedbackPayload) => {
const response = await client.post('/feedback', payload)
return response.data
},
// Suggestions
getSuggestions: async (query: string) => {
const response = await client.get<string[]>('/suggestions', {
params: { q: query },
})
return response.data
},
}
export default api