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.
This commit is contained in:
Twirre Meulenbelt
2025-10-01 22:56:03 +02:00
parent 96053e798a
commit 10522b71c3
16 changed files with 1251 additions and 116 deletions

241
package-lock.json generated
View File

@@ -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
}
}
}
}
}

View File

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

View File

@@ -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;
}

View File

@@ -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 (
<div>
{/* Should not use inline styles like this */}
<Link style={{position: "fixed", top: "1rem", left: "50%", translate: "-50%"}} to={"/"}>Home</Link>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/Template" element={<TemplatePage />} />
<Route path="/ServerComms" element = {<ServerComms />} />
<Route path="/visprog" element={<VisProg />} />
<Route path="/logging" element = {<Logging />} />
</Routes>
</div>
)
}

361
src/assets/data.ts Normal file
View File

@@ -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;
}

View File

@@ -4,3 +4,9 @@
.card {
padding: 2em;
}
.links {
display: flex;
flex-direction: column;
gap: 1em;
}

View File

@@ -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 (
<>
<div>
<div className = "logoPepperScaling">
<div className="logoPepperScaling">
<a href="https://git.science.uu.nl/ics/sp/2025/n25b" target="_blank">
<img src={pepperLogo} className="logopepper" alt="Pepper logo" />
</a>
</div>
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
<div className={styles.links}>
<Link to={"/ServerComms"}>Robot interaction </Link>
<Link to={"/visprog"}>Node editor </Link>
<Link to={"/logging"}>Logs </Link>
</div>
<h1>Vite + React</h1>
<Counter />
<Link to = '/ServerComms'>
<button className='movePage right' onClick={() : void => {}}>
Page Cool --{'>'}
</button>
</Link>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
<p className={style.read_the_docs}>
Click on the Vite and React logos to learn more
</p>
</>
)
}

View File

@@ -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;
}

View File

@@ -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<LogEntry[]>([]);
const logDiv = (
<div className={styles.DivToScroll}>
<div className={styles.DivWithScroll}>
{logs.map((log) => (
<div
key={log.id}
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "4px 0",
}}
>
<span style={{ color: "darkgrey", minWidth: 120, textAlign: "left" }}>
[{log.timestamp}]
</span>
<span style={{ flex: 1, textAlign: "center" }}>
{log.msg ? log.msg : "No message"}
</span>
<span style={{ color: getLevelColor(log.level), minWidth: 80, textAlign: "right" }}>
({log.level})
</span>
</div>
))}
</div>
</div>
)
return (
<>
<h1>Log Screen</h1>
{ logDiv }
<div className="card">
<button onClick={() => setLogs(DATA)}>
Load sample logs
</button>
</div>
</>
)
}
export default Logging

View File

