lct-hack/frontend/src/shared/api/http.ts
Ivan Gerasimov 978d7dc1aa lct-17: пульт преподавателя — сценарий, наблюдение, пометки, ссылки
Компоновка от того, по чему пульт оценит действующий преподаватель:
задать сценарий и не вмешиваться, видеть работу курсанта вживую,
получить готовый разбор, показать группе. Кнопка запуска одна и крупная,
ссылки на АРМ курсанта и монитор печатаются рядом.

Разделение прав архитектурное и здесь: пульт смотрит через observe,
пишет через control, карточка курсанта у него только для чтения.

Две ловушки запуска: scenario.start уходил из эффекта и мог повториться
при перерисовке, а открытая из истории ссылка на идущее занятие
запустила бы его заново и сбросила. Запускается только занятие,
созданное кнопкой.
2026-09-17 21:34:01 +03:00

77 lines
2.3 KiB
TypeScript

// HTTP-клиент и хуки. Типы ответов HTTP в generated.ts пока не попадают —
// там только события WS; здесь описаны руками по docs/arch/CONTRACT.md#http-api.
import { useMutation, useQuery } from "@tanstack/react-query";
import type { Level, SessionMode } from "@/shared/types/generated";
export class ApiError extends Error {
constructor(
readonly status: number,
readonly code: string,
) {
super(`${status} ${code}`);
}
}
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const response = await fetch(path, {
...init,
headers: { "Content-Type": "application/json", ...init?.headers },
});
if (!response.ok) {
const body = await response.json().catch(() => ({}));
throw new ApiError(response.status, body.detail ?? response.statusText);
}
return response.json() as Promise<T>;
}
export interface Health {
status: string;
models_ready: boolean;
embeddings_ready: boolean;
scenarios_loaded: number;
offline: boolean;
}
export interface ScenarioSummary {
id: string;
title: string;
type: string;
level: Level;
topics: string[];
modes: SessionMode[];
dds: string | null;
}
export interface SessionInfo {
session_id: string;
scenario_id: string;
mode: SessionMode;
attempt: number;
trainee_id: string | null;
group_id: string | null;
}
export const useHealth = () =>
useQuery({ queryKey: ["health"], queryFn: () => request<Health>("/api/health"), refetchInterval: 5_000 });
export const useScenarios = () =>
useQuery({ queryKey: ["scenarios"], queryFn: () => request<ScenarioSummary[]>("/api/scenarios") });
export const useSessions = (params: { trainee?: string; group?: string; mode?: SessionMode } = {}) => {
const query = new URLSearchParams(
Object.entries(params).filter(([, value]) => value) as [string, string][],
).toString();
return useQuery({
queryKey: ["sessions", query],
queryFn: () => request<SessionInfo[]>(`/api/sessions${query ? `?${query}` : ""}`),
refetchInterval: 15_000,
});
};
export const useCreateSession = () =>
useMutation({
mutationFn: (body: { scenario_id: string; mode: SessionMode; trainee?: string; group?: string }) =>
request<SessionInfo>("/api/sessions", { method: "POST", body: JSON.stringify(body) }),
});