Spaces:
Running
Running
joseph njoroge kariuki
feat: enforce zero-mock multi-user privacy, fix chat overlap, integrate ocr file upload, and launch telegram bot
5338d4a | // Typed API client — all calls go through here | |
| const BASE = import.meta.env.VITE_API_BASE | |
| async function request(method, path, body = null) { | |
| // Use dynamic import or grab from window if needed, | |
| // but we can also just import the store: | |
| const { useAuthStore } = await import('../store/authStore.js') | |
| const token = useAuthStore.getState().token | |
| const res = await fetch(`${BASE}${path}`, { | |
| method, | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| ...(token && { 'Authorization': `Bearer ${token}` }) | |
| }, | |
| body: body ? JSON.stringify(body) : null | |
| }) | |
| if (!res.ok) { | |
| if (res.status === 401 && !path.includes('/auth/login')) { | |
| useAuthStore.getState().clearAuth() | |
| window.location.href = '/login' | |
| return | |
| } | |
| const errorData = await res.json().catch(() => ({})) | |
| throw new Error(errorData.detail || 'Request failed') | |
| } | |
| return res.json() | |
| } | |
| export const api = { | |
| // Auth | |
| login: (phone, pin) => | |
| request('POST', '/auth/login', { phone, pin }), | |
| uploadDocument: async (file) => { | |
| const { useAuthStore } = await import('../store/authStore.js') | |
| const token = useAuthStore.getState().token | |
| const formData = new FormData() | |
| formData.append('file', file) | |
| const res = await fetch(`${BASE}/documents/ocr`, { | |
| method: 'POST', | |
| headers: { | |
| ...(token && { 'Authorization': `Bearer ${token}` }) | |
| }, | |
| body: formData | |
| }) | |
| if (!res.ok) { | |
| const errorData = await res.json().catch(() => ({})) | |
| throw new Error(errorData.detail || 'Upload failed') | |
| } | |
| return res.json() | |
| }, | |
| register: (name, phone, pin, user_type = 'personal', consent = false) => | |
| request('POST', '/auth/register', { name, phone, pin, user_type, consent_data_storage: consent }), | |
| // Chat | |
| process: async (query, address, flavor, country) => { | |
| const response = await request('POST', '/process', { | |
| user_query: query, | |
| user_address: address, | |
| user_name: 'User', | |
| flavor: flavor.toUpperCase(), | |
| country_code: country | |
| }) | |
| if (response && response.status === 'queued') { | |
| const taskId = response.task_id | |
| return new Promise((resolve, reject) => { | |
| let wsProto = window.location.protocol === 'https:' ? 'wss:' : 'ws:' | |
| let wsHost = window.location.host | |
| let wsUrl | |
| if (BASE && BASE.startsWith('http')) { | |
| const url = new URL(BASE) | |
| wsProto = url.protocol === 'https:' ? 'wss:' : 'ws:' | |
| wsHost = url.host | |
| wsUrl = `${wsProto}//${wsHost}${url.pathname}/ws/tasks/${taskId}` | |
| } else { | |
| const pathPrefix = BASE || '/api' | |
| wsUrl = `${wsProto}//${wsHost}${pathPrefix}/ws/tasks/${taskId}` | |
| } | |
| let completed = false | |
| const socket = new WebSocket(wsUrl) | |
| socket.onmessage = (event) => { | |
| try { | |
| const data = JSON.parse(event.data) | |
| if (data.status === 'completed') { | |
| completed = true | |
| socket.close() | |
| resolve(data.result) | |
| } else if (data.status === 'failed') { | |
| completed = true | |
| socket.close() | |
| reject(new Error(data.error || 'Deep reasoning task failed')) | |
| } | |
| } catch (err) { | |
| completed = true | |
| socket.close() | |
| reject(err) | |
| } | |
| } | |
| socket.onerror = (error) => { | |
| if (!completed) { | |
| completed = true | |
| reject(error) | |
| } | |
| } | |
| socket.onclose = () => { | |
| if (!completed) { | |
| reject(new Error('WebSocket connection closed prematurely')) | |
| } | |
| } | |
| }) | |
| } | |
| return response | |
| }, | |
| listChats: () => | |
| request('GET', '/chats'), | |
| pinChat: (id) => | |
| request('PATCH', `/chats/${id}/pin`), | |
| // Projects | |
| listProjects: () => | |
| request('GET', '/projects'), | |
| createProject: (data) => | |
| request('POST', '/projects', data), | |
| // Memory | |
| recallMemory: () => | |
| request('GET', '/memory/recall'), | |
| listFacts: () => | |
| request('GET', '/memory/facts'), | |
| saveFact: (category, key, value) => | |
| request('POST', '/memory/facts', { category, key, value }), | |
| deleteFact: (category, key) => | |
| request('DELETE', `/memory/facts?category=${encodeURIComponent(category)}&key=${encodeURIComponent(key)}`), | |
| // Files | |
| listFiles: () => | |
| request('GET', '/files'), | |
| getFileToken: (filename) => | |
| request('GET', `/files/token/${encodeURIComponent(filename)}`), | |
| // Goals | |
| listGoals: () => | |
| request('GET', '/goals'), | |
| createGoal: (data) => | |
| request('POST', '/goals', data), | |
| deleteGoal: (id) => | |
| request('DELETE', `/goals/${id}`), | |
| // Institutional | |
| institutional: { | |
| getPortfolioRisk: () => request('GET', '/institutional/v1/risk/portfolio'), | |
| getTrades: () => request('GET', '/institutional/v1/trades'), | |
| getLogs: () => request('GET', '/institutional/v1/logs'), | |
| getApprovals: () => request('GET', '/institutional/v1/approvals'), | |
| getManifest: (orgId) => request('GET', `/institutional/v1/manifest?org_id=${orgId}`), | |
| getEntityContext: (entityId) => request('GET', `/institutional/v1/context/${entityId}`) | |
| } | |
| } |