Merge branch 'main' into chore/more-warnings

This commit is contained in:
2026-01-30 12:43:52 +01:00
16 changed files with 67 additions and 25 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

@@ -17,6 +17,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.
@@ -110,7 +111,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>;
})(); })();
@@ -130,7 +131,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;
@@ -186,4 +187,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);
} }

View File

@@ -1,8 +1,8 @@
{/* /*
This program has been developed by students from the bachelor Computer Science at Utrecht This program has been developed by students from the bachelor Computer Science at Utrecht
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)
*/} */
.operator-switch { .operator-switch {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;

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",