diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 00000000..04fe194e --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,12 @@ +const BASE = import.meta.env.VITE_BACKEND_URL ?? "http://localhost:8787"; + +export async function apiFetch(path: string, init?: RequestInit): Promise { + const res = await fetch(`${BASE}${path}`, { + headers: { "content-type": "application/json" }, + ...init, + }); + if (!res.ok) throw new Error(`${res.status} ${await res.text().catch(() => "")}`); + return res.status === 204 ? (undefined as T) : ((await res.json()) as T); +} + +export { BASE }; diff --git a/frontend/src/api/models.ts b/frontend/src/api/models.ts new file mode 100644 index 00000000..13d9e041 --- /dev/null +++ b/frontend/src/api/models.ts @@ -0,0 +1,3 @@ +import { apiFetch } from "./client"; + +export const listModels = () => apiFetch<{ models: unknown[] }>("/models"); diff --git a/frontend/src/api/sessions.test.ts b/frontend/src/api/sessions.test.ts new file mode 100644 index 00000000..a31bd177 --- /dev/null +++ b/frontend/src/api/sessions.test.ts @@ -0,0 +1,13 @@ +import { http, HttpResponse } from "msw"; +import { server } from "../test/msw"; +import { createSession, listSessions } from "./sessions"; + +test("createSession POSTs and returns the id", async () => { + server.use(http.post("http://localhost:8787/sessions", () => HttpResponse.json({ id: "s1" }))); + expect(await createSession({ workspace: "w", question: "q" })).toEqual({ id: "s1" }); +}); +test("listSessions GETs the array", async () => { + server.use(http.get("http://localhost:8787/sessions", () => HttpResponse.json([{ id: "s1", status: "open", question: "q", summary: null, created_at: "t", updated_at: null, author: null }]))); + const rows = await listSessions(); + expect(rows[0].id).toBe("s1"); +}); diff --git a/frontend/src/api/sessions.ts b/frontend/src/api/sessions.ts new file mode 100644 index 00000000..466ccc32 --- /dev/null +++ b/frontend/src/api/sessions.ts @@ -0,0 +1,34 @@ +import { apiFetch } from "./client"; +import type { SessionSummary, UiResponse } from "./types"; + +export const createSession = (i: { + workspace: string; + question: string; + provider?: string; + model?: string; + thinking?: string; + name?: string; +}) => apiFetch<{ id: string }>("/sessions", { method: "POST", body: JSON.stringify(i) }); + +export const listSessions = () => apiFetch("/sessions"); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const getSession = (id: string) => apiFetch(`/sessions/${id}`); + +export const postResponse = (id: string, uiResponse: UiResponse) => + apiFetch(`/sessions/${id}/response`, { + method: "POST", + body: JSON.stringify({ ui_response: uiResponse }), + }); + +export const postSteer = (id: string, text: string) => + apiFetch(`/sessions/${id}/steer`, { + method: "POST", + body: JSON.stringify({ text }), + }); + +export const closeSession = (id: string) => + apiFetch(`/sessions/${id}/close`, { method: "POST" }); + +export const resumeSession = (id: string) => + apiFetch(`/sessions/${id}/resume`, { method: "POST" }); diff --git a/frontend/src/api/sql.ts b/frontend/src/api/sql.ts new file mode 100644 index 00000000..eac4ff40 --- /dev/null +++ b/frontend/src/api/sql.ts @@ -0,0 +1,14 @@ +import { apiFetch } from "./client"; +import type { PreviewResult } from "./types"; + +export const sqlPreview = ( + id: string, + opts: { limit?: number; offset?: number } = {} +) => + apiFetch(`/sessions/${id}/sql/preview`, { + method: "POST", + body: JSON.stringify(opts), + }); + +export const sqlExport = (id: string) => + apiFetch<{ path: string }>(`/sessions/${id}/sql/export`, { method: "POST" }); diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts new file mode 100644 index 00000000..2482b884 --- /dev/null +++ b/frontend/src/api/types.ts @@ -0,0 +1,59 @@ +export interface WidgetOption { + id: string; + label: string; + meta?: Record; + selected?: boolean; + recommended?: boolean; + opens?: WidgetDescriptor; +} + +export interface WidgetDescriptor { + id: string; + schema_version?: number; + session_id?: string; + phase?: string; + title?: string; + intro?: string; + widget: "info" | "select" | "multiselect" | "freetext" | "artifact-gate" | "artifact" | string; + options?: WidgetOption[]; + reserved?: string[]; + allow_empty?: boolean; + artifact?: { kind: string; content?: string; [k: string]: unknown }; + level?: "info" | "warning" | "error"; + text?: string; + [k: string]: unknown; +} + +export interface UiResponse { + id: string; + kind?: string; + choices?: string[]; + text?: string; + decision?: { type: string }; + control?: string; +} + +export type StreamEvent = + | { type: "ui_request"; ui_request: WidgetDescriptor } + | { type: "text_delta"; text: string } + | { type: "info"; level?: "info" | "warning" | "error"; text: string } + | { type: "system_event"; event: string; [k: string]: unknown }; + +export interface SessionSummary { + id: string; + status: string; + question: string; + summary: string | null; + created_at: string; + updated_at: string | null; + author: string | null; +} + +export interface PreviewResult { + columns: string[]; + rows: unknown[][]; + execution_ms: number; + truncated: boolean; + limit: number; + offset: number; +} diff --git a/frontend/src/api/workspaces.ts b/frontend/src/api/workspaces.ts new file mode 100644 index 00000000..43c612bc --- /dev/null +++ b/frontend/src/api/workspaces.ts @@ -0,0 +1,4 @@ +import { apiFetch } from "./client"; + +export const listWorkspaces = () => + apiFetch<{ name: string; file: string }[]>("/workspaces"); diff --git a/frontend/src/test/msw.ts b/frontend/src/test/msw.ts new file mode 100644 index 00000000..818dc20c --- /dev/null +++ b/frontend/src/test/msw.ts @@ -0,0 +1,2 @@ +import { setupServer } from "msw/node"; +export const server = setupServer(); diff --git a/frontend/src/test/setup.ts b/frontend/src/test/setup.ts index f149f27a..2e6c3aa6 100644 --- a/frontend/src/test/setup.ts +++ b/frontend/src/test/setup.ts @@ -1 +1,6 @@ import "@testing-library/jest-dom/vitest"; +import { server } from "./msw"; + +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 4f46cd45..b50126cc 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -10,7 +10,7 @@ "noEmit": true, "esModuleInterop": true, "skipLibCheck": true, - "types": ["vitest/globals", "@testing-library/jest-dom"], + "types": ["vitest/globals", "@testing-library/jest-dom", "vite/client"], "paths": { "@/*": ["./src/*"] }