feat: introduce backend url environment variable

ref: N25B-352
This commit is contained in:
Twirre Meulenbelt
2026-01-29 17:40:49 +01:00
parent 378a64c7ca
commit 4395e44dbf
15 changed files with 65 additions and 23 deletions

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
# The location of the backend
VITE_API_BASE_URL=http://localhost:8000

View File

@@ -28,6 +28,14 @@ npm run dev
It should automatically reload when you save changes. It should automatically reload when you save changes.
## Environment
Copy `.env.example` to `.env.local` and adjust values as needed:
```shell
cp .env.example .env.local
```
## Git Hooks ## Git Hooks
To activate automatic linting, branch name checks and commit message checks, run: To activate automatic linting, branch name checks and commit message checks, run:

View File

@@ -5,6 +5,7 @@ import {useCallback, useEffect, useRef, useState} from "react";
import {applyPriorityPredicates, type PriorityFilterPredicate} from "../../utils/priorityFiltering.ts"; import {applyPriorityPredicates, type PriorityFilterPredicate} from "../../utils/priorityFiltering.ts";
import {cell, type Cell} from "../../utils/cellStore.ts"; import {cell, type Cell} from "../../utils/cellStore.ts";
import { API_BASE_URL } from "../../config/api.ts";
type ExtraLevelName = 'LLM' | 'OBSERVATION' | 'ACTION' | 'CHAT'; type ExtraLevelName = 'LLM' | 'OBSERVATION' | 'ACTION' | 'CHAT';
@@ -210,7 +211,7 @@ export function useLogs(filterPredicates: Map<string, LogFilterPredicate>) {
// Only create one SSE connection for the lifetime of the hook. // Only create one SSE connection for the lifetime of the hook.
if (sseRef.current) return; if (sseRef.current) return;
const es = new EventSource("http://localhost:8000/logs/stream"); const es = new EventSource(`${API_BASE_URL}/logs/stream`);
sseRef.current = es; sseRef.current = es;
es.onmessage = (event) => { es.onmessage = (event) => {

11
src/config/api.ts Normal file
View File

@@ -0,0 +1,11 @@
declare const __VITE_API_BASE_URL__: string | undefined;
const DEFAULT_API_BASE_URL = "http://localhost:8000";
const rawApiBaseUrl =
(typeof __VITE_API_BASE_URL__ !== "undefined" ? __VITE_API_BASE_URL__ : undefined) ??
DEFAULT_API_BASE_URL;
export const API_BASE_URL = rawApiBaseUrl.endsWith("/")
? rawApiBaseUrl.slice(0, -1)
: rawApiBaseUrl;

View File

@@ -2,6 +2,7 @@
// University within the Software Project course. // University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences) // © Copyright Utrecht University (Department of Information and Computing Sciences)
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { API_BASE_URL } from '../../config/api.ts';
/** /**
* Displays the current connection status of a robot in real time. * Displays the current connection status of a robot in real time.
@@ -25,7 +26,7 @@ export default function ConnectedRobots() {
useEffect(() => { useEffect(() => {
// Open a Server-Sent Events (SSE) connection to receive live ping updates. // Open a Server-Sent Events (SSE) connection to receive live ping updates.
// We're expecting a stream of data like that looks like this: `data = False` or `data = True` // We're expecting a stream of data like that looks like this: `data = False` or `data = True`
const eventSource = new EventSource("http://localhost:8000/robot/ping_stream"); const eventSource = new EventSource(`${API_BASE_URL}/robot/ping_stream`);
eventSource.onmessage = (event) => { eventSource.onmessage = (event) => {
// Expecting messages in JSON format: `true` or `false` // Expecting messages in JSON format: `true` or `false`

View File

@@ -2,16 +2,14 @@
// University within the Software Project course. // University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences) // © Copyright Utrecht University (Department of Information and Computing Sciences)
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { API_BASE_URL } from '../../config/api.ts';
const API_BASE = "http://localhost:8000";
const API_BASE_BP = API_BASE + "/button_pressed"; //UserInterruptAgent endpoint
/** /**
* HELPER: Unified sender function * HELPER: Unified sender function
*/ */
export const sendAPICall = async (type: string, context: string, endpoint?: string) => { export const sendAPICall = async (type: string, context: string, endpoint?: string) => {
try { try {
const response = await fetch(`${API_BASE_BP}${endpoint ?? ""}`, { const response = await fetch(`${API_BASE_URL}/button_pressed${endpoint ?? ""}`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ type, context }), body: JSON.stringify({ type, context }),
@@ -76,7 +74,7 @@ export function useExperimentLogger(onUpdate?: (data: ExperimentStreamData) => v
useEffect(() => { useEffect(() => {
console.log("Connecting to Experiment Stream..."); console.log("Connecting to Experiment Stream...");
const eventSource = new EventSource(`${API_BASE}/experiment_stream`); const eventSource = new EventSource(`${API_BASE_URL}/experiment_stream`);
eventSource.onmessage = (event) => { eventSource.onmessage = (event) => {
try { try {
@@ -112,7 +110,7 @@ export function useStatusLogger(onUpdate?: (data: ExperimentStreamData) => void
}, [onUpdate]); }, [onUpdate]);
useEffect(() => { useEffect(() => {
const eventSource = new EventSource(`${API_BASE}/status_stream`); const eventSource = new EventSource(`${API_BASE_URL}/status_stream`);
eventSource.onmessage = (event) => { eventSource.onmessage = (event) => {
try { try {
const parsedData = JSON.parse(event.data); const parsedData = JSON.parse(event.data);
@@ -121,4 +119,4 @@ export function useStatusLogger(onUpdate?: (data: ExperimentStreamData) => void
}; };
return () => eventSource.close(); return () => eventSource.close();
}, []); }, []);
} }

View File

@@ -4,6 +4,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import styles from './MonitoringPage.module.css'; import styles from './MonitoringPage.module.css';
import { sendAPICall } from './MonitoringPageAPI'; import { sendAPICall } from './MonitoringPageAPI';
import { API_BASE_URL } from '../../config/api.ts';
// --- GESTURE COMPONENT --- // --- GESTURE COMPONENT ---
export const GestureControls: React.FC = () => { export const GestureControls: React.FC = () => {
@@ -201,7 +202,7 @@ export const RobotConnected = () => {
useEffect(() => { useEffect(() => {
// Open a Server-Sent Events (SSE) connection to receive live ping updates. // Open a Server-Sent Events (SSE) connection to receive live ping updates.
// We're expecting a stream of data like that looks like this: `data = False` or `data = True` // We're expecting a stream of data like that looks like this: `data = False` or `data = True`
const eventSource = new EventSource("http://localhost:8000/robot/ping_stream"); const eventSource = new EventSource(`${API_BASE_URL}/robot/ping_stream`);
eventSource.onmessage = (event) => { eventSource.onmessage = (event) => {
// Expecting messages in JSON format: `true` or `false` // Expecting messages in JSON format: `true` or `false`
@@ -232,4 +233,4 @@ export const RobotConnected = () => {
<p className={connected ? styles.connected : styles.disconnected }>{connected ? "● Robot is connected" : "● Robot is disconnected"}</p> <p className={connected ? styles.connected : styles.disconnected }>{connected ? "● Robot is connected" : "● Robot is disconnected"}</p>
</div> </div>
) )
} }

View File

@@ -14,6 +14,7 @@ import formatDuration from "../../../utils/formatDuration.ts";
import {create} from "zustand"; import {create} from "zustand";
import Dialog from "../../../components/Dialog.tsx"; import Dialog from "../../../components/Dialog.tsx";
import delayedResolve from "../../../utils/delayedResolve.ts"; import delayedResolve from "../../../utils/delayedResolve.ts";
import { API_BASE_URL } from "../../../config/api.ts";
/** /**
* Local Zustand store for logging UI preferences. * Local Zustand store for logging UI preferences.
@@ -107,7 +108,7 @@ function DownloadScreen({filenames, refresh}: {filenames: string[] | null, refre
return <ol className={`${styles.downloadList} margin-0 padding-h-lg scroll-y`}> return <ol className={`${styles.downloadList} margin-0 padding-h-lg scroll-y`}>
{filenames!.map((filename) => ( {filenames!.map((filename) => (
<li><a key={filename} href={`http://localhost:8000/api/logs/files/${filename}`} download={filename}>{filename}</a></li> <li><a key={filename} href={`${API_BASE_URL}/api/logs/files/${filename}`} download={filename}>{filename}</a></li>
))} ))}
</ol>; </ol>;
})(); })();
@@ -127,7 +128,7 @@ function DownloadButton() {
const [filenames, setFilenames] = useState<string[] | null>(null); const [filenames, setFilenames] = useState<string[] | null>(null);
async function getFiles(): Promise<string[]> { async function getFiles(): Promise<string[]> {
const response = await fetch("http://localhost:8000/api/logs/files"); const response = await fetch(`${API_BASE_URL}/api/logs/files`);
const files = await response.json(); const files = await response.json();
files.sort(); files.sort();
return files; return files;
@@ -183,4 +184,4 @@ export default function ExperimentLogs() {
</div> </div>
<LogMessages recordCells={filteredLogs} MessageComponent={ExperimentMessage} /> <LogMessages recordCells={filteredLogs} MessageComponent={ExperimentMessage} />
</aside>; </aside>;
} }

View File

@@ -2,6 +2,7 @@
// University within the Software Project course. // University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences) // © Copyright Utrecht University (Department of Information and Computing Sciences)
import { useState, useEffect, useRef } from 'react' import { useState, useEffect, useRef } from 'react'
import { API_BASE_URL } from '../../config/api.ts';
/** /**
* Displays a live robot interaction panel with user input, conversation history, * Displays a live robot interaction panel with user input, conversation history,
@@ -34,7 +35,7 @@ export default function Robot() {
*/ */
const sendMessage = async () => { const sendMessage = async () => {
try { try {
const response = await fetch("http://localhost:8000/message", { const response = await fetch(`${API_BASE_URL}/message`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -60,7 +61,7 @@ export default function Robot() {
* The connection resets whenever `conversationIndex` changes. * The connection resets whenever `conversationIndex` changes.
*/ */
useEffect(() => { useEffect(() => {
const eventSource = new EventSource("http://localhost:8000/sse"); const eventSource = new EventSource(`${API_BASE_URL}/sse`);
eventSource.onmessage = (event) => { eventSource.onmessage = (event) => {
try { try {

View File

@@ -6,6 +6,7 @@ import orderPhaseNodeArray from "../../utils/orderPhaseNodes";
import useFlowStore from './visualProgrammingUI/VisProgStores'; import useFlowStore from './visualProgrammingUI/VisProgStores';
import { NodeReduces } from './visualProgrammingUI/NodeRegistry'; import { NodeReduces } from './visualProgrammingUI/NodeRegistry';
import type { PhaseNode } from "./visualProgrammingUI/nodes/PhaseNode"; import type { PhaseNode } from "./visualProgrammingUI/nodes/PhaseNode";
import { API_BASE_URL } from "../../config/api.ts";
/** /**
* Reduces the graph into its phases' information and recursively calls their reducing function * Reduces the graph into its phases' information and recursively calls their reducing function
@@ -28,7 +29,7 @@ export function runProgram() {
const program = {phases} const program = {phases}
console.log(JSON.stringify(program, null, 2)); console.log(JSON.stringify(program, null, 2));
fetch( fetch(
"http://localhost:8000/program", `${API_BASE_URL}/program`,
{ {
method: "POST", method: "POST",
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
@@ -43,4 +44,4 @@ export function runProgram() {
useProgramStore.getState().setProgramState(structuredClone(program)); useProgramStore.getState().setProgramState(structuredClone(program));
}).catch(() => console.log("Failed to send program to the backend.")); }).catch(() => console.log("Failed to send program to the backend."));
console.log(program); console.log(program);
} }

11
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_BASE_URL?: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
declare const __VITE_API_BASE_URL__: string | undefined;

View File

@@ -6,6 +6,7 @@ import "@testing-library/jest-dom";
import {type LogRecord, useLogs} from "../../../src/components/Logging/useLogs.ts"; import {type LogRecord, useLogs} from "../../../src/components/Logging/useLogs.ts";
import {type cell, useCell} from "../../../src/utils/cellStore.ts"; import {type cell, useCell} from "../../../src/utils/cellStore.ts";
import { StrictMode } from "react"; import { StrictMode } from "react";
import { API_BASE_URL } from "../../../src/config/api.ts";
jest.mock("../../../src/utils/priorityFiltering.ts", () => ({ jest.mock("../../../src/utils/priorityFiltering.ts", () => ({
applyPriorityPredicates: jest.fn((_log, preds: any[]) => applyPriorityPredicates: jest.fn((_log, preds: any[]) =>
@@ -83,7 +84,7 @@ describe("useLogs (unit)", () => {
); );
const es = (globalThis as any).__es as MockEventSource; const es = (globalThis as any).__es as MockEventSource;
expect(es).toBeTruthy(); expect(es).toBeTruthy();
expect(es.url).toBe("http://localhost:8000/logs/stream"); expect(es.url).toBe(`${API_BASE_URL}/logs/stream`);
unmount(); unmount();
expect(es.close).toHaveBeenCalledTimes(1); expect(es.close).toHaveBeenCalledTimes(1);

View File

@@ -10,6 +10,7 @@ import {
useExperimentLogger, useExperimentLogger,
useStatusLogger useStatusLogger
} from '../../../src/pages/MonitoringPage/MonitoringPageAPI'; } from '../../../src/pages/MonitoringPage/MonitoringPageAPI';
import { API_BASE_URL } from '../../../src/config/api.ts';
// --- MOCK EVENT SOURCE SETUP --- // --- MOCK EVENT SOURCE SETUP ---
// This mocks the browser's EventSource so we can manually 'push' messages to our hooks // This mocks the browser's EventSource so we can manually 'push' messages to our hooks
@@ -72,7 +73,7 @@ describe('MonitoringPageAPI', () => {
await sendAPICall('test_type', 'test_ctx'); await sendAPICall('test_type', 'test_ctx');
expect(globalThis.fetch).toHaveBeenCalledWith( expect(globalThis.fetch).toHaveBeenCalledWith(
'http://localhost:8000/button_pressed', `${API_BASE_URL}/button_pressed`,
expect.objectContaining({ expect.objectContaining({
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -220,4 +221,4 @@ describe('MonitoringPageAPI', () => {
expect(consoleSpy).toHaveBeenCalledWith('Status stream error:', expect.any(Error)); expect(consoleSpy).toHaveBeenCalledWith('Status stream error:', expect.any(Error));
}); });
}); });
}); });

View File

@@ -3,6 +3,7 @@
// © Copyright Utrecht University (Department of Information and Computing Sciences) // © Copyright Utrecht University (Department of Information and Computing Sciences)
import { render, screen, act, cleanup, fireEvent } from '@testing-library/react'; import { render, screen, act, cleanup, fireEvent } from '@testing-library/react';
import Robot from '../../../src/pages/Robot/Robot'; import Robot from '../../../src/pages/Robot/Robot';
import { API_BASE_URL } from '../../../src/config/api.ts';
// Mock EventSource // Mock EventSource
const mockInstances: MockEventSource[] = []; const mockInstances: MockEventSource[] = [];
@@ -64,7 +65,7 @@ describe('Robot', () => {
await act(async () => fireEvent.click(button)); await act(async () => fireEvent.click(button));
expect(globalThis.fetch).toHaveBeenCalledWith( expect(globalThis.fetch).toHaveBeenCalledWith(
'http://localhost:8000/message', `${API_BASE_URL}/message`,
expect.objectContaining({ expect.objectContaining({
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -83,7 +84,7 @@ describe('Robot', () => {
); );
expect(globalThis.fetch).toHaveBeenCalledWith( expect(globalThis.fetch).toHaveBeenCalledWith(
'http://localhost:8000/message', `${API_BASE_URL}/message`,
expect.objectContaining({ expect.objectContaining({
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },

View File

@@ -4,6 +4,9 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
define: {
__VITE_API_BASE_URL__: "import.meta.env.VITE_API_BASE_URL",
},
css: { css: {
modules: { modules: {
localsConvention: "camelCase", localsConvention: "camelCase",