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:
241
package-lock.json
generated
241
package-lock.json
generated
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
109
src/App.css
109
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;
|
||||
}
|
||||
10
src/App.tsx
10
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 (
|
||||
<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
361
src/assets/data.ts
Normal 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;
|
||||
}
|
||||
|
||||
@@ -4,3 +4,9 @@
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
17
src/pages/Logging/Logging.module.css
Normal file
17
src/pages/Logging/Logging.module.css
Normal 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;
|
||||
}
|
||||
78
src/pages/Logging/Logging.tsx
Normal file
78
src/pages/Logging/Logging.tsx
Normal 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
|
||||
@@ -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
|
||||
11
src/pages/VisProgPage/VisProg.tsx
Normal file
11
src/pages/VisProgPage/VisProg.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import VisProgUI from "../../visualProgrammingUI/VisProgUI.tsx";
|
||||
|
||||
function VisProgPage() {
|
||||
return (
|
||||
<>
|
||||
<VisProgUI />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default VisProgPage
|
||||
7
src/visualProgrammingUI/VisProgUI.css
Normal file
7
src/visualProgrammingUI/VisProgUI.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.default-node {
|
||||
padding: 10px 20px;
|
||||
background-color: canvas;
|
||||
outline-style: solid;
|
||||
border-radius: 5pt;
|
||||
outline-width: 1pt;
|
||||
}
|
||||
132
src/visualProgrammingUI/VisProgUI.tsx
Normal file
132
src/visualProgrammingUI/VisProgUI.tsx
Normal 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;
|
||||
141
src/visualProgrammingUI/components/DragDropSidebar.tsx
Normal file
141
src/visualProgrammingUI/components/DragDropSidebar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
111
src/visualProgrammingUI/components/NodeDefinitions.tsx
Normal file
111
src/visualProgrammingUI/components/NodeDefinitions.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user