Merge branch 'main' of ssh://git.science.uu.nl/ics/sp/2025/n25b/pepperplus-ui into feat/face-detection

This commit is contained in:
JobvAlewijk
2026-01-30 16:06:29 +01:00
32 changed files with 131 additions and 61 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.
## Environment
Copy `.env.example` to `.env.local` and adjust values as needed:
```shell
cp .env.example .env.local
```
## Git Hooks
To activate automatic linting, branch name checks and commit message checks, run:

View File

@@ -1,8 +1,8 @@
{/*
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/}
*/
.logopepper {
height: 8em;
padding: 1.5em;

View File

@@ -1,8 +1,8 @@
{/*
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/}
*/
.filter-root {
position: relative;
display: flex;

View File

@@ -1,8 +1,8 @@
{/*
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/}
*/
.logging-container {
box-sizing: border-box;

View File

@@ -5,6 +5,7 @@ import {useCallback, useEffect, useRef, useState} from "react";
import {applyPriorityPredicates, type PriorityFilterPredicate} from "../../utils/priorityFiltering.ts";
import {cell, type Cell} from "../../utils/cellStore.ts";
import { API_BASE_URL } from "../../config/api.ts";
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.
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;
es.onmessage = (event) => {

View File

@@ -1,8 +1,8 @@
{/*
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/}
*/
.text-field {
border: 1px solid transparent;
border-radius: 5pt;

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

@@ -1,8 +1,8 @@
{/*
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/}
*/
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;

View File

@@ -2,6 +2,7 @@
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { useEffect, useState } from 'react'
import { API_BASE_URL } from '../../config/api.ts';
/**
* Displays the current connection status of a robot in real time.
@@ -25,7 +26,7 @@ export default function ConnectedRobots() {
useEffect(() => {
// 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`
const eventSource = new EventSource("http://localhost:8000/robot/ping_stream");
const eventSource = new EventSource(`${API_BASE_URL}/robot/ping_stream`);
eventSource.onmessage = (event) => {
// Expecting messages in JSON format: `true` or `false`

View File

@@ -1,8 +1,8 @@
{/*
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/}
*/
.read_the_docs {
color: #888;
}

View File

@@ -1,8 +1,8 @@
{/*
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/}
*/
.dashboardContainer {
display: grid;
grid-template-columns: 2fr 1fr; /* Left = content, Right = logs */

View File

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

View File

@@ -4,6 +4,7 @@
import React, { useEffect, useState } from 'react';
import styles from './MonitoringPage.module.css';
import { sendAPICall } from './MonitoringPageAPI';
import { API_BASE_URL } from '../../config/api.ts';
// --- GESTURE COMPONENT ---
export const GestureControls: React.FC = () => {
@@ -201,7 +202,7 @@ export const RobotConnected = () => {
useEffect(() => {
// 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`
const eventSource = new EventSource("http://localhost:8000/robot/ping_stream");
const eventSource = new EventSource(`${API_BASE_URL}/robot/ping_stream`);
eventSource.onmessage = (event) => {
// 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>
</div>
)
}
}

View File

@@ -1,3 +1,8 @@
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/
.logs {
/* grid-area used in MonitoringPage.module.css */
grid-area: logs;

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import styles from "./ExperimentLogs.module.css";
import {LogMessages} from "../../../components/Logging/Logging.tsx";
import {useEffect, useMemo, useState} from "react";
@@ -14,6 +17,7 @@ import formatDuration from "../../../utils/formatDuration.ts";
import {create} from "zustand";
import Dialog from "../../../components/Dialog.tsx";
import delayedResolve from "../../../utils/delayedResolve.ts";
import { API_BASE_URL } from "../../../config/api.ts";
/**
* Local Zustand store for logging UI preferences.
@@ -107,7 +111,7 @@ function DownloadScreen({filenames, refresh}: {filenames: string[] | null, refre
return <ol className={`${styles.downloadList} margin-0 padding-h-lg scroll-y`}>
{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>;
})();
@@ -127,7 +131,7 @@ function DownloadButton() {
const [filenames, setFilenames] = useState<string[] | null>(null);
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();
files.sort();
return files;
@@ -183,4 +187,4 @@ export default function ExperimentLogs() {
</div>
<LogMessages recordCells={filteredLogs} MessageComponent={ExperimentMessage} />
</aside>;
}
}

View File

@@ -2,6 +2,7 @@
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
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,
@@ -34,7 +35,7 @@ export default function Robot() {
*/
const sendMessage = async () => {
try {
const response = await fetch("http://localhost:8000/message", {
const response = await fetch(`${API_BASE_URL}/message`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -60,7 +61,7 @@ export default function Robot() {
* The connection resets whenever `conversationIndex` changes.
*/
useEffect(() => {
const eventSource = new EventSource("http://localhost:8000/sse");
const eventSource = new EventSource(`${API_BASE_URL}/sse`);
eventSource.onmessage = (event) => {
try {

View File

@@ -1,8 +1,8 @@
{/*
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/}
*/
/* editor UI */
.inner-editor-container {

View File

@@ -6,6 +6,7 @@ import orderPhaseNodeArray from "../../utils/orderPhaseNodes";
import useFlowStore from './visualProgrammingUI/VisProgStores';
import { NodeReduces } from './visualProgrammingUI/NodeRegistry';
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
@@ -28,7 +29,7 @@ export function runProgram() {
const program = {phases}
console.log(JSON.stringify(program, null, 2));
fetch(
"http://localhost:8000/program",
`${API_BASE_URL}/program`,
{
method: "POST",
headers: {"Content-Type": "application/json"},
@@ -43,4 +44,4 @@ export function runProgram() {
useProgramStore.getState().setProgramState(structuredClone(program));
}).catch(() => console.log("Failed to send program to the backend."));
console.log(program);
}
}

View File

@@ -1,8 +1,8 @@
{/*
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/}
*/
.gestureEditor {
display: flex;
flex-direction: column;

View File

@@ -1,8 +1,8 @@
{/*
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/}
*/
.planDialog {
overflow:visible;
width: 80vw;

View File

@@ -1,8 +1,8 @@
{/*
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/}
*/
:global(.react-flow__handle.source){
border-radius: 100%;
}

View File

@@ -1,8 +1,8 @@
{/*
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/}
*/
.save-load-panel {
border-radius: 0 0 5pt 5pt;
background-color: canvas;

View File

@@ -1,8 +1,8 @@
{/*
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/}
*/
.warnings-sidebar {
min-width: auto;
max-width: 340px;

View File

@@ -1,8 +1,8 @@
{/*
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/}
*/
.operator-switch {
display: inline-flex;
align-items: center;

View File

@@ -1,6 +1,8 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { useEffect } from "react";
import type { EditorWarning } from "../components/EditorWarnings.tsx";
import {
type NodeProps,
Position,
@@ -39,7 +41,7 @@ export type NormNode = Node<NormNodeData>
*/
export default function NormNode(props: NodeProps<NormNode>) {
const data = props.data;
const {updateNodeData} = useFlowStore();
const {updateNodeData, registerWarning, unregisterWarning} = useFlowStore();
const text_input_id = `norm_${props.id}_text_input`;
const checkbox_id = `goal_${props.id}_checkbox`;
@@ -47,10 +49,28 @@ export default function NormNode(props: NodeProps<NormNode>) {
const setValue = (value: string) => {
updateNodeData(props.id, {norm: value});
}
//this function is commented out, because of lack of backend implementation.
//If you wish to set critical norms, in the UI side, you can uncomment and use this function.
// const setCritical = (value: boolean) => {
// updateNodeData(props.id, {...data, critical: value});
// }
useEffect(() => {
const normText = data.norm || "";
const startsWithNumberWarning: EditorWarning = {
scope: { id: props.id },
type: 'ELEMENT_STARTS_WITH_NUMBER',
severity: 'ERROR',
description: "Norms are not allowed to start with a number."
};
const setCritical = (value: boolean) => {
updateNodeData(props.id, {...data, critical: value});
}
if (/^\d/.test(normText)) {
registerWarning(startsWithNumberWarning);
} else {
unregisterWarning(props.id, 'ELEMENT_STARTS_WITH_NUMBER');
}
}, [data.norm, props.id, registerWarning, unregisterWarning]);
return <>
<Toolbar nodeId={props.id} allowDelete={true}/>
@@ -64,7 +84,10 @@ export default function NormNode(props: NodeProps<NormNode>) {
placeholder={"Pepper should ..."}
/>
</div>
<div className={"flex-row gap-md align-center"}>
{/*There is no backend implementation yet of how critical norms would
be treated differently than normal norms. The commented code below shows
how you could add the UI side, if you wish to implement */}
{/* <div className={"flex-row gap-md align-center"}>
<label htmlFor={checkbox_id}>Critical:</label>
<input
id={checkbox_id}
@@ -72,7 +95,7 @@ export default function NormNode(props: NodeProps<NormNode>) {
checked={data.critical || false}
onChange={(e) => setCritical(e.target.checked)}
/>
</div>
</div> */}
{data.condition && (<div className={"flex-row gap-md align-center"} data-testid="norm-condition-information">

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

View File

@@ -10,6 +10,7 @@ import {
useExperimentLogger,
useStatusLogger
} from '../../../src/pages/MonitoringPage/MonitoringPageAPI';
import { API_BASE_URL } from '../../../src/config/api.ts';
// --- MOCK EVENT SOURCE SETUP ---
// 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');
expect(globalThis.fetch).toHaveBeenCalledWith(
'http://localhost:8000/button_pressed',
`${API_BASE_URL}/button_pressed`,
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -220,4 +221,4 @@ describe('MonitoringPageAPI', () => {
expect(consoleSpy).toHaveBeenCalledWith('Status stream error:', expect.any(Error));
});
});
});
});

View File

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

View File

@@ -699,15 +699,12 @@ describe('NormNode', () => {
/>
);
const checkbox = screen.getByLabelText('Critical:');
await user.click(checkbox);
await waitFor(() => {
const state = useFlowStore.getState();
expect(state.nodes).toHaveLength(1);
expect(state.nodes[0].id).toBe('norm-1');
expect(state.nodes[0].data.norm).toBe('');
expect(state.nodes[0].data.critical).toBe(true);
});
});

View File

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