From 10522b71c368301a6b8949def72214dfbb6b7b4b Mon Sep 17 00:00:00 2001 From: Twirre Meulenbelt <43213592+TwirreM@users.noreply.github.com> Date: Wed, 1 Oct 2025 22:56:03 +0200 Subject: [PATCH 01/14] chore: combined some branches, improved style This demo branch contains code from multiple different branches. DO NOT MERGE this branch because it looks like I'm the author of all these changes. --- package-lock.json | 241 +++++++++++- package.json | 2 + src/App.css | 109 ++++-- src/App.tsx | 20 +- src/assets/data.ts | 361 ++++++++++++++++++ src/pages/Home/Home.module.css | 6 + src/pages/Home/Home.tsx | 45 +-- src/pages/Logging/Logging.module.css | 17 + src/pages/Logging/Logging.tsx | 78 ++++ src/pages/ServerComms/ServerComms.css | 0 src/pages/ServerComms/ServerComms.tsx | 86 +++-- src/pages/VisProgPage/VisProg.tsx | 11 + src/visualProgrammingUI/VisProgUI.css | 7 + src/visualProgrammingUI/VisProgUI.tsx | 132 +++++++ .../components/DragDropSidebar.tsx | 141 +++++++ .../components/NodeDefinitions.tsx | 111 ++++++ 16 files changed, 1251 insertions(+), 116 deletions(-) create mode 100644 src/assets/data.ts create mode 100644 src/pages/Logging/Logging.module.css create mode 100644 src/pages/Logging/Logging.tsx delete mode 100644 src/pages/ServerComms/ServerComms.css create mode 100644 src/pages/VisProgPage/VisProg.tsx create mode 100644 src/visualProgrammingUI/VisProgUI.css create mode 100644 src/visualProgrammingUI/VisProgUI.tsx create mode 100644 src/visualProgrammingUI/components/DragDropSidebar.tsx create mode 100644 src/visualProgrammingUI/components/NodeDefinitions.tsx diff --git a/package-lock.json b/package-lock.json index 0b92f5c..db54d49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "pepperplus-ui", "version": "0.0.0", "dependencies": { + "@neodrag/react": "^2.3.1", + "@xyflow/react": "^12.8.6", "react": "^19.1.1", "react-dom": "^19.1.1", "react-router": "^7.9.3" @@ -1006,6 +1008,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@neodrag/react": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@neodrag/react/-/react-2.3.1.tgz", + "integrity": "sha512-mOVefo3mFmaVLs9PB5F5wMXnnclG81qjOaPHyf8YZUnw/Ciz0pAqyJDwDJk0nPTIK5I2x1JdjXSchGNdCxZNRQ==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1404,6 +1412,55 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1422,7 +1479,7 @@ "version": "19.1.13", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1730,6 +1787,38 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@xyflow/react": { + "version": "12.8.6", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.6.tgz", + "integrity": "sha512-SksAm2m4ySupjChphMmzvm55djtgMDPr+eovPDdTnyGvShf73cvydfoBfWDFllooIQ4IaiUL5yfxHRwU0c37EA==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.70", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.70.tgz", + "integrity": "sha512-PpC//u9zxdjj0tfTSmZrg3+sRbTz6kop/Amky44U2Dl51sxzDTIUfXMwETOYpmr2dqICWXBIJwXL2a9QWtX2XA==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1916,6 +2005,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1978,9 +2073,114 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -3287,6 +3487,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz", @@ -3438,6 +3647,34 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 6070bd8..252cf4f 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "preview": "vite preview" }, "dependencies": { + "@neodrag/react": "^2.3.1", + "@xyflow/react": "^12.8.6", "react": "^19.1.1", "react-dom": "^19.1.1", "react-router": "^7.9.3" diff --git a/src/App.css b/src/App.css index dcb46cf..0c641e7 100644 --- a/src/App.css +++ b/src/App.css @@ -5,18 +5,6 @@ text-align: center; } -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} .logopepper { @@ -32,27 +20,21 @@ filter: drop-shadow(0 0 10em #4eff14aa); } -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - - @keyframes logo-pepper-spin { - from { - transform: rotate(-20deg); + 0% { + transform: rotate(0); } - to { + 25% { transform: rotate(20deg); } + 75% { + transform: rotate(-20deg); + } + 100% { + transform: rotate(0); + } } - - @keyframes logo-pepper-scale { from { transform: scale(1,1); @@ -63,19 +45,13 @@ } @media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; + .logopepper:hover { + animation: logo-pepper-spin infinite 1s linear; } } @media (prefers-reduced-motion: no-preference) { - .logopepper { - animation: logo-pepper-spin infinite 1s linear alternate; - } -} - -@media (prefers-reduced-motion: no-preference) { - .logoPepperScaling { + .logoPepperScaling:hover { animation: logo-pepper-scale infinite 1s linear alternate; } } @@ -113,3 +89,64 @@ button.movePage.right{ button.movePage:hover{ background-color: rgb(0, 176, 176); } + + + + + +.flex-row { + display: flex; + flex-direction: row; +} +.flex-col { + display: flex; + flex-direction: column; +} + +.flex-1 { + flex: 1; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.align-center { + align-items: center; +} +.justify-center { + justify-content: center; +} +.justify-between { + justify-content: space-between; +} + +.gap-sm { + gap: .25rem; +} +.gap-md { + gap: .5rem; +} +.gap-lg { + gap: 1rem; +} + +.padding-sm { + padding: .25rem; +} +.padding-md { + padding: .5rem; +} +.padding-lg { + padding: 1rem; +} + +.round-sm { + border-radius: .25rem; +} +.round-md { + border-radius: .5rem; +} +.round-lg { + border-radius: 1rem; +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 757e769..0833bdb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,17 +1,25 @@ -import { Routes, Route } from 'react-router' +import { Routes, Route, Link } from 'react-router' import './App.css' import TemplatePage from './pages/TemplatePage/Template.tsx' import Home from './pages/Home/Home.tsx' import ServerComms from './pages/ServerComms/ServerComms.tsx' +import Logging from './pages/Logging/Logging.tsx' +import VisProg from "./pages/VisProgPage/VisProg.tsx"; function App(){ return ( - - } /> - } /> - } /> - +
+ {/* Should not use inline styles like this */} + Home + + } /> + } /> + } /> + } /> + } /> + +
) } diff --git a/src/assets/data.ts b/src/assets/data.ts new file mode 100644 index 0000000..c1eacfb --- /dev/null +++ b/src/assets/data.ts @@ -0,0 +1,361 @@ +export const DATA: LogEntry[] = [ + { + id: "1", + timestamp: "2025-10-01T12:00:00Z", + level: "info", + msg: "User said: Hello, Pepper!", + type: "speech", + }, + { + id: "2", + timestamp: "2025-10-01T12:00:05Z", + level: "debug", + msg: "Proximity sensor value: 0.85", + type: "sensor", + }, + { + id: "3", + timestamp: "2025-10-01T12:00:10Z", + level: "warn", + msg: "Battery level low: 15%", + type: "system", + }, + { + id: "4", + timestamp: "2025-10-01T12:00:15Z", + level: "info", + msg: "User requested weather update.", + type: "speech", + }, + { + id: "5", + timestamp: "2025-10-01T12:00:20Z", + level: "debug", + msg: "Microphone activated.", + type: "system", + }, + { + id: "6", + timestamp: "2025-10-01T12:00:25Z", + level: "warn", + msg: "Obstacle detected in front.", + type: "sensor", + }, + { + id: "7", + timestamp: "2025-10-01T12:00:30Z", + level: "info", + msg: "User said: Thank you!", + type: "speech", + }, + { + id: "8", + timestamp: "2025-10-01T12:00:35Z", + level: "debug", + msg: "Network latency: 120ms", + type: "system", + }, + { + id: "9", + timestamp: "2025-10-01T12:00:40Z", + level: "warn", + msg: "High CPU usage detected.", + type: "system", + }, + { + id: "10", + timestamp: "2025-10-01T12:00:45Z", + level: "info", + msg: "User started a new session.", + type: "system", + }, + { + id: "11", + timestamp: "2025-10-01T12:01:00Z", + level: "info", + msg: "User asked: What's the weather?", + type: "speech", + }, + { + id: "12", + timestamp: "2025-10-01T12:01:05Z", + level: "debug", + msg: "Camera initialized.", + type: "system", + }, + { + id: "13", + timestamp: "2025-10-01T12:01:10Z", + level: "warn", + msg: "Temperature sensor disconnected.", + type: "sensor", + }, + { + id: "14", + timestamp: "2025-10-01T12:01:15Z", + level: "info", + msg: "User said: Play some music.", + type: "speech", + }, + { + id: "15", + timestamp: "2025-10-01T12:01:20Z", + level: "debug", + msg: "Audio output device selected: Speaker.", + type: "system", + }, + { + id: "16", + timestamp: "2025-10-01T12:01:25Z", + level: "warn", + msg: "Low light detected in room.", + type: "sensor", + }, + { + id: "17", + timestamp: "2025-10-01T12:01:30Z", + level: "info", + msg: "User said: Turn on the lights.", + type: "speech", + }, + { + id: "18", + timestamp: "2025-10-01T12:01:35Z", + level: "debug", + msg: "Light control signal sent.", + type: "system", + }, + { + id: "19", + timestamp: "2025-10-01T12:01:40Z", + level: "warn", + msg: "Light bulb not responding.", + type: "system", + }, + { + id: "20", + timestamp: "2025-10-01T12:01:45Z", + level: "info", + msg: "User said: Good night.", + type: "speech", + }, + { + id: "21", + timestamp: "2025-10-01T12:02:00Z", + level: "info", + msg: "User asked: What's the time?", + type: "speech", + }, + { + id: "22", + timestamp: "2025-10-01T12:02:05Z", + level: "debug", + msg: "Time module loaded.", + type: "system", + }, + { + id: "23", + timestamp: "2025-10-01T12:02:10Z", + level: "warn", + msg: "WiFi signal weak.", + type: "system", + }, + { + id: "24", + timestamp: "2025-10-01T12:02:15Z", + level: "info", + msg: "User said: Set an alarm for 7 AM.", + type: "speech", + }, + { + id: "25", + timestamp: "2025-10-01T12:02:20Z", + level: "debug", + msg: "Alarm scheduled for 7:00 AM.", + type: "system", + }, + { + id: "26", + timestamp: "2025-10-01T12:02:25Z", + level: "warn", + msg: "Alarm module not responding.", + type: "system", + }, + { + id: "27", + timestamp: "2025-10-01T12:02:30Z", + level: "info", + msg: "User said: Cancel the alarm.", + type: "speech", + }, + { + id: "28", + timestamp: "2025-10-01T12:02:35Z", + level: "debug", + msg: "Alarm cancellation requested.", + type: "system", + }, + { + id: "29", + timestamp: "2025-10-01T12:02:40Z", + level: "warn", + msg: "Alarm cancellation failed.", + type: "system", + }, + { + id: "30", + timestamp: "2025-10-01T12:02:45Z", + level: "info", + msg: "User said: Open the window.", + type: "speech", + }, + { + id: "31", + timestamp: "2025-10-01T12:03:00Z", + level: "info", + msg: "User asked: What's on my calendar?", + type: "speech", + }, + { + id: "32", + timestamp: "2025-10-01T12:03:05Z", + level: "debug", + msg: "Calendar module loaded.", + type: "system", + }, + { + id: "33", + timestamp: "2025-10-01T12:03:10Z", + level: "warn", + msg: "Calendar sync failed.", + type: "system", + }, + { + id: "34", + timestamp: "2025-10-01T12:03:15Z", + level: "info", + msg: "User said: Remind me to call John.", + type: "speech", + }, + { + id: "35", + timestamp: "2025-10-01T12:03:20Z", + level: "debug", + msg: "Reminder set for John.", + type: "system", + }, + { + id: "36", + timestamp: "2025-10-01T12:03:25Z", + level: "warn", + msg: "Reminder module not available.", + type: "system", + }, + { + id: "37", + timestamp: "2025-10-01T12:03:30Z", + level: "info", + msg: "User said: What's the news?", + type: "speech", + }, + { + id: "38", + timestamp: "2025-10-01T12:03:35Z", + level: "debug", + msg: "News API request sent.", + type: "system", + }, + { + id: "39", + timestamp: "2025-10-01T12:03:40Z", + level: "warn", + msg: "News API rate limit reached.", + type: "system", + }, + { + id: "40", + timestamp: "2025-10-01T12:03:45Z", + level: "info", + msg: "User said: Tell me a joke.", + type: "speech", + }, + { + id: "41", + timestamp: "2025-10-01T12:04:00Z", + level: "info", + msg: "User asked: What's the temperature?", + type: "speech", + }, + { + id: "42", + timestamp: "2025-10-01T12:04:05Z", + level: "debug", + msg: "Temperature sensor reading: 22°C.", + type: "sensor", + }, + { + id: "43", + timestamp: "2025-10-01T12:04:10Z", + level: "warn", + msg: "Temperature sensor calibration needed.", + type: "sensor", + }, + { + id: "44", + timestamp: "2025-10-01T12:04:15Z", + level: "info", + msg: "User said: Start cleaning.", + type: "speech", + }, + { + id: "45", + timestamp: "2025-10-01T12:04:20Z", + level: "debug", + msg: "Vacuum motor started.", + type: "system", + }, + { + id: "46", + timestamp: "2025-10-01T12:04:25Z", + level: "warn", + msg: "Vacuum bin full.", + type: "system", + }, + { + id: "47", + timestamp: "2025-10-01T12:04:30Z", + level: "info", + msg: "User said: Stop cleaning.", + type: "speech", + }, + { + id: "48", + timestamp: "2025-10-01T12:04:35Z", + level: "debug", + msg: "Vacuum motor stopped.", + type: "system", + }, + { + id: "49", + timestamp: "2025-10-01T12:04:40Z", + level: "warn", + msg: "Obstacle detected during cleaning.", + type: "sensor", + }, + { + id: "50", + timestamp: "2025-10-01T12:04:45Z", + level: "info", + msg: "User said: Goodbye!", + type: "speech", + }, +]; + +interface LogEntry { + id: string; + type?: string; + timestamp: string; + level: "info" | "debug" | "warn"; + msg?: string; +} + diff --git a/src/pages/Home/Home.module.css b/src/pages/Home/Home.module.css index aed0f27..d609fe9 100644 --- a/src/pages/Home/Home.module.css +++ b/src/pages/Home/Home.module.css @@ -4,3 +4,9 @@ .card { padding: 2em; } + +.links { + display: flex; + flex-direction: column; + gap: 1em; +} \ No newline at end of file diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 8767db3..582357b 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -1,47 +1,20 @@ -//import { useState } from 'react' import { Link } from 'react-router' -import reactLogo from '../../assets/react.svg' -import viteLogo from '../../assets/vite.svg' import pepperLogo from '../../assets/pepper_transp2_small.svg' -import style from './Home.module.css' -import Counter from '../../components/components.tsx' +import styles from './Home.module.css' function Home() { - - - return ( <> -
-
- - Pepper logo - -
- - - Vite logo - - - React logo +
+ + Pepper logo
-

