winncare / frontend /src /api.ts
esra2001's picture
fix language confusion and hallucinations: centralized resolver + deterministic abstention
3999ba0
Raw
History Blame Contribute Delete
6.68 kB
// ── Token storage ──────────────────────────────────────────────────────────────
const TOKEN_KEY = 'julia_token'
export const getToken = (): string | null => localStorage.getItem(TOKEN_KEY)
export const setToken = (t: string): void => { localStorage.setItem(TOKEN_KEY, t) }
export const clearToken = (): void => { localStorage.removeItem(TOKEN_KEY) }
// ── Base fetch with auth ───────────────────────────────────────────────────────
async function request<T>(url: string, init: RequestInit = {}): Promise<T> {
const token = getToken()
const headers: Record<string, string> = {
...(init.headers as Record<string, string> ?? {}),
}
if (token) headers['Authorization'] = `Bearer ${token}`
const r = await fetch(url, { ...init, headers })
if (r.status === 401) {
clearToken()
window.location.reload()
throw new Error('Unauthorized')
}
if (!r.ok) {
const text = await r.text()
throw new Error(text || `HTTP ${r.status}`)
}
return r.json() as Promise<T>
}
// ── Auth ───────────────────────────────────────────────────────────────────────
export function login(email: string, password: string): Promise<{ token: string; email: string }> {
return request('/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
}
export function getMe(): Promise<{ email: string }> {
return request('/auth/me')
}
// ── Conversations ─────────────────────────────────────────────────────────────
export interface Conversation {
id: string
title: string
created_at: string
updated_at: string
}
export interface ConversationDetail extends Conversation {
messages: Array<{ role: string; text: string; meta?: unknown }>
}
export function listConversations(): Promise<Conversation[]> {
return request('/conversations')
}
export function getConversation(id: string): Promise<ConversationDetail> {
return request(`/conversations/${id}`)
}
export function createConversation(title: string, messages: unknown[]): Promise<Conversation> {
return request('/conversations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, messages }),
})
}
export function updateConversation(id: string, title: string, messages: unknown[]): Promise<Conversation> {
return request(`/conversations/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, messages }),
})
}
export function deleteConversation(id: string): Promise<{ status: string }> {
return request(`/conversations/${id}`, { method: 'DELETE' })
}
// ── Ingestion ─────────────────────────────────────────────────────────────────
export interface UploadResponse { job_id: string; doc_id: string; status: string }
export interface JobStatus {
job_id: string; doc_id: string; filename: string
status: 'queued' | 'processing' | 'done' | 'failed'
chunks_stored: number; error: string | null
created_at: string; updated_at: string
}
export interface Document { doc_id: string; filename: string }
export function uploadDocument(file: File, category: string): Promise<UploadResponse> {
const form = new FormData()
form.append('file', file)
form.append('category', category)
return request('/ingest/upload', { method: 'POST', body: form })
}
export function getJobStatus(jobId: string): Promise<JobStatus> {
return request(`/ingest/status/${jobId}`)
}
export function listDocuments(): Promise<{ total: number; documents: Document[] }> {
return request('/ingest/documents')
}
export function deleteDocument(docId: string): Promise<{ status: string; doc_id: string }> {
return request(`/ingest/documents/${docId}`, { method: 'DELETE' })
}
export function clearDocuments(): Promise<{ status: string }> {
return request('/ingest/documents', { method: 'DELETE' })
}
// ── Query ─────────────────────────────────────────────────────────────────────
export interface Citation { filename: string; page: number | string; snippet?: string }
export interface AskResponse {
answer: string
citations: Citation[]
source_map: Citation[]
chunks_used: number
path: 'vector' | 'sql'
sql: string | null
}
export interface HistoryMessage { role: 'user' | 'assistant'; content: string }
export async function askQuestionStream(
question: string,
history: HistoryMessage[],
topK = 6,
category = '',
onToken: (token: string) => void,
onDone: (meta: AskResponse) => void,
onError: (msg: string) => void,
onStatus?: (status: string) => void,
uiLanguage = '',
): Promise<void> {
const token = getToken()
const res = await fetch('/query/ask/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ question, history, top_k: topK, category, ui_language: uiLanguage }),
})
if (!res.ok || !res.body) { onError(`HTTP ${res.status}`); return }
const reader = res.body.getReader()
const dec = new TextDecoder()
let buf = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buf += dec.decode(value, { stream: true })
const lines = buf.split('\n')
buf = lines.pop() ?? ''
for (const line of lines) {
if (!line.startsWith('data: ')) continue
const event = JSON.parse(line.slice(6))
if (event.type === 'token') onToken(event.content)
else if (event.type === 'status') onStatus?.(event.status)
else if (event.type === 'done') onDone({
answer: event.answer ?? '',
citations: event.citations ?? [],
source_map: event.source_map ?? [],
chunks_used: event.chunks_used ?? 0,
path: event.path,
sql: event.sql ?? null,
})
else if (event.type === 'error') onError(event.message)
}
}
}