@@ -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<string>("");
const [listening, setListening] = useState(false);
const [conversation, setConversation] = useState<{"role": "user" | "assistant", "content": string}[]>([])
const conversationRef = useRef<HTMLDivElement | null>(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 (
<div className="App">
<div>
<h1>Robot interaction</h1>
<h2>Force robot speech</h2>
<div className={"flex-row gap-md justify-center"}>
<input
type="text"
value={message}
@@ -54,27 +60,35 @@ function ServerComms() {
onKeyDown={(e) => e.key === "Enter" && sendMessage().then(() => setMessage(""))}
placeholder="Enter a message"
/>
<button onClick={sendMessage}>Send Message to Backend</button>
<button onClick={sendMessage}>Speak</button>
</div>
<div>
<h2>Message from Server (SSE):</h2>
<p>{sseMessage}</p>
<div className={"flex-col gap-lg"}>
<h2>Conversation</h2>
<p>Listening {listening ? "🟢" : "🔴"}</p>
<div style={{ maxHeight: "200px", maxWidth: "600px", overflowY: "auto"}} ref={conversationRef}>
{conversation.map((item) => (
<p
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>
<h2>Spoken text (SSE):</h2>
<p>{spoken}</p>
</div>
<div>
<Link to = {"/"}> {/* here you link to the homepage, in App.tsx you can link new pages */}
<button className= 'movePage left' onClick={() :void => {}}>
Page {'<'}-- Go Home
</button>
</Link>
</div>
</div>
);
}
export default ServerComms

View File

@@ -0,0 +1,11 @@
import VisProgUI from "../../visualProgrammingUI/VisProgUI.tsx";
function VisProgPage() {
return (
<>
<VisProgUI />
</>
)
}
export default VisProgPage

View File

@@ -0,0 +1,7 @@
.default-node {
padding: 10px 20px;
background-color: canvas;
outline-style: solid;
border-radius: 5pt;
outline-width: 1pt;
}

View File

@@ -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 (
<div style={{marginInline: 'auto',display: 'flex',justifySelf: 'center', padding:'10px', alignItems: 'center', width: '80vw', height: '60vh'}}>
<div style={{outlineStyle: 'solid', borderRadius: '10pt', marginInline: '1em',width: '70%', height:'100%' }}>
<ReactFlow
nodes={nodes}
edges={edges}
defaultEdgeOptions={defaultEdgeOptions}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={nodeTypes}
snapToGrid
onReconnect={onReconnect}
onReconnectStart={onReconnectStart}
onReconnectEnd={onReconnectEnd}
onConnect={onConnect}
fitView
proOptions={{hideAttribution: true }}
>
<Controls />
<Background />
</ReactFlow>
</div>
<div style={{width: '20%', height: '100%'}}>
<Sidebar />
</div>
</div>
);
};
function VisualProgrammingUI(){
return (
<ReactFlowProvider>
<VisProgUI />
</ReactFlowProvider>
);
}
export default VisualProgrammingUI;

View File

@@ -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<HTMLDivElement>(null);
const [position, setPosition] = useState<XYPosition>({ 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 (
<div className={className === "default" ? "default-node" : "default-node" + "__" + className} ref={draggableRef}>
{children}
</div>
);
}
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 (
<aside>
<div className="description">
You can drag these nodes to the pane to create new nodes.
</div>
<DraggableNode className="default" nodeType="start" onDrop={handleNodeDrop}>
start Node
</DraggableNode>
<DraggableNode className="default" nodeType="end" onDrop={handleNodeDrop}>
end Node
</DraggableNode>
<DraggableNode className="default" nodeType="phase" onDrop={handleNodeDrop}>
phase Node
</DraggableNode>
<DraggableNode className="default" nodeType="norm" onDrop={handleNodeDrop}>
norm Node
</DraggableNode>
</aside>
);
}

View File

@@ -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 (
<NodeToolbar >
<button className="Node-toolbar__deletebutton" onClick={handleDelete}>delete</button>
</NodeToolbar>);
}
// Definitions of Nodes
type StartNodeProps = {
id: string;
data: startNodeData;
};
export const StartNode= ({ id, data }: StartNodeProps) => {
return (
<>
<Toolbar nodeId={id} />
<div className="default-node">
<div> data test {data.label} </div>
<Handle type="source" position={Position.Right} id="start" />
</div>
</>
);
};
type EndNodeProps = {
id: string;
data: endNodeData;
};
export const EndNode= ({ id, data }: EndNodeProps) => {
return (
<>
<Toolbar nodeId={id}/>
<div className="default-node">
<div> {data.label} </div>
<Handle type="target" position={Position.Left} id="end" />
</div>
</>
);
};
type PhaseNodeProps = {
id: string;
data: phaseNodeData;
};
export const PhaseNode= ({ id, data }: PhaseNodeProps) => {
return (
<>
<Toolbar nodeId={id}/>
<div className="default-node">
<div> phase {data.number} {data.label} </div>
<Handle type="target" position={Position.Left} id="target" />
<Handle type="target" position={Position.Bottom } id="norms" />
<Handle type="source" position={Position.Right} id="source" />
</div>
</>
);
};
type NormNodeProps = {
id: string;
data: normNodeData;
};
export const NormNode= ({ id, data }: NormNodeProps) => {
return (
<>
<Toolbar nodeId={id} />
<div className="default-node">
<div> Norm {data.label} </div>
<Handle type="source" position={Position.Right} id="NormSource" />
</div>
</>
);
};