Vite + React

- - - - - - -

- Edit src/App.tsx and save to test HMR -

- -

- Click on the Vite and React logos to learn more -

+
+ Robot interaction → + Node editor → + Logs → +
) } diff --git a/src/pages/Logging/Logging.module.css b/src/pages/Logging/Logging.module.css new file mode 100644 index 0000000..52191c3 --- /dev/null +++ b/src/pages/Logging/Logging.module.css @@ -0,0 +1,17 @@ +.DivToScroll{ + background-color: color-mix(in srgb, canvas, #000 5%); + border: 1px solid color-mix(in srgb, canvas, #000 15%); + border-radius: 4px 0 4px 0; + color: #3B3C3E; + font-size: 12px; + font-weight: bold; + left: -1px; + padding: 10px 7px 5px; +} + +.DivWithScroll{ + height:50vh; + width:100vh; + overflow:scroll; + overflow-x:hidden; +} diff --git a/src/pages/Logging/Logging.tsx b/src/pages/Logging/Logging.tsx new file mode 100644 index 0000000..31945c1 --- /dev/null +++ b/src/pages/Logging/Logging.tsx @@ -0,0 +1,78 @@ +import { useState } from 'react'; +import { DATA } from "../../assets/data"; +import styles from './Logging.module.css'; + + +// const dataType = DATA as { id: string; level: "debug"|"info"|"warn"|"error"; msg: string; timestamp?: string }; +type Level = "debug" | "info" | "warn" | "error"; + +// make optional fields optional +type LogEntry = { + id: string; + level: Level; + timestamp?: string; + msg?: string; + type?: "speech" | "sensor" | "system" | string; + +}; + +function getLevelColor(level: Level) { + switch (level) { + case "debug": + return "gray"; + case "info": + return "blue"; + case "warn": + return "red"; + case "error": + return "red"; + default: + return "black"; + } +} + +function Logging() { + const [logs, setLogs] = useState([]); + + const logDiv = ( +
+
+ {logs.map((log) => ( +
+ + [{log.timestamp}] + + + {log.msg ? log.msg : "No message"} + + + ({log.level}) + +
+ ))} +
+
+ ) + return ( + <> +

Log Screen

+ { logDiv } +
+ + +
+ + ) +} + +export default Logging diff --git a/src/pages/ServerComms/ServerComms.css b/src/pages/ServerComms/ServerComms.css deleted file mode 100644 index e69de29..0000000 diff --git a/src/pages/ServerComms/ServerComms.tsx b/src/pages/ServerComms/ServerComms.tsx index c16ec81..6d15524 100644 --- a/src/pages/ServerComms/ServerComms.tsx +++ b/src/pages/ServerComms/ServerComms.tsx @@ -1,15 +1,12 @@ -import { useState, useEffect } from 'react' -import { Link } from 'react-router' -//import Counter from '../../components/components.tsx' +import { useState, useEffect, useRef } from 'react' - -//this is your css file where you can style your buttons and such -//you can still use css parts from App.css, but also overwrite them - -function ServerComms() { +export default function ServerComms() { const [message, setMessage] = useState(''); - const [sseMessage, setSseMessage] = useState(''); - const [spoken, setSpoken] = useState(""); + + const [listening, setListening] = useState(false); + const [conversation, setConversation] = useState<{"role": "user" | "assistant", "content": string}[]>([]) + const conversationRef = useRef(null); + const [conversationIndex, setConversationIndex] = useState(0); const sendMessage = async () => { try { @@ -31,22 +28,31 @@ function ServerComms() { const eventSource = new EventSource("http://localhost:8000/sse"); eventSource.onmessage = (event) => { - setSseMessage(event.data); - try { const data = JSON.parse(event.data); - if (data.speech) setSpoken(data.speech); - } catch {} + 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]); + + useEffect(() => { + if (!conversationRef || !conversationRef.current) return; + conversationRef.current.scrollTop = conversationRef.current.scrollHeight; + }, [conversation]); return ( -
-
+
+

