Skip to content

Commit dff140a

Browse files
committed
feat: initialize frontend API layer and endpoint definitions established base client for communication with the Quadtrix backend.
Defined standard request/response handlers for model interaction.
1 parent 65b36e7 commit dff140a

4 files changed

Lines changed: 161 additions & 0 deletions

File tree

frontend/src/api/chat.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useMutation, useQueryClient } from "@tanstack/react-query";
2+
3+
import { apiFetch } from "./client";
4+
import type { ChatRequest, ChatResponse } from "../types";
5+
6+
export function useSendMessage() {
7+
const queryClient = useQueryClient();
8+
return useMutation({
9+
mutationFn: async (payload: ChatRequest): Promise<ChatResponse> => {
10+
try {
11+
return await apiFetch<ChatResponse>("/api/chat", {
12+
method: "POST",
13+
body: JSON.stringify(payload),
14+
});
15+
} catch (error) {
16+
throw error;
17+
}
18+
},
19+
onSuccess: async (response) => {
20+
await queryClient.invalidateQueries({ queryKey: ["sessions"] });
21+
await queryClient.invalidateQueries({ queryKey: ["messages", response.session_id] });
22+
},
23+
});
24+
}

frontend/src/api/client.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useSettingsStore } from "../store/settingsStore";
2+
import type { ApiError } from "../types";
3+
4+
export class ApiClientError extends Error {
5+
apiError: ApiError;
6+
7+
constructor(apiError: ApiError) {
8+
super(apiError.message);
9+
this.apiError = apiError;
10+
}
11+
}
12+
13+
export async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
14+
const baseUrl = useSettingsStore.getState().apiBaseUrl.replace(/\/$/, "");
15+
const headers = new Headers(init?.headers);
16+
headers.set("Content-Type", "application/json");
17+
try {
18+
const response = await fetch(`${baseUrl}${path}`, {
19+
...init,
20+
headers,
21+
});
22+
if (!response.ok) {
23+
const fallback: ApiError = { error: "request_failed", message: response.statusText, code: response.status };
24+
const error = (await response.json().catch(() => fallback)) as ApiError;
25+
throw new ApiClientError(error);
26+
}
27+
if (response.status === 204) {
28+
return undefined as T;
29+
}
30+
return (await response.json()) as T;
31+
} catch (error) {
32+
if (error instanceof ApiClientError) {
33+
throw error;
34+
}
35+
throw new ApiClientError({ error: "network_error", message: "Could not reach the API server", code: 0 });
36+
}
37+
}

frontend/src/api/health.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
3+
import { apiFetch } from "./client";
4+
import type { HealthResponse, ModelStats } from "../types";
5+
6+
export function useHealth() {
7+
return useQuery({
8+
queryKey: ["health"],
9+
queryFn: async (): Promise<HealthResponse> => {
10+
try {
11+
return await apiFetch<HealthResponse>("/api/health");
12+
} catch (error) {
13+
throw error;
14+
}
15+
},
16+
refetchInterval: 30000,
17+
retry: 1,
18+
});
19+
}
20+
21+
export function useStats() {
22+
return useQuery({
23+
queryKey: ["stats"],
24+
queryFn: async (): Promise<ModelStats> => {
25+
try {
26+
return await apiFetch<ModelStats>("/api/stats");
27+
} catch (error) {
28+
throw error;
29+
}
30+
},
31+
refetchInterval: 30000,
32+
retry: 1,
33+
});
34+
}

frontend/src/api/sessions.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2+
3+
import { apiFetch } from "./client";
4+
import type { Message, Session } from "../types";
5+
6+
export function useSessions() {
7+
return useQuery({
8+
queryKey: ["sessions"],
9+
queryFn: async (): Promise<Session[]> => {
10+
try {
11+
return await apiFetch<Session[]>("/api/sessions");
12+
} catch (error) {
13+
throw error;
14+
}
15+
},
16+
});
17+
}
18+
19+
export function useCreateSession() {
20+
const queryClient = useQueryClient();
21+
return useMutation({
22+
mutationFn: async (title?: string): Promise<Session> => {
23+
try {
24+
return await apiFetch<Session>("/api/sessions", {
25+
method: "POST",
26+
body: JSON.stringify({ title }),
27+
});
28+
} catch (error) {
29+
throw error;
30+
}
31+
},
32+
onSuccess: async () => {
33+
await queryClient.invalidateQueries({ queryKey: ["sessions"] });
34+
},
35+
});
36+
}
37+
38+
export function useDeleteSession() {
39+
const queryClient = useQueryClient();
40+
return useMutation({
41+
mutationFn: async (sessionId: string): Promise<void> => {
42+
try {
43+
await apiFetch<void>(`/api/sessions/${sessionId}`, { method: "DELETE" });
44+
} catch (error) {
45+
throw error;
46+
}
47+
},
48+
onSuccess: async () => {
49+
await queryClient.invalidateQueries({ queryKey: ["sessions"] });
50+
},
51+
});
52+
}
53+
54+
export function useSessionMessages(sessionId: string | null) {
55+
return useQuery({
56+
queryKey: ["messages", sessionId],
57+
enabled: Boolean(sessionId),
58+
queryFn: async (): Promise<Message[]> => {
59+
try {
60+
return await apiFetch<Message[]>(`/api/sessions/${sessionId}/messages`);
61+
} catch (error) {
62+
throw error;
63+
}
64+
},
65+
});
66+
}

0 commit comments

Comments
 (0)