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", "name": "pepperplus-ui",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@neodrag/react": "^2.3.1",
"@xyflow/react": "^12.8.6",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-router": "^7.9.3" "react-router": "^7.9.3"
@@ -1006,6 +1008,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@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": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1404,6 +1412,55 @@
"@babel/types": "^7.28.2" "@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": { "node_modules/@types/estree": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1422,7 +1479,7 @@
"version": "19.1.13", "version": "19.1.13",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz",
"integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
@@ -1730,6 +1787,38 @@
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" "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": { "node_modules/acorn": {
"version": "8.15.0", "version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -1916,6 +2005,12 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "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": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -1978,9 +2073,114 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true, "devOptional": true,
"license": "MIT" "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": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -3287,6 +3487,15 @@
"punycode": "^2.1.0" "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": { "node_modules/vite": {
"version": "7.1.7", "version": "7.1.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz",
@@ -3438,6 +3647,34 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "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" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@neodrag/react": "^2.3.1",
"@xyflow/react": "^12.8.6",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-router": "^7.9.3" "react-router": "^7.9.3"

View File

@@ -5,18 +5,6 @@
text-align: center; 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 { .logopepper {
@@ -32,27 +20,21 @@
filter: drop-shadow(0 0 10em #4eff14aa); filter: drop-shadow(0 0 10em #4eff14aa);
} }
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes logo-pepper-spin { @keyframes logo-pepper-spin {
from { 0% {
transform: rotate(-20deg); transform: rotate(0);
} }
to { 25% {
transform: rotate(20deg); transform: rotate(20deg);
} }
75% {
transform: rotate(-20deg);
}
100% {
transform: rotate(0);
}
} }
@keyframes logo-pepper-scale { @keyframes logo-pepper-scale {
from { from {
transform: scale(1,1); transform: scale(1,1);
@@ -63,19 +45,13 @@
} }
@media (prefers-reduced-motion: no-preference) { @media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo { .logopepper:hover {
animation: logo-spin infinite 20s linear; animation: logo-pepper-spin infinite 1s linear;
} }
} }
@media (prefers-reduced-motion: no-preference) { @media (prefers-reduced-motion: no-preference) {
.logopepper { .logoPepperScaling:hover {
animation: logo-pepper-spin infinite 1s linear alternate;
}
}
@media (prefers-reduced-motion: no-preference) {
.logoPepperScaling {
animation: logo-pepper-scale infinite 1s linear alternate; animation: logo-pepper-scale infinite 1s linear alternate;
} }
} }
@@ -113,3 +89,64 @@ button.movePage.right{
button.movePage:hover{ button.movePage:hover{
background-color: rgb(0, 176, 176); 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 './App.css'
import TemplatePage from './pages/TemplatePage/Template.tsx' import TemplatePage from './pages/TemplatePage/Template.tsx'
import Home from './pages/Home/Home.tsx' import Home from './pages/Home/Home.tsx'
import ServerComms from './pages/ServerComms/ServerComms.tsx' import ServerComms from './pages/ServerComms/ServerComms.tsx'
import Logging from './pages/Logging/Logging.tsx'
import VisProg from "./pages/VisProgPage/VisProg.tsx";
function App(){ function App(){
return ( return (
<Routes> <div>
<Route path="/" element={<Home />} /> {/* Should not use inline styles like this */}
<Route path="/Template" element={<TemplatePage />} /> <Link style={{position: "fixed", top: "1rem", left: "50%", translate: "-50%"}} to={"/"}>Home</Link>
<Route path="/ServerComms" element = {<ServerComms />} /> <Routes>
</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 { .card {
padding: 2em; 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 { 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 pepperLogo from '../../assets/pepper_transp2_small.svg'
import style from './Home.module.css' import styles from './Home.module.css'
import Counter from '../../components/components.tsx'
function Home() { function Home() {
return ( return (
<> <>
<div> <div className="logoPepperScaling">
<div className = "logoPepperScaling"> <a href="https://git.science.uu.nl/ics/sp/2025/n25b" target="_blank">
<a href="https://git.science.uu.nl/ics/sp/2025/n25b" target="_blank"> <img src={pepperLogo} className="logopepper" alt="Pepper logo" />
<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> </a>
</div> </div>
<h1>Vite + React</h1> <div className={styles.links}>
<Link to={"/ServerComms"}>Robot interaction </Link>
<Counter /> <Link to={"/visprog"}>Node editor </Link>
<Link to = '/ServerComms'> <Link to={"/logging"}>Logs </Link>
<button className='movePage right' onClick={() : void => {}}> </div>
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 { useState, useEffect, useRef } from 'react'
import { Link } from 'react-router'
//import Counter from '../../components/components.tsx'
export default function ServerComms() {
//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() {
const [message, setMessage] = useState(''); 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 () => { const sendMessage = async () => {
try { try {
@@ -31,22 +28,31 @@ function ServerComms() {
const eventSource = new EventSource("http://localhost:8000/sse"); const eventSource = new EventSource("http://localhost:8000/sse");
eventSource.onmessage = (event) => { eventSource.onmessage = (event) => {
setSseMessage(event.data);
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
if (data.speech) setSpoken(data.speech); if ("voice_active" in data) setListening(data.voice_active);
} catch {} 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 () => { return () => {
eventSource.close(); eventSource.close();
}; };
}, []); }, [conversationIndex]);
useEffect(() => {
if (!conversationRef || !conversationRef.current) return;
conversationRef.current.scrollTop = conversationRef.current.scrollHeight;
}, [conversation]);
return ( return (
<div className="App"> <div>
<div> <h1>Robot interaction</h1>
<h2>Force robot speech</h2>
<div className={"flex-row gap-md justify-center"}>
<input <input
type="text" type="text"
value={message} value={message}
@@ -54,27 +60,35 @@ function ServerComms() {
onKeyDown={(e) => e.key === "Enter" && sendMessage().then(() => setMessage(""))} onKeyDown={(e) => e.key === "Enter" && sendMessage().then(() => setMessage(""))}
placeholder="Enter a message" placeholder="Enter a message"
/> />
<button onClick={sendMessage}>Send Message to Backend</button> <button onClick={sendMessage}>Speak</button>
</div> </div>
<div> <div className={"flex-col gap-lg"}>
<h2>Message from Server (SSE):</h2> <h2>Conversation</h2>
<p>{sseMessage}</p> <p>Listening {listening ? "🟢" : "🔴"}</p>
</div> <div style={{ maxHeight: "200px", maxWidth: "600px", overflowY: "auto"}} ref={conversationRef}>
<div> {conversation.map((item) => (
<h2>Spoken text (SSE):</h2> <p
<p>{spoken}</p> style={{
</div> backgroundColor: item["role"] == "user"
<div> ? "color-mix(in oklab, canvas, blue 20%)"
<Link to = {"/"}> {/* here you link to the homepage, in App.tsx you can link new pages */} : "color-mix(in oklab, canvas, gray 20%)",
<button className= 'movePage left' onClick={() :void => {}}> whiteSpace: "pre-line",
Page {'<'}-- Go Home }}
</button> className={"round-md padding-md"}
</Link> >{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> </div>
</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>
</>
);
};