Robot interaction

+

Force robot speech

+
e.key === "Enter" && sendMessage().then(() => setMessage(""))} placeholder="Enter a message" /> - +
-
-

Message from Server (SSE):

-

{sseMessage}

-
-
-

Spoken text (SSE):

-

{spoken}

-
-
- {/* here you link to the homepage, in App.tsx you can link new pages */} - - +
+

Conversation

+

Listening {listening ? "🟢" : "🔴"}

+
+ {conversation.map((item) => ( +

{item["content"]}

+ ))} +
+
+ + +
- - ); } - -export default ServerComms \ No newline at end of file diff --git a/src/pages/VisProgPage/VisProg.tsx b/src/pages/VisProgPage/VisProg.tsx new file mode 100644 index 0000000..ec0055a --- /dev/null +++ b/src/pages/VisProgPage/VisProg.tsx @@ -0,0 +1,11 @@ +import VisProgUI from "../../visualProgrammingUI/VisProgUI.tsx"; + +function VisProgPage() { + return ( + <> + + + ) +} + +export default VisProgPage \ No newline at end of file diff --git a/src/visualProgrammingUI/VisProgUI.css b/src/visualProgrammingUI/VisProgUI.css new file mode 100644 index 0000000..8d82d09 --- /dev/null +++ b/src/visualProgrammingUI/VisProgUI.css @@ -0,0 +1,7 @@ +.default-node { + padding: 10px 20px; + background-color: canvas; + outline-style: solid; + border-radius: 5pt; + outline-width: 1pt; +} \ No newline at end of file diff --git a/src/visualProgrammingUI/VisProgUI.tsx b/src/visualProgrammingUI/VisProgUI.tsx new file mode 100644 index 0000000..a2b1e9c --- /dev/null +++ b/src/visualProgrammingUI/VisProgUI.tsx @@ -0,0 +1,132 @@ +import './VisProgUI.css' + +import { + useCallback, + useRef +} from 'react'; +import { + Background, + Controls, + ReactFlow, + ReactFlowProvider, + useNodesState, + useEdgesState, + reconnectEdge, + addEdge, + MarkerType, + type Edge, + type Connection, +} from '@xyflow/react'; +import '@xyflow/react/dist/style.css'; +import { + StartNode, + EndNode, + PhaseNode, + NormNode +} from "./components/NodeDefinitions.tsx"; + +import { Sidebar } from './components/DragDropSidebar.tsx'; + +const nodeTypes = { + start: StartNode, + end: EndNode, + phase: PhaseNode, + norm: NormNode +}; + +const initialNodes = [ + { + id: 'start', + type: 'start', + position: {x: 0, y: 0}, + data: {label: 'start'} + }, + { + id: 'genericPhase', + type: 'phase', + position: {x: 0, y: 150}, + data: {label: 'Generic Phase', number: 1}, + }, + { + id: 'end', + type: 'end', + position: {x: 0, y: 300}, + data: {label: 'End'} + } +]; +const initialEdges = [{id: 'start-end', source: 'start', target: 'end'}]; + +const defaultEdgeOptions = { + type: 'floating', + markerEnd: { + type: MarkerType.ArrowClosed, + color: '#505050', + }, +}; + +const VisProgUI = ()=> { + const edgeReconnectSuccessful = useRef(true); + const [nodes, , onNodesChange] = useNodesState(initialNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + + const onConnect = useCallback( + (params: Edge | Connection) => setEdges((els) => addEdge(params, els)), + [setEdges], + ); + + const onReconnectStart = useCallback(() => { + edgeReconnectSuccessful.current = false; + }, []); + + const onReconnect = useCallback((oldEdge: Edge, newConnection: Connection) => { + edgeReconnectSuccessful.current = true; + setEdges((els) => reconnectEdge(oldEdge, newConnection, els)); + }, [setEdges]); + + const onReconnectEnd = useCallback((_: unknown, edge: { id: string; }) => { + if (!edgeReconnectSuccessful.current) { + setEdges((eds) => eds.filter((e) => e.id !== edge.id)); + } + + edgeReconnectSuccessful.current = true; + }, [setEdges]); + + return ( +
+
+ + + + +
+
+ +
+
+ + ); +}; + +function VisualProgrammingUI(){ + return ( + + + + ); +} + +export default VisualProgrammingUI; \ No newline at end of file diff --git a/src/visualProgrammingUI/components/DragDropSidebar.tsx b/src/visualProgrammingUI/components/DragDropSidebar.tsx new file mode 100644 index 0000000..b3926d9 --- /dev/null +++ b/src/visualProgrammingUI/components/DragDropSidebar.tsx @@ -0,0 +1,141 @@ +import { useDraggable } from '@neodrag/react'; +import { + useReactFlow, + type XYPosition +} from '@xyflow/react'; +import { + type ReactNode, + useCallback, + useRef, + useState +} from 'react'; + + +// improve later to create better automatic IDs +let id = 0; +const getId = () => `dndnode_${id++}`; + + +interface DraggableNodeProps { + className?: string; + children: ReactNode; + nodeType: string; + onDrop: (nodeType: string, position: XYPosition) => void; +} + +function DraggableNode({ className, children, nodeType, onDrop }: DraggableNodeProps) { + const draggableRef = useRef(null); + const [position, setPosition] = useState({ x: 0, y: 0 }); + + + // @ts-ignore + useDraggable(draggableRef, { + position: position, + onDrag: ({ offsetX, offsetY }) => { + // Calculate position relative to the viewport + setPosition({ + x: offsetX, + y: offsetY, + }); + }, + onDragEnd: ({ event }) => { + setPosition({ x: 0, y: 0 }); + onDrop(nodeType, { + x: event.clientX, + y: event.clientY, + }); + }, + }); + + return ( +
+ {children} +
+ ); +} + +export function Sidebar() { + const { setNodes, screenToFlowPosition } = useReactFlow(); + + const handleNodeDrop = useCallback( + (nodeType: string, screenPosition: XYPosition) => { + const flow = document.querySelector('.react-flow'); + const flowRect = flow?.getBoundingClientRect(); + const isInFlow = + flowRect && + screenPosition.x >= flowRect.left && + screenPosition.x <= flowRect.right && + screenPosition.y >= flowRect.top && + screenPosition.y <= flowRect.bottom; + + // Create a new node and add it to the flow + if (isInFlow) { + const position = screenToFlowPosition(screenPosition); + + const newNode = () => { + switch (nodeType) { + case "phase": + return { + id: getId(), + type: nodeType, + position, + data: {label: `"new"`, number: (-1)}, + }; + case "start": + return { + id: getId(), + type: nodeType, + position, + data: {label: `new start node`}, + }; + case "end": + return { + id: getId(), + type: nodeType, + position, + data: {label: `new end node`}, + }; + case "norm": + return { + id: getId(), + type: nodeType, + position, + data: {label: `new norm node`}, + }; + default: { + return { + id: getId(), + type: nodeType, + position, + data: {label: `new default node`}, + }; + } + } + } + + setNodes((nds) => nds.concat(newNode())); + } + }, + [setNodes, screenToFlowPosition], + ); + + return ( + + ); +} \ No newline at end of file diff --git a/src/visualProgrammingUI/components/NodeDefinitions.tsx b/src/visualProgrammingUI/components/NodeDefinitions.tsx new file mode 100644 index 0000000..b4547b2 --- /dev/null +++ b/src/visualProgrammingUI/components/NodeDefinitions.tsx @@ -0,0 +1,111 @@ +import {Handle, NodeToolbar, Position, useReactFlow} from '@xyflow/react'; +import '@xyflow/react/dist/style.css'; +import '../VisProgUI.css'; + +// Datatypes for NodeTypes + +type defaultNodeData = { + label: string; +}; + +type startNodeData = defaultNodeData; +type endNodeData = defaultNodeData; +type normNodeData = defaultNodeData; +type phaseNodeData = defaultNodeData & { + number: number; +}; + +export type nodeData = defaultNodeData | startNodeData | phaseNodeData | endNodeData; + +// Node Toolbar definition + +type ToolbarProps= { + nodeId: string; +}; + +export function Toolbar({nodeId}:ToolbarProps) { + const { setNodes, setEdges } = useReactFlow(); + + const handleDelete = () => { + setNodes((nds) => nds.filter((n) => n.id !== nodeId)); + setEdges((eds) => eds.filter((e) => e.source !== nodeId && e.target !== nodeId)); + }; + return ( + + + ); +} + + +// Definitions of Nodes + +type StartNodeProps = { + id: string; + data: startNodeData; +}; + +export const StartNode= ({ id, data }: StartNodeProps) => { + return ( + <> + +
+
data test {data.label}
+ +
+ + ); +}; + +type EndNodeProps = { + id: string; + data: endNodeData; +}; + +export const EndNode= ({ id, data }: EndNodeProps) => { + return ( + <> + +
+
{data.label}
+ +
+ + ); +}; + + +type PhaseNodeProps = { + id: string; + data: phaseNodeData; +}; + +export const PhaseNode= ({ id, data }: PhaseNodeProps) => { + return ( + <> + +
+
phase {data.number} {data.label}
+ + + +
+ + ); +}; + +type NormNodeProps = { + id: string; + data: normNodeData; +}; + +export const NormNode= ({ id, data }: NormNodeProps) => { + return ( + <> + +
+
Norm {data.label}
+ +
+ + ); +}; \ No newline at end of file From b78cd53baa8f3fd73fc6f372cc3e42f2e07b614b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Tue, 7 Oct 2025 15:05:05 +0200 Subject: [PATCH 02/14] feat: Show connected robots in the UI when connection event is received from CB. Added two test buttons to mimic events from CB. UI will listen to port localhost:8000 for data. use the data.event = "robot_connected" and data.event = "robot_disconnected". (robot) ID is required, name and port are optional but incentivized. --- src/App.tsx | 2 + src/pages/ConnectedRobots/ConnectedRobots.tsx | 116 ++++++++++++++++++ src/pages/Home/Home.tsx | 1 + 3 files changed, 119 insertions(+) create mode 100644 src/pages/ConnectedRobots/ConnectedRobots.tsx diff --git a/src/App.tsx b/src/App.tsx index 0833bdb..fe6a9ad 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import './App.css' import TemplatePage from './pages/TemplatePage/Template.tsx' import Home from './pages/Home/Home.tsx' import ServerComms from './pages/ServerComms/ServerComms.tsx' +import ConnectedRobots from './pages/ConnectedRobots/ConnectedRobots.tsx' import Logging from './pages/Logging/Logging.tsx' import VisProg from "./pages/VisProgPage/VisProg.tsx"; @@ -18,6 +19,7 @@ function App(){ } /> } /> } /> + } />
) diff --git a/src/pages/ConnectedRobots/ConnectedRobots.tsx b/src/pages/ConnectedRobots/ConnectedRobots.tsx new file mode 100644 index 0000000..148cfbe --- /dev/null +++ b/src/pages/ConnectedRobots/ConnectedRobots.tsx @@ -0,0 +1,116 @@ +import { mergeAriaLabelConfig } from '@xyflow/system'; +import { useState, useEffect } from 'react' + +// Define the robot type +type Robot = { + id: string; + name: string; + port: number; +}; + +export default function ConnectedRobots() { + const [connectedRobots, setConnectedRobots] = useState([]); + + useEffect(() => { + const eventSource = new EventSource("http://localhost:8000/sse"); + eventSource.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + + // Example: data = { event: "robot_connected", id: "pepper_robot1", name: "Pepper", port: 1234 } + if (data.event === "robot_connected") { + + // Safeguard id in request. + if (data.id === null || data.id === undefined) { + console.log("Missing robot id in connection request.") + return () => eventSource.close(); + } + + // Safeguard duplicates + if (connectedRobots.some(robot => robot.id === data.id)) + console.log("connection request was sent for id: ", data.id, + " however this id was already present at current time"); + + // Add to connected robots while checking name and port for undefineds. + else { + const name = data.name ?? "no given name"; + const port = typeof data.port === "number" ? data.port : -1; + setConnectedRobots(robots => [...robots, { id: data.id, name: name, port: port }]); + } + } + if (data.event === "robot_disconnected") { + // Safeguard id in request. + if (data.id === null || data.id === undefined) { + console.log("Missing robot id in connection request.") + return () => eventSource.close(); + } + + // Filter out same ids (should only be one) + setConnectedRobots(robots => robots.filter(robot => robot.id !== data.id)); + } + } catch { + console.log("Unparsable SSE message:", event.data); + } + }; + return () => eventSource.close(); + }, [connectedRobots]); + + return ( +
+

