Compare commits
20 Commits
dev
...
4dcbe78abf
| Author | SHA1 | Date | |
|---|---|---|---|
| 4dcbe78abf | |||
|
|
f626a6571a | ||
| 268199a825 | |||
|
|
84bb8c5ae8 | ||
|
|
d3501cb063 | ||
|
|
00d605164c | ||
| 7deecaa160 | |||
|
|
72993b7576 | ||
|
|
4fa3946a10 | ||
| 07ad746c9d | |||
|
|
55fa4f3a8b | ||
| d919eb8471 | |||
| c0e8331fbf | |||
|
|
d514c2ef50 | ||
|
|
179b8fd75b | ||
|
|
4395e44dbf | ||
|
|
e3abf8c14a | ||
|
|
1b9dddcbf2 | ||
| 378a64c7ca | |||
| cca98dbebe |
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# The location of the backend
|
||||||
|
VITE_API_BASE_URL=http://localhost:8000
|
||||||
@@ -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:
|
||||||
|
|||||||
BIN
public/DeveloperManual.pdf
Normal file
BIN
public/DeveloperManual.pdf
Normal file
Binary file not shown.
BIN
public/UserManual.pdf
Normal file
BIN
public/UserManual.pdf
Normal file
Binary file not shown.
@@ -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)
|
||||||
*/}
|
*/
|
||||||
.logopepper {
|
.logopepper {
|
||||||
height: 8em;
|
height: 8em;
|
||||||
padding: 1.5em;
|
padding: 1.5em;
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
import { Routes, Route, Link } from 'react-router'
|
import { Routes, Route, Link } from 'react-router'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import Home from './pages/Home/Home.tsx'
|
import Home from './pages/Home/Home.tsx'
|
||||||
import Robot from './pages/Robot/Robot.tsx';
|
import UserManual from './pages/Manuals/Manuals.tsx';
|
||||||
import ConnectedRobots from './pages/ConnectedRobots/ConnectedRobots.tsx'
|
|
||||||
import VisProg from "./pages/VisProgPage/VisProg.tsx";
|
import VisProg from "./pages/VisProgPage/VisProg.tsx";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import Logging from "./components/Logging/Logging.tsx";
|
import Logging from "./components/Logging/Logging.tsx";
|
||||||
@@ -26,8 +25,7 @@ function App(){
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="/editor" element={<VisProg />} />
|
<Route path="/editor" element={<VisProg />} />
|
||||||
<Route path="/robot" element={<Robot />} />
|
<Route path="/user_manual" element={<UserManual />} />
|
||||||
<Route path="/ConnectedRobots" element={<ConnectedRobots />} />
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
{showLogs && <Logging />}
|
{showLogs && <Logging />}
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// 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)
|
|
||||||
export default function Next({ fill }: { fill?: string }) {
|
export default function Next({ fill }: { fill?: string }) {
|
||||||
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
|
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
|
||||||
<path d="M664.07-224.93v-510.14h91v510.14h-91Zm-459.14 0v-510.14L587.65-480 204.93-224.93Z"/>
|
<path d="M664.07-224.93v-510.14h91v510.14h-91Zm-459.14 0v-510.14L587.65-480 204.93-224.93Z"/>
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// 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)
|
|
||||||
export default function Pause({ fill }: { fill?: string }) {
|
export default function Pause({ fill }: { fill?: string }) {
|
||||||
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
|
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
|
||||||
<path d="M556.17-185.41v-589.18h182v589.18h-182Zm-334.34 0v-589.18h182v589.18h-182Z"/>
|
<path d="M556.17-185.41v-589.18h182v589.18h-182Zm-334.34 0v-589.18h182v589.18h-182Z"/>
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// 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)
|
|
||||||
export default function Play({ fill }: { fill?: string }) {
|
export default function Play({ fill }: { fill?: string }) {
|
||||||
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
|
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
|
||||||
<path d="M311.87-185.41v-589.18L775.07-480l-463.2 294.59Z"/>
|
<path d="M311.87-185.41v-589.18L775.07-480l-463.2 294.59Z"/>
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// 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)
|
|
||||||
export default function Redo({ fill }: { fill?: string }) {
|
export default function Redo({ fill }: { fill?: string }) {
|
||||||
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
|
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
|
||||||
<path d="M390.98-191.87q-98.44 0-168.77-65.27-70.34-65.27-70.34-161.43 0-96.15 70.34-161.54 70.33-65.39 168.77-65.39h244.11l-98.98-98.98 63.65-63.65L808.13-600 599.76-391.87l-63.65-63.65 98.98-98.98H390.98q-60.13 0-104.12 38.92-43.99 38.93-43.99 96.78 0 57.84 43.99 96.89 43.99 39.04 104.12 39.04h286.15v91H390.98Z"/>
|
<path d="M390.98-191.87q-98.44 0-168.77-65.27-70.34-65.27-70.34-161.43 0-96.15 70.34-161.54 70.33-65.39 168.77-65.39h244.11l-98.98-98.98 63.65-63.65L808.13-600 599.76-391.87l-63.65-63.65 98.98-98.98H390.98q-60.13 0-104.12 38.92-43.99 38.93-43.99 96.78 0 57.84 43.99 96.89 43.99 39.04 104.12 39.04h286.15v91H390.98Z"/>
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// 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)
|
|
||||||
export default function Replay({ fill }: { fill?: string }) {
|
export default function Replay({ fill }: { fill?: string }) {
|
||||||
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
|
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
|
||||||
<path d="M480.05-70.43q-76.72 0-143.78-29.1-67.05-29.1-116.75-78.8-49.69-49.69-78.79-116.75-29.1-67.05-29.1-143.49h91q0 115.81 80.73 196.47Q364.1-161.43 480-161.43q115.8 0 196.47-80.74 80.66-80.73 80.66-196.63 0-115.81-80.73-196.47-80.74-80.66-196.64-80.66h-6.24l60.09 60.08-58.63 60.63-166.22-166.21 166.22-166.22 58.63 60.87-59.85 59.85h6q76.74 0 143.76 29.09 67.02 29.1 116.84 78.8 49.81 49.69 78.91 116.64 29.1 66.95 29.1 143.61 0 76.66-29.1 143.71-29.1 67.06-78.79 116.75-49.7 49.7-116.7 78.8-67.01 29.1-143.73 29.1Z"/>
|
<path d="M480.05-70.43q-76.72 0-143.78-29.1-67.05-29.1-116.75-78.8-49.69-49.69-78.79-116.75-29.1-67.05-29.1-143.49h91q0 115.81 80.73 196.47Q364.1-161.43 480-161.43q115.8 0 196.47-80.74 80.66-80.73 80.66-196.63 0-115.81-80.73-196.47-80.74-80.66-196.64-80.66h-6.24l60.09 60.08-58.63 60.63-166.22-166.21 166.22-166.22 58.63 60.87-59.85 59.85h6q76.74 0 143.76 29.09 67.02 29.1 116.84 78.8 49.81 49.69 78.91 116.64 29.1 66.95 29.1 143.61 0 76.66-29.1 143.71-29.1 67.06-78.79 116.75-49.7 49.7-116.7 78.8-67.01 29.1-143.73 29.1Z"/>
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// 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 type {Cell} from "../../utils/cellStore.ts";
|
import type {Cell} from "../../utils/cellStore.ts";
|
||||||
import type {LogRecord} from "./useLogs.ts";
|
import type {LogRecord} from "./useLogs.ts";
|
||||||
|
|
||||||
|
|||||||
@@ -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
11
src/config/api.ts
Normal 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;
|
||||||
@@ -1,14 +1,9 @@
|
|||||||
{/*
|
|
||||||
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)
|
|
||||||
*/}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
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)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
// 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, useState } from 'react'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays the current connection status of a robot in real time.
|
|
||||||
*
|
|
||||||
* Opens an SSE connection to the backend (`/robot/ping_stream`) that emits
|
|
||||||
* simple boolean JSON messages (`true` or `false`). Updates automatically when
|
|
||||||
* the robot connects or disconnects.
|
|
||||||
*
|
|
||||||
* @returns A React element showing the current robot connection status.
|
|
||||||
*/
|
|
||||||
export default function ConnectedRobots() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current connection state:
|
|
||||||
* - `true`: Robot is connected.
|
|
||||||
* - `false`: Robot is not connected.
|
|
||||||
* - `null`: Connection status is unknown (initial check in progress).
|
|
||||||
*/
|
|
||||||
const [connected, setConnected] = useState<boolean | null>(null);
|
|
||||||
|
|
||||||
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");
|
|
||||||
eventSource.onmessage = (event) => {
|
|
||||||
|
|
||||||
// Expecting messages in JSON format: `true` or `false`
|
|
||||||
console.log("received message:", event.data);
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
|
|
||||||
try {
|
|
||||||
setConnected(data)
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
console.log("couldnt extract connected from incoming ping data")
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
console.log("Ping message not in correct format:", event.data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Clean up the SSE connection when the component unmounts.
|
|
||||||
return () => eventSource.close();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Is robot currently connected?</h1>
|
|
||||||
<div>
|
|
||||||
<h2>Robot is currently: {connected == null ? "checking..." : (connected ? "connected! 🟢" : "not connected... 🔴")} </h2>
|
|
||||||
<h3>
|
|
||||||
{connected == null ? "If checking continues, make sure CB is properly loaded with robot at least once." : ""}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -27,3 +27,51 @@ University within the Software Project course.
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.links {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row; /* Horizontal layout looks more like a dashboard */
|
||||||
|
gap: 1.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navCard {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
min-width: 180px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #333;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover effects */
|
||||||
|
.navCard:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
border-color: #ffcd00; /* UU Yellow accent */
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specific styling for the logo container */
|
||||||
|
.logoPepperScaling {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoPepperScaling:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logopepper {
|
||||||
|
height: 120px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
@@ -16,15 +16,20 @@ import styles from './Home.module.css'
|
|||||||
function Home() {
|
function Home() {
|
||||||
return (
|
return (
|
||||||
<div className={`flex-col ${styles.gapXl}`}>
|
<div className={`flex-col ${styles.gapXl}`}>
|
||||||
<div className="logoPepperScaling">
|
<div className={styles.logoPepperScaling}>
|
||||||
<a href="https://git.science.uu.nl/ics/sp/2025/n25b" target="_blank">
|
<a href="https://git.science.uu.nl/ics/sp/2025/n25b" target="_blank" rel="noreferrer">
|
||||||
<img src={pepperLogo} className="logopepper" alt="Pepper logo" />
|
<img src={pepperLogo} className={styles.logopepper} alt="Pepper logo" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.links}>
|
<div className={styles.links}>
|
||||||
<Link to={"/robot"}>Robot Interaction →</Link>
|
{/* Program Editor is now first */}
|
||||||
<Link to={"/editor"}>Editor →</Link>
|
<Link to="/editor" className={styles.navCard}>
|
||||||
<Link to={"/ConnectedRobots"}>Connected Robots →</Link>
|
Program Editor
|
||||||
|
</Link>
|
||||||
|
<Link to="/user_manual" className={styles.navCard}>
|
||||||
|
User and Developer Manual
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
81
src/pages/Manuals/Manuals.module.css
Normal file
81
src/pages/Manuals/Manuals.module.css
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/* 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)
|
||||||
|
*/
|
||||||
|
|
||||||
|
.manualContainer {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 4rem auto;
|
||||||
|
padding: 2rem;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manualHeader h1 {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonStack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column; /* Stacks the manual sections vertically */
|
||||||
|
gap: 3rem;
|
||||||
|
margin-top: 3rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manualEntry {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manualEntry h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manualEntry p {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadBtn {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #ffffff; /* White background as requested */
|
||||||
|
color: #000;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
border-radius: 50px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
width: 280px; /* Fixed width for uniform appearance */
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||||
|
transition: transform 0.2s ease, background-color 0.2s ease, color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadBtn:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
|
||||||
|
background-color: #247284; /* Teal hover as requested */
|
||||||
|
color: #ffffff; /* Text turns white on teal for better contrast */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateBadge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0.4rem 1rem;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
margin-top: 4rem;
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
39
src/pages/Manuals/Manuals.tsx
Normal file
39
src/pages/Manuals/Manuals.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// 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 './Manuals.module.css';
|
||||||
|
|
||||||
|
export default function Manuals() {
|
||||||
|
const userManualPath = "/UserManual.pdf";
|
||||||
|
const developerManualPath = "/DeveloperManual.pdf";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.manualContainer}>
|
||||||
|
<header className={styles.manualHeader}>
|
||||||
|
<h1>Documentation & Manuals</h1>
|
||||||
|
|
||||||
|
<span className={styles.dateBadge}>Last Updated: January 2026</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className={styles.buttonStack}>
|
||||||
|
<div className={styles.manualEntry}>
|
||||||
|
<h3>User Manual</h3>
|
||||||
|
<p>Manual for Users of the Pepper+ Software </p>
|
||||||
|
<a href={userManualPath} download="UserManual.pdf" className={styles.downloadBtn}>
|
||||||
|
Download User Manual
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.manualEntry}>
|
||||||
|
<h3>Developer Manual</h3>
|
||||||
|
<p>Technical documentation for future developers.</p>
|
||||||
|
<a href={developerManualPath} download="DeveloperManual.pdf" className={styles.downloadBtn}>
|
||||||
|
Download Developer Manual
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className={styles.divider} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
// 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 { useState, useEffect, useRef } from 'react'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays a live robot interaction panel with user input, conversation history,
|
|
||||||
* and real-time updates from the robot backend via Server-Sent Events (SSE).
|
|
||||||
*
|
|
||||||
* @returns A React element rendering the interactive robot UI.
|
|
||||||
*/
|
|
||||||
export default function Robot() {
|
|
||||||
/** The text message currently entered by the user. */
|
|
||||||
const [message, setMessage] = useState('');
|
|
||||||
|
|
||||||
/** Whether the robot’s microphone or listening mode is currently active. */
|
|
||||||
const [listening, setListening] = useState(false);
|
|
||||||
/** The ongoing conversation history as a sequence of user/assistant messages. */
|
|
||||||
const [conversation, setConversation] = useState<
|
|
||||||
{"role": "user" | "assistant", "content": string}[]>([])
|
|
||||||
/** Reference to the scrollable conversation container for auto-scrolling. */
|
|
||||||
const conversationRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
/**
|
|
||||||
* Index used to force refresh the SSE connection or clear conversation.
|
|
||||||
* Incrementing this value triggers a reset of the live data stream.
|
|
||||||
*/
|
|
||||||
const [conversationIndex, setConversationIndex] = useState(0);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a message to the robot backend.
|
|
||||||
*
|
|
||||||
* Makes a POST request to `/message` with the user’s text.
|
|
||||||
* The backend may respond with confirmation or error information.
|
|
||||||
*/
|
|
||||||
const sendMessage = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch("http://localhost:8000/message", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ message }),
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
console.log(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error sending message: ", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Establishes a persistent Server-Sent Events (SSE) connection
|
|
||||||
* to receive real-time updates from the robot backend.
|
|
||||||
*
|
|
||||||
* Handles three event types:
|
|
||||||
* - `voice_active`: whether the robot is currently listening.
|
|
||||||
* - `speech`: recognized user speech input.
|
|
||||||
* - `llm_response`: the robot’s language model-generated reply.
|
|
||||||
*
|
|
||||||
* The connection resets whenever `conversationIndex` changes.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
const eventSource = new EventSource("http://localhost:8000/sse");
|
|
||||||
|
|
||||||
eventSource.onmessage = (event) => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
if ("voice_active" in data) setListening(data.voice_active);
|
|
||||||
if ("speech" in data) setConversation(conversation => [...conversation, {"role": "user", "content": data.speech}]);
|
|
||||||
if ("llm_response" in data) setConversation(conversation => [...conversation, {"role": "assistant", "content": data.llm_response}]);
|
|
||||||
} catch {
|
|
||||||
console.log("Unparsable SSE message:", event.data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
eventSource.close();
|
|
||||||
};
|
|
||||||
}, [conversationIndex]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Automatically scrolls the conversation view to the bottom
|
|
||||||
* whenever a new message is added.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (!conversationRef || !conversationRef.current) return;
|
|
||||||
conversationRef.current.scrollTop = conversationRef.current.scrollHeight;
|
|
||||||
}, [conversation]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1>Robot interaction</h1>
|
|
||||||
<h2>Force robot speech</h2>
|
|
||||||
<div className={"flex-row gap-md justify-center"}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={message}
|
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
|
||||||
onKeyDown={(e) => e.key === "Enter" && sendMessage().then(() => setMessage(""))}
|
|
||||||
placeholder="Enter a message"
|
|
||||||
/>
|
|
||||||
<button onClick={sendMessage}>Speak</button>
|
|
||||||
</div>
|
|
||||||
<div className={"flex-col gap-lg align-center"}>
|
|
||||||
<h2>Conversation</h2>
|
|
||||||
<p>Listening {listening ? "🟢" : "🔴"}</p>
|
|
||||||
<div style={{ maxHeight: "200px", maxWidth: "600px", overflowY: "auto"}} ref={conversationRef}>
|
|
||||||
{conversation.map((item, i) => (
|
|
||||||
<p key={i}
|
|
||||||
style={{
|
|
||||||
backgroundColor: item["role"] == "user"
|
|
||||||
? "color-mix(in oklab, canvas, blue 20%)"
|
|
||||||
: "color-mix(in oklab, canvas, gray 20%)",
|
|
||||||
whiteSpace: "pre-line",
|
|
||||||
}}
|
|
||||||
className={"round-md padding-md"}
|
|
||||||
>{item["content"]}</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className={"flex-row gap-md justify-center"}>
|
|
||||||
<button onClick={() => {
|
|
||||||
setConversationIndex((conversationIndex) => conversationIndex + 1)
|
|
||||||
setConversation([])
|
|
||||||
}}>Reset</button>
|
|
||||||
<button onClick={() => {
|
|
||||||
setConversationIndex((conversationIndex) => conversationIndex == -1 ? 0 : -1)
|
|
||||||
setConversation([])
|
|
||||||
}}>{conversationIndex == -1 ? "Start" : "Stop"}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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"},
|
||||||
|
|||||||
@@ -90,9 +90,20 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
|
|||||||
*/
|
*/
|
||||||
onNodesChange: (changes) => set({nodes: applyNodeChanges(changes, get().nodes)}),
|
onNodesChange: (changes) => set({nodes: applyNodeChanges(changes, get().nodes)}),
|
||||||
|
|
||||||
onNodesDelete: (nodes) => nodes.forEach((_node) => {
|
onNodesDelete: (deletedNodes) => {
|
||||||
return;
|
|
||||||
}),
|
const allNodes = get().nodes;
|
||||||
|
const deletedIds = new Set(deletedNodes.map(n => n.id));
|
||||||
|
|
||||||
|
deletedNodes.forEach((node) => {
|
||||||
|
get().unregisterNodeRules(node.id);
|
||||||
|
get().unregisterWarningsForId(node.id);
|
||||||
|
});
|
||||||
|
const remainingNodes = allNodes.filter((node) => !deletedIds.has(node.id));
|
||||||
|
|
||||||
|
// Validate only the survivors
|
||||||
|
get().validateDuplicateNames(remainingNodes);
|
||||||
|
},
|
||||||
|
|
||||||
onEdgesDelete: (edges) => {
|
onEdgesDelete: (edges) => {
|
||||||
// we make sure any affected nodes get updated to reflect removal of edges
|
// we make sure any affected nodes get updated to reflect removal of edges
|
||||||
@@ -240,10 +251,14 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
|
|||||||
).then(() => {
|
).then(() => {
|
||||||
get().unregisterNodeRules(nodeId);
|
get().unregisterNodeRules(nodeId);
|
||||||
get().unregisterWarningsForId(nodeId);
|
get().unregisterWarningsForId(nodeId);
|
||||||
|
// Re-validate after deletion is finished
|
||||||
|
get().validateDuplicateNames(get().nodes);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
const remainingNodes = get().nodes.filter((n) => n.id !== nodeId);
|
||||||
|
get().validateDuplicateNames(remainingNodes); // Re-validate survivors
|
||||||
set({
|
set({
|
||||||
nodes: get().nodes.filter((n) => n.id !== nodeId),
|
nodes: remainingNodes,
|
||||||
edges: get().edges.filter((e) => e.source !== nodeId && e.target !== nodeId),
|
edges: get().edges.filter((e) => e.source !== nodeId && e.target !== nodeId),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -265,16 +280,50 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
|
|||||||
*/
|
*/
|
||||||
updateNodeData: (nodeId, data) => {
|
updateNodeData: (nodeId, data) => {
|
||||||
get().pushSnapshot();
|
get().pushSnapshot();
|
||||||
set({
|
const updatedNodes = get().nodes.map((node) => {
|
||||||
nodes: get().nodes.map((node) => {
|
|
||||||
if (node.id === nodeId) {
|
if (node.id === nodeId) {
|
||||||
node = { ...node, data: { ...node.data, ...data }};
|
return { ...node, data: { ...node.data, ...data } };
|
||||||
}
|
}
|
||||||
return node;
|
return node;
|
||||||
}),
|
});
|
||||||
|
|
||||||
|
get().validateDuplicateNames(updatedNodes); // Re-validate after update
|
||||||
|
set({ nodes: updatedNodes });
|
||||||
|
},
|
||||||
|
|
||||||
|
//helper function to see if any of the nodes have duplicate names
|
||||||
|
validateDuplicateNames: (nodes: Node[]) => {
|
||||||
|
const nameMap = new Map<string, string[]>();
|
||||||
|
|
||||||
|
// 1. Group IDs by their identifier (name, norm, or label)
|
||||||
|
nodes.forEach((n) => {
|
||||||
|
const name = (n.data.name || n.data.norm )?.toString().trim();
|
||||||
|
if (name) {
|
||||||
|
if (!nameMap.has(name)) nameMap.set(name, []);
|
||||||
|
nameMap.get(name)!.push(n.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Scan nodes and toggle the warning
|
||||||
|
nodes.forEach((n) => {
|
||||||
|
const name = (n.data.name || n.data.norm )?.toString().trim();
|
||||||
|
const isDuplicate = name ? (nameMap.get(name)?.length || 0) > 1 : false;
|
||||||
|
|
||||||
|
if (isDuplicate) {
|
||||||
|
get().registerWarning({
|
||||||
|
scope: { id: n.id },
|
||||||
|
type: 'DUPLICATE_ELEMENT_NAME',
|
||||||
|
severity: 'ERROR',
|
||||||
|
description: `The name "${name}" is already used by another element.`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// This clears the warning if the "twin" was deleted or renamed
|
||||||
|
get().unregisterWarning(n.id, 'DUPLICATE_ELEMENT_NAME');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new node to the flow store.
|
* Adds a new node to the flow store.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -96,6 +96,11 @@ export type FlowState = {
|
|||||||
*/
|
*/
|
||||||
updateNodeData: (nodeId: string, data: object) => void;
|
updateNodeData: (nodeId: string, data: object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that all node names are unique across the workspace.
|
||||||
|
*/
|
||||||
|
validateDuplicateNames: (nodes: Node[]) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new node to the flow.
|
* Adds a new node to the flow.
|
||||||
* @param node - the Node object to add
|
* @param node - the Node object to add
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export type WarningType =
|
|||||||
| 'PLAN_IS_UNDEFINED'
|
| 'PLAN_IS_UNDEFINED'
|
||||||
| 'INCOMPLETE_PROGRAM'
|
| 'INCOMPLETE_PROGRAM'
|
||||||
| 'NOT_CONNECTED_TO_PROGRAM'
|
| 'NOT_CONNECTED_TO_PROGRAM'
|
||||||
|
| 'ELEMENT_STARTS_WITH_NUMBER' //(non-phase)elements are not allowed to be or start with a number
|
||||||
|
| 'DUPLICATE_ELEMENT_NAME' // elements are not allowed to have the same name as another element
|
||||||
| string
|
| string
|
||||||
|
|
||||||
export type WarningSeverity =
|
export type WarningSeverity =
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export default function GoalNode({id, data}: NodeProps<GoalNode>) {
|
|||||||
updateNodeData(id, {...data, can_fail: value});
|
updateNodeData(id, {...data, can_fail: value});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//undefined plan warning
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const noPlanWarning : EditorWarning = {
|
const noPlanWarning : EditorWarning = {
|
||||||
scope: {
|
scope: {
|
||||||
@@ -81,12 +81,31 @@ export default function GoalNode({id, data}: NodeProps<GoalNode>) {
|
|||||||
description: "This goalNode is missing a plan, please make sure to create a plan by using the create plan button"
|
description: "This goalNode is missing a plan, please make sure to create a plan by using the create plan button"
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!data.plan){
|
if (!data.plan || data.plan.steps?.length === 0){
|
||||||
registerWarning(noPlanWarning);
|
registerWarning(noPlanWarning);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
unregisterWarning(id, noPlanWarning.type);
|
unregisterWarning(id, noPlanWarning.type);
|
||||||
},[data.plan, id, registerWarning, unregisterWarning])
|
},[data.plan, id, registerWarning, unregisterWarning])
|
||||||
|
|
||||||
|
//starts with number warning
|
||||||
|
useEffect(() => {
|
||||||
|
const name = data.name || "";
|
||||||
|
|
||||||
|
const startsWithNumberWarning: EditorWarning = {
|
||||||
|
scope: { id: id },
|
||||||
|
type: 'ELEMENT_STARTS_WITH_NUMBER',
|
||||||
|
severity: 'ERROR',
|
||||||
|
description: "Norms are not allowed to start with a number."
|
||||||
|
};
|
||||||
|
|
||||||
|
if (/^\d/.test(name)) {
|
||||||
|
registerWarning(startsWithNumberWarning);
|
||||||
|
} else {
|
||||||
|
unregisterWarning(id, 'ELEMENT_STARTS_WITH_NUMBER');
|
||||||
|
}
|
||||||
|
}, [data.name, id, registerWarning, unregisterWarning]);
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Toolbar nodeId={id} allowDelete={true}/>
|
<Toolbar nodeId={id} allowDelete={true}/>
|
||||||
<div className={`${styles.defaultNode} ${styles.nodeGoal} flex-col gap-sm`}>
|
<div className={`${styles.defaultNode} ${styles.nodeGoal} flex-col gap-sm`}>
|
||||||
|
|||||||
@@ -1,6 +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)
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import type { EditorWarning } from "../components/EditorWarnings.tsx";
|
||||||
import {
|
import {
|
||||||
type NodeProps,
|
type NodeProps,
|
||||||
Position,
|
Position,
|
||||||
@@ -39,7 +41,7 @@ export type NormNode = Node<NormNodeData>
|
|||||||
*/
|
*/
|
||||||
export default function NormNode(props: NodeProps<NormNode>) {
|
export default function NormNode(props: NodeProps<NormNode>) {
|
||||||
const data = props.data;
|
const data = props.data;
|
||||||
const {updateNodeData} = useFlowStore();
|
const {updateNodeData, registerWarning, unregisterWarning} = useFlowStore();
|
||||||
|
|
||||||
const text_input_id = `norm_${props.id}_text_input`;
|
const text_input_id = `norm_${props.id}_text_input`;
|
||||||
const checkbox_id = `goal_${props.id}_checkbox`;
|
const checkbox_id = `goal_${props.id}_checkbox`;
|
||||||
@@ -47,10 +49,44 @@ export default function NormNode(props: NodeProps<NormNode>) {
|
|||||||
const setValue = (value: string) => {
|
const setValue = (value: string) => {
|
||||||
updateNodeData(props.id, {norm: value});
|
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) => {
|
// const setCritical = (value: boolean) => {
|
||||||
updateNodeData(props.id, {...data, critical: value});
|
// 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."
|
||||||
|
};
|
||||||
|
|
||||||
|
if (/^\d/.test(normText)) {
|
||||||
|
registerWarning(startsWithNumberWarning);
|
||||||
|
} else {
|
||||||
|
unregisterWarning(props.id, 'ELEMENT_STARTS_WITH_NUMBER');
|
||||||
}
|
}
|
||||||
|
}, [data.norm, props.id, registerWarning, unregisterWarning]);
|
||||||
|
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."
|
||||||
|
};
|
||||||
|
|
||||||
|
if (/^\d/.test(normText)) {
|
||||||
|
registerWarning(startsWithNumberWarning);
|
||||||
|
} else {
|
||||||
|
unregisterWarning(props.id, 'ELEMENT_STARTS_WITH_NUMBER');
|
||||||
|
}
|
||||||
|
}, [data.norm, props.id, registerWarning, unregisterWarning]);
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Toolbar nodeId={props.id} allowDelete={true}/>
|
<Toolbar nodeId={props.id} allowDelete={true}/>
|
||||||
@@ -64,7 +100,10 @@ export default function NormNode(props: NodeProps<NormNode>) {
|
|||||||
placeholder={"Pepper should ..."}
|
placeholder={"Pepper should ..."}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
<label htmlFor={checkbox_id}>Critical:</label>
|
||||||
<input
|
<input
|
||||||
id={checkbox_id}
|
id={checkbox_id}
|
||||||
@@ -72,7 +111,7 @@ export default function NormNode(props: NodeProps<NormNode>) {
|
|||||||
checked={data.critical || false}
|
checked={data.critical || false}
|
||||||
onChange={(e) => setCritical(e.target.checked)}
|
onChange={(e) => setCritical(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
|
|
||||||
{data.condition && (<div className={"flex-row gap-md align-center"} data-testid="norm-condition-information">
|
{data.condition && (<div className={"flex-row gap-md align-center"} data-testid="norm-condition-information">
|
||||||
|
|||||||
@@ -115,12 +115,27 @@ export default function TriggerNode(props: NodeProps<TriggerNode>) {
|
|||||||
description: "This triggerNode is missing a plan, please make sure to create a plan by using the create plan button"
|
description: "This triggerNode is missing a plan, please make sure to create a plan by using the create plan button"
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!data.plan && outputCons.length !== 0){
|
if ((!data.plan || data.plan.steps?.length === 0) && outputCons.length !== 0){
|
||||||
registerWarning(noPlanWarning);
|
registerWarning(noPlanWarning);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
unregisterWarning(props.id, noPlanWarning.type);
|
unregisterWarning(props.id, noPlanWarning.type);
|
||||||
},[data.plan, outputCons.length, props.id, registerWarning, unregisterWarning])
|
},[data.plan, outputCons.length, props.id, registerWarning, unregisterWarning])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const name = data.name || "";
|
||||||
|
|
||||||
|
if (/^\d/.test(name)) {
|
||||||
|
registerWarning({
|
||||||
|
scope: { id: props.id },
|
||||||
|
type: 'ELEMENT_STARTS_WITH_NUMBER',
|
||||||
|
severity: 'ERROR',
|
||||||
|
description: "Trigger names are not allowed to start with a number."
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
unregisterWarning(props.id, 'ELEMENT_STARTS_WITH_NUMBER');
|
||||||
|
}
|
||||||
|
}, [data.name, props.id, registerWarning, unregisterWarning]);
|
||||||
return <>
|
return <>
|
||||||
|
|
||||||
<Toolbar nodeId={props.id} allowDelete={true}/>
|
<Toolbar nodeId={props.id} allowDelete={true}/>
|
||||||
|
|||||||
11
src/vite-env.d.ts
vendored
Normal file
11
src/vite-env.d.ts
vendored
Normal 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;
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
// 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 { render, screen, act, cleanup, waitFor } from '@testing-library/react';
|
|
||||||
import ConnectedRobots from '../../../src/pages/ConnectedRobots/ConnectedRobots';
|
|
||||||
|
|
||||||
// Mock event source
|
|
||||||
const mockInstances: MockEventSource[] = [];
|
|
||||||
class MockEventSource {
|
|
||||||
url: string;
|
|
||||||
onmessage: ((event: MessageEvent) => void) | null = null;
|
|
||||||
closed = false;
|
|
||||||
|
|
||||||
constructor(url: string) {
|
|
||||||
this.url = url;
|
|
||||||
mockInstances.push(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendMessage(data: string) {
|
|
||||||
// Trigger whatever the component listens to
|
|
||||||
this.onmessage?.({ data } as MessageEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.closed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// mock event source generation with fake function that returns our fake mock source
|
|
||||||
beforeAll(() => {
|
|
||||||
// Cast globalThis to a type exposing EventSource and assign a mocked constructor.
|
|
||||||
(globalThis as unknown as { EventSource?: typeof EventSource }).EventSource =
|
|
||||||
jest.fn((url: string) => new MockEventSource(url)) as unknown as typeof EventSource;
|
|
||||||
});
|
|
||||||
|
|
||||||
// clean after tests
|
|
||||||
afterEach(() => {
|
|
||||||
cleanup();
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
mockInstances.length = 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('ConnectedRobots', () => {
|
|
||||||
test('renders initial state correctly', () => {
|
|
||||||
render(<ConnectedRobots />);
|
|
||||||
|
|
||||||
// Check initial texts (before connection)
|
|
||||||
expect(screen.getByText('Is robot currently connected?')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(/Robot is currently:\s*checking/i)).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getByText(/If checking continues, make sure CB is properly loaded/i)
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('updates to connected when message data is true', async () => {
|
|
||||||
render(<ConnectedRobots />);
|
|
||||||
const eventSource = mockInstances[0];
|
|
||||||
expect(eventSource).toBeDefined();
|
|
||||||
|
|
||||||
// Check state after getting 'true' message
|
|
||||||
await act(async () => {
|
|
||||||
eventSource.sendMessage('true');
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText(/connected! 🟢/i)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('updates to not connected when message data is false', async () => {
|
|
||||||
render(<ConnectedRobots />);
|
|
||||||
const eventSource = mockInstances[0];
|
|
||||||
|
|
||||||
// Check statew after getting 'false' message
|
|
||||||
await act(async () => {
|
|
||||||
eventSource.sendMessage('false');
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText(/not connected.*🔴/i)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('handles invalid JSON gracefully', async () => {
|
|
||||||
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
||||||
render(<ConnectedRobots />);
|
|
||||||
const eventSource = mockInstances[0];
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
eventSource.sendMessage('not-json');
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(logSpy).toHaveBeenCalledWith(
|
|
||||||
'Ping message not in correct format:',
|
|
||||||
'not-json'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('closes EventSource on unmount', () => {
|
|
||||||
render(<ConnectedRobots />);
|
|
||||||
const eventSource = mockInstances[0];
|
|
||||||
const closeSpy = jest.spyOn(eventSource, 'close');
|
|
||||||
cleanup();
|
|
||||||
expect(closeSpy).toHaveBeenCalled();
|
|
||||||
expect(eventSource.closed).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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' },
|
||||||
|
|||||||
@@ -1,170 +0,0 @@
|
|||||||
// 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 { render, screen, act, cleanup, fireEvent } from '@testing-library/react';
|
|
||||||
import Robot from '../../../src/pages/Robot/Robot';
|
|
||||||
|
|
||||||
// Mock EventSource
|
|
||||||
const mockInstances: MockEventSource[] = [];
|
|
||||||
class MockEventSource {
|
|
||||||
url: string;
|
|
||||||
onmessage: ((event: MessageEvent) => void) | null = null;
|
|
||||||
closed = false;
|
|
||||||
|
|
||||||
constructor(url: string) {
|
|
||||||
this.url = url;
|
|
||||||
mockInstances.push(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendMessage(data: string) {
|
|
||||||
this.onmessage?.({ data } as MessageEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.closed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock global EventSource
|
|
||||||
beforeAll(() => {
|
|
||||||
(globalThis as any).EventSource = jest.fn((url: string) => new MockEventSource(url));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock fetch
|
|
||||||
beforeEach(() => {
|
|
||||||
globalThis.fetch = jest.fn(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
json: () => Promise.resolve({ reply: 'ok' }),
|
|
||||||
})
|
|
||||||
) as jest.Mock;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
afterEach(() => {
|
|
||||||
cleanup();
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
mockInstances.length = 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Robot', () => {
|
|
||||||
test('renders initial state', () => {
|
|
||||||
render(<Robot />);
|
|
||||||
expect(screen.getByText('Robot interaction')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Force robot speech')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Listening 🔴')).toBeInTheDocument();
|
|
||||||
expect(screen.getByPlaceholderText('Enter a message')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('sends message via button', async () => {
|
|
||||||
render(<Robot />);
|
|
||||||
const input = screen.getByPlaceholderText('Enter a message');
|
|
||||||
const button = screen.getByText('Speak');
|
|
||||||
|
|
||||||
fireEvent.change(input, { target: { value: 'Hello' } });
|
|
||||||
await act(async () => fireEvent.click(button));
|
|
||||||
|
|
||||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
||||||
'http://localhost:8000/message',
|
|
||||||
expect.objectContaining({
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ message: 'Hello' }),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('sends message via Enter key', async () => {
|
|
||||||
render(<Robot />);
|
|
||||||
const input = screen.getByPlaceholderText('Enter a message');
|
|
||||||
fireEvent.change(input, { target: { value: 'Hi Enter' } });
|
|
||||||
|
|
||||||
await act(async () =>
|
|
||||||
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter', charCode: 13 })
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
||||||
'http://localhost:8000/message',
|
|
||||||
expect.objectContaining({
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ message: 'Hi Enter' }),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect((input as HTMLInputElement).value).toBe('');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('handles fetch errors', async () => {
|
|
||||||
globalThis.fetch = jest.fn(() => Promise.reject('Network error')) as jest.Mock;
|
|
||||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
||||||
|
|
||||||
render(<Robot />);
|
|
||||||
const input = screen.getByPlaceholderText('Enter a message');
|
|
||||||
const button = screen.getByText('Speak');
|
|
||||||
fireEvent.change(input, { target: { value: 'Error test' } });
|
|
||||||
|
|
||||||
await act(async () => fireEvent.click(button));
|
|
||||||
|
|
||||||
expect(consoleSpy).toHaveBeenCalledWith(
|
|
||||||
'Error sending message: ',
|
|
||||||
'Network error'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('updates conversation on SSE', async () => {
|
|
||||||
render(<Robot />);
|
|
||||||
const eventSource = mockInstances[0];
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
eventSource.sendMessage(JSON.stringify({ voice_active: true }));
|
|
||||||
eventSource.sendMessage(JSON.stringify({ speech: 'User says hi' }));
|
|
||||||
eventSource.sendMessage(JSON.stringify({ llm_response: 'Assistant replies' }));
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByText('Listening 🟢')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('User says hi')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Assistant replies')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('handles invalid SSE JSON', async () => {
|
|
||||||
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
||||||
render(<Robot />);
|
|
||||||
const eventSource = mockInstances[0];
|
|
||||||
|
|
||||||
await act(async () => eventSource.sendMessage('bad-json'));
|
|
||||||
|
|
||||||
expect(logSpy).toHaveBeenCalledWith('Unparsable SSE message:', 'bad-json');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('resets conversation with Reset button', async () => {
|
|
||||||
render(<Robot />);
|
|
||||||
const eventSource = mockInstances[0];
|
|
||||||
|
|
||||||
await act(async () =>
|
|
||||||
eventSource.sendMessage(JSON.stringify({ speech: 'Hello' }))
|
|
||||||
);
|
|
||||||
expect(screen.getByText('Hello')).toBeInTheDocument();
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByText('Reset'));
|
|
||||||
expect(screen.queryByText('Hello')).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('toggles conversationIndex with Stop/Start button', () => {
|
|
||||||
render(<Robot />);
|
|
||||||
const stopButton = screen.getByText('Stop');
|
|
||||||
fireEvent.click(stopButton);
|
|
||||||
expect(screen.getByText('Start')).toBeInTheDocument();
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByText('Start'));
|
|
||||||
expect(screen.getByText('Stop')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('closes EventSource on unmount', () => {
|
|
||||||
const { unmount } = render(<Robot />);
|
|
||||||
const eventSource = mockInstances[0];
|
|
||||||
const closeSpy = jest.spyOn(eventSource, 'close');
|
|
||||||
|
|
||||||
unmount();
|
|
||||||
expect(closeSpy).toHaveBeenCalled();
|
|
||||||
expect(eventSource.closed).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -648,3 +648,46 @@ describe('FlowStore Functionality', () => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Extended Coverage Tests', () => {
|
||||||
|
|
||||||
|
|
||||||
|
test('calls deleteElements and performs async cleanup', async () => {
|
||||||
|
const { deleteNode } = useFlowStore.getState();
|
||||||
|
|
||||||
|
useFlowStore.setState({
|
||||||
|
nodes: [{ id: 'target-node', type: 'phase', data: { label: 'T' }, position: { x: 0, y: 0 } }],
|
||||||
|
edges: [{ id: 'edge-1', source: 'other', target: 'target-node' }]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock the deleteElements function required by the 'if' block
|
||||||
|
const deleteElementsMock = jest.fn().mockResolvedValue(true);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
deleteNode('target-node', deleteElementsMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deleteElementsMock).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
nodes: expect.arrayContaining([expect.objectContaining({ id: 'target-node' })]),
|
||||||
|
edges: expect.arrayContaining([expect.objectContaining({ id: 'edge-1' })])
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
test('triggers duplicate warning when two nodes share the same name', () => {
|
||||||
|
const { validateDuplicateNames } = useFlowStore.getState();
|
||||||
|
|
||||||
|
const collidingNodes: Node[] = [
|
||||||
|
{ id: 'node-1', type: 'phase', data: { name: 'Collision' }, position: { x: 0, y: 0 } },
|
||||||
|
{ id: 'node-2', type: 'phase', data: { name: ' Collision ' }, position: { x: 10, y: 10 } }
|
||||||
|
];
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
validateDuplicateNames(collidingNodes);
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = useFlowStore.getState();
|
||||||
|
// Assuming warnings are stored in a way accessible via get().warnings or similar from editorWarningRegistry
|
||||||
|
// Since validateDuplicateNames calls registerWarning:
|
||||||
|
expect(state.nodes).toBeDefined();
|
||||||
|
// You should check your 'warnings' state here to ensure DUPLICATE_ELEMENT_NAME exists
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -699,15 +699,12 @@ describe('NormNode', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const checkbox = screen.getByLabelText('Critical:');
|
|
||||||
await user.click(checkbox);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const state = useFlowStore.getState();
|
const state = useFlowStore.getState();
|
||||||
expect(state.nodes).toHaveLength(1);
|
expect(state.nodes).toHaveLength(1);
|
||||||
expect(state.nodes[0].id).toBe('norm-1');
|
expect(state.nodes[0].id).toBe('norm-1');
|
||||||
expect(state.nodes[0].data.norm).toBe('');
|
expect(state.nodes[0].data.norm).toBe('');
|
||||||
expect(state.nodes[0].data.critical).toBe(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user