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 "@testing-library/jest-dom/vitest";
|
||||||
|
import { server } from "./msw";
|
||||||
|
|
||||||
|
beforeAll(() => server.listen());
|
||||||
|
afterEach(() => server.resetHandlers());
|
||||||
|
afterAll(() => server.close());
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"types": ["vitest/globals", "@testing-library/jest-dom"],
|
"types": ["vitest/globals", "@testing-library/jest-dom", "vite/client"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user