Robots Connected

+
+

Connected Robots

+
    + {connectedRobots.map(robot => +
  • + {robot.name} (ID: {robot.id}, Port: {robot.port === -1 ? "No given port" : robot.port}) +
  • + )} +
+
+
+
+ + + + +
+
+
+ ); +} diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 582357b..422ccfc 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -14,6 +14,7 @@ function Home() { Robot interaction → Node editor → Logs → + Connected Robots →
) From ec4f45b984528a4dde896486cf00fb132e16a903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Wed, 8 Oct 2025 12:40:01 +0200 Subject: [PATCH 03/14] fix: Keep the conencted robots in a global list ref: N25B-142 --- src/App.tsx | 23 +++++++++++++++++-- src/pages/ConnectedRobots/ConnectedRobots.tsx | 13 ++++++++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index fe6a9ad..eff9667 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ import { Routes, Route, Link } from 'react-router' +import { useState, useEffect } from 'react' import './App.css' import TemplatePage from './pages/TemplatePage/Template.tsx' import Home from './pages/Home/Home.tsx' @@ -8,7 +9,16 @@ import Logging from './pages/Logging/Logging.tsx' import VisProg from "./pages/VisProgPage/VisProg.tsx"; function App(){ - + + // Define what our conencted robot should include + type Robot = { + id: string; + name: string; + port: number; + }; + + const [connectedRobots, setConnectedRobots] = useState([]); + return (
{/* Should not use inline styles like this */} @@ -19,7 +29,16 @@ function App(){ } /> } /> } /> - } /> + + } + />
) diff --git a/src/pages/ConnectedRobots/ConnectedRobots.tsx b/src/pages/ConnectedRobots/ConnectedRobots.tsx index 148cfbe..cd88158 100644 --- a/src/pages/ConnectedRobots/ConnectedRobots.tsx +++ b/src/pages/ConnectedRobots/ConnectedRobots.tsx @@ -8,15 +8,22 @@ type Robot = { port: number; }; -export default function ConnectedRobots() { - const [connectedRobots, setConnectedRobots] = useState([]); +// Define the expected arguments +type ConnectedRobotsProps = { + connectedRobots: Robot[]; + setConnectedRobots: React.Dispatch>; +}; +export default function ConnectedRobots({ + connectedRobots, setConnectedRobots}: ConnectedRobotsProps) { + useEffect(() => { const eventSource = new EventSource("http://localhost:8000/sse"); eventSource.onmessage = (event) => { try { + console.log("message received :", event.data) const data = JSON.parse(event.data); - + // Example: data = { event: "robot_connected", id: "pepper_robot1", name: "Pepper", port: 1234 } if (data.event === "robot_connected") { From 72d61e398506e135d8aefe037d474c18b1c5e638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Wed, 8 Oct 2025 14:35:20 +0200 Subject: [PATCH 04/14] chore: fixed wrong imports and deleted some unnecessary prints. ref: N25B-142 --- src/App.tsx | 3 ++- src/pages/ConnectedRobots/ConnectedRobots.tsx | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index eff9667..1dd4c9d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import { Routes, Route, Link } from 'react-router' -import { useState, useEffect } from 'react' +import { useState } from 'react' import './App.css' import TemplatePage from './pages/TemplatePage/Template.tsx' import Home from './pages/Home/Home.tsx' @@ -17,6 +17,7 @@ function App(){ port: number; }; + // (Acces to) the array of connected robots const [connectedRobots, setConnectedRobots] = useState([]); return ( diff --git a/src/pages/ConnectedRobots/ConnectedRobots.tsx b/src/pages/ConnectedRobots/ConnectedRobots.tsx index cd88158..45559dd 100644 --- a/src/pages/ConnectedRobots/ConnectedRobots.tsx +++ b/src/pages/ConnectedRobots/ConnectedRobots.tsx @@ -1,5 +1,4 @@ -import { mergeAriaLabelConfig } from '@xyflow/system'; -import { useState, useEffect } from 'react' +import { useEffect } from 'react' // Define the robot type type Robot = { @@ -21,7 +20,6 @@ export default function ConnectedRobots({ const eventSource = new EventSource("http://localhost:8000/sse"); eventSource.onmessage = (event) => { try { - console.log("message received :", event.data) const data = JSON.parse(event.data); // Example: data = { event: "robot_connected", id: "pepper_robot1", name: "Pepper", port: 1234 } @@ -69,6 +67,7 @@ export default function ConnectedRobots({

Connected Robots

    {connectedRobots.map(robot => + // Map all the robots in an unordered list
  • {robot.name} (ID: {robot.id}, Port: {robot.port === -1 ? "No given port" : robot.port})
  • @@ -100,6 +99,7 @@ export default function ConnectedRobots({ setConnectedRobots(robots => [...robots, randomConnectionDummy]); }}>'Sent connected event'
diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index cb70de0..0c811ac 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -13,6 +13,7 @@ function Home() {
Robot Interaction → Template → + Connected Robots →
) From fa046e6b2a8524643dbf41a70a370ec7e399cae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Wed, 8 Oct 2025 17:41:29 +0200 Subject: [PATCH 06/14] feat: dummy reload from CB added. ref: N25B-153 --- src/pages/ConnectedRobots/ConnectedRobots.tsx | 78 ++++++++++++++++--- 1 file changed, 67 insertions(+), 11 deletions(-) diff --git a/src/pages/ConnectedRobots/ConnectedRobots.tsx b/src/pages/ConnectedRobots/ConnectedRobots.tsx index 45559dd..abce238 100644 --- a/src/pages/ConnectedRobots/ConnectedRobots.tsx +++ b/src/pages/ConnectedRobots/ConnectedRobots.tsx @@ -1,4 +1,5 @@ import { useEffect } from 'react' +import Logging from '../Logging/Logging'; // Define the robot type type Robot = { @@ -14,20 +15,21 @@ type ConnectedRobotsProps = { }; export default function ConnectedRobots({ - connectedRobots, setConnectedRobots}: ConnectedRobotsProps) { - + connectedRobots, setConnectedRobots }: ConnectedRobotsProps) { + useEffect(() => { const eventSource = new EventSource("http://localhost:8000/sse"); eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); - + // Example: data = { event: "robot_connected", id: "pepper_robot1", name: "Pepper", port: 1234 } if (data.event === "robot_connected") { // Safeguard id in request. if (data.id === null || data.id === undefined) { - console.log("Missing robot id in connection request.") + console.log(`Missing robot id in connection request. + Use format: 'data: {event = 'robot_connected', id = , (optional) name = , (optional) port = }'.`) return () => eventSource.close(); } @@ -46,13 +48,23 @@ export default function ConnectedRobots({ if (data.event === "robot_disconnected") { // Safeguard id in request. if (data.id === null || data.id === undefined) { - console.log("Missing robot id in connection request.") + console.log("Missing robot id in connection request. Use format: 'data: {event = 'robot_disconnected', id = }'."); return () => eventSource.close(); } // Filter out same ids (should only be one) setConnectedRobots(robots => robots.filter(robot => robot.id !== data.id)); } + if (data.event === "robot_list") { + if (data.list === null || data.list === undefined) { + console.log("Missing list in robot_list request. Use format: 'data: {event = 'robot_list', list = }'."); + return () => eventSource.close(); + } + + // Set the robot list to the one found in CB + setConnectedRobots(data.list); + } + } catch { console.log("Unparsable SSE message:", event.data); } @@ -77,8 +89,52 @@ export default function ConnectedRobots({
+ // Let's test the reload function. + const example_list = [{ id: "pepper_robot1", name: "Pepper1", port: 1234 }, + { id: "pepper_robot2", name: "Pepper2", port: 1235 }, + { id: "pepper_robot3", name: "Pepper3", port: 1236 }, + { id: "pepper_robot4", name: "Pepper4", port: 1237 }] + + const example_event = `{ + "event": "robot_list", "list": + [{ "id": "pepper_robot1", + "name": "Pepper1", + "port": 1234 },{ + + "id": "pepper_robot2", + "name": "Pepper2", + "port": 1235 },{ + + "id": "pepper_robot3", + "name": "Pepper3", + "port": 1236 }, { + + "id": "pepper_robot4", + "name": "Pepper4", + "port": 1237 }]}` + + // Now let's put it through the same steps as the event would do. :) + try { + const data = JSON.parse(example_event); + if (data.event === "robot_list") { + if (data.list === null || data.list === undefined) { + console.log("Missing list in robot_list request. Use format: 'data: {event = 'robot_list', list = }'."); + return; + } + // Check if it is as expected. + if (JSON.stringify(data.list) !== JSON.stringify(example_list)) { + console.log("Dummy reload failed: list don't match.") + } + else { + console.log("Dummy reload succes!!") + } + } else { + console.log("Dummy reload failed, didn't parse to 'data.event === 'robot_list'.'") + } + } catch { + console.log("Dummy reload failed: didnt parse correctly.") + } + }}>Dummy Reload from CB - - - -
+

Robot is currently: {connected == null ? "checking..." : (connected ? "connected! 🟢" : "not connected... 🔴")}

); From 6a88aa3d755db2ec9f9ed76019545846f39f3a16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Thu, 30 Oct 2025 14:57:50 +0100 Subject: [PATCH 08/14] merge branch dev into show-connected-robots pt2 --- package-lock.json | 127 ------------------------------ src/App.tsx | 3 - src/pages/VisProgPage/VisProg.tsx | 11 --- 3 files changed, 141 deletions(-) diff --git a/package-lock.json b/package-lock.json index b3caba6..c7346fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3323,69 +3323,6 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/@xyflow/react": { - "version": "12.8.6", - "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.6.tgz", - "integrity": "sha512-SksAm2m4ySupjChphMmzvm55djtgMDPr+eovPDdTnyGvShf73cvydfoBfWDFllooIQ4IaiUL5yfxHRwU0c37EA==", - "license": "MIT", - "dependencies": { - "@xyflow/system": "0.0.70", - "classcat": "^5.0.3", - "zustand": "^4.4.0" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, -<<<<<<< HEAD -======= - "node_modules/@xyflow/react/node_modules/zustand": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", - "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0.6", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } - } - }, ->>>>>>> origin/dev - "node_modules/@xyflow/system": { - "version": "0.0.70", - "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.70.tgz", - "integrity": "sha512-PpC//u9zxdjj0tfTSmZrg3+sRbTz6kop/Amky44U2Dl51sxzDTIUfXMwETOYpmr2dqICWXBIJwXL2a9QWtX2XA==", - "license": "MIT", - "dependencies": { - "@types/d3-drag": "^3.0.7", - "@types/d3-interpolate": "^3.0.4", - "@types/d3-selection": "^3.0.10", - "@types/d3-transition": "^3.0.8", - "@types/d3-zoom": "^3.0.8", - "d3-drag": "^3.0.0", - "d3-interpolate": "^3.0.1", - "d3-selection": "^3.0.0", - "d3-zoom": "^3.0.0" - } - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -7740,21 +7677,6 @@ "punycode": "^2.1.0" } }, - "node_modules/use-sync-external-store": { -<<<<<<< HEAD - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", -======= - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", ->>>>>>> origin/dev - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -8237,55 +8159,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zustand": { -<<<<<<< HEAD - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", - "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0.6", - "react": ">=16.8" -======= - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", - "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", - "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" ->>>>>>> origin/dev - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true -<<<<<<< HEAD -======= - }, - "use-sync-external-store": { - "optional": true ->>>>>>> origin/dev - } - } } } } diff --git a/src/App.tsx b/src/App.tsx index fd9404c..819dae4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,11 +4,8 @@ import './App.css' import TemplatePage from './pages/TemplatePage/Template.tsx' import Home from './pages/Home/Home.tsx' import Robot from './pages/Robot/Robot.tsx'; -<<<<<<< HEAD import ConnectedRobots from './pages/ConnectedRobots/ConnectedRobots.tsx' -======= import VisProg from "./pages/VisProgPage/VisProg.tsx"; ->>>>>>> origin/dev function App(){ diff --git a/src/pages/VisProgPage/VisProg.tsx b/src/pages/VisProgPage/VisProg.tsx index ec2078c..4b8944c 100644 --- a/src/pages/VisProgPage/VisProg.tsx +++ b/src/pages/VisProgPage/VisProg.tsx @@ -1,13 +1,3 @@ -<<<<<<< HEAD -import VisProgUI from "../../visualProgrammingUI/VisProgUI.tsx"; - -function VisProgPage() { - return ( - <> - - - ) -======= import { Background, Controls, @@ -144,7 +134,6 @@ function VisProgPage() { ) ->>>>>>> origin/dev } export default VisProgPage \ No newline at end of file From 5e707224cffabb3320e87e8810f77fecb1b8305e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Thu, 30 Oct 2025 15:47:09 +0100 Subject: [PATCH 09/14] feat: Show connected robots finished with unit test 94% coverage ref: N25B-142 --- package-lock.json | 98 +++++++++++++++++ src/App.tsx | 22 +--- src/pages/ConnectedRobots/ConnectedRobots.tsx | 6 ++ .../connectedRobots/ConnectedRobots.test.tsx | 102 ++++++++++++++++++ 4 files changed, 207 insertions(+), 21 deletions(-) create mode 100644 test/pages/connectedRobots/ConnectedRobots.test.tsx diff --git a/package-lock.json b/package-lock.json index c7346fa..40f413f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3323,6 +3323,66 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@xyflow/react": { + "version": "12.9.1", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.9.1.tgz", + "integrity": "sha512-JRPCT5p7NnPdVSIh15AFvUSSm+8GUyz2I6iuBEC1LG2lKgig/L48AM/ImMHCc3ZUCg+AgTOJDaX2fcRyPA9BTA==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.72", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@xyflow/react/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.72", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.72.tgz", + "integrity": "sha512-WBI5Aau0fXTXwxHPzceLNS6QdXggSWnGjDtj/gG669crApN8+SCmEtkBth1m7r6pStNo/5fI9McEi7Dk0ymCLA==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -7677,6 +7737,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -8159,6 +8228,35 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/src/App.tsx b/src/App.tsx index 819dae4..4fb4c42 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,4 @@ import { Routes, Route, Link } from 'react-router' -import { useState } from 'react' import './App.css' import TemplatePage from './pages/TemplatePage/Template.tsx' import Home from './pages/Home/Home.tsx' @@ -9,16 +8,6 @@ import VisProg from "./pages/VisProgPage/VisProg.tsx"; function App(){ - // Define what our conencted robot should include - type Robot = { - id: string; - name: string; - port: number; - }; - - // (Acces to) the array of connected robots - const [connectedRobots, setConnectedRobots] = useState([]); - return (
@@ -30,16 +19,7 @@ function App(){ } /> } /> } /> - - } - /> + } />
diff --git a/src/pages/ConnectedRobots/ConnectedRobots.tsx b/src/pages/ConnectedRobots/ConnectedRobots.tsx index a6f7ce7..8b4b898 100644 --- a/src/pages/ConnectedRobots/ConnectedRobots.tsx +++ b/src/pages/ConnectedRobots/ConnectedRobots.tsx @@ -3,6 +3,9 @@ import { useEffect, useState } from 'react' export default function ConnectedRobots() { const [connected, setConnected] = useState(null); + + + useEffect(() => { // We're excepting a stream of data like that looks like this: `data = False` or `data = True` const eventSource = new EventSource("http://localhost:8000/robot/ping_stream"); @@ -33,6 +36,9 @@ export default function ConnectedRobots() {

Is robot currently connected?

Robot is currently: {connected == null ? "checking..." : (connected ? "connected! 🟢" : "not connected... 🔴")}

+

+ {connected == null ? "If checking continues, make sure CB is properly loaded with robot at least once." : ""} +

); diff --git a/test/pages/connectedRobots/ConnectedRobots.test.tsx b/test/pages/connectedRobots/ConnectedRobots.test.tsx new file mode 100644 index 0000000..ffea6e3 --- /dev/null +++ b/test/pages/connectedRobots/ConnectedRobots.test.tsx @@ -0,0 +1,102 @@ +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: any) { + // 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(() => { + (globalThis as any).EventSource = jest.fn((url: string) => new MockEventSource(url)); +}); + +// clean after tests +afterEach(() => { + cleanup(); + jest.restoreAllMocks(); + mockInstances.length = 0; +}); + +describe('ConnectedRobots', () => { + test('renders initial state correctly', () => { + render(); + + // 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(); + 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(); + 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(); + 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(); + const eventSource = mockInstances[0]; + const closeSpy = jest.spyOn(eventSource, 'close'); + cleanup(); + expect(closeSpy).toHaveBeenCalled(); + expect(eventSource.closed).toBe(true); + }); +}); \ No newline at end of file From 333bd6e6fd1b7db071e1b1399d0260de606a375f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Wed, 5 Nov 2025 16:11:36 +0100 Subject: [PATCH 10/14] chore: single typing change --- test/pages/connectedRobots/ConnectedRobots.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/pages/connectedRobots/ConnectedRobots.test.tsx b/test/pages/connectedRobots/ConnectedRobots.test.tsx index ffea6e3..c2d3749 100644 --- a/test/pages/connectedRobots/ConnectedRobots.test.tsx +++ b/test/pages/connectedRobots/ConnectedRobots.test.tsx @@ -13,7 +13,7 @@ class MockEventSource { mockInstances.push(this); } - sendMessage(data: any) { + sendMessage(data: string) { // Trigger whatever the component listens to this.onmessage?.({ data } as MessageEvent); } From 1b8095376b31027fd7bc39199c9d8c96d4e9ba69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Wed, 5 Nov 2025 17:21:36 +0100 Subject: [PATCH 11/14] fix: fixed npx eslint (also accounting for justins part) ref: N25B-142 --- src/visualProgrammingUI/components/DragDropSidebar.tsx | 3 +-- test/pages/connectedRobots/ConnectedRobots.test.tsx | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/visualProgrammingUI/components/DragDropSidebar.tsx b/src/visualProgrammingUI/components/DragDropSidebar.tsx index b3926d9..f1bf6bd 100644 --- a/src/visualProgrammingUI/components/DragDropSidebar.tsx +++ b/src/visualProgrammingUI/components/DragDropSidebar.tsx @@ -27,8 +27,7 @@ function DraggableNode({ className, children, nodeType, onDrop }: DraggableNodeP const draggableRef = useRef(null); const [position, setPosition] = useState({ x: 0, y: 0 }); - - // @ts-ignore + // @ts-expect-error we expect the null referece here. useDraggable(draggableRef, { position: position, onDrag: ({ offsetX, offsetY }) => { diff --git a/test/pages/connectedRobots/ConnectedRobots.test.tsx b/test/pages/connectedRobots/ConnectedRobots.test.tsx index c2d3749..017b2a2 100644 --- a/test/pages/connectedRobots/ConnectedRobots.test.tsx +++ b/test/pages/connectedRobots/ConnectedRobots.test.tsx @@ -25,7 +25,9 @@ class MockEventSource { // mock event source generation with fake function that returns our fake mock source beforeAll(() => { - (globalThis as any).EventSource = jest.fn((url: string) => new MockEventSource(url)); + // 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 From 8733bb3c040706f301ba08a9c042257e254577cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Tue, 11 Nov 2025 10:25:27 +0100 Subject: [PATCH 12/14] chore: remove old remnants from project --- src/visualProgrammingUI/VisProgUI.css | 7 - src/visualProgrammingUI/VisProgUI.tsx | 132 ----------------- .../components/DragDropSidebar.tsx | 140 ------------------ .../components/NodeDefinitions.tsx | 111 -------------- 4 files changed, 390 deletions(-) delete mode 100644 src/visualProgrammingUI/VisProgUI.css delete mode 100644 src/visualProgrammingUI/VisProgUI.tsx delete mode 100644 src/visualProgrammingUI/components/DragDropSidebar.tsx delete mode 100644 src/visualProgrammingUI/components/NodeDefinitions.tsx diff --git a/src/visualProgrammingUI/VisProgUI.css b/src/visualProgrammingUI/VisProgUI.css deleted file mode 100644 index 8d82d09..0000000 --- a/src/visualProgrammingUI/VisProgUI.css +++ /dev/null @@ -1,7 +0,0 @@ -.default-node { - padding: 10px 20px; - background-color: canvas; - outline-style: solid; - border-radius: 5pt; - outline-width: 1pt; -} \ No newline at end of file diff --git a/src/visualProgrammingUI/VisProgUI.tsx b/src/visualProgrammingUI/VisProgUI.tsx deleted file mode 100644 index a2b1e9c..0000000 --- a/src/visualProgrammingUI/VisProgUI.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import './VisProgUI.css' - -import { - useCallback, - useRef -} from 'react'; -import { - Background, - Controls, - ReactFlow, - ReactFlowProvider, - useNodesState, - useEdgesState, - reconnectEdge, - addEdge, - MarkerType, - type Edge, - type Connection, -} from '@xyflow/react'; -import '@xyflow/react/dist/style.css'; -import { - StartNode, - EndNode, - PhaseNode, - NormNode -} from "./components/NodeDefinitions.tsx"; - -import { Sidebar } from './components/DragDropSidebar.tsx'; - -const nodeTypes = { - start: StartNode, - end: EndNode, - phase: PhaseNode, - norm: NormNode -}; - -const initialNodes = [ - { - id: 'start', - type: 'start', - position: {x: 0, y: 0}, - data: {label: 'start'} - }, - { - id: 'genericPhase', - type: 'phase', - position: {x: 0, y: 150}, - data: {label: 'Generic Phase', number: 1}, - }, - { - id: 'end', - type: 'end', - position: {x: 0, y: 300}, - data: {label: 'End'} - } -]; -const initialEdges = [{id: 'start-end', source: 'start', target: 'end'}]; - -const defaultEdgeOptions = { - type: 'floating', - markerEnd: { - type: MarkerType.ArrowClosed, - color: '#505050', - }, -}; - -const VisProgUI = ()=> { - const edgeReconnectSuccessful = useRef(true); - const [nodes, , onNodesChange] = useNodesState(initialNodes); - const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); - - const onConnect = useCallback( - (params: Edge | Connection) => setEdges((els) => addEdge(params, els)), - [setEdges], - ); - - const onReconnectStart = useCallback(() => { - edgeReconnectSuccessful.current = false; - }, []); - - const onReconnect = useCallback((oldEdge: Edge, newConnection: Connection) => { - edgeReconnectSuccessful.current = true; - setEdges((els) => reconnectEdge(oldEdge, newConnection, els)); - }, [setEdges]); - - const onReconnectEnd = useCallback((_: unknown, edge: { id: string; }) => { - if (!edgeReconnectSuccessful.current) { - setEdges((eds) => eds.filter((e) => e.id !== edge.id)); - } - - edgeReconnectSuccessful.current = true; - }, [setEdges]); - - return ( -
-
- - - - -
-
- -
-
- - ); -}; - -function VisualProgrammingUI(){ - return ( - - - - ); -} - -export default VisualProgrammingUI; \ No newline at end of file diff --git a/src/visualProgrammingUI/components/DragDropSidebar.tsx b/src/visualProgrammingUI/components/DragDropSidebar.tsx deleted file mode 100644 index f1bf6bd..0000000 --- a/src/visualProgrammingUI/components/DragDropSidebar.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { useDraggable } from '@neodrag/react'; -import { - useReactFlow, - type XYPosition -} from '@xyflow/react'; -import { - type ReactNode, - useCallback, - useRef, - useState -} from 'react'; - - -// improve later to create better automatic IDs -let id = 0; -const getId = () => `dndnode_${id++}`; - - -interface DraggableNodeProps { - className?: string; - children: ReactNode; - nodeType: string; - onDrop: (nodeType: string, position: XYPosition) => void; -} - -function DraggableNode({ className, children, nodeType, onDrop }: DraggableNodeProps) { - const draggableRef = useRef(null); - const [position, setPosition] = useState({ x: 0, y: 0 }); - - // @ts-expect-error we expect the null referece here. - useDraggable(draggableRef, { - position: position, - onDrag: ({ offsetX, offsetY }) => { - // Calculate position relative to the viewport - setPosition({ - x: offsetX, - y: offsetY, - }); - }, - onDragEnd: ({ event }) => { - setPosition({ x: 0, y: 0 }); - onDrop(nodeType, { - x: event.clientX, - y: event.clientY, - }); - }, - }); - - return ( -
- {children} -
- ); -} - -export function Sidebar() { - const { setNodes, screenToFlowPosition } = useReactFlow(); - - const handleNodeDrop = useCallback( - (nodeType: string, screenPosition: XYPosition) => { - const flow = document.querySelector('.react-flow'); - const flowRect = flow?.getBoundingClientRect(); - const isInFlow = - flowRect && - screenPosition.x >= flowRect.left && - screenPosition.x <= flowRect.right && - screenPosition.y >= flowRect.top && - screenPosition.y <= flowRect.bottom; - - // Create a new node and add it to the flow - if (isInFlow) { - const position = screenToFlowPosition(screenPosition); - - const newNode = () => { - switch (nodeType) { - case "phase": - return { - id: getId(), - type: nodeType, - position, - data: {label: `"new"`, number: (-1)}, - }; - case "start": - return { - id: getId(), - type: nodeType, - position, - data: {label: `new start node`}, - }; - case "end": - return { - id: getId(), - type: nodeType, - position, - data: {label: `new end node`}, - }; - case "norm": - return { - id: getId(), - type: nodeType, - position, - data: {label: `new norm node`}, - }; - default: { - return { - id: getId(), - type: nodeType, - position, - data: {label: `new default node`}, - }; - } - } - } - - setNodes((nds) => nds.concat(newNode())); - } - }, - [setNodes, screenToFlowPosition], - ); - - return ( - - ); -} \ No newline at end of file diff --git a/src/visualProgrammingUI/components/NodeDefinitions.tsx b/src/visualProgrammingUI/components/NodeDefinitions.tsx deleted file mode 100644 index b4547b2..0000000 --- a/src/visualProgrammingUI/components/NodeDefinitions.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import {Handle, NodeToolbar, Position, useReactFlow} from '@xyflow/react'; -import '@xyflow/react/dist/style.css'; -import '../VisProgUI.css'; - -// Datatypes for NodeTypes - -type defaultNodeData = { - label: string; -}; - -type startNodeData = defaultNodeData; -type endNodeData = defaultNodeData; -type normNodeData = defaultNodeData; -type phaseNodeData = defaultNodeData & { - number: number; -}; - -export type nodeData = defaultNodeData | startNodeData | phaseNodeData | endNodeData; - -// Node Toolbar definition - -type ToolbarProps= { - nodeId: string; -}; - -export function Toolbar({nodeId}:ToolbarProps) { - const { setNodes, setEdges } = useReactFlow(); - - const handleDelete = () => { - setNodes((nds) => nds.filter((n) => n.id !== nodeId)); - setEdges((eds) => eds.filter((e) => e.source !== nodeId && e.target !== nodeId)); - }; - return ( - - - ); -} - - -// Definitions of Nodes - -type StartNodeProps = { - id: string; - data: startNodeData; -}; - -export const StartNode= ({ id, data }: StartNodeProps) => { - return ( - <> - -
-
data test {data.label}
- -
- - ); -}; - -type EndNodeProps = { - id: string; - data: endNodeData; -}; - -export const EndNode= ({ id, data }: EndNodeProps) => { - return ( - <> - -
-
{data.label}
- -
- - ); -}; - - -type PhaseNodeProps = { - id: string; - data: phaseNodeData; -}; - -export const PhaseNode= ({ id, data }: PhaseNodeProps) => { - return ( - <> - -
-
phase {data.number} {data.label}
- - - -
- - ); -}; - -type NormNodeProps = { - id: string; - data: normNodeData; -}; - -export const NormNode= ({ id, data }: NormNodeProps) => { - return ( - <> - -
-
Norm {data.label}
- -
- - ); -}; \ No newline at end of file From df4346150e2bf6a81d9629cdbc48884572bae3a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Tue, 11 Nov 2025 11:11:46 +0100 Subject: [PATCH 13/14] chore: remove old code pt 2 --- src/assets/data.ts | 361 --------------------------- src/pages/Logging/Logging.module.css | 17 -- src/pages/Logging/Logging.tsx | 78 ------ 3 files changed, 456 deletions(-) delete mode 100644 src/assets/data.ts delete mode 100644 src/pages/Logging/Logging.module.css delete mode 100644 src/pages/Logging/Logging.tsx diff --git a/src/assets/data.ts b/src/assets/data.ts deleted file mode 100644 index c1eacfb..0000000 --- a/src/assets/data.ts +++ /dev/null @@ -1,361 +0,0 @@ -export const DATA: LogEntry[] = [ - { - id: "1", - timestamp: "2025-10-01T12:00:00Z", - level: "info", - msg: "User said: Hello, Pepper!", - type: "speech", - }, - { - id: "2", - timestamp: "2025-10-01T12:00:05Z", - level: "debug", - msg: "Proximity sensor value: 0.85", - type: "sensor", - }, - { - id: "3", - timestamp: "2025-10-01T12:00:10Z", - level: "warn", - msg: "Battery level low: 15%", - type: "system", - }, - { - id: "4", - timestamp: "2025-10-01T12:00:15Z", - level: "info", - msg: "User requested weather update.", - type: "speech", - }, - { - id: "5", - timestamp: "2025-10-01T12:00:20Z", - level: "debug", - msg: "Microphone activated.", - type: "system", - }, - { - id: "6", - timestamp: "2025-10-01T12:00:25Z", - level: "warn", - msg: "Obstacle detected in front.", - type: "sensor", - }, - { - id: "7", - timestamp: "2025-10-01T12:00:30Z", - level: "info", - msg: "User said: Thank you!", - type: "speech", - }, - { - id: "8", - timestamp: "2025-10-01T12:00:35Z", - level: "debug", - msg: "Network latency: 120ms", - type: "system", - }, - { - id: "9", - timestamp: "2025-10-01T12:00:40Z", - level: "warn", - msg: "High CPU usage detected.", - type: "system", - }, - { - id: "10", - timestamp: "2025-10-01T12:00:45Z", - level: "info", - msg: "User started a new session.", - type: "system", - }, - { - id: "11", - timestamp: "2025-10-01T12:01:00Z", - level: "info", - msg: "User asked: What's the weather?", - type: "speech", - }, - { - id: "12", - timestamp: "2025-10-01T12:01:05Z", - level: "debug", - msg: "Camera initialized.", - type: "system", - }, - { - id: "13", - timestamp: "2025-10-01T12:01:10Z", - level: "warn", - msg: "Temperature sensor disconnected.", - type: "sensor", - }, - { - id: "14", - timestamp: "2025-10-01T12:01:15Z", - level: "info", - msg: "User said: Play some music.", - type: "speech", - }, - { - id: "15", - timestamp: "2025-10-01T12:01:20Z", - level: "debug", - msg: "Audio output device selected: Speaker.", - type: "system", - }, - { - id: "16", - timestamp: "2025-10-01T12:01:25Z", - level: "warn", - msg: "Low light detected in room.", - type: "sensor", - }, - { - id: "17", - timestamp: "2025-10-01T12:01:30Z", - level: "info", - msg: "User said: Turn on the lights.", - type: "speech", - }, - { - id: "18", - timestamp: "2025-10-01T12:01:35Z", - level: "debug", - msg: "Light control signal sent.", - type: "system", - }, - { - id: "19", - timestamp: "2025-10-01T12:01:40Z", - level: "warn", - msg: "Light bulb not responding.", - type: "system", - }, - { - id: "20", - timestamp: "2025-10-01T12:01:45Z", - level: "info", - msg: "User said: Good night.", - type: "speech", - }, - { - id: "21", - timestamp: "2025-10-01T12:02:00Z", - level: "info", - msg: "User asked: What's the time?", - type: "speech", - }, - { - id: "22", - timestamp: "2025-10-01T12:02:05Z", - level: "debug", - msg: "Time module loaded.", - type: "system", - }, - { - id: "23", - timestamp: "2025-10-01T12:02:10Z", - level: "warn", - msg: "WiFi signal weak.", - type: "system", - }, - { - id: "24", - timestamp: "2025-10-01T12:02:15Z", - level: "info", - msg: "User said: Set an alarm for 7 AM.", - type: "speech", - }, - { - id: "25", - timestamp: "2025-10-01T12:02:20Z", - level: "debug", - msg: "Alarm scheduled for 7:00 AM.", - type: "system", - }, - { - id: "26", - timestamp: "2025-10-01T12:02:25Z", - level: "warn", - msg: "Alarm module not responding.", - type: "system", - }, - { - id: "27", - timestamp: "2025-10-01T12:02:30Z", - level: "info", - msg: "User said: Cancel the alarm.", - type: "speech", - }, - { - id: "28", - timestamp: "2025-10-01T12:02:35Z", - level: "debug", - msg: "Alarm cancellation requested.", - type: "system", - }, - { - id: "29", - timestamp: "2025-10-01T12:02:40Z", - level: "warn", - msg: "Alarm cancellation failed.", - type: "system", - }, - { - id: "30", - timestamp: "2025-10-01T12:02:45Z", - level: "info", - msg: "User said: Open the window.", - type: "speech", - }, - { - id: "31", - timestamp: "2025-10-01T12:03:00Z", - level: "info", - msg: "User asked: What's on my calendar?", - type: "speech", - }, - { - id: "32", - timestamp: "2025-10-01T12:03:05Z", - level: "debug", - msg: "Calendar module loaded.", - type: "system", - }, - { - id: "33", - timestamp: "2025-10-01T12:03:10Z", - level: "warn", - msg: "Calendar sync failed.", - type: "system", - }, - { - id: "34", - timestamp: "2025-10-01T12:03:15Z", - level: "info", - msg: "User said: Remind me to call John.", - type: "speech", - }, - { - id: "35", - timestamp: "2025-10-01T12:03:20Z", - level: "debug", - msg: "Reminder set for John.", - type: "system", - }, - { - id: "36", - timestamp: "2025-10-01T12:03:25Z", - level: "warn", - msg: "Reminder module not available.", - type: "system", - }, - { - id: "37", - timestamp: "2025-10-01T12:03:30Z", - level: "info", - msg: "User said: What's the news?", - type: "speech", - }, - { - id: "38", - timestamp: "2025-10-01T12:03:35Z", - level: "debug", - msg: "News API request sent.", - type: "system", - }, - { - id: "39", - timestamp: "2025-10-01T12:03:40Z", - level: "warn", - msg: "News API rate limit reached.", - type: "system", - }, - { - id: "40", - timestamp: "2025-10-01T12:03:45Z", - level: "info", - msg: "User said: Tell me a joke.", - type: "speech", - }, - { - id: "41", - timestamp: "2025-10-01T12:04:00Z", - level: "info", - msg: "User asked: What's the temperature?", - type: "speech", - }, - { - id: "42", - timestamp: "2025-10-01T12:04:05Z", - level: "debug", - msg: "Temperature sensor reading: 22°C.", - type: "sensor", - }, - { - id: "43", - timestamp: "2025-10-01T12:04:10Z", - level: "warn", - msg: "Temperature sensor calibration needed.", - type: "sensor", - }, - { - id: "44", - timestamp: "2025-10-01T12:04:15Z", - level: "info", - msg: "User said: Start cleaning.", - type: "speech", - }, - { - id: "45", - timestamp: "2025-10-01T12:04:20Z", - level: "debug", - msg: "Vacuum motor started.", - type: "system", - }, - { - id: "46", - timestamp: "2025-10-01T12:04:25Z", - level: "warn", - msg: "Vacuum bin full.", - type: "system", - }, - { - id: "47", - timestamp: "2025-10-01T12:04:30Z", - level: "info", - msg: "User said: Stop cleaning.", - type: "speech", - }, - { - id: "48", - timestamp: "2025-10-01T12:04:35Z", - level: "debug", - msg: "Vacuum motor stopped.", - type: "system", - }, - { - id: "49", - timestamp: "2025-10-01T12:04:40Z", - level: "warn", - msg: "Obstacle detected during cleaning.", - type: "sensor", - }, - { - id: "50", - timestamp: "2025-10-01T12:04:45Z", - level: "info", - msg: "User said: Goodbye!", - type: "speech", - }, -]; - -interface LogEntry { - id: string; - type?: string; - timestamp: string; - level: "info" | "debug" | "warn"; - msg?: string; -} - diff --git a/src/pages/Logging/Logging.module.css b/src/pages/Logging/Logging.module.css deleted file mode 100644 index 52191c3..0000000 --- a/src/pages/Logging/Logging.module.css +++ /dev/null @@ -1,17 +0,0 @@ -.DivToScroll{ - background-color: color-mix(in srgb, canvas, #000 5%); - border: 1px solid color-mix(in srgb, canvas, #000 15%); - border-radius: 4px 0 4px 0; - color: #3B3C3E; - font-size: 12px; - font-weight: bold; - left: -1px; - padding: 10px 7px 5px; -} - -.DivWithScroll{ - height:50vh; - width:100vh; - overflow:scroll; - overflow-x:hidden; -} diff --git a/src/pages/Logging/Logging.tsx b/src/pages/Logging/Logging.tsx deleted file mode 100644 index 31945c1..0000000 --- a/src/pages/Logging/Logging.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useState } from 'react'; -import { DATA } from "../../assets/data"; -import styles from './Logging.module.css'; - - -// const dataType = DATA as { id: string; level: "debug"|"info"|"warn"|"error"; msg: string; timestamp?: string }; -type Level = "debug" | "info" | "warn" | "error"; - -// make optional fields optional -type LogEntry = { - id: string; - level: Level; - timestamp?: string; - msg?: string; - type?: "speech" | "sensor" | "system" | string; - -}; - -function getLevelColor(level: Level) { - switch (level) { - case "debug": - return "gray"; - case "info": - return "blue"; - case "warn": - return "red"; - case "error": - return "red"; - default: - return "black"; - } -} - -function Logging() { - const [logs, setLogs] = useState([]); - - const logDiv = ( -
-
- {logs.map((log) => ( -
- - [{log.timestamp}] - - - {log.msg ? log.msg : "No message"} - - - ({log.level}) - -
- ))} -
-
- ) - return ( - <> -

Log Screen

- { logDiv } -
- - -
- - ) -} - -export default Logging From 87cf723c95d2f0e235a53ce6d2d42273f80f8dea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Tue, 11 Nov 2025 11:42:28 +0100 Subject: [PATCH 14/14] chore: fixed merge request suggestion for adding depency array --- src/pages/ConnectedRobots/ConnectedRobots.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/ConnectedRobots/ConnectedRobots.tsx b/src/pages/ConnectedRobots/ConnectedRobots.tsx index 8b4b898..b7ec65f 100644 --- a/src/pages/ConnectedRobots/ConnectedRobots.tsx +++ b/src/pages/ConnectedRobots/ConnectedRobots.tsx @@ -4,8 +4,6 @@ export default function ConnectedRobots() { const [connected, setConnected] = useState(null); - - useEffect(() => { // We're excepting a stream of data like that looks like this: `data = False` or `data = True` const eventSource = new EventSource("http://localhost:8000/robot/ping_stream"); @@ -29,7 +27,7 @@ export default function ConnectedRobots() { } }; return () => eventSource.close(); - }); + }, []); return (