Newer
Older
navi-1 / webclient / src / api / index.js
const BASE = import.meta.env.DEV ? '/api' : ''

// Global safety net: log unhandled rejections so the UI doesn't silently break.
window.addEventListener('unhandledrejection', (ev) => {
  console.error('[API] Unhandled rejection:', ev.reason)
})

async function request(method, path, body) {
  const opts = {
    method,
    headers: {},
    credentials: 'include',
  }
  if (body !== undefined) {
    opts.headers['Content-Type'] = 'application/json'
    opts.body = JSON.stringify(body)
  }
  const res = await fetch(`${BASE}${path}`, opts)

  // 401 must surface immediately: invalidate local auth so the login overlay appears.
  if (res.status === 401) {
    try {
      const { useAuthStore } = await import('@/stores/auth.js')
      const auth = useAuthStore()
      auth.user = null
    } catch {
      // Pinia not ready — ignore, the login overlay won't appear but the error still throws
    }
    const text = await res.text().catch(() => '')
    throw new Error(`${method} ${path} → 401: ${text}`)
  }

  if (!res.ok) {
    const text = await res.text().catch(() => '')
    throw new Error(`${method} ${path} → ${res.status}: ${text}`)
  }
  if (res.status === 204) return null
  return res.json()
}

// ─── Auth ─────────────────────────────────────────────────────────────────
export function getMe() {
  return request('GET', '/auth/me')
}

export function logout() {
  return request('POST', '/auth/logout')
}

// ─── Profiles ──────────────────────────────────────────────────────────────
export function getProfiles() {
  return request('GET', '/agents/profiles')
}

// ─── Sessions ──────────────────────────────────────────────────────────────
export function getSessions({ limit = 30, offset = 0, profileId = null, search = null } = {}) {
  const params = new URLSearchParams({
    limit: String(limit),
    offset: String(offset),
  })
  if (profileId) params.set('profile_id', profileId)
  if (search) params.set('search', search)
  return request('GET', `/sessions?${params.toString()}`)
}

export function getSession(id) {
  return request('GET', `/sessions/${id}`)
}

export function createSession(profileId) {
  return request('POST', '/sessions', { profile_id: profileId })
}

export function deleteSession(id) {
  return request('DELETE', `/sessions/${id}`)
}

export function pinSession(id, pinned) {
  return request('PATCH', `/sessions/${id}/pin`, { pinned })
}

export function stopSession(id) {
  return request('POST', `/sessions/${id}/stop`)
}

export function generateSessionName(id) {
  return request('POST', `/sessions/${id}/generate-name`)
}

export function getSessionContent(id) {
  return request('GET', `/sessions/${id}/content`)
}

// ─── Eval feedback ─────────────────────────────────────────────────────────
export function getFeedback(sessionId) {
  return request('GET', `/eval/feedback/${sessionId}`)
}

export function setFeedback(sessionId, messageIndex, rating) {
  return request('POST', '/eval/feedback', {
    session_id: sessionId,
    message_index: messageIndex,
    rating
  })
}

// ─── Files ─────────────────────────────────────────────────────────────────
export function listSessionFiles(sessionId) {
  return request('GET', `/sessions/${sessionId}/files`)
}

export async function uploadFile(sessionId, file) {
  const form = new FormData()
  form.append('file', file)
  const res = await fetch(`${BASE}/sessions/${sessionId}/files`, {
    method: 'POST',
    credentials: 'include',
    body: form
  })
  if (!res.ok) {
    const text = await res.text().catch(() => '')
    throw new Error(`Upload failed: ${res.status}: ${text}`)
  }
  return res.json()
}