277 lines
7.2 KiB
TypeScript
277 lines
7.2 KiB
TypeScript
export const COURSEWARE_RUNTIME_CONFIG_PATH = "/courseware-runtime.json";
|
|
export const LAB1_DEFAULT_NETRON_URL = "http://127.0.0.1:8338";
|
|
export const LAB2_DEFAULT_OLLAMA_URL = "http://127.0.0.1:11434";
|
|
export const LAB2_DEFAULT_OLLAMA_MODELS = [
|
|
{
|
|
label: "Gemma 4 E2B Q4",
|
|
value: "batiai/gemma4-e2b:q4",
|
|
},
|
|
{
|
|
label: "Gemma 4 E2B Q6",
|
|
value: "batiai/gemma4-e2b:q6",
|
|
},
|
|
] as const;
|
|
export const LAB3_DEFAULT_TERMINAL_PATH = "/wetty";
|
|
export const COURSEWARE_SERVICE_IDS = [
|
|
"open-webui",
|
|
"promptfoo",
|
|
"chunkviz",
|
|
"embedding-atlas",
|
|
"unsloth",
|
|
"ssh",
|
|
] as const;
|
|
|
|
export type CoursewareRuntimeModelOption = {
|
|
label: string;
|
|
value: string;
|
|
};
|
|
|
|
export type CoursewareServiceId = (typeof COURSEWARE_SERVICE_IDS)[number];
|
|
|
|
export type CoursewareRuntimeServices = Partial<
|
|
Record<CoursewareServiceId, string>
|
|
>;
|
|
|
|
export type ResolvedCoursewareRuntimeServices = Record<
|
|
CoursewareServiceId,
|
|
string
|
|
>;
|
|
|
|
export const COURSEWARE_SERVICE_DEFAULTS: ResolvedCoursewareRuntimeServices = {
|
|
"chunkviz": "http://127.0.0.1:3000",
|
|
"embedding-atlas": "http://127.0.0.1:5055",
|
|
"open-webui": "http://127.0.0.1:8080",
|
|
"promptfoo": "http://127.0.0.1:15500",
|
|
"ssh": "ssh://127.0.0.1:22",
|
|
"unsloth": "http://127.0.0.1:8888",
|
|
};
|
|
|
|
export type CoursewareRuntimeConfig = {
|
|
lab1NetronUrl?: string;
|
|
lab2OllamaModels?: CoursewareRuntimeModelOption[];
|
|
lab2OllamaUrl?: string;
|
|
lab3TerminalUrl?: string;
|
|
services?: CoursewareRuntimeServices;
|
|
};
|
|
|
|
export type ResolvedCoursewareRuntimeConfig = {
|
|
lab1NetronUrl: string;
|
|
lab2OllamaModels: CoursewareRuntimeModelOption[];
|
|
lab2OllamaUrl: string;
|
|
lab3TerminalUrl: string;
|
|
services: ResolvedCoursewareRuntimeServices;
|
|
};
|
|
|
|
const loopbackHosts = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
|
|
function rewriteLoopbackHost(urlValue: string, currentHostname?: string) {
|
|
try {
|
|
const url = new URL(urlValue);
|
|
if (!currentHostname || !loopbackHosts.has(url.hostname)) {
|
|
return url.toString();
|
|
}
|
|
|
|
url.hostname = currentHostname;
|
|
return url.toString();
|
|
} catch {
|
|
return urlValue;
|
|
}
|
|
}
|
|
|
|
function getCurrentHostname() {
|
|
if (typeof window === "undefined") {
|
|
return undefined;
|
|
}
|
|
|
|
const hostname = window.location.hostname?.trim();
|
|
return hostname || undefined;
|
|
}
|
|
|
|
export function isCoursewareServiceId(
|
|
value: string,
|
|
): value is CoursewareServiceId {
|
|
return (COURSEWARE_SERVICE_IDS as readonly string[]).includes(value);
|
|
}
|
|
|
|
export function getCoursewareServiceBaseUrl(
|
|
serviceId: CoursewareServiceId,
|
|
envValue?: string,
|
|
currentHostname = getCurrentHostname(),
|
|
) {
|
|
const trimmedValue = envValue?.trim();
|
|
const baseUrl = trimmedValue || COURSEWARE_SERVICE_DEFAULTS[serviceId];
|
|
return rewriteLoopbackHost(baseUrl, currentHostname);
|
|
}
|
|
|
|
export function getCoursewareServices(
|
|
envValue?: CoursewareRuntimeServices,
|
|
currentHostname = getCurrentHostname(),
|
|
): ResolvedCoursewareRuntimeServices {
|
|
const services = {} as ResolvedCoursewareRuntimeServices;
|
|
|
|
for (const serviceId of COURSEWARE_SERVICE_IDS) {
|
|
services[serviceId] = getCoursewareServiceBaseUrl(
|
|
serviceId,
|
|
envValue?.[serviceId],
|
|
currentHostname,
|
|
);
|
|
}
|
|
|
|
return services;
|
|
}
|
|
|
|
function appendUrlPath(urlValue: string, pathSuffix: string) {
|
|
const trimmedSuffix = pathSuffix.trim();
|
|
if (!trimmedSuffix) {
|
|
return urlValue;
|
|
}
|
|
|
|
try {
|
|
const url = new URL(urlValue);
|
|
const normalizedSuffix = trimmedSuffix.startsWith("/")
|
|
? trimmedSuffix
|
|
: `/${trimmedSuffix}`;
|
|
const basePath = url.pathname === "/" ? "" : url.pathname.replace(/\/$/, "");
|
|
|
|
url.pathname = `${basePath}${normalizedSuffix}`;
|
|
return url.toString();
|
|
} catch {
|
|
return urlValue;
|
|
}
|
|
}
|
|
|
|
export function resolveCoursewareServiceBaseUrl(
|
|
runtimeConfig: Pick<ResolvedCoursewareRuntimeConfig, "services">,
|
|
serviceId: CoursewareServiceId,
|
|
) {
|
|
return runtimeConfig.services[serviceId];
|
|
}
|
|
|
|
export function resolveCoursewareServiceUrl(
|
|
runtimeConfig: Pick<ResolvedCoursewareRuntimeConfig, "services">,
|
|
serviceId: CoursewareServiceId,
|
|
pathSuffix?: string,
|
|
) {
|
|
const baseUrl = resolveCoursewareServiceBaseUrl(runtimeConfig, serviceId);
|
|
if (!pathSuffix?.trim()) {
|
|
return baseUrl;
|
|
}
|
|
|
|
return appendUrlPath(baseUrl, pathSuffix);
|
|
}
|
|
|
|
export function resolveCoursewareServiceAddress(
|
|
runtimeConfig: Pick<ResolvedCoursewareRuntimeConfig, "services">,
|
|
serviceId: CoursewareServiceId,
|
|
) {
|
|
const baseUrl = resolveCoursewareServiceBaseUrl(runtimeConfig, serviceId);
|
|
|
|
try {
|
|
const url = new URL(baseUrl);
|
|
return url.port ? `${url.hostname}:${url.port}` : url.hostname;
|
|
} catch {
|
|
return baseUrl;
|
|
}
|
|
}
|
|
|
|
export function getLab1NetronUrl(
|
|
envValue?: string,
|
|
currentHostname = getCurrentHostname(),
|
|
) {
|
|
const trimmedValue = envValue?.trim();
|
|
|
|
if (!trimmedValue) {
|
|
return rewriteLoopbackHost(LAB1_DEFAULT_NETRON_URL, currentHostname);
|
|
}
|
|
|
|
return rewriteLoopbackHost(trimmedValue, currentHostname);
|
|
}
|
|
|
|
export function getLab2OllamaUrl(
|
|
envValue?: string,
|
|
currentHostname = getCurrentHostname(),
|
|
) {
|
|
const trimmedValue = envValue?.trim();
|
|
|
|
if (!trimmedValue) {
|
|
return rewriteLoopbackHost(LAB2_DEFAULT_OLLAMA_URL, currentHostname);
|
|
}
|
|
|
|
return rewriteLoopbackHost(trimmedValue, currentHostname);
|
|
}
|
|
|
|
export function getLab2OllamaModels(
|
|
envValue?: CoursewareRuntimeModelOption[],
|
|
) {
|
|
if (!Array.isArray(envValue) || envValue.length === 0) {
|
|
return LAB2_DEFAULT_OLLAMA_MODELS.map((model) => ({ ...model }));
|
|
}
|
|
|
|
const normalizedModels = envValue
|
|
.map((model) => {
|
|
const label = model?.label?.trim();
|
|
const value = model?.value?.trim();
|
|
|
|
if (!label || !value) {
|
|
return null;
|
|
}
|
|
|
|
return { label, value } satisfies CoursewareRuntimeModelOption;
|
|
})
|
|
.filter(
|
|
(model): model is CoursewareRuntimeModelOption => model !== null,
|
|
);
|
|
|
|
if (normalizedModels.length === 0) {
|
|
return LAB2_DEFAULT_OLLAMA_MODELS.map((model) => ({ ...model }));
|
|
}
|
|
|
|
return normalizedModels;
|
|
}
|
|
|
|
export function getLab3TerminalPath(
|
|
envValue?: string,
|
|
currentHostname = getCurrentHostname(),
|
|
) {
|
|
const trimmedValue = envValue?.trim();
|
|
|
|
if (!trimmedValue) {
|
|
return LAB3_DEFAULT_TERMINAL_PATH;
|
|
}
|
|
|
|
if (/^https?:\/\//i.test(trimmedValue)) {
|
|
return rewriteLoopbackHost(trimmedValue, currentHostname);
|
|
}
|
|
|
|
return trimmedValue.startsWith("/") ? trimmedValue : `/${trimmedValue}`;
|
|
}
|
|
|
|
export function normalizeCoursewareRuntimeConfig(
|
|
config?: CoursewareRuntimeConfig,
|
|
currentHostname = getCurrentHostname(),
|
|
): ResolvedCoursewareRuntimeConfig {
|
|
return {
|
|
lab1NetronUrl: getLab1NetronUrl(config?.lab1NetronUrl, currentHostname),
|
|
lab2OllamaModels: getLab2OllamaModels(config?.lab2OllamaModels),
|
|
lab2OllamaUrl: getLab2OllamaUrl(config?.lab2OllamaUrl, currentHostname),
|
|
lab3TerminalUrl: getLab3TerminalPath(
|
|
config?.lab3TerminalUrl,
|
|
currentHostname,
|
|
),
|
|
services: getCoursewareServices(config?.services, currentHostname),
|
|
};
|
|
}
|
|
|
|
export async function fetchCoursewareRuntimeConfig() {
|
|
const response = await fetch(COURSEWARE_RUNTIME_CONFIG_PATH, {
|
|
cache: "no-store",
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Runtime config request failed: ${response.status}`);
|
|
}
|
|
|
|
const config = (await response.json()) as CoursewareRuntimeConfig;
|
|
return normalizeCoursewareRuntimeConfig(config);
|
|
}
|