Ver código fonte

feat(webui): typed API client + types

Fszontagh 2 meses atrás
pai
commit
22dbbb0c03
3 arquivos alterados com 114 adições e 0 exclusões
  1. 74 0
      webui/src/api/client.ts
  2. 14 0
      webui/src/stores/authStore.ts
  3. 26 0
      webui/src/types/index.ts

+ 74 - 0
webui/src/api/client.ts

@@ -0,0 +1,74 @@
+import type {
+  LoginResponse, CollectionMeta, ApiKeyPublic, ApiKeyCreated,
+  SearchResult, ProjectStats, GlobalStats, FindResult,
+} from '@/types'
+import { ApiError } from '@/types'
+
+async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
+  const res = await fetch(path, {
+    ...options,
+    credentials: 'include',
+    headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
+  })
+  if (res.status === 401 && !path.endsWith('/ui/login')) {
+    const { useAuthStore } = await import('@/stores/authStore')
+    useAuthStore.getState().reset()
+    if (location.pathname !== '/login') location.assign('/login')
+    throw new ApiError(401, 'unauthorized')
+  }
+  const text = await res.text()
+  const body = text ? JSON.parse(text) : {}
+  if (!res.ok) throw new ApiError(res.status, body?.error?.message || res.statusText)
+  return body as T
+}
+
+const enc = encodeURIComponent
+const P = (project: string) => `/api/v1/projects/${enc(project)}`
+
+export const api = {
+  login: (key: string) => request<LoginResponse>('/ui/login', { method: 'POST', body: JSON.stringify({ key }) }),
+  logout: () => request<{ ok: boolean }>('/ui/logout', { method: 'POST' }),
+
+  // projects
+  listProjects: () => request<{ projects: string[] }>('/api/v1/projects'),
+  createProject: (name: string) => request<{ name: string }>('/api/v1/projects', { method: 'POST', body: JSON.stringify({ name }) }),
+  deleteProject: (name: string) => request<{ deleted: string }>(`/api/v1/projects/${enc(name)}`, { method: 'DELETE' }),
+
+  // keys (admin)
+  listKeys: () => request<{ keys: ApiKeyPublic[] }>('/api/v1/keys'),
+  createKey: (label: string, projects: string[], admin: boolean) =>
+    request<ApiKeyCreated>('/api/v1/keys', { method: 'POST', body: JSON.stringify({ label, projects, admin }) }),
+  updateKey: (key: string, patch: Partial<{ label: string; projects: string[]; admin: boolean }>) =>
+    request<{ updated: boolean }>(`/api/v1/keys/${enc(key)}`, { method: 'PATCH', body: JSON.stringify(patch) }),
+  deleteKey: (key: string) => request<{ deleted: boolean }>(`/api/v1/keys/${enc(key)}`, { method: 'DELETE' }),
+
+  // collections
+  listCollections: (project: string) => request<{ collections: CollectionMeta[] }>(`${P(project)}/collections`),
+  getCollection: (project: string, name: string) => request<CollectionMeta>(`${P(project)}/collections/${enc(name)}`),
+  createCollection: (project: string, body: { name: string; kind: 'json' | 'vector'; vector_dimension?: number; embedding_model?: string }) =>
+    request<CollectionMeta>(`${P(project)}/collections`, { method: 'POST', body: JSON.stringify(body) }),
+  deleteCollection: (project: string, name: string) =>
+    request<{ deleted: string }>(`${P(project)}/collections/${enc(name)}`, { method: 'DELETE' }),
+
+  // documents
+  findDocuments: (project: string, coll: string, qs = '') =>
+    request<FindResult>(`${P(project)}/collections/${enc(coll)}/documents${qs}`),
+  getDocument: (project: string, coll: string, id: string) =>
+    request<Record<string, unknown>>(`${P(project)}/collections/${enc(coll)}/documents/${enc(id)}`),
+  insertDocument: (project: string, coll: string, data: unknown) =>
+    request<{ id: string }>(`${P(project)}/collections/${enc(coll)}/documents`, { method: 'POST', body: JSON.stringify({ data }) }),
+  putDocument: (project: string, coll: string, id: string, data: unknown) =>
+    request<{ id: string }>(`${P(project)}/collections/${enc(coll)}/documents/${enc(id)}`, { method: 'PUT', body: JSON.stringify(data) }),
+  deleteDocument: (project: string, coll: string, id: string) =>
+    request<{ deleted: string }>(`${P(project)}/collections/${enc(coll)}/documents/${enc(id)}`, { method: 'DELETE' }),
+
+  // vectors / rag
+  storeVector: (project: string, coll: string, body: { id?: string; text?: string; vector?: number[]; metadata?: Record<string, unknown> }) =>
+    request<{ id: string }>(`${P(project)}/collections/${enc(coll)}/vectors`, { method: 'POST', body: JSON.stringify(body) }),
+  search: (project: string, coll: string, body: { query_text?: string; query_vector?: number[]; top_k?: number; min_score?: number }) =>
+    request<{ results: SearchResult[] }>(`${P(project)}/collections/${enc(coll)}/search`, { method: 'POST', body: JSON.stringify(body) }),
+
+  // stats
+  projectStats: (project: string) => request<ProjectStats>(`${P(project)}/stats`),
+  globalStats: () => request<GlobalStats>('/api/v1/stats'),
+}

+ 14 - 0
webui/src/stores/authStore.ts

@@ -0,0 +1,14 @@
+// STUB — replaced by W3 with the real zustand store.
+// Minimal shape so the dynamic import in api/client.ts resolves at build time.
+
+interface AuthState {
+  reset: () => void
+}
+
+const state: AuthState = {
+  reset: () => {},
+}
+
+export const useAuthStore = {
+  getState: (): AuthState => state,
+}

+ 26 - 0
webui/src/types/index.ts

@@ -0,0 +1,26 @@
+export interface LoginResponse { ok: boolean; admin: boolean; projects: string[] }
+export interface CollectionMeta {
+  name: string; kind: 'json' | 'vector'; vector_dimension: number;
+  embedding_model: string; created_at: number;
+  document_count?: number; size_bytes?: number;
+}
+export interface ApiKeyPublic {
+  label: string; projects: string[]; admin: boolean; created_at: number; key_prefix: string;
+}
+export interface ApiKeyCreated extends ApiKeyPublic { key: string }
+export interface SearchResult { id: string; score: number; data: Record<string, unknown> }
+export interface ProjectStats { project: string; collections: number; documents: number }
+export interface GlobalStats {
+  total_documents: number; total_collections: number; memory_used_bytes: number;
+  insert_count: number; query_count: number;
+  memory_pressure_level: string; memory_pressure_percent: number; projects: number;
+}
+export interface FindResult { documents: Record<string, unknown>[]; count: number }
+export class ApiError extends Error {
+  status: number
+  constructor(status: number, message: string) {
+    super(message)
+    this.name = 'ApiError'
+    this.status = status
+  }
+}