feat(frontend): contract types + REST client (MSW-tested)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
const BASE = import.meta.env.VITE_BACKEND_URL ?? "http://localhost:8787";
|
||||
|
||||
export async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
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 };
|
||||
@@ -0,0 +1,3 @@
|
||||
import { apiFetch } from "./client";
|
||||
|
||||
export const listModels = () => apiFetch<{ models: unknown[] }>("/models");
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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<SessionSummary[]>("/sessions");
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const getSession = (id: string) => apiFetch<any>(`/sessions/${id}`);
|
||||
|
||||
export const postResponse = (id: string, uiResponse: UiResponse) =>
|
||||
apiFetch<void>(`/sessions/${id}/response`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ ui_response: uiResponse }),
|
||||
});
|
||||
|
||||
export const postSteer = (id: string, text: string) =>
|
||||
apiFetch<void>(`/sessions/${id}/steer`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ text }),
|
||||
});
|
||||
|
||||
export const closeSession = (id: string) =>
|
||||
apiFetch<void>(`/sessions/${id}/close`, { method: "POST" });
|
||||
|
||||
export const resumeSession = (id: string) =>
|
||||
apiFetch<void>(`/sessions/${id}/resume`, { method: "POST" });
|
||||
@@ -0,0 +1,14 @@
|
||||
import { apiFetch } from "./client";
|
||||
import type { PreviewResult } from "./types";
|
||||
|
||||
export const sqlPreview = (
|
||||
id: string,
|
||||
opts: { limit?: number; offset?: number } = {}
|
||||
) =>
|
||||
apiFetch<PreviewResult>(`/sessions/${id}/sql/preview`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(opts),
|
||||
});
|
||||
|
||||
export const sqlExport = (id: string) =>
|
||||
apiFetch<{ path: string }>(`/sessions/${id}/sql/export`, { method: "POST" });
|
||||
@@ -0,0 +1,59 @@
|
||||
export interface WidgetOption {
|
||||
id: string;
|
||||
label: string;
|
||||
meta?: Record<string, unknown>;
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { apiFetch } from "./client";
|
||||
|
||||
export const listWorkspaces = () =>
|
||||
apiFetch<{ name: string; file: string }[]>("/workspaces");
|
||||
@@ -0,0 +1,2 @@
|
||||
import { setupServer } from "msw/node";
|
||||
export const server = setupServer();
|
||||
@@ -1 +1,6 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { server } from "./msw";
|
||||
|
||||
beforeAll(() => server.listen());
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
|
||||
@@ -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/*"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user