| 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 }) |
| } |
| } |
|
|