const TOKEN_KEY = 'owngpt_v2_token' export function getToken() { if (typeof window === 'undefined') return null return window.localStorage.getItem(TOKEN_KEY) } export function saveToken(token) { if (typeof window === 'undefined') return if (token) { window.localStorage.setItem(TOKEN_KEY, token) } else { window.localStorage.removeItem(TOKEN_KEY) } } async function parseErrorResponse(response) { const text = await response.text().catch(() => '') try { const payload = JSON.parse(text) if (Array.isArray(payload.detail)) { return payload.detail.map((item) => item.msg || JSON.stringify(item)).join('; ') } if (typeof payload.detail === 'string') { return payload.detail } if (payload.detail) { return JSON.stringify(payload.detail) } } catch { return text || `HTTP ${response.status}` } return text || `HTTP ${response.status}` } async function request(url, options = {}) { const { token = getToken(), headers: customHeaders, body, ...rest } = options const headers = { ...(customHeaders || {}) } if (token) { headers.Authorization = `Bearer ${token}` } const response = await fetch(url, { ...rest, headers, body, }) if (!response.ok) { const detail = await parseErrorResponse(response) const error = new Error(detail || `HTTP ${response.status}`) error.status = response.status throw error } const contentType = response.headers.get('content-type') || '' if (contentType.includes('application/json')) { return response.json() } return response.text() } export const api = { get: (url, options = {}) => request(url, { ...options, method: 'GET' }), delete: (url, options = {}) => request(url, { ...options, method: 'DELETE' }), post: (url, data, options = {}) => request(url, { ...options, method: 'POST', headers: { 'Content-Type': 'application/json', ...(options.headers || {}) }, body: JSON.stringify(data), }), patch: (url, data, options = {}) => request(url, { ...options, method: 'PATCH', headers: { 'Content-Type': 'application/json', ...(options.headers || {}) }, body: JSON.stringify(data), }), postForm: (url, formData, options = {}) => request(url, { ...options, method: 'POST', body: formData, }), } function handleSseBlock(block, { onChunk, onDone, onError }) { const lines = block.split(/\r?\n/) let eventName = 'message' let dataString = '' for (const line of lines) { if (line.startsWith('event: ')) { eventName = line.slice(7) } if (line.startsWith('data: ')) { dataString += line.slice(6) } } if (!dataString) return let data try { data = JSON.parse(dataString) } catch { return } if (eventName === 'chunk' && data.text) { onChunk?.(data.text) } else if (eventName === 'done') { onDone?.(data) } else if (eventName === 'error') { onError?.(data.detail || 'Streaming failed') } } export async function streamChat({ payload, token = getToken(), signal, onChunk, onDone, onError, }) { const response = await fetch('/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: JSON.stringify(payload), signal, }) if (!response.ok) { const detail = await parseErrorResponse(response) throw new Error(detail || `HTTP ${response.status}`) } const reader = response.body?.getReader() if (!reader) { throw new Error('Streaming is not supported by this browser.') } const decoder = new TextDecoder() let buffer = '' let done = false while (!done) { const result = await reader.read() done = result.done buffer += decoder.decode(result.value || new Uint8Array(), { stream: !done }) const blocks = buffer.split(/\r?\n\r?\n/) buffer = blocks.pop() || '' for (const block of blocks) { handleSseBlock(block, { onChunk, onDone, onError }) } } if (buffer.trim()) { handleSseBlock(buffer, { onChunk, onDone, onError }) } }