Spaces:
Sleeping
Sleeping
| // ββ 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) | |
| } | |
| } | |
| } | |