Compare commits
33 Commits
feat/monit
...
af196529f8
| Author | SHA1 | Date | |
|---|---|---|---|
| af196529f8 | |||
| 901159ae2d | |||
| 4dcbe78abf | |||
|
|
f626a6571a | ||
| 268199a825 | |||
|
|
84bb8c5ae8 | ||
|
|
d3501cb063 | ||
|
|
00d605164c | ||
| 7deecaa160 | |||
|
|
72993b7576 | ||
|
|
4fa3946a10 | ||
| 07ad746c9d | |||
|
|
55fa4f3a8b | ||
| d919eb8471 | |||
| c0e8331fbf | |||
|
|
d514c2ef50 | ||
|
|
179b8fd75b | ||
|
|
4395e44dbf | ||
|
|
e3abf8c14a | ||
|
|
1b9dddcbf2 | ||
| 378a64c7ca | |||
|
|
9e06bf079b | ||
|
|
eb5a6cddd7 | ||
|
|
60f7bad5d1 | ||
|
|
835de03a29 | ||
|
|
78901ee25b | ||
|
|
82785dc8cb | ||
|
|
f9e0eb95f8 | ||
|
|
47c5e94b8f | ||
|
|
b17d1e7618 | ||
|
|
ec211ccbc3 | ||
|
|
6f4471ce6f | ||
| cca98dbebe |
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
# The location of the backend
|
||||
VITE_API_BASE_URL=http://localhost:8000
|
||||
@@ -28,6 +28,14 @@ npm run dev
|
||||
|
||||
It should automatically reload when you save changes.
|
||||
|
||||
## Environment
|
||||
|
||||
Copy `.env.example` to `.env.local` and adjust values as needed:
|
||||
|
||||
```shell
|
||||
cp .env.example .env.local
|
||||
```
|
||||
|
||||
## Git Hooks
|
||||
|
||||
To activate automatic linting, branch name checks and commit message checks, run:
|
||||
|
||||
532
package-lock.json
generated
532
package-lock.json
generated
@@ -10,9 +10,11 @@
|
||||
"dependencies": {
|
||||
"@neodrag/react": "^2.3.1",
|
||||
"@xyflow/react": "^12.8.6",
|
||||
"clsx": "^2.1.1",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-router": "^7.9.3",
|
||||
"reactflow": "^11.11.4",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -29,7 +31,6 @@
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.4.0",
|
||||
"husky": "^9.1.7",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^30.2.0",
|
||||
"jest-environment-jsdom": "^30.2.0",
|
||||
@@ -2053,6 +2054,276 @@
|
||||
"url": "https://opencollective.com/pkgr"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/background": {
|
||||
"version": "11.3.14",
|
||||
"resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz",
|
||||
"integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reactflow/core": "11.11.4",
|
||||
"classcat": "^5.0.3",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/background/node_modules/zustand": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/controls": {
|
||||
"version": "11.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz",
|
||||
"integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reactflow/core": "11.11.4",
|
||||
"classcat": "^5.0.3",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/controls/node_modules/zustand": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/core": {
|
||||
"version": "11.11.4",
|
||||
"resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz",
|
||||
"integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/d3-drag": "^3.0.1",
|
||||
"@types/d3-selection": "^3.0.3",
|
||||
"@types/d3-zoom": "^3.0.1",
|
||||
"classcat": "^5.0.3",
|
||||
"d3-drag": "^3.0.0",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-zoom": "^3.0.0",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/core/node_modules/zustand": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/minimap": {
|
||||
"version": "11.7.14",
|
||||
"resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz",
|
||||
"integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reactflow/core": "11.11.4",
|
||||
"@types/d3-selection": "^3.0.3",
|
||||
"@types/d3-zoom": "^3.0.1",
|
||||
"classcat": "^5.0.3",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-zoom": "^3.0.0",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/minimap/node_modules/zustand": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/node-resizer": {
|
||||
"version": "2.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz",
|
||||
"integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reactflow/core": "11.11.4",
|
||||
"classcat": "^5.0.4",
|
||||
"d3-drag": "^3.0.0",
|
||||
"d3-selection": "^3.0.0",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/node-resizer/node_modules/zustand": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/node-toolbar": {
|
||||
"version": "1.3.14",
|
||||
"resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz",
|
||||
"integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reactflow/core": "11.11.4",
|
||||
"classcat": "^5.0.3",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/node-toolbar/node_modules/zustand": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.35",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.35.tgz",
|
||||
@@ -2647,12 +2918,102 @@
|
||||
"@babel/types": "^7.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
|
||||
"integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-array": "*",
|
||||
"@types/d3-axis": "*",
|
||||
"@types/d3-brush": "*",
|
||||
"@types/d3-chord": "*",
|
||||
"@types/d3-color": "*",
|
||||
"@types/d3-contour": "*",
|
||||
"@types/d3-delaunay": "*",
|
||||
"@types/d3-dispatch": "*",
|
||||
"@types/d3-drag": "*",
|
||||
"@types/d3-dsv": "*",
|
||||
"@types/d3-ease": "*",
|
||||
"@types/d3-fetch": "*",
|
||||
"@types/d3-force": "*",
|
||||
"@types/d3-format": "*",
|
||||
"@types/d3-geo": "*",
|
||||
"@types/d3-hierarchy": "*",
|
||||
"@types/d3-interpolate": "*",
|
||||
"@types/d3-path": "*",
|
||||
"@types/d3-polygon": "*",
|
||||
"@types/d3-quadtree": "*",
|
||||
"@types/d3-random": "*",
|
||||
"@types/d3-scale": "*",
|
||||
"@types/d3-scale-chromatic": "*",
|
||||
"@types/d3-selection": "*",
|
||||
"@types/d3-shape": "*",
|
||||
"@types/d3-time": "*",
|
||||
"@types/d3-time-format": "*",
|
||||
"@types/d3-timer": "*",
|
||||
"@types/d3-transition": "*",
|
||||
"@types/d3-zoom": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-axis": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
|
||||
"integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-brush": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
|
||||
"integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-chord": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
|
||||
"integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"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-contour": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
|
||||
"integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-array": "*",
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-delaunay": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
|
||||
"integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-dispatch": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
|
||||
"integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
|
||||
"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",
|
||||
@@ -2662,6 +3023,54 @@
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-dsv": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
|
||||
"integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-ease": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-fetch": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
|
||||
"integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-dsv": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-force": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
|
||||
"integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-format": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
|
||||
"integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-geo": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
|
||||
"integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-hierarchy": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
|
||||
"integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
@@ -2671,12 +3080,78 @@
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-path": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-polygon": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
|
||||
"integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-quadtree": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
|
||||
"integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-random": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
|
||||
"integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-scale": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-time": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-scale-chromatic": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
||||
"integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"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-shape": {
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
|
||||
"integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-path": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-time": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-time-format": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
|
||||
"integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-timer": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||
"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",
|
||||
@@ -2703,6 +3178,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
"version": "7946.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||
@@ -3971,6 +4452,15 @@
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
@@ -5053,22 +5543,6 @@
|
||||
"node": ">=10.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/husky": {
|
||||
"version": "9.1.7",
|
||||
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
|
||||
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"husky": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/typicode"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
@@ -6945,9 +7419,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.9.3",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.3.tgz",
|
||||
"integrity": "sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg==",
|
||||
"version": "7.12.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz",
|
||||
"integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
@@ -6966,6 +7440,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/reactflow": {
|
||||
"version": "11.11.4",
|
||||
"resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz",
|
||||
"integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reactflow/background": "11.3.14",
|
||||
"@reactflow/controls": "11.2.14",
|
||||
"@reactflow/core": "11.11.4",
|
||||
"@reactflow/minimap": "11.7.14",
|
||||
"@reactflow/node-resizer": "2.2.14",
|
||||
"@reactflow/node-toolbar": "1.3.14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/redent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||
|
||||
@@ -8,15 +8,16 @@
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint src test",
|
||||
"preview": "vite preview",
|
||||
"test": "jest",
|
||||
"prepare": "husky"
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@neodrag/react": "^2.3.1",
|
||||
"@xyflow/react": "^12.8.6",
|
||||
"clsx": "^2.1.1",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-router": "^7.9.3",
|
||||
"reactflow": "^11.11.4",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -33,7 +34,6 @@
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.4.0",
|
||||
"husky": "^9.1.7",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^30.2.0",
|
||||
"jest-environment-jsdom": "^30.2.0",
|
||||
|
||||
BIN
public/DeveloperManual.pdf
Normal file
BIN
public/DeveloperManual.pdf
Normal file
Binary file not shown.
BIN
public/UserManual.pdf
Normal file
BIN
public/UserManual.pdf
Normal file
Binary file not shown.
43
src/App.css
43
src/App.css
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
University within the Software Project course.
|
||||
© Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
*/
|
||||
.logopepper {
|
||||
height: 8em;
|
||||
padding: 1.5em;
|
||||
@@ -161,7 +166,13 @@ input[type="checkbox"] {
|
||||
.margin-0 {
|
||||
margin: 0;
|
||||
}
|
||||
.margin-lg {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.padding-0 {
|
||||
padding: 0;
|
||||
}
|
||||
.padding-sm {
|
||||
padding: .25rem;
|
||||
}
|
||||
@@ -171,11 +182,9 @@ input[type="checkbox"] {
|
||||
.padding-lg {
|
||||
padding: 1rem;
|
||||
}
|
||||
.padding-b-sm {
|
||||
padding-bottom: .25rem;
|
||||
}
|
||||
.padding-b-md {
|
||||
padding-bottom: .5rem;
|
||||
.padding-h-lg {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.padding-b-lg {
|
||||
padding-bottom: 1rem;
|
||||
@@ -204,6 +213,27 @@ input[type="checkbox"] {
|
||||
border: 3px solid canvastext;
|
||||
}
|
||||
|
||||
.shadow-sm {
|
||||
box-shadow: 0 0 0.25rem rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.shadow-md {
|
||||
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.shadow-lg {
|
||||
box-shadow: 0 0 1rem rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.shadow-sm {
|
||||
box-shadow: 0 0 0.25rem rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.shadow-md {
|
||||
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.shadow-lg {
|
||||
box-shadow: 0 0 1rem rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.font-small {
|
||||
font-size: .75rem;
|
||||
}
|
||||
@@ -220,6 +250,9 @@ input[type="checkbox"] {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
|
||||
15
src/App.tsx
15
src/App.tsx
@@ -1,30 +1,31 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import { Routes, Route, Link } from 'react-router'
|
||||
import './App.css'
|
||||
import TemplatePage from './pages/TemplatePage/Template.tsx'
|
||||
import Home from './pages/Home/Home.tsx'
|
||||
import Robot from './pages/Robot/Robot.tsx';
|
||||
import ConnectedRobots from './pages/ConnectedRobots/ConnectedRobots.tsx'
|
||||
import UserManual from './pages/Manuals/Manuals.tsx';
|
||||
import VisProg from "./pages/VisProgPage/VisProg.tsx";
|
||||
import {useState} from "react";
|
||||
import Logging from "./components/Logging/Logging.tsx";
|
||||
|
||||
|
||||
function App(){
|
||||
const [showLogs, setShowLogs] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header>
|
||||
<span>© Utrecht University (ICS)</span>
|
||||
<Link to={"/"}>Home</Link>
|
||||
<button onClick={() => setShowLogs(!showLogs)}>Toggle Logging</button>
|
||||
<button onClick={() => setShowLogs(!showLogs)}>Developer Logs</button>
|
||||
</header>
|
||||
<div className={"flex-row justify-center flex-1 min-height-0"}>
|
||||
<main className={"flex-col align-center flex-1 scroll-y"}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/template" element={<TemplatePage />} />
|
||||
<Route path="/editor" element={<VisProg />} />
|
||||
<Route path="/robot" element={<Robot />} />
|
||||
<Route path="/ConnectedRobots" element={<ConnectedRobots />} />
|
||||
<Route path="/user_manual" element={<UserManual />} />
|
||||
</Routes>
|
||||
</main>
|
||||
{showLogs && <Logging />}
|
||||
|
||||
48
src/components/Dialog.tsx
Normal file
48
src/components/Dialog.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import {type ReactNode, type RefObject, useEffect, useRef} from "react";
|
||||
|
||||
export default function Dialog({
|
||||
open,
|
||||
close,
|
||||
classname,
|
||||
children,
|
||||
}: {
|
||||
open: boolean;
|
||||
close: () => void;
|
||||
classname?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const ref: RefObject<HTMLDialogElement | null> = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
ref.current?.showModal();
|
||||
} else {
|
||||
ref.current?.close();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
function handleClickOutside(event: React.MouseEvent) {
|
||||
if (!ref.current) return;
|
||||
|
||||
const dialogDimensions = ref.current.getBoundingClientRect()
|
||||
if (
|
||||
event.clientX < dialogDimensions.left ||
|
||||
event.clientX > dialogDimensions.right ||
|
||||
event.clientY < dialogDimensions.top ||
|
||||
event.clientY > dialogDimensions.bottom
|
||||
) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<dialog
|
||||
ref={ref}
|
||||
onCancel={close}
|
||||
onPointerDown={handleClickOutside}
|
||||
className={classname}
|
||||
>
|
||||
{children}
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
5
src/components/Icons/Next.tsx
Normal file
5
src/components/Icons/Next.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
export default function Next({ fill }: { fill?: string }) {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
|
||||
<path d="M664.07-224.93v-510.14h91v510.14h-91Zm-459.14 0v-510.14L587.65-480 204.93-224.93Z"/>
|
||||
</svg>;
|
||||
}
|
||||
5
src/components/Icons/Pause.tsx
Normal file
5
src/components/Icons/Pause.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
export default function Pause({ fill }: { fill?: string }) {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
|
||||
<path d="M556.17-185.41v-589.18h182v589.18h-182Zm-334.34 0v-589.18h182v589.18h-182Z"/>
|
||||
</svg>;
|
||||
}
|
||||
5
src/components/Icons/Play.tsx
Normal file
5
src/components/Icons/Play.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
export default function Play({ fill }: { fill?: string }) {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
|
||||
<path d="M311.87-185.41v-589.18L775.07-480l-463.2 294.59Z"/>
|
||||
</svg>;
|
||||
}
|
||||
5
src/components/Icons/Redo.tsx
Normal file
5
src/components/Icons/Redo.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
export default function Redo({ fill }: { fill?: string }) {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
|
||||
<path d="M390.98-191.87q-98.44 0-168.77-65.27-70.34-65.27-70.34-161.43 0-96.15 70.34-161.54 70.33-65.39 168.77-65.39h244.11l-98.98-98.98 63.65-63.65L808.13-600 599.76-391.87l-63.65-63.65 98.98-98.98H390.98q-60.13 0-104.12 38.92-43.99 38.93-43.99 96.78 0 57.84 43.99 96.89 43.99 39.04 104.12 39.04h286.15v91H390.98Z"/>
|
||||
</svg>;
|
||||
}
|
||||
5
src/components/Icons/Replay.tsx
Normal file
5
src/components/Icons/Replay.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
export default function Replay({ fill }: { fill?: string }) {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
|
||||
<path d="M480.05-70.43q-76.72 0-143.78-29.1-67.05-29.1-116.75-78.8-49.69-49.69-78.79-116.75-29.1-67.05-29.1-143.49h91q0 115.81 80.73 196.47Q364.1-161.43 480-161.43q115.8 0 196.47-80.74 80.66-80.73 80.66-196.63 0-115.81-80.73-196.47-80.74-80.66-196.64-80.66h-6.24l60.09 60.08-58.63 60.63-166.22-166.21 166.22-166.22 58.63 60.87-59.85 59.85h6q76.74 0 143.76 29.09 67.02 29.1 116.84 78.8 49.81 49.69 78.91 116.64 29.1 66.95 29.1 143.61 0 76.66-29.1 143.71-29.1 67.06-78.79 116.75-49.7 49.7-116.7 78.8-67.01 29.1-143.73 29.1Z"/>
|
||||
</svg>;
|
||||
}
|
||||
31
src/components/Logging/Definitions.ts
Normal file
31
src/components/Logging/Definitions.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type {Cell} from "../../utils/cellStore.ts";
|
||||
import type {LogRecord} from "./useLogs.ts";
|
||||
|
||||
/**
|
||||
* Zustand store definition for managing user preferences related to logging.
|
||||
*
|
||||
* Includes flags for toggling relative timestamps and automatic scroll behavior.
|
||||
*/
|
||||
export type LoggingSettings = {
|
||||
/** Whether to display log timestamps as relative (e.g., "2m 15s ago") instead of absolute. */
|
||||
showRelativeTime: boolean;
|
||||
/** Updates the `showRelativeTime` setting. */
|
||||
setShowRelativeTime: (showRelativeTime: boolean) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for any component that renders a single log message entry.
|
||||
*
|
||||
* @param recordCell - A reactive `Cell` containing a single `LogRecord`.
|
||||
* @param onUpdate - Optional callback triggered when the log entry updates.
|
||||
*/
|
||||
export type MessageComponentProps = {
|
||||
recordCell: Cell<LogRecord>,
|
||||
onUpdate?: () => void,
|
||||
};
|
||||
|
||||
/**
|
||||
* Key used for the experiment filter predicate in the filter map, to exclude experiment logs from the developer logs.
|
||||
*/
|
||||
export const EXPERIMENT_FILTER_KEY = "experiment_filter";
|
||||
export const EXPERIMENT_LOGGER_NAME = "experiment";
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
University within the Software Project course.
|
||||
© Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
*/
|
||||
.filter-root {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
|
||||
import type {LogFilterPredicate} from "./useLogs.ts";
|
||||
@@ -13,9 +16,8 @@ type Setter<T> = (value: T | ((prev: T) => T)) => void;
|
||||
* Mapping of log level names to their corresponding numeric severity.
|
||||
* Used for comparison in log filtering predicates.
|
||||
*/
|
||||
const optionMapping = new Map([
|
||||
const optionMapping: Map<string, number> = new Map([
|
||||
["ALL", 0],
|
||||
["LLM", 9],
|
||||
["DEBUG", 10],
|
||||
["INFO", 20],
|
||||
["WARNING", 30],
|
||||
@@ -93,7 +95,7 @@ function GlobalLevelFilter({
|
||||
filterPredicates: Map<string, LogFilterPredicate>;
|
||||
setFilterPredicates: Setter<Map<string, LogFilterPredicate>>;
|
||||
}) {
|
||||
const selected = filterPredicates.get(GLOBAL_LOG_LEVEL_PREDICATE_KEY)?.value || "ALL";
|
||||
const selected = filterPredicates.get(GLOBAL_LOG_LEVEL_PREDICATE_KEY)?.value ?? "ALL";
|
||||
const setSelected = (selected: string | null) => {
|
||||
if (!selected || !optionMapping.has(selected)) return;
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
University within the Software Project course.
|
||||
© Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
*/
|
||||
.logging-container {
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -5,7 +10,6 @@
|
||||
flex-shrink: 0;
|
||||
|
||||
box-shadow: 0 0 1rem black;
|
||||
padding: 1rem 1rem 0 1rem;
|
||||
}
|
||||
|
||||
.no-numbers {
|
||||
@@ -15,8 +19,6 @@
|
||||
}
|
||||
|
||||
.log-container {
|
||||
margin-bottom: .5rem;
|
||||
|
||||
.accented-0, .accented-10 {
|
||||
background-color: color-mix(in oklab, canvas, rgb(159, 159, 159) 35%)
|
||||
}
|
||||
@@ -32,7 +34,7 @@
|
||||
}
|
||||
|
||||
.floating-button {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
box-shadow: 0 0 1rem rgba(0, 0, 0, 0.5);
|
||||
|
||||
@@ -1,38 +1,26 @@
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import {create} from "zustand";
|
||||
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {type ComponentType, useEffect, useRef, useState} from "react";
|
||||
import formatDuration from "../../utils/formatDuration.ts";
|
||||
import {type LogFilterPredicate, type LogRecord, useLogs} from "./useLogs.ts";
|
||||
import Filters from "./Filters.tsx";
|
||||
import {type Cell, useCell} from "../../utils/cellStore.ts";
|
||||
|
||||
import styles from "./Logging.module.css";
|
||||
|
||||
import {
|
||||
EXPERIMENT_FILTER_KEY,
|
||||
EXPERIMENT_LOGGER_NAME,
|
||||
type LoggingSettings,
|
||||
type MessageComponentProps
|
||||
} from "./Definitions.ts";
|
||||
import {create} from "zustand";
|
||||
|
||||
/**
|
||||
* Zustand store definition for managing user preferences related to logging.
|
||||
*
|
||||
* Includes flags for toggling relative timestamps and automatic scroll behavior.
|
||||
*/
|
||||
type LoggingSettings = {
|
||||
/** Whether to display log timestamps as relative (e.g., "2m 15s ago") instead of absolute. */
|
||||
showRelativeTime: boolean;
|
||||
/** Updates the `showRelativeTime` setting. */
|
||||
setShowRelativeTime: (showRelativeTime: boolean) => void;
|
||||
/** Whether the log view should automatically scroll to the newest entry. */
|
||||
scrollToBottom: boolean;
|
||||
/** Updates the `scrollToBottom` setting. */
|
||||
setScrollToBottom: (scrollToBottom: boolean) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Global Zustand store for logging UI preferences.
|
||||
* Local Zustand store for logging UI preferences.
|
||||
*/
|
||||
const useLoggingSettings = create<LoggingSettings>((set) => ({
|
||||
showRelativeTime: false,
|
||||
setShowRelativeTime: (showRelativeTime: boolean) => set({ showRelativeTime }),
|
||||
scrollToBottom: true,
|
||||
setScrollToBottom: (scrollToBottom: boolean) => set({ scrollToBottom }),
|
||||
}));
|
||||
|
||||
/**
|
||||
@@ -45,13 +33,7 @@ const useLoggingSettings = create<LoggingSettings>((set) => ({
|
||||
* @param onUpdate - Optional callback triggered when the log entry updates.
|
||||
* @returns A JSX element displaying a formatted log message.
|
||||
*/
|
||||
function LogMessage({
|
||||
recordCell,
|
||||
onUpdate,
|
||||
}: {
|
||||
recordCell: Cell<LogRecord>,
|
||||
onUpdate?: () => void,
|
||||
}) {
|
||||
function LogMessage({ recordCell, onUpdate }: MessageComponentProps) {
|
||||
const { showRelativeTime, setShowRelativeTime } = useLoggingSettings();
|
||||
const record = useCell(recordCell);
|
||||
|
||||
@@ -69,7 +51,7 @@ function LogMessage({
|
||||
/** Simplifies the logger name by showing only the last path segment. */
|
||||
const normalizedName = record.name.split(".").pop() || record.name;
|
||||
|
||||
// Notify parent component (e.g. for scroll updates) when this record changes.
|
||||
// Notify the parent component (e.g., for scroll updates) when this record changes.
|
||||
useEffect(() => {
|
||||
if (onUpdate) onUpdate();
|
||||
}, [record, onUpdate]);
|
||||
@@ -77,11 +59,10 @@ function LogMessage({
|
||||
return <div className={`${styles.logContainer} round-md border-lg flex-row gap-md`}>
|
||||
<div className={`${styles[`accented${normalizedLevelNo}`]} flex-col padding-sm justify-between`}>
|
||||
<span className={"mono bold"}>{record.levelname}</span>
|
||||
<span className={"mono clickable font-small"}
|
||||
onClick={() => setShowRelativeTime(!showRelativeTime)}
|
||||
>{showRelativeTime
|
||||
? formatDuration(record.relativeCreated)
|
||||
: new Date(record.created * 1000).toLocaleTimeString()
|
||||
<span className={"mono clickable font-small"} onClick={() => setShowRelativeTime(!showRelativeTime)}>{
|
||||
showRelativeTime
|
||||
? formatDuration(record.relativeCreated)
|
||||
: new Date(record.created * 1000).toLocaleTimeString()
|
||||
}</span>
|
||||
</div>
|
||||
<div className={"flex-col flex-1 padding-sm"}>
|
||||
@@ -100,12 +81,18 @@ function LogMessage({
|
||||
* - A floating "Scroll to bottom" button when not at the bottom.
|
||||
*
|
||||
* @param recordCells - Array of reactive log records to display.
|
||||
* @param MessageComponent - A component to use to render each log message entry.
|
||||
* @returns A scrollable log list component.
|
||||
*/
|
||||
function LogMessages({ recordCells }: { recordCells: Cell<LogRecord>[] }) {
|
||||
export function LogMessages({
|
||||
recordCells,
|
||||
MessageComponent,
|
||||
}: {
|
||||
recordCells: Cell<LogRecord>[],
|
||||
MessageComponent: ComponentType<MessageComponentProps>,
|
||||
}) {
|
||||
const scrollableRef = useRef<HTMLDivElement>(null);
|
||||
const lastElementRef = useRef<HTMLLIElement>(null)
|
||||
const { scrollToBottom, setScrollToBottom } = useLoggingSettings();
|
||||
const [scrollToBottom, setScrollToBottom] = useState(true);
|
||||
|
||||
// Disable auto-scroll if the user manually scrolls.
|
||||
useEffect(() => {
|
||||
@@ -124,30 +111,28 @@ function LogMessages({ recordCells }: { recordCells: Cell<LogRecord>[] }) {
|
||||
}, [scrollableRef, setScrollToBottom]);
|
||||
|
||||
/**
|
||||
* Scrolls the last log message into view if auto-scroll is enabled,
|
||||
* or if forced (e.g., user clicks "Scroll to bottom").
|
||||
* Scrolls the log messages to the bottom, making the latest messages visible.
|
||||
*
|
||||
* @param force - If true, forces scrolling even if `scrollToBottom` is false.
|
||||
*/
|
||||
function scrollLastElementIntoView(force = false) {
|
||||
if ((!scrollToBottom && !force) || !lastElementRef.current) return;
|
||||
lastElementRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
function showBottom(force = false) {
|
||||
if ((!scrollToBottom && !force) || !scrollableRef.current) return;
|
||||
scrollableRef.current.scrollTo({top: scrollableRef.current.scrollHeight, left: 0, behavior: "smooth"});
|
||||
}
|
||||
|
||||
return <div ref={scrollableRef} className={"min-height-0 scroll-y padding-b-md"}>
|
||||
return <div ref={scrollableRef} className={"min-height-0 scroll-y padding-h-lg padding-b-lg flex-1"}>
|
||||
<ol className={`${styles.noNumbers} margin-0 flex-col gap-md`}>
|
||||
{recordCells.map((recordCell, i) => (
|
||||
<li key={`${i}_${recordCell.get().firstRelativeCreated}`}>
|
||||
<LogMessage recordCell={recordCell} onUpdate={scrollLastElementIntoView} />
|
||||
<MessageComponent recordCell={recordCell} onUpdate={showBottom} />
|
||||
</li>
|
||||
))}
|
||||
<li ref={lastElementRef}></li>
|
||||
</ol>
|
||||
{!scrollToBottom && <button
|
||||
className={styles.floatingButton}
|
||||
onClick={() => {
|
||||
setScrollToBottom(true);
|
||||
scrollLastElementIntoView(true);
|
||||
showBottom(true);
|
||||
}}
|
||||
>
|
||||
Scroll to bottom
|
||||
@@ -164,16 +149,27 @@ function LogMessages({ recordCells }: { recordCells: Cell<LogRecord>[] }) {
|
||||
* - Zustand-managed UI settings (auto-scroll, timestamp display).
|
||||
*
|
||||
* This component uses the `useLogs` hook to fetch and filter logs based on
|
||||
* active predicates, and re-renders automatically as new logs arrive.
|
||||
* active predicates and re-renders automatically as new logs arrive.
|
||||
*
|
||||
* @returns The complete logging UI as a React element.
|
||||
*/
|
||||
export default function Logging() {
|
||||
const [filterPredicates, setFilterPredicates] = useState(new Map<string, LogFilterPredicate>());
|
||||
// By default, filter experiment logs from this debug logger
|
||||
const [filterPredicates, setFilterPredicates] = useState(new Map<string, LogFilterPredicate>([
|
||||
[
|
||||
EXPERIMENT_FILTER_KEY,
|
||||
{
|
||||
predicate: (r) => r.name == EXPERIMENT_LOGGER_NAME ? false : null,
|
||||
priority: 999,
|
||||
value: null,
|
||||
} as LogFilterPredicate,
|
||||
],
|
||||
]));
|
||||
const { filteredLogs, distinctNames } = useLogs(filterPredicates)
|
||||
distinctNames.delete(EXPERIMENT_LOGGER_NAME);
|
||||
|
||||
return <div className={`flex-col gap-lg min-height-0 ${styles.loggingContainer}`}>
|
||||
<div className={"flex-row gap-lg justify-between align-center"}>
|
||||
return <div className={`flex-col min-height-0 relative ${styles.loggingContainer}`}>
|
||||
<div className={"flex-row gap-lg justify-between align-center padding-lg"}>
|
||||
<h2 className={"margin-0"}>Logs</h2>
|
||||
<Filters
|
||||
filterPredicates={filterPredicates}
|
||||
@@ -181,6 +177,6 @@ export default function Logging() {
|
||||
agentNames={distinctNames}
|
||||
/>
|
||||
</div>
|
||||
<LogMessages recordCells={filteredLogs} />
|
||||
<LogMessages recordCells={filteredLogs} MessageComponent={LogMessage} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {useCallback, useEffect, useRef, useState} from "react";
|
||||
|
||||
import {applyPriorityPredicates, type PriorityFilterPredicate} from "../../utils/priorityFiltering.ts";
|
||||
import {cell, type Cell} from "../../utils/cellStore.ts";
|
||||
import { API_BASE_URL } from "../../config/api.ts";
|
||||
|
||||
type ExtraLevelName = 'LLM' | 'OBSERVATION' | 'ACTION' | 'CHAT';
|
||||
|
||||
export type LevelName = ExtraLevelName | 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL' | string;
|
||||
|
||||
/**
|
||||
* Extra fields that are added to log records in the backend but are not part of the standard `LogRecord` type.
|
||||
*
|
||||
* @property reference - (Optional) A reference identifier linking related log messages.
|
||||
* @property role - (Optional) For chat log messages, the role of the agent that generated the message.
|
||||
*/
|
||||
type ExtraLogRecordFields = {
|
||||
reference?: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a single log record emitted by the backend logging system.
|
||||
@@ -12,21 +31,19 @@ import {cell, type Cell} from "../../utils/cellStore.ts";
|
||||
* @property levelno - The numeric severity value corresponding to `levelname`.
|
||||
* @property created - The UNIX timestamp (in seconds) when this record was created.
|
||||
* @property relativeCreated - The time (in milliseconds) since the logging system started.
|
||||
* @property reference - (Optional) A reference identifier linking related log messages.
|
||||
* @property firstCreated - Timestamp of the first log in this reference group.
|
||||
* @property firstRelativeCreated - Relative timestamp of the first log in this reference group.
|
||||
*/
|
||||
export type LogRecord = {
|
||||
name: string;
|
||||
message: string;
|
||||
levelname: 'LLM' | 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL' | string;
|
||||
levelname: LevelName;
|
||||
levelno: number;
|
||||
created: number;
|
||||
relativeCreated: number;
|
||||
reference?: string;
|
||||
firstCreated: number;
|
||||
firstRelativeCreated: number;
|
||||
};
|
||||
} & ExtraLogRecordFields;
|
||||
|
||||
/**
|
||||
* A log filter predicate with priority support, used to determine whether
|
||||
@@ -37,7 +54,7 @@ export type LogRecord = {
|
||||
*
|
||||
* @template T - The type of record being filtered (here, `LogRecord`).
|
||||
*/
|
||||
export type LogFilterPredicate = PriorityFilterPredicate<LogRecord> & {
|
||||
export type LogFilterPredicate = PriorityFilterPredicate<LogRecord> & {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
value: any };
|
||||
|
||||
@@ -194,7 +211,7 @@ export function useLogs(filterPredicates: Map<string, LogFilterPredicate>) {
|
||||
// Only create one SSE connection for the lifetime of the hook.
|
||||
if (sseRef.current) return;
|
||||
|
||||
const es = new EventSource("http://localhost:8000/logs/stream");
|
||||
const es = new EventSource(`${API_BASE_URL}/logs/stream`);
|
||||
sseRef.current = es;
|
||||
|
||||
es.onmessage = (event) => {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import styles from "./TextField.module.css";
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {useEffect, useRef} from "react";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
University within the Software Project course.
|
||||
© Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
*/
|
||||
.text-field {
|
||||
border: 1px solid transparent;
|
||||
border-radius: 5pt;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {useEffect, useState} from "react";
|
||||
import styles from "./TextField.module.css";
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import { useState } from 'react'
|
||||
|
||||
/**
|
||||
|
||||
11
src/config/api.ts
Normal file
11
src/config/api.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
declare const __VITE_API_BASE_URL__: string | undefined;
|
||||
|
||||
const DEFAULT_API_BASE_URL = "http://localhost:8000";
|
||||
|
||||
const rawApiBaseUrl =
|
||||
(typeof __VITE_API_BASE_URL__ !== "undefined" ? __VITE_API_BASE_URL__ : undefined) ??
|
||||
DEFAULT_API_BASE_URL;
|
||||
|
||||
export const API_BASE_URL = rawApiBaseUrl.endsWith("/")
|
||||
? rawApiBaseUrl.slice(0, -1)
|
||||
: rawApiBaseUrl;
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
University within the Software Project course.
|
||||
© Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
*/
|
||||
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
@@ -8,6 +14,9 @@
|
||||
background-color: #242424;
|
||||
|
||||
--accent-color: #008080;
|
||||
--panel-shadow:
|
||||
0 1px 2px white,
|
||||
0 8px 24px rgba(190, 186, 186, 0.253);
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
@@ -15,6 +24,14 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--panel-shadow:
|
||||
0 1px 2px rgba(221, 221, 221, 0.178),
|
||||
0 8px 24px rgba(27, 27, 27, 0.507);
|
||||
}
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -41,7 +58,7 @@ button {
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
background-color: canvas;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
@@ -64,9 +81,6 @@ button:focus-visible {
|
||||
--dropdown-menu-background-color: rgb(247, 247, 247);
|
||||
--dropdown-menu-border: rgba(207, 207, 207, 0.986);
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { BrowserRouter } from 'react-router'
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Displays the current connection status of a robot in real time.
|
||||
*
|
||||
* Opens an SSE connection to the backend (`/robot/ping_stream`) that emits
|
||||
* simple boolean JSON messages (`true` or `false`). Updates automatically when
|
||||
* the robot connects or disconnects.
|
||||
*
|
||||
* @returns A React element showing the current robot connection status.
|
||||
*/
|
||||
export default function ConnectedRobots() {
|
||||
|
||||
/**
|
||||
* The current connection state:
|
||||
* - `true`: Robot is connected.
|
||||
* - `false`: Robot is not connected.
|
||||
* - `null`: Connection status is unknown (initial check in progress).
|
||||
*/
|
||||
const [connected, setConnected] = useState<boolean | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Open a Server-Sent Events (SSE) connection to receive live ping updates.
|
||||
// We're expecting a stream of data like that looks like this: `data = False` or `data = True`
|
||||
const eventSource = new EventSource("http://localhost:8000/robot/ping_stream");
|
||||
eventSource.onmessage = (event) => {
|
||||
|
||||
// Expecting messages in JSON format: `true` or `false`
|
||||
console.log("received message:", event.data);
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
try {
|
||||
setConnected(data)
|
||||
}
|
||||
catch {
|
||||
console.log("couldnt extract connected from incoming ping data")
|
||||
}
|
||||
|
||||
} catch {
|
||||
console.log("Ping message not in correct format:", event.data);
|
||||
}
|
||||
};
|
||||
|
||||
// Clean up the SSE connection when the component unmounts.
|
||||
return () => eventSource.close();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Is robot currently connected?</h1>
|
||||
<div>
|
||||
<h2>Robot is currently: {connected == null ? "checking..." : (connected ? "connected! 🟢" : "not connected... 🔴")} </h2>
|
||||
<h3>
|
||||
{connected == null ? "If checking continues, make sure CB is properly loaded with robot at least once." : ""}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
University within the Software Project course.
|
||||
© Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
*/
|
||||
.read_the_docs {
|
||||
color: #888;
|
||||
}
|
||||
@@ -21,4 +26,52 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
flex-direction: row; /* Horizontal layout looks more like a dashboard */
|
||||
gap: 1.5rem;
|
||||
justify-content: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.navCard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem 2rem;
|
||||
min-width: 180px;
|
||||
background-color: #ffffff;
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
/* Hover effects */
|
||||
.navCard:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||
border-color: #ffcd00; /* UU Yellow accent */
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Specific styling for the logo container */
|
||||
.logoPepperScaling {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.logoPepperScaling:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.logopepper {
|
||||
height: 120px;
|
||||
width: auto;
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import { Link } from 'react-router'
|
||||
import pepperLogo from '../../assets/pepper_transp2_small.svg'
|
||||
import styles from './Home.module.css'
|
||||
@@ -13,16 +16,20 @@ import styles from './Home.module.css'
|
||||
function Home() {
|
||||
return (
|
||||
<div className={`flex-col ${styles.gapXl}`}>
|
||||
<div className="logoPepperScaling">
|
||||
<a href="https://git.science.uu.nl/ics/sp/2025/n25b" target="_blank">
|
||||
<img src={pepperLogo} className="logopepper" alt="Pepper logo" />
|
||||
<div className={styles.logoPepperScaling}>
|
||||
<a href="https://git.science.uu.nl/ics/sp/2025/n25b" target="_blank" rel="noreferrer">
|
||||
<img src={pepperLogo} className={styles.logopepper} alt="Pepper logo" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className={styles.links}>
|
||||
<Link to={"/robot"}>Robot Interaction →</Link>
|
||||
<Link to={"/editor"}>Editor →</Link>
|
||||
<Link to={"/template"}>Template →</Link>
|
||||
<Link to={"/ConnectedRobots"}>Connected Robots →</Link>
|
||||
{/* Program Editor is now first */}
|
||||
<Link to="/editor" className={styles.navCard}>
|
||||
Program Editor
|
||||
</Link>
|
||||
<Link to="/user_manual" className={styles.navCard}>
|
||||
User and Developer Manual
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
81
src/pages/Manuals/Manuals.module.css
Normal file
81
src/pages/Manuals/Manuals.module.css
Normal file
@@ -0,0 +1,81 @@
|
||||
/* This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
University within the Software Project course.
|
||||
© Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
*/
|
||||
|
||||
.manualContainer {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
margin: 4rem auto;
|
||||
padding: 2rem;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.manualHeader h1 {
|
||||
font-size: 2.2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.buttonStack {
|
||||
display: flex;
|
||||
flex-direction: column; /* Stacks the manual sections vertically */
|
||||
gap: 3rem;
|
||||
margin-top: 3rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.manualEntry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.manualEntry h3 {
|
||||
margin: 0;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.manualEntry p {
|
||||
color: #666;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.downloadBtn {
|
||||
display: inline-block;
|
||||
background-color: #ffffff; /* White background as requested */
|
||||
color: #000;
|
||||
padding: 1rem 2rem;
|
||||
border-radius: 50px;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
width: 280px; /* Fixed width for uniform appearance */
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||
transition: transform 0.2s ease, background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.downloadBtn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
|
||||
background-color: #247284; /* Teal hover as requested */
|
||||
color: #ffffff; /* Text turns white on teal for better contrast */
|
||||
}
|
||||
|
||||
.dateBadge {
|
||||
display: inline-block;
|
||||
margin-top: 1rem;
|
||||
padding: 0.4rem 1rem;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin-top: 4rem;
|
||||
border: 0;
|
||||
border-top: 1px solid #eee;
|
||||
width: 60%;
|
||||
}
|
||||
39
src/pages/Manuals/Manuals.tsx
Normal file
39
src/pages/Manuals/Manuals.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import styles from './Manuals.module.css';
|
||||
|
||||
export default function Manuals() {
|
||||
const userManualPath = "/UserManual.pdf";
|
||||
const developerManualPath = "/DeveloperManual.pdf";
|
||||
|
||||
return (
|
||||
<div className={styles.manualContainer}>
|
||||
<header className={styles.manualHeader}>
|
||||
<h1>Documentation & Manuals</h1>
|
||||
|
||||
<span className={styles.dateBadge}>Last Updated: January 2026</span>
|
||||
</header>
|
||||
|
||||
<div className={styles.buttonStack}>
|
||||
<div className={styles.manualEntry}>
|
||||
<h3>User Manual</h3>
|
||||
<p>Manual for Users of the Pepper+ Software </p>
|
||||
<a href={userManualPath} download="UserManual.pdf" className={styles.downloadBtn}>
|
||||
Download User Manual
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className={styles.manualEntry}>
|
||||
<h3>Developer Manual</h3>
|
||||
<p>Technical documentation for future developers.</p>
|
||||
<a href={developerManualPath} download="DeveloperManual.pdf" className={styles.downloadBtn}>
|
||||
Download Developer Manual
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className={styles.divider} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
276
src/pages/MonitoringPage/MonitoringPage.module.css
Normal file
276
src/pages/MonitoringPage/MonitoringPage.module.css
Normal file
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
University within the Software Project course.
|
||||
© Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
*/
|
||||
.dashboardContainer {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr; /* Left = content, Right = logs */
|
||||
grid-template-rows: auto 1fr auto; /* Header, Main, Footer */
|
||||
grid-template-areas:
|
||||
"header logs"
|
||||
"main logs"
|
||||
"footer footer";
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background-color: var(--bg-main);
|
||||
color: var(--text-main);
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* HEADER */
|
||||
.experimentOverview {
|
||||
grid-area: header;
|
||||
display: flex;
|
||||
color: color;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-main);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 1rem;
|
||||
box-shadow: var(--panel-shadow);
|
||||
position: static; /* ensures it scrolls away */
|
||||
}
|
||||
|
||||
.controlsButtons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: .25rem;
|
||||
max-width: 260px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.phaseProgress {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.phase {
|
||||
display: inline-block;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
margin: 0 3px;
|
||||
text-align: center;
|
||||
line-height: 25px;
|
||||
background: gray;
|
||||
}
|
||||
|
||||
.completed {
|
||||
background-color: green;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.current {
|
||||
background-color: rgb(255, 123, 0);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.connected {
|
||||
color: green;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.disconnected {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pausePlayInactive{
|
||||
background-color: gray;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pausePlayActive{
|
||||
background-color: green;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.next {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stop {
|
||||
background-color: red;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.restartExperiment{
|
||||
background-color: red;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* MAIN GRID */
|
||||
.phaseOverview {
|
||||
grid-area: main;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, auto);
|
||||
gap: 1rem;
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-main);
|
||||
padding: 1rem;
|
||||
box-shadow: var(--panel-shadow);
|
||||
|
||||
}
|
||||
|
||||
.phaseBox {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: var(--panel-shadow);
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.05);
|
||||
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.phaseBox ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
overflow-y: auto;
|
||||
flex-grow: 1;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.phaseBox ul::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.phaseBox ul::-webkit-scrollbar-thumb {
|
||||
background-color: #ccc;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.phaseOverviewText {
|
||||
grid-column: 1 / -1; /* make the title span across both columns */
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
margin: 0; /* remove default section margin */
|
||||
padding: 0.25rem 0; /* smaller internal space */
|
||||
}
|
||||
|
||||
.phaseOverviewText h3{
|
||||
margin: 0; /* removes top/bottom whitespace */
|
||||
padding: 0; /* keeps spacing tight */
|
||||
}
|
||||
|
||||
.phaseBox h3 {
|
||||
margin-top: 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.checked::before {
|
||||
content: '✔️ ';
|
||||
}
|
||||
|
||||
.statusIndicator {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
user-select: none;
|
||||
transition: transform 0.1s ease;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.statusIndicator.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.statusIndicator.clickable:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clickable:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.statusItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.itemDescription {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* FOOTER */
|
||||
.controlsSection {
|
||||
grid-area: footer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-main);
|
||||
box-shadow: var(--panel-shadow);
|
||||
padding: 1rem;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.controlsSection button {
|
||||
background: var(--bg-surface);
|
||||
box-shadow: var(--panel-shadow);
|
||||
margin-top: 0.5rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.gestures,
|
||||
.speech,
|
||||
.directSpeech {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.speechInput {
|
||||
display: flex;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.speechInput input {
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
background-color: Canvas;
|
||||
color: CanvasText;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.speechInput button {
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
background-color: Canvas;
|
||||
color: CanvasText;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* RESPONSIVE */
|
||||
@media (max-width: 900px) {
|
||||
.phaseOverview {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.controlsSection {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
411
src/pages/MonitoringPage/MonitoringPage.tsx
Normal file
411
src/pages/MonitoringPage/MonitoringPage.tsx
Normal file
@@ -0,0 +1,411 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import styles from './MonitoringPage.module.css';
|
||||
|
||||
// Store & API
|
||||
import useProgramStore from "../../utils/programStore";
|
||||
import {
|
||||
nextPhase,
|
||||
stopExperiment,
|
||||
useExperimentLogger,
|
||||
useStatusLogger,
|
||||
pauseExperiment,
|
||||
playExperiment,
|
||||
type ExperimentStreamData,
|
||||
type GoalUpdate,
|
||||
type TriggerUpdate,
|
||||
type CondNormsStateUpdate,
|
||||
type PhaseUpdate
|
||||
} from "./MonitoringPageAPI";
|
||||
import { graphReducer, runProgram } from '../VisProgPage/VisProgLogic.ts';
|
||||
|
||||
// Types
|
||||
import type { NormNodeData } from '../VisProgPage/visualProgrammingUI/nodes/NormNode';
|
||||
import type { GoalNode } from '../VisProgPage/visualProgrammingUI/nodes/GoalNode';
|
||||
import type { TriggerNode } from '../VisProgPage/visualProgrammingUI/nodes/TriggerNode';
|
||||
|
||||
// Sub-components
|
||||
import {
|
||||
GestureControls,
|
||||
SpeechPresets,
|
||||
DirectSpeechInput,
|
||||
StatusList,
|
||||
RobotConnected
|
||||
} from './MonitoringPageComponents';
|
||||
import ExperimentLogs from "./components/ExperimentLogs.tsx";
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 1. State management
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Manages the state of the active experiment, including phase progression,
|
||||
* goal tracking, and stream event listeners.
|
||||
*/
|
||||
function useExperimentLogic() {
|
||||
const getPhaseIds = useProgramStore((s) => s.getPhaseIds);
|
||||
const getPhaseNames = useProgramStore((s) => s.getPhaseNames);
|
||||
const getGoalsInPhase = useProgramStore((s) => s.getGoalsInPhase);
|
||||
const setProgramState = useProgramStore((state) => state.setProgramState);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeIds, setActiveIds] = useState<Record<string, boolean>>({});
|
||||
const [goalIndex, setGoalIndex] = useState(0);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [phaseIndex, setPhaseIndex] = useState(0);
|
||||
const [isFinished, setIsFinished] = useState(false);
|
||||
|
||||
const phaseIds = getPhaseIds();
|
||||
const phaseNames = getPhaseNames();
|
||||
|
||||
// --- Stream Handlers ---
|
||||
|
||||
const handleStreamUpdate = useCallback((data: ExperimentStreamData) => {
|
||||
if (data.type === 'phase_update' && data.id) {
|
||||
const payload = data as PhaseUpdate;
|
||||
console.log(`${data.type} received, id : ${data.id}`);
|
||||
|
||||
if (payload.id === "end") {
|
||||
setIsFinished(true);
|
||||
} else {
|
||||
setIsFinished(false);
|
||||
const newIndex = getPhaseIds().indexOf(payload.id);
|
||||
if (newIndex !== -1) {
|
||||
setPhaseIndex(newIndex);
|
||||
setGoalIndex(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (data.type === 'goal_update') {
|
||||
const payload = data as GoalUpdate;
|
||||
const currentPhaseGoals = getGoalsInPhase(phaseIds[phaseIndex]) as GoalNode[];
|
||||
const gIndex = currentPhaseGoals.findIndex((g) => g.id === payload.id);
|
||||
|
||||
console.log(`${data.type} received, id : ${data.id}`);
|
||||
|
||||
if (gIndex === -1) {
|
||||
console.warn(`Goal ${payload.id} not found in phase ${phaseNames[phaseIndex]}`);
|
||||
} else {
|
||||
setGoalIndex(gIndex);
|
||||
// Mark all previous goals as achieved
|
||||
setActiveIds((prev) => {
|
||||
const nextState = { ...prev };
|
||||
for (let i = 0; i < gIndex; i++) {
|
||||
nextState[currentPhaseGoals[i].id] = true;
|
||||
}
|
||||
return nextState;
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (data.type === 'trigger_update') {
|
||||
const payload = data as TriggerUpdate;
|
||||
setActiveIds((prev) => ({ ...prev, [payload.id]: payload.achieved }));
|
||||
}
|
||||
}, [getPhaseIds, getGoalsInPhase, phaseIds, phaseIndex, phaseNames]);
|
||||
|
||||
const handleStatusUpdate = useCallback((data: unknown) => {
|
||||
const payload = data as CondNormsStateUpdate;
|
||||
if (payload.type !== 'cond_norms_state_update') return;
|
||||
|
||||
setActiveIds((prev) => {
|
||||
const hasChanges = payload.norms.some((u) => prev[u.id] !== u.active);
|
||||
if (!hasChanges) return prev;
|
||||
|
||||
const nextState = { ...prev };
|
||||
payload.norms.forEach((u) => { nextState[u.id] = u.active; });
|
||||
return nextState;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Connect listeners
|
||||
useExperimentLogger(handleStreamUpdate);
|
||||
useStatusLogger(handleStatusUpdate);
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
const resetExperiment = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const phases = graphReducer();
|
||||
setProgramState({ phases });
|
||||
|
||||
setActiveIds({});
|
||||
setPhaseIndex(0);
|
||||
setGoalIndex(0);
|
||||
setIsFinished(false);
|
||||
|
||||
await runProgram();
|
||||
console.log("Experiment & UI successfully reset.");
|
||||
} catch (err) {
|
||||
console.error("Failed to reset program:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [setProgramState]);
|
||||
|
||||
const handleControlAction = async (action: "pause" | "play" | "nextPhase" | "stop") => {
|
||||
try {
|
||||
setLoading(true);
|
||||
switch (action) {
|
||||
case "pause":
|
||||
setIsPlaying(false);
|
||||
await pauseExperiment();
|
||||
break;
|
||||
case "play":
|
||||
setIsPlaying(true);
|
||||
await playExperiment();
|
||||
break;
|
||||
case "nextPhase":
|
||||
await nextPhase();
|
||||
break;
|
||||
case "stop":
|
||||
await stopExperiment();
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
loading,
|
||||
isPlaying,
|
||||
isFinished,
|
||||
phaseIds,
|
||||
phaseNames,
|
||||
phaseIndex,
|
||||
goalIndex,
|
||||
activeIds,
|
||||
setActiveIds,
|
||||
resetExperiment,
|
||||
handleControlAction,
|
||||
};
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 2. Smaller Presentation Components
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Visual indicator of progress through experiment phases.
|
||||
*/
|
||||
function PhaseProgressBar({
|
||||
phaseIds,
|
||||
phaseIndex,
|
||||
isFinished
|
||||
}: {
|
||||
phaseIds: string[],
|
||||
phaseIndex: number,
|
||||
isFinished: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.phaseProgress}>
|
||||
{phaseIds.map((id, index) => {
|
||||
let statusClass = "";
|
||||
if (isFinished || index < phaseIndex) statusClass = styles.completed;
|
||||
else if (index === phaseIndex) statusClass = styles.current;
|
||||
|
||||
return (
|
||||
<span key={id} className={`${styles.phase} ${statusClass}`}>
|
||||
{index + 1}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main control buttons (Play, Pause, Next, Reset).
|
||||
*/
|
||||
function ControlPanel({
|
||||
loading,
|
||||
isPlaying,
|
||||
onAction,
|
||||
onReset
|
||||
}: {
|
||||
loading: boolean,
|
||||
isPlaying: boolean,
|
||||
onAction: (a: "pause" | "play" | "nextPhase" | "stop") => void,
|
||||
onReset: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.experimentControls}>
|
||||
<h3>Experiment Controls</h3>
|
||||
<div className={styles.controlsButtons}>
|
||||
<button
|
||||
className={!isPlaying ? styles.pausePlayActive : styles.pausePlayInactive}
|
||||
onClick={() => onAction("pause")}
|
||||
disabled={loading}
|
||||
>❚❚</button>
|
||||
|
||||
<button
|
||||
className={isPlaying ? styles.pausePlayActive : styles.pausePlayInactive}
|
||||
onClick={() => onAction("play")}
|
||||
disabled={loading}
|
||||
>▶</button>
|
||||
|
||||
<button
|
||||
className={styles.next}
|
||||
onClick={() => onAction("nextPhase")}
|
||||
disabled={loading}
|
||||
>⏭</button>
|
||||
|
||||
<button
|
||||
className={styles.restartExperiment}
|
||||
onClick={onReset}
|
||||
disabled={loading}
|
||||
>⟲</button>
|
||||
|
||||
<button
|
||||
className={styles.stop}
|
||||
onClick={() => onAction("stop")}
|
||||
disabled={loading}
|
||||
>⏹</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays lists of Goals, Triggers, and Norms for the current phase.
|
||||
*/
|
||||
function PhaseDashboard({
|
||||
phaseId,
|
||||
activeIds,
|
||||
setActiveIds,
|
||||
goalIndex
|
||||
}: {
|
||||
phaseId: string,
|
||||
activeIds: Record<string, boolean>,
|
||||
setActiveIds: React.Dispatch<React.SetStateAction<Record<string, boolean>>>,
|
||||
goalIndex: number
|
||||
}) {
|
||||
const getGoalsWithDepth = useProgramStore((s) => s.getGoalsWithDepth);
|
||||
const getTriggers = useProgramStore((s) => s.getTriggersInPhase);
|
||||
const getNorms = useProgramStore((s) => s.getNormsInPhase);
|
||||
|
||||
// Prepare data view models
|
||||
const goals = getGoalsWithDepth(phaseId).map((g) => ({
|
||||
...g,
|
||||
id: g.id as string,
|
||||
name: g.name as string,
|
||||
achieved: activeIds[g.id as string] ?? false,
|
||||
level: g.level, // Pass this new property to the UI
|
||||
}));
|
||||
|
||||
const triggers = (getTriggers(phaseId) as TriggerNode[]).map(t => ({
|
||||
...t,
|
||||
achieved: activeIds[t.id] ?? false,
|
||||
}));
|
||||
|
||||
const norms = (getNorms(phaseId) as NormNodeData[])
|
||||
.filter(n => !n.condition)
|
||||
.map(n => ({ ...n, label: n.norm }));
|
||||
|
||||
const conditionalNorms = (getNorms(phaseId) as (NormNodeData & { id: string })[])
|
||||
.filter(n => !!n.condition)
|
||||
.map(n => ({
|
||||
...n,
|
||||
achieved: activeIds[n.id] ?? false
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<StatusList title="Goals" items={goals} type="goal" activeIds={activeIds} setActiveIds={setActiveIds} currentGoalIndex={goalIndex} />
|
||||
<StatusList title="Triggers" items={triggers} type="trigger" activeIds={activeIds} />
|
||||
<StatusList title="Norms" items={norms} type="norm" activeIds={activeIds} />
|
||||
<StatusList title="Conditional Norms" items={conditionalNorms} type="cond_norm" activeIds={activeIds} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 3. Main Component
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const MonitoringPage: React.FC = () => {
|
||||
const {
|
||||
loading,
|
||||
isPlaying,
|
||||
isFinished,
|
||||
phaseIds,
|
||||
phaseNames,
|
||||
phaseIndex,
|
||||
goalIndex,
|
||||
activeIds,
|
||||
setActiveIds,
|
||||
resetExperiment,
|
||||
handleControlAction
|
||||
} = useExperimentLogic();
|
||||
|
||||
if (phaseIds.length === 0) {
|
||||
return <p className={styles.empty}>No program loaded.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.dashboardContainer}>
|
||||
{/* HEADER */}
|
||||
<header className={styles.experimentOverview}>
|
||||
<div className={styles.phaseName}>
|
||||
<h2>Experiment Overview</h2>
|
||||
<p>
|
||||
{isFinished ? (
|
||||
<strong>Experiment finished</strong>
|
||||
) : (
|
||||
<><strong>Phase {phaseIndex + 1}:</strong> {phaseNames[phaseIndex]}</>
|
||||
)}
|
||||
</p>
|
||||
<PhaseProgressBar phaseIds={phaseIds} phaseIndex={phaseIndex} isFinished={isFinished} />
|
||||
</div>
|
||||
|
||||
<ControlPanel
|
||||
loading={loading}
|
||||
isPlaying={isPlaying}
|
||||
onAction={handleControlAction}
|
||||
onReset={resetExperiment}
|
||||
/>
|
||||
|
||||
<div className={styles.connectionStatus}>
|
||||
<RobotConnected />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* MAIN GRID */}
|
||||
<main className={styles.phaseOverview}>
|
||||
<section className={styles.phaseOverviewText}>
|
||||
<h3>Phase Overview</h3>
|
||||
</section>
|
||||
|
||||
{isFinished ? (
|
||||
<div className={styles.finishedMessage}>
|
||||
<p>All phases have been successfully completed.</p>
|
||||
</div>
|
||||
) : (
|
||||
<PhaseDashboard
|
||||
phaseId={phaseIds[phaseIndex]}
|
||||
activeIds={activeIds}
|
||||
setActiveIds={setActiveIds}
|
||||
goalIndex={goalIndex}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* LOGS */}
|
||||
<ExperimentLogs />
|
||||
|
||||
{/* FOOTER */}
|
||||
<footer className={styles.controlsSection}>
|
||||
<GestureControls />
|
||||
<SpeechPresets />
|
||||
<DirectSpeechInput />
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MonitoringPage;
|
||||
128
src/pages/MonitoringPage/MonitoringPageAPI.ts
Normal file
128
src/pages/MonitoringPage/MonitoringPageAPI.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import React, { useEffect } from 'react';
|
||||
import { API_BASE_URL } from '../../config/api.ts';
|
||||
|
||||
/**
|
||||
* HELPER: Unified sender function
|
||||
*/
|
||||
export const sendAPICall = async (type: string, context: string, endpoint?: string) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/button_pressed${endpoint ?? ""}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ type, context }),
|
||||
});
|
||||
if (!response.ok) throw new Error("Backend response error");
|
||||
console.log(`API Call send - Type: ${type}, Context: ${context} ${endpoint ? `, Endpoint: ${endpoint}` : ""}`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to send api call:`, err);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sends an API call to the CB for going to the next phase.
|
||||
* In case we can't go to the next phase, the function will throw an error.
|
||||
*/
|
||||
export async function nextPhase(): Promise<void> {
|
||||
const type = "next_phase"
|
||||
const context = ""
|
||||
sendAPICall(type, context)
|
||||
}
|
||||
|
||||
export async function stopExperiment(): Promise<void> {
|
||||
const type = "stop"
|
||||
const context = ""
|
||||
sendAPICall(type, context)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sends an API call to the CB for going to pause experiment
|
||||
*/
|
||||
export async function pauseExperiment(): Promise<void> {
|
||||
const type = "pause"
|
||||
const context = "true"
|
||||
sendAPICall(type, context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an API call to the CB for going to resume experiment
|
||||
*/
|
||||
export async function playExperiment(): Promise<void> {
|
||||
const type = "pause"
|
||||
const context = "false"
|
||||
sendAPICall(type, context)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Types for the experiment stream messages
|
||||
*/
|
||||
export type PhaseUpdate = { type: 'phase_update'; id: string };
|
||||
export type GoalUpdate = { type: 'goal_update'; id: string };
|
||||
export type TriggerUpdate = { type: 'trigger_update'; id: string; achieved: boolean };
|
||||
export type CondNormsStateUpdate = { type: 'cond_norms_state_update'; norms: { id: string; active: boolean }[] };
|
||||
export type ExperimentStreamData = PhaseUpdate | GoalUpdate | TriggerUpdate | CondNormsStateUpdate | Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* A hook that listens to the experiment stream that updates current state of the program
|
||||
* via updates sent from the backend
|
||||
*/
|
||||
export function useExperimentLogger(onUpdate?: (data: ExperimentStreamData) => void) {
|
||||
const callbackRef = React.useRef(onUpdate);
|
||||
// Ref is updated every time with on update
|
||||
React.useEffect(() => {
|
||||
callbackRef.current = onUpdate;
|
||||
}, [onUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Connecting to Experiment Stream...");
|
||||
const eventSource = new EventSource(`${API_BASE_URL}/experiment_stream`);
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const parsedData = JSON.parse(event.data) as ExperimentStreamData;
|
||||
//call function using the ref
|
||||
callbackRef.current?.(parsedData);
|
||||
} catch (err) {
|
||||
console.warn("Stream parse error:", err);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = (err) => {
|
||||
console.error("SSE Connection Error:", err);
|
||||
eventSource.close();
|
||||
};
|
||||
|
||||
return () => {
|
||||
console.log("Closing Experiment Stream...");
|
||||
eventSource.close();
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook that listens to the status stream that updates active conditional norms
|
||||
* via updates sent from the backend
|
||||
*/
|
||||
export function useStatusLogger(onUpdate?: (data: ExperimentStreamData) => void) {
|
||||
const callbackRef = React.useRef(onUpdate);
|
||||
|
||||
React.useEffect(() => {
|
||||
callbackRef.current = onUpdate;
|
||||
}, [onUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
const eventSource = new EventSource(`${API_BASE_URL}/status_stream`);
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const parsedData = JSON.parse(event.data);
|
||||
callbackRef.current?.(parsedData);
|
||||
} catch (err) { console.warn("Status stream error:", err); }
|
||||
};
|
||||
return () => eventSource.close();
|
||||
}, []);
|
||||
}
|
||||
236
src/pages/MonitoringPage/MonitoringPageComponents.tsx
Normal file
236
src/pages/MonitoringPage/MonitoringPageComponents.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import styles from './MonitoringPage.module.css';
|
||||
import { sendAPICall } from './MonitoringPageAPI';
|
||||
import { API_BASE_URL } from '../../config/api.ts';
|
||||
|
||||
// --- GESTURE COMPONENT ---
|
||||
export const GestureControls: React.FC = () => {
|
||||
const [selectedGesture, setSelectedGesture] = useState("animations/Stand/BodyTalk/Speaking/BodyTalk_1");
|
||||
|
||||
const gestures = [
|
||||
{ label: "Wave", value: "animations/Stand/Gestures/Hey_1" },
|
||||
{ label: "Think", value: "animations/Stand/Emotions/Neutral/Puzzled_1" },
|
||||
{ label: "Explain", value: "animations/Stand/Gestures/Explain_4" },
|
||||
{ label: "You", value: "animations/Stand/Gestures/You_1" },
|
||||
{ label: "Happy", value: "animations/Stand/Emotions/Positive/Happy_1" },
|
||||
{ label: "Laugh", value: "animations/Stand/Emotions/Positive/Laugh_2" },
|
||||
{ label: "Lonely", value: "animations/Stand/Emotions/Neutral/Lonely_1" },
|
||||
{ label: "Suprise", value: "animations/Stand/Emotions/Negative/Surprise_1" },
|
||||
{ label: "Hurt", value: "animations/Stand/Emotions/Negative/Hurt_2" },
|
||||
{ label: "Angry", value: "animations/Stand/Emotions/Negative/Angry_4" },
|
||||
];
|
||||
return (
|
||||
<div className={styles.gestures}>
|
||||
<h4>Gestures</h4>
|
||||
<div className={styles.gestureInputGroup}>
|
||||
<select
|
||||
value={selectedGesture}
|
||||
onChange={(e) => setSelectedGesture(e.target.value)}
|
||||
>
|
||||
{gestures.map(g => <option key={g.value} value={g.value}>{g.label}</option>)}
|
||||
</select>
|
||||
<button onClick={() => sendAPICall("gesture", selectedGesture)}>
|
||||
Actuate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// --- PRESET SPEECH COMPONENT ---
|
||||
export const SpeechPresets: React.FC = () => {
|
||||
const phrases = [
|
||||
{ label: "Hello, I'm Pepper", text: "Hello, I'm Pepper" },
|
||||
{ label: "Repeat please", text: "Could you repeat that please" },
|
||||
{ label: "About yourself", text: "Tell me something about yourself" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.speech}>
|
||||
<h4>Speech Presets</h4>
|
||||
<ul>
|
||||
{phrases.map((phrase, i) => (
|
||||
<li key={i}>
|
||||
<button
|
||||
className={styles.speechBtn}
|
||||
onClick={() => sendAPICall("speech", phrase.text)}
|
||||
>
|
||||
"{phrase.label}"
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// --- DIRECT SPEECH (INPUT) COMPONENT ---
|
||||
export const DirectSpeechInput: React.FC = () => {
|
||||
const [text, setText] = useState("");
|
||||
|
||||
const handleSend = () => {
|
||||
if (!text.trim()) return;
|
||||
sendAPICall("speech", text);
|
||||
setText(""); // Clear after sending
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.directSpeech}>
|
||||
<h4>Direct Pepper Speech</h4>
|
||||
<div className={styles.speechInput}>
|
||||
<input
|
||||
type="text"
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
placeholder="Type message..."
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
|
||||
/>
|
||||
<button onClick={handleSend}>Send</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// --- interface for goals/triggers/norms/conditional norms ---
|
||||
export type StatusItem = {
|
||||
id?: string | number;
|
||||
achieved?: boolean;
|
||||
description?: string;
|
||||
label?: string;
|
||||
norm?: string;
|
||||
name?: string;
|
||||
level?: number;
|
||||
};
|
||||
|
||||
interface StatusListProps {
|
||||
title: string;
|
||||
items: StatusItem[];
|
||||
type: 'goal' | 'trigger' | 'norm'| 'cond_norm';
|
||||
activeIds: Record<string, boolean>;
|
||||
setActiveIds?: React.Dispatch<React.SetStateAction<Record<string, boolean>>>;
|
||||
currentGoalIndex?: number;
|
||||
}
|
||||
|
||||
// --- STATUS LIST COMPONENT ---
|
||||
export const StatusList: React.FC<StatusListProps> = ({
|
||||
title,
|
||||
items,
|
||||
type,
|
||||
activeIds,
|
||||
setActiveIds,
|
||||
currentGoalIndex // Destructure this prop
|
||||
}) => {
|
||||
return (
|
||||
<section className={styles.phaseBox}>
|
||||
<h3>{title}</h3>
|
||||
<ul>
|
||||
{items.map((item, idx) => {
|
||||
if (item.id === undefined) return null;
|
||||
const isActive = !!activeIds[item.id];
|
||||
const showIndicator = type !== 'norm';
|
||||
const isCurrentGoal = type === 'goal' && idx === currentGoalIndex;
|
||||
const canOverride = (showIndicator && !isActive) || (type === 'cond_norm' && isActive);
|
||||
|
||||
const indentation = (item.level || 0) * 20;
|
||||
|
||||
const handleOverrideClick = () => {
|
||||
if (!canOverride) return;
|
||||
if (type === 'cond_norm' && isActive){
|
||||
{/* Unachieve conditional norm */}
|
||||
sendAPICall("override_unachieve", String(item.id));
|
||||
}
|
||||
else {
|
||||
if(type === 'goal')
|
||||
if(setActiveIds)
|
||||
{setActiveIds(prev => ({ ...prev, [String(item.id)]: true }));}
|
||||
|
||||
sendAPICall("override", String(item.id));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<li key={item.id ?? idx}
|
||||
className={styles.statusItem}
|
||||
style={{ paddingLeft: `${indentation}px` }}
|
||||
>
|
||||
{showIndicator && (
|
||||
<span
|
||||
className={`${styles.statusIndicator} ${isActive ? styles.active : styles.inactive} ${canOverride ? styles.clickable : ''}`}
|
||||
onClick={handleOverrideClick}
|
||||
>
|
||||
{isActive ? "✔️" : "❌"}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={styles.itemDescription}
|
||||
style={{
|
||||
// Visual Feedback
|
||||
textDecoration: isCurrentGoal ? 'underline' : 'none',
|
||||
fontWeight: isCurrentGoal ? 'bold' : 'normal',
|
||||
color: isCurrentGoal ? '#007bff' : 'inherit',
|
||||
backgroundColor: isCurrentGoal ? '#e7f3ff' : 'transparent', // Added subtle highlight
|
||||
padding: isCurrentGoal ? '2px 4px' : '0',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
>
|
||||
{item.name || item.norm}
|
||||
{isCurrentGoal && " (Current)"}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// --- Robot Connected ---
|
||||
export const RobotConnected = () => {
|
||||
|
||||
/**
|
||||
* The current connection state:
|
||||
* - `true`: Robot is connected.
|
||||
* - `false`: Robot is not connected.
|
||||
* - `null`: Connection status is unknown (initial check in progress).
|
||||
*/
|
||||
const [connected, setConnected] = useState<boolean | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Open a Server-Sent Events (SSE) connection to receive live ping updates.
|
||||
// We're expecting a stream of data like that looks like this: `data = False` or `data = True`
|
||||
const eventSource = new EventSource(`${API_BASE_URL}/robot/ping_stream`);
|
||||
eventSource.onmessage = (event) => {
|
||||
|
||||
// Expecting messages in JSON format: `true` or `false`
|
||||
//commented out this log as it clutters console logs, but might be useful to debug
|
||||
//console.log("received message:", event.data);
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
try {
|
||||
setConnected(data)
|
||||
}
|
||||
catch {
|
||||
console.log("couldnt extract connected from incoming ping data")
|
||||
}
|
||||
|
||||
} catch {
|
||||
console.log("Ping message not in correct format:", event.data);
|
||||
}
|
||||
};
|
||||
|
||||
// Clean up the SSE connection when the component unmounts.
|
||||
return () => eventSource.close();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>Connection:</h3>
|
||||
<p className={connected ? styles.connected : styles.disconnected }>{connected ? "● Robot is connected" : "● Robot is disconnected"}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
University within the Software Project course.
|
||||
© Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
*/
|
||||
.logs {
|
||||
/* grid-area used in MonitoringPage.module.css */
|
||||
grid-area: logs;
|
||||
box-shadow: var(--panel-shadow);
|
||||
|
||||
height: 900px;
|
||||
width: 450px;
|
||||
|
||||
.live {
|
||||
width: .5rem;
|
||||
height: .5rem;
|
||||
left: .5rem;
|
||||
background: red;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message.alternate {
|
||||
align-items: end;
|
||||
text-align: end;
|
||||
|
||||
background-color: color-mix(in oklab, canvas, 75% #86c4fa);
|
||||
|
||||
.message-head {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
.download-list {
|
||||
box-sizing: border-box;
|
||||
list-style: none;
|
||||
height: 50dvh;
|
||||
min-width: 300px;
|
||||
}
|
||||
190
src/pages/MonitoringPage/components/ExperimentLogs.tsx
Normal file
190
src/pages/MonitoringPage/components/ExperimentLogs.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import styles from "./ExperimentLogs.module.css";
|
||||
import {LogMessages} from "../../../components/Logging/Logging.tsx";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {type LogFilterPredicate, type LogRecord, useLogs} from "../../../components/Logging/useLogs.ts";
|
||||
import capitalize from "../../../utils/capitalize.ts";
|
||||
import {useCell} from "../../../utils/cellStore.ts";
|
||||
import {
|
||||
EXPERIMENT_FILTER_KEY,
|
||||
EXPERIMENT_LOGGER_NAME,
|
||||
type LoggingSettings,
|
||||
type MessageComponentProps,
|
||||
} from "../../../components/Logging/Definitions.ts";
|
||||
import formatDuration from "../../../utils/formatDuration.ts";
|
||||
import {create} from "zustand";
|
||||
import Dialog from "../../../components/Dialog.tsx";
|
||||
import delayedResolve from "../../../utils/delayedResolve.ts";
|
||||
import { API_BASE_URL } from "../../../config/api.ts";
|
||||
|
||||
/**
|
||||
* Local Zustand store for logging UI preferences.
|
||||
*/
|
||||
const useLoggingSettings = create<LoggingSettings>((set) => ({
|
||||
showRelativeTime: false,
|
||||
setShowRelativeTime: (showRelativeTime: boolean) => set({ showRelativeTime }),
|
||||
}));
|
||||
|
||||
/**
|
||||
* A dedicated component for rendering chat messages.
|
||||
*
|
||||
* @param record The chat record to render.
|
||||
*/
|
||||
function ChatMessage({ record }: { record: LogRecord }) {
|
||||
const { showRelativeTime, setShowRelativeTime } = useLoggingSettings();
|
||||
|
||||
const reverse = record.role === "user" ? styles.alternate : "";
|
||||
|
||||
return <div className={`${styles.chatMessage} ${reverse} flex-col padding-md padding-h-lg shadow-md round-md`}>
|
||||
<div className={`${styles.messageHead} flex-row gap-md align-center`}>
|
||||
<span className={"bold"}>{capitalize(record.role ?? "unknown")}</span>
|
||||
<span className={"font-small"}>•</span>
|
||||
<span className={"mono clickable font-small"} onClick={() => setShowRelativeTime(!showRelativeTime)}>{
|
||||
showRelativeTime
|
||||
? formatDuration(record.relativeCreated)
|
||||
: new Date(record.created * 1000).toLocaleTimeString()
|
||||
}</span>
|
||||
</div>
|
||||
<span>{record.message}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic log message component showing the log level, time, and message text.
|
||||
*
|
||||
* @param record The log record to render.
|
||||
*/
|
||||
function DefaultMessage({ record }: { record: LogRecord }) {
|
||||
const { showRelativeTime, setShowRelativeTime } = useLoggingSettings();
|
||||
|
||||
return <div>
|
||||
<div className={"flex-row gap-md align-center"}>
|
||||
<span className={"font-small"}>{record.levelname}</span>
|
||||
<span className={"font-small"}>•</span>
|
||||
<span className={"mono clickable font-small"} onClick={() => setShowRelativeTime(!showRelativeTime)}>{
|
||||
showRelativeTime
|
||||
? formatDuration(record.relativeCreated)
|
||||
: new Date(record.created * 1000).toLocaleTimeString()
|
||||
}</span>
|
||||
</div>
|
||||
<span>{record.message}</span>
|
||||
</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom component for rendering experiment messages, which might include chat messages.
|
||||
*
|
||||
* @param recordCell The cell containing the log record to render.
|
||||
* @param onUpdate A callback to notify the parent component when the record changes.
|
||||
*/
|
||||
function ExperimentMessage({recordCell, onUpdate}: MessageComponentProps) {
|
||||
const record = useCell(recordCell);
|
||||
|
||||
// Notify the parent component (e.g., for scroll updates) when this record changes.
|
||||
useEffect(() => {
|
||||
if (onUpdate) onUpdate();
|
||||
}, [record, onUpdate]);
|
||||
|
||||
if (record.levelname == "CHAT") {
|
||||
return <ChatMessage record={record} />
|
||||
} else {
|
||||
return <DefaultMessage record={record} />
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A download dialog listing experiment logs to download.
|
||||
*
|
||||
* @param filenames The list of available experiment logs to download.
|
||||
* @param refresh A callback to refresh the list of available experiment logs.
|
||||
*/
|
||||
function DownloadScreen({filenames, refresh}: {filenames: string[] | null, refresh: () => void}) {
|
||||
const list = (() => {
|
||||
if (filenames == null) return <div className={`${styles.downloadList} flex-col align-center justify-center`}>
|
||||
<p>Loading...</p>
|
||||
</div>;
|
||||
if (filenames.length === 0) return <div className={`${styles.downloadList} flex-col align-center justify-center`}>
|
||||
<p>No files available.</p>
|
||||
</div>
|
||||
|
||||
return <ol className={`${styles.downloadList} margin-0 padding-h-lg scroll-y`}>
|
||||
{filenames!.map((filename) => (
|
||||
<li><a key={filename} href={`${API_BASE_URL}/api/logs/files/${filename}`} download={filename}>{filename}</a></li>
|
||||
))}
|
||||
</ol>;
|
||||
})();
|
||||
|
||||
return <div className={"flex-col"}>
|
||||
<p className={"margin-lg"}>Select a file to download:</p>
|
||||
{list}
|
||||
<button onClick={refresh} className={"margin-lg shadow-sm"}>Refresh</button>
|
||||
</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A button that opens a download dialog for experiment logs when pressed.
|
||||
*/
|
||||
function DownloadButton() {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [filenames, setFilenames] = useState<string[] | null>(null);
|
||||
|
||||
async function getFiles(): Promise<string[]> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/logs/files`);
|
||||
const files = await response.json();
|
||||
files.sort();
|
||||
return files;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getFiles().then(setFilenames);
|
||||
}, [showModal]);
|
||||
|
||||
return <>
|
||||
<button className={"shadow-sm"} onClick={() => setShowModal((curr) => !curr)}>Download...</button>
|
||||
<Dialog open={showModal} close={() => setShowModal(false)} classname={"padding-0 round-lg"}>
|
||||
<DownloadScreen filenames={filenames} refresh={async () => {
|
||||
setFilenames(null);
|
||||
const files = await delayedResolve(getFiles(), 250);
|
||||
setFilenames(files);
|
||||
}} />
|
||||
</Dialog>
|
||||
</>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component for rendering experiment logs. This component uses the `useLogs` hook with a filter to show only
|
||||
* experiment logs.
|
||||
*/
|
||||
export default function ExperimentLogs() {
|
||||
// Show only experiment logs in this logger
|
||||
const filters = useMemo(() => new Map<string, LogFilterPredicate>([
|
||||
[
|
||||
EXPERIMENT_FILTER_KEY,
|
||||
{
|
||||
predicate: (r) => r.name == EXPERIMENT_LOGGER_NAME,
|
||||
priority: 999,
|
||||
value: null,
|
||||
} as LogFilterPredicate,
|
||||
],
|
||||
]), []);
|
||||
|
||||
const { filteredLogs } = useLogs(filters);
|
||||
|
||||
return <aside className={`${styles.logs} flex-col relative`}>
|
||||
<div className={`${styles.head} padding-lg`}>
|
||||
<div className={"flex-row align-center justify-between"}>
|
||||
<h3>Logs</h3>
|
||||
<div className={"flex-row gap-md align-center"}>
|
||||
<div className={`flex-row align-center gap-md relative padding-md shadow-sm round-md`}>
|
||||
<div className={styles.live}></div>
|
||||
<span>Live</span>
|
||||
</div>
|
||||
<DownloadButton />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<LogMessages recordCells={filteredLogs} MessageComponent={ExperimentMessage} />
|
||||
</aside>;
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
|
||||
/**
|
||||
* Displays a live robot interaction panel with user input, conversation history,
|
||||
* and real-time updates from the robot backend via Server-Sent Events (SSE).
|
||||
*
|
||||
* @returns A React element rendering the interactive robot UI.
|
||||
*/
|
||||
export default function Robot() {
|
||||
/** The text message currently entered by the user. */
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
/** Whether the robot’s microphone or listening mode is currently active. */
|
||||
const [listening, setListening] = useState(false);
|
||||
/** The ongoing conversation history as a sequence of user/assistant messages. */
|
||||
const [conversation, setConversation] = useState<
|
||||
{"role": "user" | "assistant", "content": string}[]>([])
|
||||
/** Reference to the scrollable conversation container for auto-scrolling. */
|
||||
const conversationRef = useRef<HTMLDivElement | null>(null);
|
||||
/**
|
||||
* Index used to force refresh the SSE connection or clear conversation.
|
||||
* Incrementing this value triggers a reset of the live data stream.
|
||||
*/
|
||||
const [conversationIndex, setConversationIndex] = useState(0);
|
||||
|
||||
/**
|
||||
* Sends a message to the robot backend.
|
||||
*
|
||||
* Makes a POST request to `/message` with the user’s text.
|
||||
* The backend may respond with confirmation or error information.
|
||||
*/
|
||||
const sendMessage = async () => {
|
||||
try {
|
||||
const response = await fetch("http://localhost:8000/message", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ message }),
|
||||
});
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
} catch (error) {
|
||||
console.error("Error sending message: ", error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Establishes a persistent Server-Sent Events (SSE) connection
|
||||
* to receive real-time updates from the robot backend.
|
||||
*
|
||||
* Handles three event types:
|
||||
* - `voice_active`: whether the robot is currently listening.
|
||||
* - `speech`: recognized user speech input.
|
||||
* - `llm_response`: the robot’s language model-generated reply.
|
||||
*
|
||||
* The connection resets whenever `conversationIndex` changes.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const eventSource = new EventSource("http://localhost:8000/sse");
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if ("voice_active" in data) setListening(data.voice_active);
|
||||
if ("speech" in data) setConversation(conversation => [...conversation, {"role": "user", "content": data.speech}]);
|
||||
if ("llm_response" in data) setConversation(conversation => [...conversation, {"role": "assistant", "content": data.llm_response}]);
|
||||
} catch {
|
||||
console.log("Unparsable SSE message:", event.data);
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
eventSource.close();
|
||||
};
|
||||
}, [conversationIndex]);
|
||||
|
||||
/**
|
||||
* Automatically scrolls the conversation view to the bottom
|
||||
* whenever a new message is added.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!conversationRef || !conversationRef.current) return;
|
||||
conversationRef.current.scrollTop = conversationRef.current.scrollHeight;
|
||||
}, [conversation]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Robot interaction</h1>
|
||||
<h2>Force robot speech</h2>
|
||||
<div className={"flex-row gap-md justify-center"}>
|
||||
<input
|
||||
type="text"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && sendMessage().then(() => setMessage(""))}
|
||||
placeholder="Enter a message"
|
||||
/>
|
||||
<button onClick={sendMessage}>Speak</button>
|
||||
</div>
|
||||
<div className={"flex-col gap-lg align-center"}>
|
||||
<h2>Conversation</h2>
|
||||
<p>Listening {listening ? "🟢" : "🔴"}</p>
|
||||
<div style={{ maxHeight: "200px", maxWidth: "600px", overflowY: "auto"}} ref={conversationRef}>
|
||||
{conversation.map((item, i) => (
|
||||
<p key={i}
|
||||
style={{
|
||||
backgroundColor: item["role"] == "user"
|
||||
? "color-mix(in oklab, canvas, blue 20%)"
|
||||
: "color-mix(in oklab, canvas, gray 20%)",
|
||||
whiteSpace: "pre-line",
|
||||
}}
|
||||
className={"round-md padding-md"}
|
||||
>{item["content"]}</p>
|
||||
))}
|
||||
</div>
|
||||
<div className={"flex-row gap-md justify-center"}>
|
||||
<button onClick={() => {
|
||||
setConversationIndex((conversationIndex) => conversationIndex + 1)
|
||||
setConversation([])
|
||||
}}>Reset</button>
|
||||
<button onClick={() => {
|
||||
setConversationIndex((conversationIndex) => conversationIndex == -1 ? 0 : -1)
|
||||
setConversation([])
|
||||
}}>{conversationIndex == -1 ? "Start" : "Stop"}</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import Counter from '../../components/components.tsx'
|
||||
|
||||
function TemplatePage() {
|
||||
return (
|
||||
<>
|
||||
<Counter />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TemplatePage
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
University within the Software Project course.
|
||||
© Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
*/
|
||||
/* editor UI */
|
||||
|
||||
.inner-editor-container {
|
||||
@@ -84,7 +89,10 @@
|
||||
filter: drop-shadow(0 0 0.25rem plum);
|
||||
}
|
||||
|
||||
|
||||
.node-inferred_belief {
|
||||
outline: mediumpurple solid 2pt;
|
||||
filter: drop-shadow(0 0 0.25rem mediumpurple);
|
||||
}
|
||||
|
||||
.draggable-node {
|
||||
padding: 3px 10px;
|
||||
@@ -158,6 +166,14 @@
|
||||
filter: drop-shadow(0 0 0.25rem plum);
|
||||
}
|
||||
|
||||
.draggable-node-inferred_belief {
|
||||
padding: 3px 10px;
|
||||
background-color: canvas;
|
||||
border-radius: 5pt;
|
||||
outline: mediumpurple solid 2pt;
|
||||
filter: drop-shadow(0 0 0.25rem mediumpurple);
|
||||
}
|
||||
|
||||
.planNoIterate {
|
||||
opacity: 0.5;
|
||||
font-style: italic;
|
||||
@@ -172,6 +188,18 @@
|
||||
left: 60% !important;
|
||||
}
|
||||
|
||||
.planNoIterate {
|
||||
opacity: 0.5;
|
||||
font-style: italic;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.backButton {
|
||||
background: var(--bg-surface);
|
||||
box-shadow: var(--panel-shadow);
|
||||
margin-top: 0.5rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.node-toolbar-tooltip {
|
||||
background-color: darkgray;
|
||||
color: white;
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {
|
||||
Background,
|
||||
Controls,
|
||||
Panel,
|
||||
ReactFlow,
|
||||
ReactFlowProvider,
|
||||
MarkerType,
|
||||
MarkerType, getOutgoers
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import warningStyles from './visualProgrammingUI/components/WarningSidebar.module.css'
|
||||
import {type CSSProperties, useEffect, useState} from "react";
|
||||
import {useShallow} from 'zustand/react/shallow';
|
||||
import orderPhaseNodeArray from "../../utils/orderPhaseNodes.ts";
|
||||
import useProgramStore from "../../utils/programStore.ts";
|
||||
import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx';
|
||||
import type {PhaseNode} from "./visualProgrammingUI/nodes/PhaseNode.tsx";
|
||||
import {type EditorWarning, globalWarning} from "./visualProgrammingUI/components/EditorWarnings.tsx";
|
||||
import {WarningsSidebar} from "./visualProgrammingUI/components/WarningSidebar.tsx";
|
||||
import useFlowStore from './visualProgrammingUI/VisProgStores.tsx';
|
||||
import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx';
|
||||
import styles from './VisProg.module.css'
|
||||
import { NodeReduces, NodeTypes } from './visualProgrammingUI/NodeRegistry.ts';
|
||||
import {NodeTypes} from './visualProgrammingUI/NodeRegistry.ts';
|
||||
import SaveLoadPanel from './visualProgrammingUI/components/SaveLoadPanel.tsx';
|
||||
import MonitoringPage from '../MonitoringPage/MonitoringPage.tsx';
|
||||
import {graphReducer, runProgram} from './VisProgLogic.ts';
|
||||
|
||||
// --| config starting params for flow |--
|
||||
|
||||
@@ -42,6 +48,7 @@ const selector = (state: FlowState) => ({
|
||||
nodes: state.nodes,
|
||||
edges: state.edges,
|
||||
onNodesChange: state.onNodesChange,
|
||||
onNodesDelete: state.onNodesDelete,
|
||||
onEdgesDelete: state.onEdgesDelete,
|
||||
onEdgesChange: state.onEdgesChange,
|
||||
onConnect: state.onConnect,
|
||||
@@ -67,6 +74,7 @@ const VisProgUI = () => {
|
||||
const {
|
||||
nodes, edges,
|
||||
onNodesChange,
|
||||
onNodesDelete,
|
||||
onEdgesDelete,
|
||||
onEdgesChange,
|
||||
onConnect,
|
||||
@@ -89,15 +97,36 @@ const VisProgUI = () => {
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
});
|
||||
const {unregisterWarning, registerWarning} = useFlowStore();
|
||||
useEffect(() => {
|
||||
|
||||
if (checkPhaseChain()) {
|
||||
unregisterWarning(globalWarning,'INCOMPLETE_PROGRAM');
|
||||
} else {
|
||||
// create global warning for incomplete program chain
|
||||
const incompleteProgramWarning : EditorWarning = {
|
||||
scope: {
|
||||
id: globalWarning,
|
||||
handleId: undefined
|
||||
},
|
||||
type: 'INCOMPLETE_PROGRAM',
|
||||
severity: "ERROR",
|
||||
description: "there is no complete phase chain from the startNode to the EndNode"
|
||||
}
|
||||
|
||||
registerWarning(incompleteProgramWarning);
|
||||
}
|
||||
},[edges, registerWarning, unregisterWarning])
|
||||
|
||||
return (
|
||||
<div className={`${styles.innerEditorContainer} round-lg border-lg`} style={({'--flow-zoom': zoom} as CSSProperties)}>
|
||||
<div className={`${styles.innerEditorContainer} round-lg border-lg flex-row`} style={({'--flow-zoom': zoom} as CSSProperties)}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
defaultEdgeOptions={DEFAULT_EDGE_OPTIONS}
|
||||
nodeTypes={NodeTypes}
|
||||
onNodesChange={onNodesChange}
|
||||
onNodesDelete={onNodesDelete}
|
||||
onEdgesDelete={onEdgesDelete}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onReconnect={onReconnect}
|
||||
@@ -108,9 +137,11 @@ const VisProgUI = () => {
|
||||
onNodeDragStop={endBatchAction}
|
||||
preventScrolling={scrollable}
|
||||
onMove={(_, viewport) => setZoom(viewport.zoom)}
|
||||
reconnectRadius={15}
|
||||
snapToGrid
|
||||
fitView
|
||||
proOptions={{hideAttribution: true}}
|
||||
style={{flexGrow: 3}}
|
||||
>
|
||||
<Panel position="top-center" className={styles.dndPanel}>
|
||||
<DndToolbar/> {/* contains the drag and drop panel for nodes */}
|
||||
@@ -119,12 +150,16 @@ const VisProgUI = () => {
|
||||
<SaveLoadPanel></SaveLoadPanel>
|
||||
</Panel>
|
||||
<Panel position="bottom-center">
|
||||
<button onClick={() => undo()}>undo</button>
|
||||
<button onClick={() => undo()}>Undo</button>
|
||||
<button onClick={() => redo()}>Redo</button>
|
||||
</Panel>
|
||||
<Panel position="center-right" className={warningStyles.warningsSidebar}>
|
||||
<WarningsSidebar/>
|
||||
</Panel>
|
||||
<Controls/>
|
||||
<Background/>
|
||||
</ReactFlow>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -144,41 +179,23 @@ function VisualProgrammingUI() {
|
||||
);
|
||||
}
|
||||
|
||||
// currently outputs the prepared program to the console
|
||||
function runProgram() {
|
||||
const phases = graphReducer();
|
||||
const program = {phases}
|
||||
console.log(JSON.stringify(program, null, 2));
|
||||
fetch(
|
||||
"http://localhost:8000/program",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify(program),
|
||||
}
|
||||
).then((res) => {
|
||||
if (!res.ok) throw new Error("Failed communicating with the backend.")
|
||||
console.log("Successfully sent the program to the backend.");
|
||||
const checkPhaseChain = (): boolean => {
|
||||
const {nodes, edges} = useFlowStore.getState();
|
||||
|
||||
// store reduced program in global program store for further use in the UI
|
||||
// when the program was sent to the backend successfully:
|
||||
useProgramStore.getState().setProgramState(structuredClone(program));
|
||||
}).catch(() => console.log("Failed to send program to the backend."));
|
||||
}
|
||||
function checkForCompleteChain(currentNodeId: string): boolean {
|
||||
const outgoingPhases = getOutgoers({id: currentNodeId}, nodes, edges)
|
||||
.filter(node => ["end", "phase"].includes(node.type!));
|
||||
|
||||
/**
|
||||
* Reduces the graph into its phases' information and recursively calls their reducing function
|
||||
*/
|
||||
function graphReducer() {
|
||||
const { nodes } = useFlowStore.getState();
|
||||
return orderPhaseNodeArray(nodes.filter((n) => n.type == 'phase') as PhaseNode [])
|
||||
.map((n) => {
|
||||
const reducer = NodeReduces['phase'];
|
||||
return reducer(n, nodes)
|
||||
});
|
||||
}
|
||||
if (outgoingPhases.length === 0) return false;
|
||||
if (outgoingPhases.some(node => node.type === "end" )) return true;
|
||||
|
||||
const next = outgoingPhases.map(node => checkForCompleteChain(node.id))
|
||||
.find(result => result);
|
||||
return !!next;
|
||||
}
|
||||
|
||||
return checkForCompleteChain('start');
|
||||
};
|
||||
|
||||
/**
|
||||
* houses the entire page, so also UI elements
|
||||
@@ -186,10 +203,44 @@ function graphReducer() {
|
||||
* @constructor
|
||||
*/
|
||||
function VisProgPage() {
|
||||
const [showSimpleProgram, setShowSimpleProgram] = useState(false);
|
||||
const [programValidity, setProgramValidity] = useState<boolean>(true);
|
||||
const {isProgramValid, severityIndex} = useFlowStore();
|
||||
const setProgramState = useProgramStore((state) => state.setProgramState);
|
||||
|
||||
const validity = () => {return isProgramValid();}
|
||||
|
||||
useEffect(() => {
|
||||
setProgramValidity(validity);
|
||||
// the following eslint disable is required as it wants us to use all possible dependencies for the useEffect statement,
|
||||
// however this would cause unneeded updates
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [severityIndex]);
|
||||
|
||||
const processProgram = () => {
|
||||
const phases = graphReducer(); // reduce graph
|
||||
setProgramState({ phases }); // <-- save to store
|
||||
setShowSimpleProgram(true); // show SimpleProgram
|
||||
runProgram(); // send to backend if needed
|
||||
};
|
||||
|
||||
if (showSimpleProgram) {
|
||||
return (
|
||||
<div>
|
||||
<button className={styles.backButton} onClick={() => setShowSimpleProgram(false)}>
|
||||
Back to Editor ◀
|
||||
</button>
|
||||
<MonitoringPage/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<VisualProgrammingUI/>
|
||||
<button onClick={runProgram}>run program</button>
|
||||
<button onClick={processProgram} disabled={!programValidity}>Run Program</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
47
src/pages/VisProgPage/VisProgLogic.ts
Normal file
47
src/pages/VisProgPage/VisProgLogic.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import useProgramStore from "../../utils/programStore";
|
||||
import orderPhaseNodeArray from "../../utils/orderPhaseNodes";
|
||||
import useFlowStore from './visualProgrammingUI/VisProgStores';
|
||||
import { NodeReduces } from './visualProgrammingUI/NodeRegistry';
|
||||
import type { PhaseNode } from "./visualProgrammingUI/nodes/PhaseNode";
|
||||
import { API_BASE_URL } from "../../config/api.ts";
|
||||
|
||||
/**
|
||||
* Reduces the graph into its phases' information and recursively calls their reducing function
|
||||
*/
|
||||
export function graphReducer() {
|
||||
const { nodes } = useFlowStore.getState();
|
||||
return orderPhaseNodeArray(nodes.filter((n) => n.type == 'phase') as PhaseNode [])
|
||||
.map((n) => {
|
||||
const reducer = NodeReduces['phase'];
|
||||
return reducer(n, nodes)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Outputs the prepared program to the console and sends it to the backend
|
||||
*/
|
||||
export function runProgram() {
|
||||
const phases = graphReducer();
|
||||
const program = {phases}
|
||||
console.log(JSON.stringify(program, null, 2));
|
||||
fetch(
|
||||
`${API_BASE_URL}/program`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify(program),
|
||||
}
|
||||
).then((res) => {
|
||||
if (!res.ok) throw new Error("Failed communicating with the backend.")
|
||||
console.log("Successfully sent the program to the backend.");
|
||||
|
||||
// store reduced program in global program store for further use in the UI
|
||||
// when the program was sent to the backend successfully:
|
||||
useProgramStore.getState().setProgramState(structuredClone(program));
|
||||
}).catch(() => console.log("Failed to send program to the backend."));
|
||||
console.log(program);
|
||||
}
|
||||
@@ -1,10 +1,21 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import type {Edge, Node} from "@xyflow/react";
|
||||
import type {StateCreator, StoreApi } from 'zustand/vanilla';
|
||||
import type {
|
||||
SeverityIndex,
|
||||
WarningRegistry
|
||||
} from "./components/EditorWarnings.tsx";
|
||||
import type {FlowState} from "./VisProgTypes.tsx";
|
||||
|
||||
export type FlowSnapshot = {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
warnings: {
|
||||
warningRegistry: WarningRegistry;
|
||||
severityIndex: SeverityIndex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,7 +52,11 @@ export const UndoRedo = (
|
||||
*/
|
||||
const getSnapshot = (state : BaseFlowState) : FlowSnapshot => (structuredClone({
|
||||
nodes: state.nodes,
|
||||
edges: state.edges
|
||||
edges: state.edges,
|
||||
warnings: {
|
||||
warningRegistry: state.editorWarningRegistry,
|
||||
severityIndex: state.severityIndex,
|
||||
}
|
||||
}));
|
||||
|
||||
const initialState = config(set, get, api);
|
||||
@@ -78,6 +93,8 @@ export const UndoRedo = (
|
||||
set({
|
||||
nodes: snapshot.nodes,
|
||||
edges: snapshot.edges,
|
||||
editorWarningRegistry: snapshot.warnings.warningRegistry,
|
||||
severityIndex: snapshot.warnings.severityIndex,
|
||||
});
|
||||
|
||||
state.future.push(currentSnapshot); // push current to redo
|
||||
@@ -97,6 +114,8 @@ export const UndoRedo = (
|
||||
set({
|
||||
nodes: snapshot.nodes,
|
||||
edges: snapshot.edges,
|
||||
editorWarningRegistry: snapshot.warnings.warningRegistry,
|
||||
severityIndex: snapshot.warnings.severityIndex,
|
||||
});
|
||||
|
||||
state.past.push(currentSnapshot); // push current to undo
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {type Connection} from "@xyflow/react";
|
||||
import {useEffect} from "react";
|
||||
import useFlowStore from "./VisProgStores.tsx";
|
||||
@@ -107,4 +110,16 @@ export function useHandleRules(
|
||||
// finally we return a function that evaluates all rules using the created context
|
||||
return evaluateRules(targetRules, connection, context);
|
||||
};
|
||||
}
|
||||
|
||||
export function validateConnectionWithRules(
|
||||
connection: Connection,
|
||||
context: ConnectionContext
|
||||
): RuleResult {
|
||||
const rules = useFlowStore.getState().getTargetRules(
|
||||
connection.target!,
|
||||
connection.targetHandle!
|
||||
);
|
||||
|
||||
return evaluateRules(rules,connection, context);
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {
|
||||
type HandleRule,
|
||||
ruleResult
|
||||
@@ -43,3 +46,4 @@ export const noSelfConnections : HandleRule =
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import EndNode, {
|
||||
EndConnectionTarget,
|
||||
EndConnectionSource,
|
||||
@@ -52,15 +55,24 @@ import TriggerNode, {
|
||||
TriggerTooltip
|
||||
} from "./nodes/TriggerNode";
|
||||
import { TriggerNodeDefaults } from "./nodes/TriggerNode.default";
|
||||
import InferredBeliefNode, {
|
||||
InferredBeliefConnectionTarget,
|
||||
InferredBeliefConnectionSource,
|
||||
InferredBeliefDisconnectionTarget,
|
||||
InferredBeliefDisconnectionSource,
|
||||
InferredBeliefReduce, InferredBeliefTooltip
|
||||
} from "./nodes/InferredBeliefNode";
|
||||
import { InferredBeliefNodeDefaults } from "./nodes/InferredBeliefNode.default";
|
||||
import BasicBeliefNode, {
|
||||
BasicBeliefConnectionSource,
|
||||
BasicBeliefConnectionTarget,
|
||||
BasicBeliefDisconnectionSource,
|
||||
BasicBeliefDisconnectionTarget,
|
||||
BasicBeliefReduce,
|
||||
BasicBeliefReduce
|
||||
,
|
||||
BasicBeliefTooltip
|
||||
} from "./nodes/BasicBeliefNode";
|
||||
import { BasicBeliefNodeDefaults } from "./nodes/BasicBeliefNode.default";
|
||||
} from "./nodes/BasicBeliefNode.tsx";
|
||||
import { BasicBeliefNodeDefaults } from "./nodes/BasicBeliefNode.default.ts";
|
||||
|
||||
/**
|
||||
* Registered node types in the visual programming system.
|
||||
@@ -76,6 +88,7 @@ export const NodeTypes = {
|
||||
goal: GoalNode,
|
||||
trigger: TriggerNode,
|
||||
basic_belief: BasicBeliefNode,
|
||||
inferred_belief: InferredBeliefNode,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -91,6 +104,7 @@ export const NodeDefaults = {
|
||||
goal: GoalNodeDefaults,
|
||||
trigger: TriggerNodeDefaults,
|
||||
basic_belief: BasicBeliefNodeDefaults,
|
||||
inferred_belief: InferredBeliefNodeDefaults,
|
||||
};
|
||||
|
||||
|
||||
@@ -108,6 +122,7 @@ export const NodeReduces = {
|
||||
goal: GoalReduce,
|
||||
trigger: TriggerReduce,
|
||||
basic_belief: BasicBeliefReduce,
|
||||
inferred_belief: InferredBeliefReduce,
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +141,7 @@ export const NodeConnections = {
|
||||
goal: GoalConnectionTarget,
|
||||
trigger: TriggerConnectionTarget,
|
||||
basic_belief: BasicBeliefConnectionTarget,
|
||||
inferred_belief: InferredBeliefConnectionTarget,
|
||||
},
|
||||
Sources: {
|
||||
start: StartConnectionSource,
|
||||
@@ -134,7 +150,8 @@ export const NodeConnections = {
|
||||
norm: NormConnectionSource,
|
||||
goal: GoalConnectionSource,
|
||||
trigger: TriggerConnectionSource,
|
||||
basic_belief: BasicBeliefConnectionSource
|
||||
basic_belief: BasicBeliefConnectionSource,
|
||||
inferred_belief: InferredBeliefConnectionSource,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +170,7 @@ export const NodeDisconnections = {
|
||||
goal: GoalDisconnectionTarget,
|
||||
trigger: TriggerDisconnectionTarget,
|
||||
basic_belief: BasicBeliefDisconnectionTarget,
|
||||
inferred_belief: InferredBeliefDisconnectionTarget,
|
||||
},
|
||||
Sources: {
|
||||
start: StartDisconnectionSource,
|
||||
@@ -162,6 +180,7 @@ export const NodeDisconnections = {
|
||||
goal: GoalDisconnectionSource,
|
||||
trigger: TriggerDisconnectionSource,
|
||||
basic_belief: BasicBeliefDisconnectionSource,
|
||||
inferred_belief: InferredBeliefDisconnectionSource,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -186,6 +205,7 @@ export const NodesInPhase = {
|
||||
end: () => false,
|
||||
phase: () => false,
|
||||
basic_belief: () => false,
|
||||
inferred_belief: () => false,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,4 +219,5 @@ export const NodeTooltips = {
|
||||
goal: GoalTooltip,
|
||||
trigger: TriggerTooltip,
|
||||
basic_belief: BasicBeliefTooltip,
|
||||
inferred_belief: InferredBeliefTooltip,
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import { create } from 'zustand';
|
||||
import {
|
||||
applyNodeChanges,
|
||||
@@ -9,6 +12,8 @@ import {
|
||||
type XYPosition,
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import {type ConnectionContext, validateConnectionWithRules} from "./HandleRuleLogic.ts";
|
||||
import {editorWarningRegistry} from "./components/EditorWarnings.tsx";
|
||||
import type { FlowState } from './VisProgTypes';
|
||||
import {
|
||||
NodeDefaults,
|
||||
@@ -43,19 +48,18 @@ function createNode(id: string, type: string, position: XYPosition, data: Record
|
||||
}
|
||||
}
|
||||
|
||||
//* Initial nodes, created by using createNode. */
|
||||
// Start and End don't need to apply the UUID, since they are technically never compiled into a program.
|
||||
const startNode = createNode('start', 'start', {x: 110, y: 100}, {label: "Start"}, false)
|
||||
const endNode = createNode('end', 'end', {x: 590, y: 100}, {label: "End"}, false)
|
||||
const initialPhaseNode = createNode(crypto.randomUUID(), 'phase', {x:235, y:100}, {label: "Phase 1", children : [], isFirstPhase: false, nextPhaseId: null})
|
||||
//* Initial nodes, created by using createNode. */
|
||||
// Start and End don't need to apply the UUID, since they are technically never compiled into a program.
|
||||
const startNode = createNode('start', 'start', {x: 110, y: 100}, {label: "Start"}, false)
|
||||
const endNode = createNode('end', 'end', {x: 590, y: 100}, {label: "End"}, false)
|
||||
const initialPhaseNode = createNode(crypto.randomUUID(), 'phase', {x:235, y:100}, {label: "Phase 1", children : [], isFirstPhase: false, nextPhaseId: null})
|
||||
|
||||
const initialNodes : Node[] = [startNode, endNode, initialPhaseNode,];
|
||||
const initialNodes : Node[] = [startNode, endNode, initialPhaseNode];
|
||||
|
||||
// Initial edges, leave empty as setting initial edges...
|
||||
// ...breaks logic that is dependent on connection events
|
||||
const initialEdges: Edge[] = [];
|
||||
|
||||
|
||||
/**
|
||||
* useFlowStore contains the implementation for all editor functionality
|
||||
* and stores the current state of the visual programming editor
|
||||
@@ -86,7 +90,20 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
|
||||
*/
|
||||
onNodesChange: (changes) => set({nodes: applyNodeChanges(changes, get().nodes)}),
|
||||
|
||||
onNodesDelete: (nodes) => nodes.forEach(node => get().unregisterNodeRules(node.id)),
|
||||
onNodesDelete: (deletedNodes) => {
|
||||
|
||||
const allNodes = get().nodes;
|
||||
const deletedIds = new Set(deletedNodes.map(n => n.id));
|
||||
|
||||
deletedNodes.forEach((node) => {
|
||||
get().unregisterNodeRules(node.id);
|
||||
get().unregisterWarningsForId(node.id);
|
||||
});
|
||||
const remainingNodes = allNodes.filter((node) => !deletedIds.has(node.id));
|
||||
|
||||
// Validate only the survivors
|
||||
get().validateDuplicateNames(remainingNodes);
|
||||
},
|
||||
|
||||
onEdgesDelete: (edges) => {
|
||||
// we make sure any affected nodes get updated to reflect removal of edges
|
||||
@@ -129,7 +146,41 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
|
||||
* Handles reconnecting an edge between nodes.
|
||||
*/
|
||||
onReconnect: (oldEdge, newConnection) => {
|
||||
get().edgeReconnectSuccessful = true;
|
||||
|
||||
function createContext(
|
||||
source: {id: string, handleId: string},
|
||||
target: {id: string, handleId: string}
|
||||
) : ConnectionContext {
|
||||
const edges = get().edges;
|
||||
const targetConnections = edges.filter(edge => edge.target === target.id && edge.targetHandle === target.handleId).length
|
||||
return {
|
||||
connectionCount: targetConnections,
|
||||
source: source,
|
||||
target: target
|
||||
}
|
||||
}
|
||||
|
||||
// connection validation
|
||||
const context: ConnectionContext = oldEdge.source === newConnection.source
|
||||
? createContext({id: newConnection.source, handleId: newConnection.sourceHandle!}, {id: newConnection.target, handleId: newConnection.targetHandle!})
|
||||
: createContext({id: newConnection.target, handleId: newConnection.targetHandle!}, {id: newConnection.source, handleId: newConnection.sourceHandle!});
|
||||
|
||||
const result = validateConnectionWithRules(
|
||||
newConnection,
|
||||
context
|
||||
);
|
||||
|
||||
if (!result.isSatisfied) {
|
||||
set({
|
||||
edges: get().edges.map(e =>
|
||||
e.id === oldEdge.id ? oldEdge : e
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// further reconnect logic
|
||||
set({ edgeReconnectSuccessful: true });
|
||||
set({ edges: reconnectEdge(oldEdge, newConnection, get().edges) });
|
||||
|
||||
// We make sure to perform any required data updates on the newly reconnected nodes
|
||||
@@ -182,19 +233,36 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
|
||||
* Deletes a node by ID, respecting NodeDeletes rules.
|
||||
* Also removes all edges connected to that node.
|
||||
*/
|
||||
deleteNode: (nodeId) => {
|
||||
deleteNode: (nodeId, deleteElements) => {
|
||||
get().pushSnapshot();
|
||||
|
||||
// Let's find our node to check if they have a special deletion function
|
||||
const ourNode = get().nodes.find((n)=>n.id==nodeId);
|
||||
const ourFunction = Object.entries(NodeDeletes).find(([t])=>t==ourNode?.type)?.[1]
|
||||
|
||||
|
||||
|
||||
|
||||
// If there's no function, OR, our function tells us we can delete it, let's do so...
|
||||
if (ourFunction == undefined || ourFunction()) {
|
||||
set({
|
||||
nodes: get().nodes.filter((n) => n.id !== nodeId),
|
||||
edges: get().edges.filter((e) => e.source !== nodeId && e.target !== nodeId),
|
||||
})}
|
||||
if (deleteElements){
|
||||
deleteElements({
|
||||
nodes: get().nodes.filter((n) => n.id === nodeId),
|
||||
edges: get().edges.filter((e) => e.source !== nodeId && e.target === nodeId)}
|
||||
).then(() => {
|
||||
get().unregisterNodeRules(nodeId);
|
||||
get().unregisterWarningsForId(nodeId);
|
||||
// Re-validate after deletion is finished
|
||||
get().validateDuplicateNames(get().nodes);
|
||||
});
|
||||
} else {
|
||||
const remainingNodes = get().nodes.filter((n) => n.id !== nodeId);
|
||||
get().validateDuplicateNames(remainingNodes); // Re-validate survivors
|
||||
set({
|
||||
nodes: remainingNodes,
|
||||
edges: get().edges.filter((e) => e.source !== nodeId && e.target !== nodeId),
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -212,15 +280,49 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
|
||||
*/
|
||||
updateNodeData: (nodeId, data) => {
|
||||
get().pushSnapshot();
|
||||
set({
|
||||
nodes: get().nodes.map((node) => {
|
||||
if (node.id === nodeId) {
|
||||
node = { ...node, data: { ...node.data, ...data }};
|
||||
}
|
||||
return node;
|
||||
}),
|
||||
const updatedNodes = get().nodes.map((node) => {
|
||||
if (node.id === nodeId) {
|
||||
return { ...node, data: { ...node.data, ...data } };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
|
||||
get().validateDuplicateNames(updatedNodes); // Re-validate after update
|
||||
set({ nodes: updatedNodes });
|
||||
},
|
||||
|
||||
//helper function to see if any of the nodes have duplicate names
|
||||
validateDuplicateNames: (nodes: Node[]) => {
|
||||
const nameMap = new Map<string, string[]>();
|
||||
|
||||
// 1. Group IDs by their identifier (name, norm, or label)
|
||||
nodes.forEach((n) => {
|
||||
const name = (n.data.name || n.data.norm )?.toString().trim();
|
||||
if (name) {
|
||||
if (!nameMap.has(name)) nameMap.set(name, []);
|
||||
nameMap.get(name)!.push(n.id);
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Scan nodes and toggle the warning
|
||||
nodes.forEach((n) => {
|
||||
const name = (n.data.name || n.data.norm )?.toString().trim();
|
||||
const isDuplicate = name ? (nameMap.get(name)?.length || 0) > 1 : false;
|
||||
|
||||
if (isDuplicate) {
|
||||
get().registerWarning({
|
||||
scope: { id: n.id },
|
||||
type: 'DUPLICATE_ELEMENT_NAME',
|
||||
severity: 'ERROR',
|
||||
description: `The name "${name}" is already used by another element.`
|
||||
});
|
||||
} else {
|
||||
// This clears the warning if the "twin" was deleted or renamed
|
||||
get().unregisterWarning(n.id, 'DUPLICATE_ELEMENT_NAME');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Adds a new node to the flow store.
|
||||
@@ -306,8 +408,12 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
|
||||
})
|
||||
return { ruleRegistry: registry };
|
||||
})
|
||||
}
|
||||
},
|
||||
...editorWarningRegistry(get, set),
|
||||
}))
|
||||
);
|
||||
|
||||
|
||||
|
||||
export default useFlowStore;
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
// VisProgTypes.ts
|
||||
import type {
|
||||
Edge,
|
||||
@@ -7,8 +10,9 @@ import type {
|
||||
OnReconnect,
|
||||
Node,
|
||||
OnEdgesDelete,
|
||||
OnNodesDelete
|
||||
OnNodesDelete, DeleteElementsOptions
|
||||
} from '@xyflow/react';
|
||||
import type {EditorWarningRegistry} from "./components/EditorWarnings.tsx";
|
||||
import type {HandleRule} from "./HandleRuleLogic.ts";
|
||||
import type { NodeTypes } from './NodeRegistry';
|
||||
import type {FlowSnapshot} from "./EditorUndoRedo.ts";
|
||||
@@ -68,7 +72,10 @@ export type FlowState = {
|
||||
* Deletes a node and any connected edges.
|
||||
* @param nodeId - the ID of the node to delete
|
||||
*/
|
||||
deleteNode: (nodeId: string) => void;
|
||||
deleteNode: (nodeId: string, deleteElements?: (params: DeleteElementsOptions) => Promise<{
|
||||
deletedNodes: Node[]
|
||||
deletedEdges: Edge[]
|
||||
}>) => void;
|
||||
|
||||
/**
|
||||
* Replaces the current nodes array in the store.
|
||||
@@ -89,12 +96,17 @@ export type FlowState = {
|
||||
*/
|
||||
updateNodeData: (nodeId: string, data: object) => void;
|
||||
|
||||
/**
|
||||
* Validates that all node names are unique across the workspace.
|
||||
*/
|
||||
validateDuplicateNames: (nodes: Node[]) => void;
|
||||
|
||||
/**
|
||||
* Adds a new node to the flow.
|
||||
* @param node - the Node object to add
|
||||
*/
|
||||
addNode: (node: Node) => void;
|
||||
} & UndoRedoState & HandleRuleRegistry;
|
||||
} & UndoRedoState & HandleRuleRegistry & EditorWarningRegistry;
|
||||
|
||||
export type UndoRedoState = {
|
||||
// UndoRedo Types
|
||||
@@ -129,4 +141,7 @@ export type HandleRuleRegistry = {
|
||||
|
||||
// cleans up all registered rules of all handles of the provided node
|
||||
unregisterNodeRules: (nodeId: string) => void
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import { useDraggable } from '@neodrag/react';
|
||||
import { useReactFlow, type XYPosition } from '@xyflow/react';
|
||||
import { type ReactNode, useCallback, useRef, useState } from 'react';
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
/* contains all logic for the VisProgEditor warning system
|
||||
*
|
||||
* Missing but desirable features:
|
||||
* - Warning filtering:
|
||||
* - if there is no completely connected chain of startNode-[PhaseNodes]-EndNode
|
||||
* then hide any startNode, phaseNode, or endNode specific warnings
|
||||
*/
|
||||
import useFlowStore from "../VisProgStores.tsx";
|
||||
import type {FlowState} from "../VisProgTypes.tsx";
|
||||
|
||||
// --| Type definitions |--
|
||||
|
||||
export type WarningId = NodeId | "GLOBAL_WARNINGS";
|
||||
export type NodeId = string;
|
||||
|
||||
|
||||
export type WarningType =
|
||||
| 'MISSING_INPUT'
|
||||
| 'MISSING_OUTPUT'
|
||||
| 'PLAN_IS_UNDEFINED'
|
||||
| 'INCOMPLETE_PROGRAM'
|
||||
| 'NOT_CONNECTED_TO_PROGRAM'
|
||||
| 'ELEMENT_STARTS_WITH_NUMBER' //(non-phase)elements are not allowed to be or start with a number
|
||||
| 'DUPLICATE_ELEMENT_NAME' // elements are not allowed to have the same name as another element
|
||||
| string
|
||||
|
||||
export type WarningSeverity =
|
||||
| 'INFO' // Acceptable, but important to be aware of
|
||||
| 'WARNING' // Acceptable, but probably undesirable behavior
|
||||
| 'ERROR' // Prevents running program, should be fixed before running program is allowed
|
||||
|
||||
/**
|
||||
* warning scope, include a handleId if the warning is handle specific
|
||||
*/
|
||||
export type WarningScope = {
|
||||
id: string;
|
||||
handleId?: string;
|
||||
}
|
||||
|
||||
export type EditorWarning = {
|
||||
scope: WarningScope;
|
||||
type: WarningType;
|
||||
severity: WarningSeverity;
|
||||
description: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* a scoped WarningKey,
|
||||
* the handleId scoping is only needed for handle specific errors
|
||||
*
|
||||
* "`WarningType`:`handleId`"
|
||||
*/
|
||||
export type WarningKey = string; // for warnings that can occur on a per-handle basis
|
||||
|
||||
/**
|
||||
* a composite key used in the severityIndex
|
||||
*
|
||||
* "`WarningId`|`WarningKey`"
|
||||
*/
|
||||
export type CompositeWarningKey = string;
|
||||
|
||||
export type WarningRegistry = Map<WarningId , Map<WarningKey, EditorWarning>>;
|
||||
export type SeverityIndex = Map<WarningSeverity, Set<CompositeWarningKey>>;
|
||||
|
||||
type ZustandSet = (partial: Partial<FlowState> | ((state: FlowState) => Partial<FlowState>)) => void;
|
||||
type ZustandGet = () => FlowState;
|
||||
|
||||
export type EditorWarningRegistry = {
|
||||
/**
|
||||
* stores all editor warnings
|
||||
*/
|
||||
editorWarningRegistry: WarningRegistry;
|
||||
/**
|
||||
* index of warnings by severity
|
||||
*/
|
||||
severityIndex: SeverityIndex;
|
||||
|
||||
/**
|
||||
* gets all warnings and returns them as a list of warnings
|
||||
* @returns {EditorWarning[]}
|
||||
*/
|
||||
getWarnings: () => EditorWarning[];
|
||||
|
||||
/**
|
||||
* gets all warnings with the current severity
|
||||
* @param {WarningSeverity} warningSeverity
|
||||
* @returns {EditorWarning[]}
|
||||
*/
|
||||
getWarningsBySeverity: (warningSeverity: WarningSeverity) => EditorWarning[];
|
||||
|
||||
/**
|
||||
* checks if there are no warnings of breaking severity
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isProgramValid: () => boolean;
|
||||
|
||||
/**
|
||||
* registers a warning to the warningRegistry and the SeverityIndex
|
||||
* @param {EditorWarning} warning
|
||||
*/
|
||||
registerWarning: (warning: EditorWarning) => void;
|
||||
|
||||
/**
|
||||
* unregisters a warning from the warningRegistry and the SeverityIndex
|
||||
* @param {EditorWarning} warning
|
||||
*/
|
||||
unregisterWarning: (id: WarningId, warningKey: WarningKey) => void
|
||||
|
||||
/**
|
||||
* unregisters warnings from the warningRegistry and the SeverityIndex
|
||||
* @param {WarningId} warning
|
||||
*/
|
||||
unregisterWarningsForId: (id: WarningId) => void;
|
||||
}
|
||||
|
||||
// --| implemented logic |--
|
||||
|
||||
/**
|
||||
* the id to use for global editor warnings
|
||||
* @type {string}
|
||||
*/
|
||||
export const globalWarning = "GLOBAL_WARNINGS";
|
||||
|
||||
export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : EditorWarningRegistry { return {
|
||||
editorWarningRegistry: new Map<NodeId, Map<WarningKey, EditorWarning>>(),
|
||||
severityIndex: new Map([
|
||||
['INFO', new Set<CompositeWarningKey>()],
|
||||
['WARNING', new Set<CompositeWarningKey>()],
|
||||
['ERROR', new Set<CompositeWarningKey>()],
|
||||
]),
|
||||
|
||||
getWarningsBySeverity: (warningSeverity) => {
|
||||
const wRegistry = new Map([...get().editorWarningRegistry].map(([k, v]) => [k, new Map(v)]));
|
||||
const sIndex = new Map(get().severityIndex);
|
||||
const warningKeys = sIndex.get(warningSeverity);
|
||||
const warnings: EditorWarning[] = [];
|
||||
|
||||
warningKeys?.forEach(
|
||||
(compositeKey) => {
|
||||
const [id, warningKey] = compositeKey.split('|');
|
||||
const warning = wRegistry.get(id)?.get(warningKey);
|
||||
|
||||
if (warning) {
|
||||
warnings.push(warning);
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return warnings;
|
||||
},
|
||||
|
||||
isProgramValid: () => {
|
||||
const sIndex = get().severityIndex;
|
||||
return (sIndex.get("ERROR")!.size === 0);
|
||||
},
|
||||
|
||||
getWarnings: () => Array.from(get().editorWarningRegistry.values())
|
||||
.flatMap(innerMap => Array.from(innerMap.values())),
|
||||
|
||||
|
||||
registerWarning: (warning) => {
|
||||
const { scope: {id, handleId}, type, severity } = warning;
|
||||
const warningKey = handleId ? `${type}:${handleId}` : type;
|
||||
const compositeKey = `${id}|${warningKey}`;
|
||||
const wRegistry = new Map([...get().editorWarningRegistry].map(([k, v]) => [k, new Map(v)]));
|
||||
const sIndex = new Map(get().severityIndex);
|
||||
// add to warning registry
|
||||
if (!wRegistry.has(id)) {
|
||||
wRegistry.set(id, new Map());
|
||||
}
|
||||
wRegistry.get(id)!.set(warningKey, warning);
|
||||
|
||||
|
||||
// add to severityIndex
|
||||
if (!sIndex.get(severity)!.has(compositeKey)) {
|
||||
sIndex.get(severity)!.add(compositeKey);
|
||||
}
|
||||
|
||||
set({
|
||||
editorWarningRegistry: wRegistry,
|
||||
severityIndex: sIndex
|
||||
})
|
||||
},
|
||||
|
||||
unregisterWarning: (id, warningKey) => {
|
||||
const wRegistry = new Map([...get().editorWarningRegistry].map(([k, v]) => [k, new Map(v)]));
|
||||
const sIndex = new Map(get().severityIndex);
|
||||
// verify if the warning was created already
|
||||
const warning = wRegistry.get(id)?.get(warningKey);
|
||||
if (!warning) return;
|
||||
|
||||
// remove from warning registry
|
||||
wRegistry.get(id)!.delete(warningKey);
|
||||
|
||||
|
||||
// remove from severityIndex
|
||||
sIndex.get(warning.severity)!.delete(`${id}|${warningKey}`);
|
||||
|
||||
set({
|
||||
editorWarningRegistry: wRegistry,
|
||||
severityIndex: sIndex
|
||||
})
|
||||
},
|
||||
|
||||
unregisterWarningsForId: (id) => {
|
||||
const wRegistry = new Map([...get().editorWarningRegistry].map(([k, v]) => [k, new Map(v)]));
|
||||
const sIndex = new Map(get().severityIndex);
|
||||
|
||||
const nodeWarnings = wRegistry.get(id);
|
||||
|
||||
// remove from severity index
|
||||
if (nodeWarnings) {
|
||||
nodeWarnings.forEach((warning) => {
|
||||
const warningKey = warning.scope.handleId
|
||||
? `${warning.type}:${warning.scope.handleId}`
|
||||
: warning.type;
|
||||
sIndex.get(warning.severity)?.delete(`${id}|${warningKey}`);
|
||||
});
|
||||
}
|
||||
|
||||
// remove from warning registry
|
||||
wRegistry.delete(id);
|
||||
|
||||
set({
|
||||
editorWarningRegistry: wRegistry,
|
||||
severityIndex: sIndex
|
||||
})
|
||||
},
|
||||
}}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* returns a summary of the warningRegistry
|
||||
* @returns {{info: number, warning: number, error: number, isValid: boolean}}
|
||||
*/
|
||||
export function warningSummary() {
|
||||
const {severityIndex, isProgramValid} = useFlowStore.getState();
|
||||
return {
|
||||
info: severityIndex.get('INFO')!.size,
|
||||
warning: severityIndex.get('WARNING')!.size,
|
||||
error: severityIndex.get('ERROR')!.size,
|
||||
isValid: isProgramValid(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
|
||||
/*
|
||||
This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
University within the Software Project course.
|
||||
© Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
*/
|
||||
.gestureEditor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import { useState, useRef } from "react";
|
||||
import styles from './GestureValueEditor.module.css'
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import {NodeToolbar} from '@xyflow/react';
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {NodeToolbar, useReactFlow} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import {type JSX, useState} from "react";
|
||||
import {createPortal} from "react-dom";
|
||||
@@ -30,10 +33,11 @@ type ToolbarProps = {
|
||||
*/
|
||||
export function Toolbar({nodeId, allowDelete}: ToolbarProps) {
|
||||
const {nodes, deleteNode} = useFlowStore();
|
||||
|
||||
const { deleteElements } = useReactFlow();
|
||||
|
||||
const deleteParentNode = () => {
|
||||
deleteNode(nodeId);
|
||||
|
||||
deleteNode(nodeId, deleteElements);
|
||||
};
|
||||
|
||||
const nodeType = nodes.find((node) => node.id === nodeId)?.type as keyof typeof NodeTooltips;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import type { Plan, PlanElement } from "./Plan";
|
||||
|
||||
export const defaultPlan: Plan = {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import { type Node } from "@xyflow/react"
|
||||
import { GoalReduce } from "../nodes/GoalNode"
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
// This file is to avoid sharing both functions and components which eslint dislikes. :)
|
||||
import type { GoalNode } from "../nodes/GoalNode"
|
||||
import type { Goal, Plan } from "./Plan"
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
University within the Software Project course.
|
||||
© Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
*/
|
||||
.planDialog {
|
||||
overflow:visible;
|
||||
width: 80vw;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {useRef, useState} from "react";
|
||||
import useFlowStore from "../VisProgStores.tsx";
|
||||
import styles from './PlanEditor.module.css';
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
/*
|
||||
This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
University within the Software Project course.
|
||||
© Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
*/
|
||||
:global(.react-flow__handle.source){
|
||||
border-radius: 100%;
|
||||
}
|
||||
:global(.react-flow__handle.target){
|
||||
border-radius: 15%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
:global(.react-flow__handle.connected) {
|
||||
background: lightgray;
|
||||
border-color: green;
|
||||
filter: drop-shadow(0 0 0.25rem green);
|
||||
filter: drop-shadow(0 0 0.15rem green);
|
||||
}
|
||||
|
||||
:global(.singleConnectionHandle.connected) {
|
||||
@@ -16,19 +30,19 @@
|
||||
:global(.singleConnectionHandle.unconnected){
|
||||
background: lightsalmon;
|
||||
border-color: #ff6060;
|
||||
filter: drop-shadow(0 0 0.25rem #ff6060);
|
||||
filter: drop-shadow(0 0 0.15rem #ff6060);
|
||||
}
|
||||
|
||||
:global(.react-flow__handle.connectingto) {
|
||||
background: #ff6060;
|
||||
border-color: coral;
|
||||
filter: drop-shadow(0 0 0.25rem coral);
|
||||
filter: drop-shadow(0 0 0.15rem coral);
|
||||
}
|
||||
|
||||
:global(.react-flow__handle.valid) {
|
||||
background: #55dd99;
|
||||
border-color: green;
|
||||
filter: drop-shadow(0 0 0.25rem green);
|
||||
filter: drop-shadow(0 0 0.15rem green);
|
||||
}
|
||||
|
||||
:global(.react-flow__handle) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {
|
||||
Handle,
|
||||
type HandleProps,
|
||||
type Connection,
|
||||
useNodeId, useNodeConnections
|
||||
} from '@xyflow/react';
|
||||
import {useState} from 'react';
|
||||
import { type HandleRule, useHandleRules} from "../HandleRuleLogic.ts";
|
||||
import "./RuleBasedHandle.module.css";
|
||||
|
||||
@@ -29,21 +31,16 @@ export function MultiConnectionHandle({
|
||||
handleId: id!
|
||||
})
|
||||
|
||||
// initialise the handles state with { isValid: true } to show that connections are possible
|
||||
const [handleState, setHandleState] = useState<{ isSatisfied: boolean, message?: string }>({ isSatisfied: true });
|
||||
|
||||
return (
|
||||
<Handle
|
||||
{...otherProps}
|
||||
id={id}
|
||||
type={type}
|
||||
className={"multiConnectionHandle" + (connections.length === 0 ? " unconnected" : " connected")}
|
||||
className={"multiConnectionHandle" + (connections.length === 0 ? " unconnected" : " connected") + ` ${type}`}
|
||||
isValidConnection={(connection) => {
|
||||
const result = validate(connection as Connection);
|
||||
setHandleState(result);
|
||||
return result.isSatisfied;
|
||||
}}
|
||||
title={handleState.message}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -66,22 +63,18 @@ export function SingleConnectionHandle({
|
||||
handleId: id!
|
||||
})
|
||||
|
||||
// initialise the handles state with { isValid: true } to show that connections are possible
|
||||
const [handleState, setHandleState] = useState<{ isSatisfied: boolean, message?: string }>({ isSatisfied: true });
|
||||
|
||||
return (
|
||||
<Handle
|
||||
{...otherProps}
|
||||
id={id}
|
||||
type={type}
|
||||
className={"singleConnectionHandle" + (connections.length === 0 ? " unconnected" : " connected")}
|
||||
className={"singleConnectionHandle" + (connections.length === 0 ? " unconnected" : " connected") + ` ${type}`}
|
||||
isConnectable={connections.length === 0}
|
||||
isValidConnection={(connection) => {
|
||||
const result = validate(connection as Connection);
|
||||
setHandleState(result);
|
||||
return result.isSatisfied;
|
||||
}}
|
||||
title={handleState.message}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
University within the Software Project course.
|
||||
© Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
*/
|
||||
.save-load-panel {
|
||||
border-radius: 0 0 5pt 5pt;
|
||||
background-color: canvas;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {type ChangeEvent, useRef, useState} from "react";
|
||||
import useFlowStore from "../VisProgStores";
|
||||
import visProgStyles from "../../VisProg.module.css";
|
||||
@@ -29,6 +32,8 @@ export default function SaveLoadPanel() {
|
||||
const text = await file.text();
|
||||
const parsed = JSON.parse(text) as SavedProject;
|
||||
if (!parsed.nodes || !parsed.edges) throw new Error("Invalid file format");
|
||||
const {nodes, unregisterWarningsForId} = useFlowStore.getState();
|
||||
nodes.forEach((node) => {unregisterWarningsForId(node.id);});
|
||||
setNodes(parsed.nodes);
|
||||
setEdges(parsed.edges);
|
||||
} catch (e) {
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
University within the Software Project course.
|
||||
© Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
*/
|
||||
.warnings-sidebar {
|
||||
min-width: auto;
|
||||
max-width: 340px;
|
||||
margin-right: 0;
|
||||
height: 100%;
|
||||
background: canvas;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.warnings-toggle-bar {
|
||||
background-color: ButtonFace;
|
||||
justify-items: center;
|
||||
align-content: center;
|
||||
width: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.warnings-toggle-bar.error:first-child:has(.arrow-right){
|
||||
background-color: hsl(from red h s 75%);
|
||||
}
|
||||
.warnings-toggle-bar.warning:first-child:has(.arrow-right) {
|
||||
background-color: hsl(from orange h s 75%);
|
||||
}
|
||||
.warnings-toggle-bar.info:first-child:has(.arrow-right) {
|
||||
background-color: hsl(from steelblue h s 75%);
|
||||
}
|
||||
|
||||
.warnings-toggle-bar:hover {
|
||||
background-color: GrayText !important ;
|
||||
.arrow-left {
|
||||
border-right-color: ButtonFace;
|
||||
transition: transform 0.15s ease-in-out;
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
.arrow-right {
|
||||
border-left-color: ButtonFace;
|
||||
transition: transform 0.15s ease-in-out;
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.warnings-content {
|
||||
width: 320px;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
border-left: 2px solid CanvasText;
|
||||
}
|
||||
|
||||
.warnings-header {
|
||||
padding: 12px;
|
||||
border-bottom: 2px solid CanvasText;
|
||||
}
|
||||
|
||||
.severity-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.severity-tab {
|
||||
flex: 1;
|
||||
padding: 4px;
|
||||
background: ButtonFace;
|
||||
color: GrayText;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.count {
|
||||
padding: 4px;
|
||||
color: GrayText;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.severity-tab.active {
|
||||
color: ButtonText;
|
||||
border: 2px solid currentColor;
|
||||
.count {
|
||||
color: ButtonText;
|
||||
}
|
||||
}
|
||||
|
||||
.warning-group-header {
|
||||
background: ButtonFace;
|
||||
padding: 6px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.warnings-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.warnings-empty {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.warning-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 5px;
|
||||
gap: 2px;
|
||||
padding: 0;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
color: GrayText;
|
||||
}
|
||||
|
||||
.warning-item:hover {
|
||||
background: ButtonFace;
|
||||
}
|
||||
|
||||
.warning-item--error {
|
||||
border: 2px solid red;
|
||||
background-color: hsl(from red h s 96%);
|
||||
.item-header{
|
||||
background-color: red;
|
||||
.type{
|
||||
color: hsl(from red h s 96%);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.warning-item--error:hover {
|
||||
background-color: hsl(from red h s 75%);
|
||||
}
|
||||
|
||||
.warning-item--warning {
|
||||
border: 2px solid orange;
|
||||
background-color: hsl(from orange h s 96%);
|
||||
.item-header{
|
||||
background-color: orange;
|
||||
.type{
|
||||
color: hsl(from orange h s 96%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.warning-item--warning:hover {
|
||||
background-color: hsl(from orange h s 75%);
|
||||
}
|
||||
|
||||
.warning-item--info {
|
||||
border: 2px solid steelblue;
|
||||
background-color: hsl(from steelblue h s 96%);
|
||||
.item-header{
|
||||
background-color: steelblue;
|
||||
.type{
|
||||
color: hsl(from steelblue h s 96%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.warning-item--info:hover {
|
||||
background-color: hsl(from steelblue h s 75%);
|
||||
}
|
||||
|
||||
.warning-item .item-header {
|
||||
padding: 8px 8px;
|
||||
opacity: 1;
|
||||
font-weight: bolder;
|
||||
}
|
||||
.warning-item .item-header .type{
|
||||
padding: 2px 8px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.warning-item .description {
|
||||
padding: 5px 10px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.auto-hide {
|
||||
background-color: Canvas;
|
||||
border-top: 2px solid CanvasText;
|
||||
margin-top: auto;
|
||||
width: 100%;
|
||||
height: 2.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
/* arrows for toggleBar */
|
||||
.arrow-right {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 0.5rem solid transparent;
|
||||
border-bottom: 0.5rem solid transparent;
|
||||
border-left: 0.6rem solid GrayText;
|
||||
}
|
||||
|
||||
.arrow-left {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 0.5rem solid transparent;
|
||||
border-bottom: 0.5rem solid transparent;
|
||||
border-right: 0.6rem solid GrayText;
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {useReactFlow, useStoreApi} from "@xyflow/react";
|
||||
import clsx from "clsx";
|
||||
import {useEffect, useState} from "react";
|
||||
import useFlowStore from "../VisProgStores.tsx";
|
||||
import {
|
||||
warningSummary,
|
||||
type WarningSeverity,
|
||||
type EditorWarning, globalWarning
|
||||
} from "./EditorWarnings.tsx";
|
||||
import styles from "./WarningSidebar.module.css";
|
||||
|
||||
/**
|
||||
* the warning sidebar, shows all warnings
|
||||
*
|
||||
* @returns {React.JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
export function WarningsSidebar() {
|
||||
const warnings = useFlowStore.getState().getWarnings();
|
||||
const [hide, setHide] = useState(false);
|
||||
const [severityFilter, setSeverityFilter] = useState<WarningSeverity | 'ALL'>('ALL');
|
||||
const [autoHide, setAutoHide] = useState(false);
|
||||
|
||||
// let autohide change hide status only when autohide is toggled
|
||||
// and allow for user to change the hide state even if autohide is enabled
|
||||
const hasWarnings = warnings.length > 0;
|
||||
useEffect(() => {
|
||||
if (autoHide) {
|
||||
setHide(!hasWarnings);
|
||||
}
|
||||
}, [autoHide, hasWarnings]);
|
||||
|
||||
const filtered = severityFilter === 'ALL'
|
||||
? warnings
|
||||
: warnings.filter(w => w.severity === severityFilter);
|
||||
|
||||
|
||||
const summary = warningSummary();
|
||||
// Finds the first key where the count > 0
|
||||
const getHighestSeverity = () => {
|
||||
if (summary.error > 0) return styles.error;
|
||||
if (summary.warning > 0) return styles.warning;
|
||||
if (summary.info > 0) return styles.info;
|
||||
return '';
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className={`flex-row`} >
|
||||
<div
|
||||
className={`${styles.warningsToggleBar} ${getHighestSeverity()}`}
|
||||
onClick={() => setHide(!hide)}
|
||||
title={"toggle warnings"}
|
||||
>
|
||||
<div className={`${hide ? styles.arrowRight : styles.arrowLeft}`}></div>
|
||||
</div>
|
||||
<div
|
||||
id="warningSidebar"
|
||||
className={styles.warningsContent}
|
||||
style={hide ? {display: "none"} : {display: "flex"}}
|
||||
>
|
||||
<WarningsHeader
|
||||
severityFilter={severityFilter}
|
||||
onChange={setSeverityFilter}
|
||||
/>
|
||||
|
||||
<WarningsList warnings={filtered} />
|
||||
<div className={styles.autoHide}>
|
||||
<input
|
||||
id="autoHideSwitch"
|
||||
type={"checkbox"}
|
||||
checked={autoHide}
|
||||
onChange={(e) => setAutoHide(e.target.checked)}
|
||||
/><label>Hide if there are no warnings</label>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* the header of the warning sidebar, contains severity filtering buttons
|
||||
*
|
||||
* @param {WarningSeverity | "ALL"} severityFilter
|
||||
* @param {(severity: (WarningSeverity | "ALL")) => void} onChange
|
||||
* @returns {React.JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
function WarningsHeader({
|
||||
severityFilter,
|
||||
onChange,
|
||||
}: {
|
||||
severityFilter: WarningSeverity | 'ALL';
|
||||
onChange: (severity: WarningSeverity | 'ALL') => void;
|
||||
}) {
|
||||
const summary = warningSummary();
|
||||
|
||||
return (
|
||||
<div className={styles.warningsHeader}>
|
||||
<h3>Warnings</h3>
|
||||
<div className={styles.severityTabs}>
|
||||
{(['ALL', 'ERROR', 'WARNING', 'INFO'] as const).map(severity => (
|
||||
<button
|
||||
key={severity}
|
||||
className={clsx(styles.severityTab, severityFilter === severity && styles.active)}
|
||||
onClick={() => onChange(severity)}
|
||||
>
|
||||
{severity}
|
||||
{severity !== 'ALL' && (
|
||||
<span className={styles.count}>
|
||||
{summary[severity.toLowerCase() as keyof typeof summary]}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* the list of warnings in the warning sidebar
|
||||
*
|
||||
* @param {{warnings: EditorWarning[]}} props
|
||||
* @returns {React.JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
function WarningsList(props: { warnings: EditorWarning[] }) {
|
||||
const splitWarnings = {
|
||||
global: props.warnings.filter(w => w.scope.id === globalWarning),
|
||||
other: props.warnings.filter(w => w.scope.id !== globalWarning),
|
||||
}
|
||||
if (props.warnings.length === 0) {
|
||||
return (
|
||||
<div className={styles.warningsEmpty}>
|
||||
No warnings!
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className={styles.warningsList}>
|
||||
<div className={styles.warningGroupHeader}>global:</div>
|
||||
<div className={styles.warningsGroup}>
|
||||
{splitWarnings.global.map((warning) => (
|
||||
<WarningListItem warning={warning} key={`${warning.scope.id}|${warning.type}` + (warning.scope.handleId
|
||||
? `:${warning.scope.handleId}`
|
||||
: "")}
|
||||
/>
|
||||
))}
|
||||
{splitWarnings.global.length === 0 && "No global warnings!"}
|
||||
</div>
|
||||
<div className={styles.warningGroupHeader}>other:</div>
|
||||
<div className={styles.warningsGroup}>
|
||||
{splitWarnings.other.map((warning) => (
|
||||
<WarningListItem warning={warning} key={`${warning.scope.id}|${warning.type}` + (warning.scope.handleId
|
||||
? `:${warning.scope.handleId}`
|
||||
: "")}
|
||||
/>
|
||||
))}
|
||||
{splitWarnings.other.length === 0 && "No other warnings!"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* a single warning in the warning sidebar
|
||||
*
|
||||
* @param {{warning: EditorWarning, key: string}} props
|
||||
* @returns {React.JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
function WarningListItem(props: { warning: EditorWarning, key: string}) {
|
||||
const jumpToNode = useJumpToNode();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.warningItem, styles[`warning-item--${props.warning.severity.toLowerCase()}`],)}
|
||||
onClick={() => jumpToNode(props.warning.scope.id)}
|
||||
>
|
||||
<div className={styles.itemHeader}>
|
||||
<span className={styles.type}>{props.warning.type}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.description}>
|
||||
{props.warning.description}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* moves the editor to the provided node
|
||||
* @returns {(nodeId: string) => void}
|
||||
*/
|
||||
function useJumpToNode() {
|
||||
const { getNode, setCenter, getViewport } = useReactFlow();
|
||||
const { addSelectedNodes } = useStoreApi().getState();
|
||||
|
||||
|
||||
return (nodeId: string) => {
|
||||
// user can't jump to global warning, so prevent further logic from running if the warning is a globalWarning
|
||||
if (nodeId === globalWarning) return;
|
||||
const node = getNode(nodeId);
|
||||
if (!node) return;
|
||||
|
||||
const nodeElement = document.querySelector(`.react-flow__node[data-id="${nodeId}"]`) as HTMLElement;
|
||||
const { position } = node;
|
||||
const viewport = getViewport();
|
||||
const { width, height } = nodeElement.getBoundingClientRect();
|
||||
|
||||
//move to node
|
||||
setCenter(
|
||||
position!.x + ((width / viewport.zoom) / 2),
|
||||
position!.y + ((height / viewport.zoom) / 2),
|
||||
{duration: 300, interpolate: "smooth" }
|
||||
).then(() => {
|
||||
addSelectedNodes([nodeId]);
|
||||
});
|
||||
|
||||
|
||||
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { BasicBeliefNodeData } from "./BasicBeliefNode";
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import type { BasicBeliefNodeData } from "./BasicBeliefNode.tsx";
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {
|
||||
type NodeProps,
|
||||
Position,
|
||||
type Node,
|
||||
} from '@xyflow/react';
|
||||
import { Toolbar } from '../components/NodeComponents';
|
||||
import { Toolbar } from '../components/NodeComponents.tsx';
|
||||
import styles from '../../VisProg.module.css';
|
||||
import {MultiConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
||||
import {allowOnlyConnectionsFromHandle} from "../HandleRules.ts";
|
||||
import useFlowStore from '../VisProgStores';
|
||||
import { TextField } from '../../../../components/TextField';
|
||||
import { MultilineTextField } from '../../../../components/MultilineTextField';
|
||||
import useFlowStore from '../VisProgStores.tsx';
|
||||
import { TextField } from '../../../../components/TextField.tsx';
|
||||
import { MultilineTextField } from '../../../../components/MultilineTextField.tsx';
|
||||
import {noMatchingLeftRightBelief} from "./BeliefGlobals.ts";
|
||||
|
||||
/**
|
||||
* The default data structure for a BasicBelief node
|
||||
@@ -31,7 +35,7 @@ export type BasicBeliefNodeData = {
|
||||
};
|
||||
|
||||
// These are all the types a basic belief could be.
|
||||
type BasicBeliefType = Keyword | Semantic | DetectedObject | Emotion
|
||||
export type BasicBeliefType = Keyword | Semantic | DetectedObject | Emotion
|
||||
type Keyword = { type: "keyword", id: string, value: string, label: "Keyword said:"};
|
||||
type Semantic = { type: "semantic", id: string, value: string, description: string, label: "Detected with LLM:"};
|
||||
type DetectedObject = { type: "object", id: string, value: string, label: "Object found:"};
|
||||
@@ -112,9 +116,7 @@ export default function BasicBeliefNode(props: NodeProps<BasicBeliefNode>) {
|
||||
updateNodeData(props.id, {...data, belief: {...data.belief, description: value}});
|
||||
}
|
||||
|
||||
// Use this
|
||||
const emotionOptions = ["Happy", "Angry", "Sad", "Cheerful"]
|
||||
|
||||
const emotionOptions = ["sad", "angry", "surprise", "fear", "happy", "disgust", "neutral"];
|
||||
|
||||
let placeholder = ""
|
||||
let wrapping = ""
|
||||
@@ -189,8 +191,9 @@ export default function BasicBeliefNode(props: NodeProps<BasicBeliefNode>) {
|
||||
</div>
|
||||
)}
|
||||
<MultiConnectionHandle type="source" position={Position.Right} id="source" rules={[
|
||||
allowOnlyConnectionsFromHandle([{nodeType:"trigger",handleId:"TriggerBeliefs"}, {nodeType:"norm",handleId:"NormBeliefs"}]),
|
||||
]}/>
|
||||
noMatchingLeftRightBelief,
|
||||
allowOnlyConnectionsFromHandle([{nodeType:"trigger",handleId:"TriggerBeliefs"}, {nodeType:"norm",handleId:"NormBeliefs"},{nodeType:"InferredBelief",handleId:"inferred_belief"}]),
|
||||
]} title="Connect to any number of trigger and/or normNode(-s)"/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {getOutgoers, type Node} from '@xyflow/react';
|
||||
import {type HandleRule, type RuleResult, ruleResult} from "../HandleRuleLogic.ts";
|
||||
import useFlowStore from "../VisProgStores.tsx";
|
||||
import {BasicBeliefReduce} from "./BasicBeliefNode.tsx";
|
||||
import {type InferredBeliefNodeData, InferredBeliefReduce} from "./InferredBeliefNode.tsx";
|
||||
|
||||
export function BeliefGlobalReduce(beliefNode: Node, nodes: Node[]) {
|
||||
switch (beliefNode.type) {
|
||||
case 'basic_belief':
|
||||
return BasicBeliefReduce(beliefNode, nodes);
|
||||
case 'inferred_belief':
|
||||
return InferredBeliefReduce(beliefNode, nodes);
|
||||
}
|
||||
}
|
||||
|
||||
export const noMatchingLeftRightBelief : HandleRule = (connection, _)=> {
|
||||
const { nodes } = useFlowStore.getState();
|
||||
const thisNode = nodes.find(node => node.id === connection.target && node.type === 'inferred_belief');
|
||||
if (!thisNode) return ruleResult.satisfied;
|
||||
|
||||
const iBelief = (thisNode.data as InferredBeliefNodeData).inferredBelief;
|
||||
return (iBelief.left === connection.source || iBelief.right === connection.source)
|
||||
? ruleResult.notSatisfied("Connecting one belief to both input handles of an inferred belief node is not allowed")
|
||||
: ruleResult.satisfied;
|
||||
}
|
||||
/**
|
||||
* makes it impossible to connect Inferred belief nodes
|
||||
* if the connection would create a cyclical connection between inferred beliefs
|
||||
*/
|
||||
export const noBeliefCycles: HandleRule = (connection, _): RuleResult => {
|
||||
const {nodes, edges} = useFlowStore.getState();
|
||||
const defaultErrorMessage = "Cyclical connection exists between inferred beliefs";
|
||||
|
||||
/**
|
||||
* recursively checks for cyclical connections between InferredBelief nodes
|
||||
*
|
||||
* to check for a cycle provide the source of an attempted connection as the targetNode for the cycle check,
|
||||
* the currentNodeId should be initialised with the id of the targetNode of the attempted connection.
|
||||
*
|
||||
* @param {string} targetNodeId - the id of the node we are looking for as the endpoint of a cyclical connection
|
||||
* @param {string} currentNodeId - the id of the node we are checking for outgoing connections to the provided target node
|
||||
* @returns {RuleResult}
|
||||
*/
|
||||
function checkForCycle(targetNodeId: string, currentNodeId: string): RuleResult {
|
||||
const outgoingBeliefs = getOutgoers({id: currentNodeId}, nodes, edges)
|
||||
.filter(node => node.type === 'inferred_belief');
|
||||
|
||||
if (outgoingBeliefs.length === 0) return ruleResult.satisfied;
|
||||
if (outgoingBeliefs.some(node => node.id === targetNodeId)) return ruleResult
|
||||
.notSatisfied(defaultErrorMessage);
|
||||
|
||||
const next = outgoingBeliefs.map(node => checkForCycle(targetNodeId, node.id))
|
||||
.find(result => !result.isSatisfied);
|
||||
|
||||
return next
|
||||
? next
|
||||
: ruleResult.satisfied;
|
||||
}
|
||||
|
||||
return connection.source === connection.target
|
||||
? ruleResult.notSatisfied(defaultErrorMessage)
|
||||
: checkForCycle(connection.source, connection.target);
|
||||
};
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import type { EndNodeData } from "./EndNode";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {
|
||||
type NodeProps,
|
||||
Position,
|
||||
type Node,
|
||||
type Node, useNodeConnections
|
||||
} from '@xyflow/react';
|
||||
import {useEffect} from "react";
|
||||
import type {EditorWarning} from "../components/EditorWarnings.tsx";
|
||||
import { Toolbar } from '../components/NodeComponents';
|
||||
import styles from '../../VisProg.module.css';
|
||||
import {SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
||||
import {allowOnlyConnectionsFromType} from "../HandleRules.ts";
|
||||
import useFlowStore from "../VisProgStores.tsx";
|
||||
|
||||
|
||||
|
||||
@@ -27,6 +33,27 @@ export type EndNode = Node<EndNodeData>
|
||||
* @returns React.JSX.Element
|
||||
*/
|
||||
export default function EndNode(props: NodeProps<EndNode>) {
|
||||
const {registerWarning, unregisterWarning} = useFlowStore.getState();
|
||||
const connections = useNodeConnections({
|
||||
id: props.id,
|
||||
handleId: 'target'
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const noConnectionWarning : EditorWarning = {
|
||||
scope: {
|
||||
id: props.id,
|
||||
handleId: 'target'
|
||||
},
|
||||
type: 'MISSING_INPUT',
|
||||
severity: "ERROR",
|
||||
description: "the endNode does not have an incoming connection from a phaseNode"
|
||||
}
|
||||
|
||||
if (connections.length === 0) { registerWarning(noConnectionWarning); }
|
||||
else { unregisterWarning(props.id, `${noConnectionWarning.type}:target`); }
|
||||
}, [connections.length, props.id, registerWarning, unregisterWarning]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toolbar nodeId={props.id} allowDelete={false}/>
|
||||
@@ -36,7 +63,7 @@ export default function EndNode(props: NodeProps<EndNode>) {
|
||||
</div>
|
||||
<SingleConnectionHandle type="target" position={Position.Left} id="target" rules={[
|
||||
allowOnlyConnectionsFromType(["phase"])
|
||||
]}/>
|
||||
]} title="Connect to a phaseNode"/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import type { GoalNodeData } from "./GoalNode";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {
|
||||
type NodeProps,
|
||||
Position,
|
||||
type Node,
|
||||
type Node
|
||||
} from '@xyflow/react';
|
||||
import {useEffect} from "react";
|
||||
import type {EditorWarning} from "../components/EditorWarnings.tsx";
|
||||
import { Toolbar } from '../components/NodeComponents';
|
||||
import styles from '../../VisProg.module.css';
|
||||
import { TextField } from '../../../../components/TextField';
|
||||
@@ -44,7 +49,7 @@ export type GoalNode = Node<GoalNodeData>
|
||||
* @returns React.JSX.Element
|
||||
*/
|
||||
export default function GoalNode({id, data}: NodeProps<GoalNode>) {
|
||||
const {updateNodeData} = useFlowStore();
|
||||
const {updateNodeData, registerWarning, unregisterWarning} = useFlowStore();
|
||||
const _nodes = useFlowStore().nodes;
|
||||
|
||||
const text_input_id = `goal_${id}_text_input`;
|
||||
@@ -64,6 +69,43 @@ export default function GoalNode({id, data}: NodeProps<GoalNode>) {
|
||||
updateNodeData(id, {...data, can_fail: value});
|
||||
}
|
||||
|
||||
//undefined plan warning
|
||||
useEffect(() => {
|
||||
const noPlanWarning : EditorWarning = {
|
||||
scope: {
|
||||
id: id,
|
||||
handleId: undefined
|
||||
},
|
||||
type: 'PLAN_IS_UNDEFINED',
|
||||
severity: 'ERROR',
|
||||
description: "This goalNode is missing a plan, please make sure to create a plan by using the create plan button"
|
||||
};
|
||||
|
||||
if (!data.plan || data.plan.steps?.length === 0){
|
||||
registerWarning(noPlanWarning);
|
||||
return;
|
||||
}
|
||||
unregisterWarning(id, noPlanWarning.type);
|
||||
},[data.plan, id, registerWarning, unregisterWarning])
|
||||
|
||||
//starts with number warning
|
||||
useEffect(() => {
|
||||
const name = data.name || "";
|
||||
|
||||
const startsWithNumberWarning: EditorWarning = {
|
||||
scope: { id: id },
|
||||
type: 'ELEMENT_STARTS_WITH_NUMBER',
|
||||
severity: 'ERROR',
|
||||
description: "Norms are not allowed to start with a number."
|
||||
};
|
||||
|
||||
if (/^\d/.test(name)) {
|
||||
registerWarning(startsWithNumberWarning);
|
||||
} else {
|
||||
unregisterWarning(id, 'ELEMENT_STARTS_WITH_NUMBER');
|
||||
}
|
||||
}, [data.name, id, registerWarning, unregisterWarning]);
|
||||
|
||||
return <>
|
||||
<Toolbar nodeId={id} allowDelete={true}/>
|
||||
<div className={`${styles.defaultNode} ${styles.nodeGoal} flex-col gap-sm`}>
|
||||
@@ -118,9 +160,11 @@ export default function GoalNode({id, data}: NodeProps<GoalNode>) {
|
||||
</div>
|
||||
<MultiConnectionHandle type="source" position={Position.Right} id="GoalSource" rules={[
|
||||
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}]),
|
||||
]}/>
|
||||
]} title="Connect to any number of phase and/or goalNode(-s)"/>
|
||||
|
||||
<MultiConnectionHandle type="target" position={Position.Bottom} id="GoalTarget" rules={[allowOnlyConnectionsFromType(["goal"])]}/>
|
||||
<MultiConnectionHandle type="target" position={Position.Bottom} id="GoalTarget" rules={[
|
||||
allowOnlyConnectionsFromType(["goal"])]
|
||||
} title="Connect to any number of goalNode(-s)"/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import type { InferredBeliefNodeData } from "./InferredBeliefNode.tsx";
|
||||
|
||||
|
||||
/**
|
||||
* Default data for this node
|
||||
*/
|
||||
export const InferredBeliefNodeDefaults: InferredBeliefNodeData = {
|
||||
label: "AND/OR",
|
||||
droppable: true,
|
||||
inferredBelief: {
|
||||
left: undefined,
|
||||
operator: true,
|
||||
right: undefined
|
||||
},
|
||||
hasReduce: true,
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
University within the Software Project course.
|
||||
© Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
*/
|
||||
.operator-switch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
cursor: pointer;
|
||||
font-family: sans-serif;
|
||||
/* Change this font-size to scale the whole component */
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* hide the default checkbox */
|
||||
.operator-switch input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* The Track */
|
||||
.switch-visual {
|
||||
position: relative;
|
||||
/* height is now 3x the font size */
|
||||
height: 3em;
|
||||
aspect-ratio: 1 / 2;
|
||||
background-color: ButtonFace;
|
||||
border-radius: 2em;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
/* The Knob */
|
||||
.switch-visual::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0.1em;
|
||||
left: 0.1em;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
background: Canvas;
|
||||
border: 0.175em solid mediumpurple;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s ease-in-out, border-color 0.2s;
|
||||
}
|
||||
|
||||
/* Labels */
|
||||
.switch-labels {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 3em; /* Matches the track height */
|
||||
font-weight: 800;
|
||||
color: Canvas;
|
||||
line-height: 1.4;
|
||||
padding: 0.2em 0;
|
||||
}
|
||||
|
||||
.operator-switch input:checked + .switch-visual::after {
|
||||
/* Moves the slider down */
|
||||
transform: translateY(1.4em);
|
||||
}
|
||||
|
||||
/*change the colours to highlight the selected operator*/
|
||||
.operator-switch input:checked ~ .switch-labels{
|
||||
:first-child {
|
||||
transition: ease-in-out color 0.2s;
|
||||
color: ButtonFace;
|
||||
}
|
||||
:last-child {
|
||||
transition: ease-in-out color 0.2s;
|
||||
color: mediumpurple;
|
||||
}
|
||||
}
|
||||
|
||||
.operator-switch input:not(:checked) ~ .switch-labels{
|
||||
:first-child {
|
||||
transition: ease-in-out color 0.2s;
|
||||
color: mediumpurple;
|
||||
}
|
||||
:last-child {
|
||||
transition: ease-in-out color 0.2s;
|
||||
color: ButtonFace;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {getConnectedEdges, type Node, type NodeProps, Position, useNodeConnections} from '@xyflow/react';
|
||||
import {useEffect, useState} from "react";
|
||||
import styles from '../../VisProg.module.css';
|
||||
import type {EditorWarning} from "../components/EditorWarnings.tsx";
|
||||
import {Toolbar} from '../components/NodeComponents.tsx';
|
||||
import {MultiConnectionHandle, SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
||||
import {allowOnlyConnectionsFromType} from "../HandleRules.ts";
|
||||
import useFlowStore from "../VisProgStores.tsx";
|
||||
import {BeliefGlobalReduce, noBeliefCycles, noMatchingLeftRightBelief} from "./BeliefGlobals.ts";
|
||||
import switchStyles from './InferredBeliefNode.module.css';
|
||||
|
||||
|
||||
/**
|
||||
* The default data structure for an InferredBelief node
|
||||
*/
|
||||
export type InferredBeliefNodeData = {
|
||||
label: string;
|
||||
droppable: boolean;
|
||||
inferredBelief: InferredBelief;
|
||||
hasReduce: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* stores a boolean to represent the operator
|
||||
* and a left and right BeliefNode (can be both an inferred and a basic belief)
|
||||
* in the form of their corresponding id's
|
||||
*/
|
||||
export type InferredBelief = {
|
||||
left: string | undefined,
|
||||
operator: boolean,
|
||||
right: string | undefined,
|
||||
}
|
||||
|
||||
export type InferredBeliefNode = Node<InferredBeliefNodeData>;
|
||||
|
||||
/**
|
||||
* This function is called whenever a connection is made with this node type as the target
|
||||
* @param _thisNode the node of this node type which function is called
|
||||
* @param _sourceNodeId the source of the received connection
|
||||
*/
|
||||
export function InferredBeliefConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
||||
const data = _thisNode.data as InferredBeliefNodeData;
|
||||
|
||||
if ((useFlowStore.getState().nodes.find((node) => node.id === _sourceNodeId
|
||||
&& ['basic_belief', 'inferred_belief'].includes(node.type!)))
|
||||
) {
|
||||
const connectedEdges = getConnectedEdges([_thisNode], useFlowStore.getState().edges);
|
||||
switch(connectedEdges.find(edge => edge.source === _sourceNodeId)?.targetHandle){
|
||||
case 'beliefLeft': data.inferredBelief.left = _sourceNodeId; break;
|
||||
case 'beliefRight': data.inferredBelief.right = _sourceNodeId; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called whenever a connection is made with this node type as the source
|
||||
* @param _thisNode the node of this node type which function is called
|
||||
* @param _targetNodeId the target of the created connection
|
||||
*/
|
||||
export function InferredBeliefConnectionSource(_thisNode: Node, _targetNodeId: string) {
|
||||
// no additional connection logic exists yet
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called whenever a connection is disconnected with this node type as the target
|
||||
* @param _thisNode the node of this node type which function is called
|
||||
* @param _sourceNodeId the source of the disconnected connection
|
||||
*/
|
||||
export function InferredBeliefDisconnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
||||
const data = _thisNode.data as InferredBeliefNodeData;
|
||||
|
||||
if (_sourceNodeId === data.inferredBelief.left) data.inferredBelief.left = undefined;
|
||||
if (_sourceNodeId === data.inferredBelief.right) data.inferredBelief.right = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called whenever a connection is disconnected with this node type as the source
|
||||
* @param _thisNode the node of this node type which function is called
|
||||
* @param _targetNodeId the target of the diconnected connection
|
||||
*/
|
||||
export function InferredBeliefDisconnectionSource(_thisNode: Node, _targetNodeId: string) {
|
||||
// no additional connection logic exists yet
|
||||
}
|
||||
|
||||
export const InferredBeliefTooltip = `
|
||||
Combines two beliefs into a single belief using logical inference,
|
||||
the node can be toggled between using "AND" and "OR" mode for inference`;
|
||||
/**
|
||||
* Defines how an InferredBelief node should be rendered
|
||||
* @param {NodeProps<InferredBeliefNode>} props - Node properties provided by React Flow, including `id` and `data`.
|
||||
* @returns The rendered InferredBeliefNode React element. (React.JSX.Element)
|
||||
*/
|
||||
export default function InferredBeliefNode(props: NodeProps<InferredBeliefNode>) {
|
||||
const data = props.data;
|
||||
const { updateNodeData, registerWarning, unregisterWarning } = useFlowStore();
|
||||
// start of as an AND operator, true: "AND", false: "OR"
|
||||
const [enforceAllBeliefs, setEnforceAllBeliefs] = useState(true);
|
||||
|
||||
// used to toggle operator
|
||||
function onToggle() {
|
||||
const newOperator = !enforceAllBeliefs; // compute the new value
|
||||
setEnforceAllBeliefs(newOperator);
|
||||
|
||||
updateNodeData(props.id, {
|
||||
...data,
|
||||
inferredBelief: {
|
||||
...data.inferredBelief,
|
||||
operator: enforceAllBeliefs,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const beliefConnections = useNodeConnections({
|
||||
id: props.id,
|
||||
handleType: "target",
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const noBeliefsWarning : EditorWarning = {
|
||||
scope: {
|
||||
id: props.id,
|
||||
handleId: undefined
|
||||
},
|
||||
type: 'MISSING_INPUT',
|
||||
severity: 'ERROR',
|
||||
description: `This AND/OR node is missing one or more beliefs,
|
||||
please make sure to use both inputs of an AND/OR node`
|
||||
};
|
||||
|
||||
if (beliefConnections.length < 2){
|
||||
registerWarning(noBeliefsWarning);
|
||||
return;
|
||||
}
|
||||
unregisterWarning(props.id, noBeliefsWarning.type);
|
||||
},[beliefConnections.length, props.id, registerWarning, unregisterWarning])
|
||||
return (
|
||||
<>
|
||||
<Toolbar nodeId={props.id} allowDelete={true}/>
|
||||
<div className={`${styles.defaultNode} ${styles.nodeInferredBelief}`}>
|
||||
{/* The checkbox used to toggle the operator between 'AND' and 'OR' */}
|
||||
<label className={switchStyles.operatorSwitch}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={data.inferredBelief.operator}
|
||||
onChange={onToggle}
|
||||
/>
|
||||
<div className={switchStyles.switchVisual}></div>
|
||||
<div className={switchStyles.switchLabels}>
|
||||
<span title={"Belief is fulfilled if either of the supplied beliefs is true"}>OR</span>
|
||||
<span title={"Belief is fulfilled if all of the supplied beliefs are true"}>AND</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
|
||||
{/* outgoing connections */}
|
||||
<MultiConnectionHandle type="source" position={Position.Right} id="source" rules={[
|
||||
allowOnlyConnectionsFromType(["norm", "trigger"]),
|
||||
noBeliefCycles,
|
||||
noMatchingLeftRightBelief
|
||||
]}/>
|
||||
|
||||
{/* incoming connections */}
|
||||
<SingleConnectionHandle type="target" position={Position.Left} style={{top: '30%'}} id="beliefLeft" rules={[
|
||||
allowOnlyConnectionsFromType(["basic_belief", "inferred_belief"]),
|
||||
noBeliefCycles,
|
||||
noMatchingLeftRightBelief
|
||||
]}/>
|
||||
<SingleConnectionHandle type="target" position={Position.Left} style={{top: '70%'}} id="beliefRight" rules={[
|
||||
allowOnlyConnectionsFromType(["basic_belief", "inferred_belief"]),
|
||||
noBeliefCycles,
|
||||
noMatchingLeftRightBelief
|
||||
]}/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reduces each BasicBelief, including its children down into its core data.
|
||||
* @param {Node} node - The BasicBelief node to reduce.
|
||||
* @param {Node[]} nodes - The list of all nodes in the current flow graph.
|
||||
* @returns A simplified object containing the node label and its list of BasicBeliefs.
|
||||
*/
|
||||
export function InferredBeliefReduce(node: Node, nodes: Node[]) {
|
||||
const data = node.data as InferredBeliefNodeData;
|
||||
const leftBelief = nodes.find((node) => node.id === data.inferredBelief.left);
|
||||
const rightBelief = nodes.find((node) => node.id === data.inferredBelief.right);
|
||||
|
||||
if (!leftBelief) { throw new Error("No Left belief found")}
|
||||
if (!rightBelief) { throw new Error("No Right Belief found")}
|
||||
|
||||
const result: Record<string, unknown> = {
|
||||
id: node.id,
|
||||
left: BeliefGlobalReduce(leftBelief, nodes),
|
||||
operator: data.inferredBelief.operator ? "AND" : "OR",
|
||||
right: BeliefGlobalReduce(rightBelief, nodes),
|
||||
};
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import type { NormNodeData } from "./NormNode";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import { useEffect } from "react";
|
||||
import type { EditorWarning } from "../components/EditorWarnings.tsx";
|
||||
import {
|
||||
type NodeProps,
|
||||
Position,
|
||||
@@ -9,7 +14,7 @@ import { TextField } from '../../../../components/TextField';
|
||||
import {MultiConnectionHandle, SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
||||
import {allowOnlyConnectionsFromHandle, allowOnlyConnectionsFromType} from "../HandleRules.ts";
|
||||
import useFlowStore from '../VisProgStores';
|
||||
import { BasicBeliefReduce } from './BasicBeliefNode';
|
||||
import {BeliefGlobalReduce} from "./BeliefGlobals.ts";
|
||||
|
||||
/**
|
||||
* The default data dot a phase node
|
||||
@@ -36,7 +41,7 @@ export type NormNode = Node<NormNodeData>
|
||||
*/
|
||||
export default function NormNode(props: NodeProps<NormNode>) {
|
||||
const data = props.data;
|
||||
const {updateNodeData} = useFlowStore();
|
||||
const {updateNodeData, registerWarning, unregisterWarning} = useFlowStore();
|
||||
|
||||
const text_input_id = `norm_${props.id}_text_input`;
|
||||
const checkbox_id = `goal_${props.id}_checkbox`;
|
||||
@@ -44,10 +49,44 @@ export default function NormNode(props: NodeProps<NormNode>) {
|
||||
const setValue = (value: string) => {
|
||||
updateNodeData(props.id, {norm: value});
|
||||
}
|
||||
//this function is commented out, because of lack of backend implementation.
|
||||
//If you wish to set critical norms, in the UI side, you can uncomment and use this function.
|
||||
|
||||
// const setCritical = (value: boolean) => {
|
||||
// updateNodeData(props.id, {...data, critical: value});
|
||||
// }
|
||||
useEffect(() => {
|
||||
const normText = data.norm || "";
|
||||
|
||||
const startsWithNumberWarning: EditorWarning = {
|
||||
scope: { id: props.id },
|
||||
type: 'ELEMENT_STARTS_WITH_NUMBER',
|
||||
severity: 'ERROR',
|
||||
description: "Norms are not allowed to start with a number."
|
||||
};
|
||||
|
||||
const setCritical = (value: boolean) => {
|
||||
updateNodeData(props.id, {...data, critical: value});
|
||||
}
|
||||
if (/^\d/.test(normText)) {
|
||||
registerWarning(startsWithNumberWarning);
|
||||
} else {
|
||||
unregisterWarning(props.id, 'ELEMENT_STARTS_WITH_NUMBER');
|
||||
}
|
||||
}, [data.norm, props.id, registerWarning, unregisterWarning]);
|
||||
useEffect(() => {
|
||||
const normText = data.norm || "";
|
||||
|
||||
const startsWithNumberWarning: EditorWarning = {
|
||||
scope: { id: props.id },
|
||||
type: 'ELEMENT_STARTS_WITH_NUMBER',
|
||||
severity: 'ERROR',
|
||||
description: "Norms are not allowed to start with a number."
|
||||
};
|
||||
|
||||
if (/^\d/.test(normText)) {
|
||||
registerWarning(startsWithNumberWarning);
|
||||
} else {
|
||||
unregisterWarning(props.id, 'ELEMENT_STARTS_WITH_NUMBER');
|
||||
}
|
||||
}, [data.norm, props.id, registerWarning, unregisterWarning]);
|
||||
|
||||
return <>
|
||||
<Toolbar nodeId={props.id} allowDelete={true}/>
|
||||
@@ -61,7 +100,10 @@ export default function NormNode(props: NodeProps<NormNode>) {
|
||||
placeholder={"Pepper should ..."}
|
||||
/>
|
||||
</div>
|
||||
<div className={"flex-row gap-md align-center"}>
|
||||
{/*There is no backend implementation yet of how critical norms would
|
||||
be treated differently than normal norms. The commented code below shows
|
||||
how you could add the UI side, if you wish to implement */}
|
||||
{/* <div className={"flex-row gap-md align-center"}>
|
||||
<label htmlFor={checkbox_id}>Critical:</label>
|
||||
<input
|
||||
id={checkbox_id}
|
||||
@@ -69,7 +111,7 @@ export default function NormNode(props: NodeProps<NormNode>) {
|
||||
checked={data.critical || false}
|
||||
onChange={(e) => setCritical(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
|
||||
{data.condition && (<div className={"flex-row gap-md align-center"} data-testid="norm-condition-information">
|
||||
@@ -79,10 +121,10 @@ export default function NormNode(props: NodeProps<NormNode>) {
|
||||
|
||||
<MultiConnectionHandle type="source" position={Position.Right} id="norms" rules={[
|
||||
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}])
|
||||
]}/>
|
||||
]} title="Connect to any number of phaseNode(-s)"/>
|
||||
<SingleConnectionHandle type="target" position={Position.Bottom} id="NormBeliefs" rules={[
|
||||
allowOnlyConnectionsFromType(["basic_belief"])
|
||||
]}/>
|
||||
allowOnlyConnectionsFromType(["basic_belief", "inferred_belief"])
|
||||
]} title="Connect to a beliefNode or a set of beliefs combined using the AND/OR node"/>
|
||||
</div>
|
||||
</>;
|
||||
};
|
||||
@@ -105,11 +147,10 @@ export function NormReduce(node: Node, nodes: Node[]) {
|
||||
};
|
||||
|
||||
if (data.condition) {
|
||||
const reducer = BasicBeliefReduce; // TODO: also add inferred.
|
||||
const conditionNode = nodes.find((node) => node.id === data.condition);
|
||||
// In case something went wrong, and our condition doesn't actually exist;
|
||||
if (conditionNode == undefined) return result;
|
||||
result["condition"] = reducer(conditionNode, nodes)
|
||||
result["condition"] = BeliefGlobalReduce(conditionNode, nodes)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -126,7 +167,7 @@ export const NormTooltip = `
|
||||
export function NormConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
||||
const data = _thisNode.data as NormNodeData;
|
||||
// If we got a belief connected, this is the condition for the norm.
|
||||
if ((useFlowStore.getState().nodes.find((node) => node.id === _sourceNodeId && node.type === 'basic_belief' /* TODO: Add the option for an inferred belief */))) {
|
||||
if ((useFlowStore.getState().nodes.find((node) => node.id === _sourceNodeId && ['basic_belief', 'inferred_belief'].includes(node.type!)))) {
|
||||
data.condition = _sourceNodeId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import type { PhaseNodeData } from "./PhaseNode";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {
|
||||
type NodeProps,
|
||||
Position,
|
||||
type Node
|
||||
type Node, useNodeConnections
|
||||
} from '@xyflow/react';
|
||||
import {useEffect, useRef} from "react";
|
||||
import {type EditorWarning} from "../components/EditorWarnings.tsx";
|
||||
import { Toolbar } from '../components/NodeComponents';
|
||||
import styles from '../../VisProg.module.css';
|
||||
import {SingleConnectionHandle, MultiConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
||||
@@ -37,10 +42,107 @@ export type PhaseNode = Node<PhaseNodeData>
|
||||
*/
|
||||
export default function PhaseNode(props: NodeProps<PhaseNode>) {
|
||||
const data = props.data;
|
||||
const {updateNodeData} = useFlowStore();
|
||||
const {updateNodeData, registerWarning, unregisterWarning} = useFlowStore();
|
||||
const updateLabel = (value: string) => updateNodeData(props.id, {...data, label: value});
|
||||
const label_input_id = `phase_${props.id}_label_input`;
|
||||
|
||||
const connections = useNodeConnections({
|
||||
id: props.id,
|
||||
handleType: "target",
|
||||
handleId: 'data'
|
||||
})
|
||||
|
||||
const phaseOutCons = useNodeConnections({
|
||||
id: props.id,
|
||||
handleType: "source",
|
||||
handleId: 'source',
|
||||
})
|
||||
|
||||
const phaseInCons = useNodeConnections({
|
||||
id: props.id,
|
||||
handleType: "target",
|
||||
handleId: 'target',
|
||||
})
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const noConnectionWarning : EditorWarning = {
|
||||
scope: {
|
||||
id: props.id,
|
||||
handleId: 'data'
|
||||
},
|
||||
type: 'MISSING_INPUT',
|
||||
severity: "WARNING",
|
||||
description: "the phaseNode has no incoming goals, norms, and/or triggers"
|
||||
}
|
||||
|
||||
if (connections.length === 0) { registerWarning(noConnectionWarning); return; }
|
||||
unregisterWarning(props.id, `${noConnectionWarning.type}:data`);
|
||||
}, [connections.length, props.id, registerWarning, unregisterWarning]);
|
||||
|
||||
useEffect(() => {
|
||||
const notConnectedInfo : EditorWarning = {
|
||||
scope: {
|
||||
id: props.id,
|
||||
handleId: undefined,
|
||||
},
|
||||
type: 'NOT_CONNECTED_TO_PROGRAM',
|
||||
severity: "INFO",
|
||||
description: "The PhaseNode is not connected to other nodes"
|
||||
};
|
||||
const noIncomingPhaseWarning : EditorWarning = {
|
||||
scope: {
|
||||
id: props.id,
|
||||
handleId: 'target'
|
||||
},
|
||||
type: 'MISSING_INPUT',
|
||||
severity: "WARNING",
|
||||
description: "the phaseNode has no incoming connection from a phase or the startNode"
|
||||
}
|
||||
const noOutgoingPhaseWarning : EditorWarning = {
|
||||
scope: {
|
||||
id: props.id,
|
||||
handleId: 'source'
|
||||
},
|
||||
type: 'MISSING_OUTPUT',
|
||||
severity: "WARNING",
|
||||
description: "the phaseNode has no outgoing connection to a phase or the endNode"
|
||||
}
|
||||
|
||||
// register relevant warning and unregister others
|
||||
if (phaseInCons.length === 0 && phaseOutCons.length === 0) {
|
||||
registerWarning(notConnectedInfo);
|
||||
unregisterWarning(props.id, `${noOutgoingPhaseWarning.type}:${noOutgoingPhaseWarning.scope.handleId}`);
|
||||
unregisterWarning(props.id, `${noIncomingPhaseWarning.type}:${noIncomingPhaseWarning.scope.handleId}`);
|
||||
return;
|
||||
}
|
||||
if (phaseOutCons.length === 0) {
|
||||
registerWarning(noOutgoingPhaseWarning);
|
||||
unregisterWarning(props.id, `${noIncomingPhaseWarning.type}:${noIncomingPhaseWarning.scope.handleId}`);
|
||||
unregisterWarning(notConnectedInfo.scope.id, notConnectedInfo.type);
|
||||
return;
|
||||
}
|
||||
if (phaseInCons.length === 0) {
|
||||
registerWarning(noIncomingPhaseWarning);
|
||||
unregisterWarning(props.id, `${noOutgoingPhaseWarning.type}:${noOutgoingPhaseWarning.scope.handleId}`);
|
||||
unregisterWarning(notConnectedInfo.scope.id, notConnectedInfo.type);
|
||||
return;
|
||||
}
|
||||
// unregister all warnings if none should be present
|
||||
unregisterWarning(notConnectedInfo.scope.id, notConnectedInfo.type);
|
||||
unregisterWarning(props.id, `${noOutgoingPhaseWarning.type}:${noOutgoingPhaseWarning.scope.handleId}`);
|
||||
unregisterWarning(props.id, `${noIncomingPhaseWarning.type}:${noIncomingPhaseWarning.scope.handleId}`);
|
||||
}, [phaseInCons.length, phaseOutCons.length, props.id, registerWarning, unregisterWarning]);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
const { width, height } = ref.current.getBoundingClientRect();
|
||||
|
||||
console.log('Node width:', width, 'height:', height);
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<Toolbar nodeId={props.id} allowDelete={true}/>
|
||||
@@ -57,14 +159,14 @@ export default function PhaseNode(props: NodeProps<PhaseNode>) {
|
||||
<SingleConnectionHandle type="target" position={Position.Left} id="target" rules={[
|
||||
noSelfConnections,
|
||||
allowOnlyConnectionsFromType(["phase", "start"]),
|
||||
]}/>
|
||||
]} title="Connect to a phase or the startNode"/>
|
||||
<MultiConnectionHandle type="target" position={Position.Bottom} id="data" rules={[
|
||||
allowOnlyConnectionsFromType(["norm", "goal", "trigger"])
|
||||
]}/>
|
||||
]} title="Connect to any number of norm, goal, and TriggerNode(-s)"/>
|
||||
<SingleConnectionHandle type="source" position={Position.Right} id="source" rules={[
|
||||
noSelfConnections,
|
||||
allowOnlyConnectionsFromType(["phase", "end"]),
|
||||
]}/>
|
||||
]} title="Connect to a phase or the endNode"/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import type { StartNodeData } from "./StartNode";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {
|
||||
type NodeProps,
|
||||
Position,
|
||||
type Node,
|
||||
type Node, useNodeConnections
|
||||
} from '@xyflow/react';
|
||||
import {useEffect} from "react";
|
||||
import { Toolbar } from '../components/NodeComponents';
|
||||
import styles from '../../VisProg.module.css';
|
||||
import {SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
||||
import {type EditorWarning} from "../components/EditorWarnings.tsx";
|
||||
import {allowOnlyConnectionsFromHandle} from "../HandleRules.ts";
|
||||
import useFlowStore from "../VisProgStores.tsx";
|
||||
|
||||
|
||||
export type StartNodeData = {
|
||||
@@ -25,6 +31,27 @@ export type StartNode = Node<StartNodeData>
|
||||
* @returns React.JSX.Element
|
||||
*/
|
||||
export default function StartNode(props: NodeProps<StartNode>) {
|
||||
const {registerWarning, unregisterWarning} = useFlowStore.getState();
|
||||
const connections = useNodeConnections({
|
||||
id: props.id,
|
||||
handleId: 'source'
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const noConnectionWarning : EditorWarning = {
|
||||
scope: {
|
||||
id: props.id,
|
||||
handleId: 'source'
|
||||
},
|
||||
type: 'MISSING_OUTPUT',
|
||||
severity: "ERROR",
|
||||
description: "the startNode does not have an outgoing connection to a phaseNode"
|
||||
}
|
||||
|
||||
if (connections.length === 0) { registerWarning(noConnectionWarning); }
|
||||
else { unregisterWarning(props.id, `${noConnectionWarning.type}:source`); }
|
||||
}, [connections.length, props.id, registerWarning, unregisterWarning]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toolbar nodeId={props.id} allowDelete={false}/>
|
||||
@@ -34,7 +61,7 @@ export default function StartNode(props: NodeProps<StartNode>) {
|
||||
</div>
|
||||
<SingleConnectionHandle type="source" position={Position.Right} id="source" rules={[
|
||||
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"target"}])
|
||||
]}/>
|
||||
]} title="Connect to a phaseNode"/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import type { TriggerNodeData } from "./TriggerNode";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {
|
||||
type NodeProps,
|
||||
Position,
|
||||
type Node,
|
||||
type Node, useNodeConnections
|
||||
} from '@xyflow/react';
|
||||
import {useEffect} from "react";
|
||||
import type {EditorWarning} from "../components/EditorWarnings.tsx";
|
||||
import { Toolbar } from '../components/NodeComponents';
|
||||
import styles from '../../VisProg.module.css';
|
||||
import {MultiConnectionHandle, SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
||||
import {allowOnlyConnectionsFromHandle, allowOnlyConnectionsFromType} from "../HandleRules.ts";
|
||||
import useFlowStore from '../VisProgStores';
|
||||
import {PlanReduce, type Plan } from '../components/Plan';
|
||||
import PlanEditorDialog from '../components/PlanEditor';
|
||||
import { BasicBeliefReduce } from './BasicBeliefNode';
|
||||
import PlanEditorDialog from '../components/PlanEditor';
|
||||
import {BeliefGlobalReduce} from "./BeliefGlobals.ts";
|
||||
import type { GoalNode } from './GoalNode.tsx';
|
||||
import { defaultPlan } from '../components/Plan.default.ts';
|
||||
import { deleteGoalInPlanByID, insertGoalInPlan } from '../components/PlanEditingFunctions.tsx';
|
||||
@@ -45,14 +50,94 @@ export type TriggerNode = Node<TriggerNodeData>
|
||||
*/
|
||||
export default function TriggerNode(props: NodeProps<TriggerNode>) {
|
||||
const data = props.data;
|
||||
const {updateNodeData} = useFlowStore();
|
||||
const {updateNodeData, registerWarning, unregisterWarning} = useFlowStore();
|
||||
|
||||
const setName= (value: string) => {
|
||||
updateNodeData(props.id, {...data, name: value})
|
||||
}
|
||||
|
||||
const beliefInput = useNodeConnections({
|
||||
id: props.id,
|
||||
handleType: "target",
|
||||
handleId: "TriggerBeliefs"
|
||||
})
|
||||
|
||||
const outputCons = useNodeConnections({
|
||||
id: props.id,
|
||||
handleType: "source",
|
||||
handleId: "TriggerSource"
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const noPhaseConnectionWarning : EditorWarning = {
|
||||
scope: {
|
||||
id: props.id,
|
||||
handleId: 'TriggerSource'
|
||||
},
|
||||
type: 'MISSING_OUTPUT',
|
||||
severity: 'INFO',
|
||||
description: "This triggerNode is missing a condition/belief, please make sure to connect a belief node to "
|
||||
};
|
||||
|
||||
if (outputCons.length === 0){
|
||||
registerWarning(noPhaseConnectionWarning);
|
||||
return;
|
||||
}
|
||||
unregisterWarning(props.id, `${noPhaseConnectionWarning.type}:${noPhaseConnectionWarning.scope.handleId}`);
|
||||
},[outputCons.length, props.id, registerWarning, unregisterWarning])
|
||||
|
||||
useEffect(() => {
|
||||
const noBeliefWarning : EditorWarning = {
|
||||
scope: {
|
||||
id: props.id,
|
||||
handleId: 'TriggerBeliefs'
|
||||
},
|
||||
type: 'MISSING_INPUT',
|
||||
severity: 'ERROR',
|
||||
description: "This triggerNode is missing a condition/belief, please make sure to connect a belief node to "
|
||||
};
|
||||
|
||||
if (beliefInput.length === 0 && outputCons.length !== 0){
|
||||
registerWarning(noBeliefWarning);
|
||||
return;
|
||||
}
|
||||
unregisterWarning(props.id, `${noBeliefWarning.type}:${noBeliefWarning.scope.handleId}`);
|
||||
},[beliefInput.length, outputCons.length, props.id, registerWarning, unregisterWarning])
|
||||
|
||||
useEffect(() => {
|
||||
const noPlanWarning : EditorWarning = {
|
||||
scope: {
|
||||
id: props.id,
|
||||
handleId: undefined
|
||||
},
|
||||
type: 'PLAN_IS_UNDEFINED',
|
||||
severity: 'ERROR',
|
||||
description: "This triggerNode is missing a plan, please make sure to create a plan by using the create plan button"
|
||||
};
|
||||
|
||||
if ((!data.plan || data.plan.steps?.length === 0) && outputCons.length !== 0){
|
||||
registerWarning(noPlanWarning);
|
||||
return;
|
||||
}
|
||||
unregisterWarning(props.id, noPlanWarning.type);
|
||||
},[data.plan, outputCons.length, props.id, registerWarning, unregisterWarning])
|
||||
|
||||
useEffect(() => {
|
||||
const name = data.name || "";
|
||||
|
||||
if (/^\d/.test(name)) {
|
||||
registerWarning({
|
||||
scope: { id: props.id },
|
||||
type: 'ELEMENT_STARTS_WITH_NUMBER',
|
||||
severity: 'ERROR',
|
||||
description: "Trigger names are not allowed to start with a number."
|
||||
});
|
||||
} else {
|
||||
unregisterWarning(props.id, 'ELEMENT_STARTS_WITH_NUMBER');
|
||||
}
|
||||
}, [data.name, props.id, registerWarning, unregisterWarning]);
|
||||
return <>
|
||||
|
||||
|
||||
<Toolbar nodeId={props.id} allowDelete={true}/>
|
||||
<div className={`${styles.defaultNode} ${styles.nodeTrigger} flex-col gap-sm`}>
|
||||
<TextField
|
||||
@@ -65,15 +150,16 @@ export default function TriggerNode(props: NodeProps<TriggerNode>) {
|
||||
<div className={"flex-row gap-md"}>Plan{data.plan ? (": " + data.plan.name) : ""} is currently {data.plan ? "" : "not"} set. {data.plan ? "🟢" : "🔴"}</div>
|
||||
<MultiConnectionHandle type="source" position={Position.Right} id="TriggerSource" rules={[
|
||||
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}]),
|
||||
]}/>
|
||||
]} title="Connect to any number of phaseNodes"/>
|
||||
<SingleConnectionHandle
|
||||
type="target"
|
||||
position={Position.Bottom}
|
||||
id="TriggerBeliefs"
|
||||
style={{ left: '40%' }}
|
||||
style={{ left: '40%' }}
|
||||
rules={[
|
||||
allowOnlyConnectionsFromType(['basic_belief']),
|
||||
allowOnlyConnectionsFromType(['basic_belief','inferred_belief']),
|
||||
]}
|
||||
title="Connect to a beliefNode or a set of beliefs combined using the AND/OR node"
|
||||
/>
|
||||
|
||||
<MultiConnectionHandle
|
||||
@@ -84,6 +170,7 @@ export default function TriggerNode(props: NodeProps<TriggerNode>) {
|
||||
rules={[
|
||||
allowOnlyConnectionsFromType(['goal']),
|
||||
]}
|
||||
title="Connect to any number of goalNodes"
|
||||
/>
|
||||
|
||||
<PlanEditorDialog
|
||||
@@ -102,13 +189,13 @@ export default function TriggerNode(props: NodeProps<TriggerNode>) {
|
||||
/**
|
||||
* Reduces each Trigger, including its children down into its core data.
|
||||
* @param node - The Trigger node to reduce.
|
||||
* @param _nodes - The list of all nodes in the current flow graph.
|
||||
* @param nodes - The list of all nodes in the current flow graph.
|
||||
* @returns A simplified object containing the node label and its list of triggers.
|
||||
*/
|
||||
export function TriggerReduce(node: Node, nodes: Node[]) {
|
||||
const data = node.data as TriggerNodeData;
|
||||
const conditionNode = data.condition ? nodes.find((n)=>n.id===data.condition) : undefined
|
||||
const conditionData = conditionNode ? BasicBeliefReduce(conditionNode, nodes) : ""
|
||||
const conditionData = conditionNode ? BeliefGlobalReduce(conditionNode, nodes) : ""
|
||||
return {
|
||||
id: node.id,
|
||||
name: node.data.name,
|
||||
@@ -136,7 +223,7 @@ export function TriggerConnectionTarget(_thisNode: Node, _sourceNodeId: string)
|
||||
const otherNode = nodes.find((x) => x.id === _sourceNodeId)
|
||||
if (!otherNode) return;
|
||||
|
||||
if (otherNode.type === 'basic_belief' /* TODO: Add the option for an inferred belief */) {
|
||||
if (['basic_belief', 'inferred_belief'].includes(otherNode.type!)) {
|
||||
data.condition = _sourceNodeId;
|
||||
}
|
||||
|
||||
@@ -172,7 +259,7 @@ export function TriggerDisconnectionTarget(_thisNode: Node, _sourceNodeId: strin
|
||||
const data = _thisNode.data as TriggerNodeData;
|
||||
// remove if the target of disconnection was our condition
|
||||
if (_sourceNodeId == data.condition) data.condition = undefined
|
||||
|
||||
|
||||
data.plan = deleteGoalInPlanByID(structuredClone(data.plan) as Plan, _sourceNodeId)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {type Edge, type Node } from "@xyflow/react";
|
||||
|
||||
export type SavedProject = {
|
||||
|
||||
3
src/utils/capitalize.ts
Normal file
3
src/utils/capitalize.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function (s: string) {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {useSyncExternalStore} from "react";
|
||||
|
||||
type Unsub = () => void;
|
||||
|
||||
7
src/utils/delayedResolve.ts
Normal file
7
src/utils/delayedResolve.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export default async function <T>(promise: Promise<T>, minDelayMs: number): Promise<T> {
|
||||
const [result] = await Promise.all([
|
||||
promise,
|
||||
new Promise(resolve => setTimeout(resolve, minDelayMs))
|
||||
]);
|
||||
return result;
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
/**
|
||||
* Find the indices of all elements that occur more than once.
|
||||
*
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
/**
|
||||
* Format a time duration like `HH:MM:SS.mmm`.
|
||||
*
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import type {PhaseNode} from "../pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
export type PriorityFilterPredicate<T> = {
|
||||
priority: number;
|
||||
predicate: (element: T) => boolean | null; // The predicate and its priority are ignored if it returns null.
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a list of priority predicates to an element. For all predicates that don't return null, if the ones with the highest level return true, then this function returns true.
|
||||
* Applies a list of priority predicates to an element. For all predicates that don't return null, if the ones with the highest level return true, then this function returns true. Or conversely, if the one with the highest level returns false, then this function returns false.
|
||||
* @param element The element to apply the predicates to.
|
||||
* @param predicates The list of predicates to apply.
|
||||
*/
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {create} from "zustand";
|
||||
|
||||
// the type of a reduced program
|
||||
export type ReducedProgram = { phases: Record<string, unknown>[] };
|
||||
|
||||
export type GoalWithDepth = Record<string, unknown> & { level: number };
|
||||
|
||||
/**
|
||||
* the type definition of the programStore
|
||||
*/
|
||||
@@ -15,8 +20,10 @@ export type ProgramState = {
|
||||
// Utility functions:
|
||||
// to avoid having to manually go through the entire state for every instance where data is required
|
||||
getPhaseIds: () => string[];
|
||||
getPhaseNames: () => string[];
|
||||
getNormsInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
||||
getGoalsInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
||||
getGoalsWithDepth: (currentPhaseId: string) => GoalWithDepth[];
|
||||
getTriggersInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
||||
// if more specific utility functions are needed they can be added here:
|
||||
}
|
||||
@@ -43,6 +50,10 @@ const useProgramStore = create<ProgramState>((set, get) => ({
|
||||
* gets the ids of all phases in the program
|
||||
*/
|
||||
getPhaseIds: () => get().currentProgram.phases.map(entry => entry["id"] as string),
|
||||
/**
|
||||
* gets the names of all phases in the program
|
||||
*/
|
||||
getPhaseNames: () => get().currentProgram.phases.map((entry) => (entry["name"] as string)),
|
||||
/**
|
||||
* gets the norms for the provided phase
|
||||
*/
|
||||
@@ -65,6 +76,50 @@ const useProgramStore = create<ProgramState>((set, get) => ({
|
||||
}
|
||||
throw new Error(`phase with id:"${currentPhaseId}" not found`)
|
||||
},
|
||||
|
||||
getGoalsWithDepth: (currentPhaseId: string) => {
|
||||
const program = get().currentProgram;
|
||||
const phase = program.phases.find(val => val["id"] === currentPhaseId);
|
||||
|
||||
if (!phase) {
|
||||
throw new Error(`phase with id:"${currentPhaseId}" not found`);
|
||||
}
|
||||
|
||||
const rootGoals = phase["goals"] as Record<string, unknown>[];
|
||||
const flatList: GoalWithDepth[] = [];
|
||||
|
||||
const isGoal = (item: Record<string, unknown>) => {
|
||||
return item["plan"] !== undefined;
|
||||
};
|
||||
|
||||
// Recursive helper function
|
||||
const traverse = (goals: Record<string, unknown>[], depth: number) => {
|
||||
goals.forEach((goal) => {
|
||||
// 1. Add the current goal to the list
|
||||
flatList.push({ ...goal, level: depth });
|
||||
|
||||
// 2. Check for children
|
||||
const plan = goal["plan"] as Record<string, unknown> | undefined;
|
||||
|
||||
if (plan && Array.isArray(plan["steps"])) {
|
||||
const steps = plan["steps"] as Record<string, unknown>[];
|
||||
|
||||
// 3. FILTER: Only recurse on steps that are actually goals
|
||||
// If we just passed 'steps', we might accidentally add Actions/Speeches to the goal list
|
||||
const childGoals = steps.filter(isGoal);
|
||||
|
||||
if (childGoals.length > 0) {
|
||||
traverse(childGoals, depth + 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Start traversal
|
||||
traverse(rootGoals, 0);
|
||||
|
||||
return flatList;
|
||||
},
|
||||
/**
|
||||
* gets the triggers for the provided phase
|
||||
*/
|
||||
|
||||
11
src/vite-env.d.ts
vendored
Normal file
11
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_BASE_URL?: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
declare const __VITE_API_BASE_URL__: string | undefined;
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render, screen} from '@testing-library/react';
|
||||
import Counter from '../src/components/components';
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {render, screen, waitFor, fireEvent} from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import * as React from "react";
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import {render, screen, fireEvent, act, waitFor} from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import "@testing-library/jest-dom";
|
||||
@@ -11,8 +14,6 @@ const loggingStoreRef: { current: null | { setState: (state: Partial<LoggingSett
|
||||
type LoggingSettingsState = {
|
||||
showRelativeTime: boolean;
|
||||
setShowRelativeTime: (show: boolean) => void;
|
||||
scrollToBottom: boolean;
|
||||
setScrollToBottom: (scroll: boolean) => void;
|
||||
};
|
||||
|
||||
jest.mock("zustand", () => {
|
||||
@@ -59,8 +60,8 @@ type LoggingComponent = typeof import("../../../src/components/Logging/Logging.t
|
||||
let Logging: LoggingComponent;
|
||||
|
||||
beforeAll(async () => {
|
||||
if (!Element.prototype.scrollIntoView) {
|
||||
Object.defineProperty(Element.prototype, "scrollIntoView", {
|
||||
if (!Element.prototype.scrollTo) {
|
||||
Object.defineProperty(Element.prototype, "scrollTo", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: function () {},
|
||||
@@ -84,7 +85,6 @@ afterEach(() => {
|
||||
function resetLoggingStore() {
|
||||
loggingStoreRef.current?.setState({
|
||||
showRelativeTime: false,
|
||||
scrollToBottom: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ describe("Logging component", () => {
|
||||
];
|
||||
mockUseLogs.mockReturnValue({filteredLogs: logs, distinctNames: new Set()});
|
||||
|
||||
const scrollSpy = jest.spyOn(Element.prototype, "scrollIntoView").mockImplementation(() => {});
|
||||
const scrollSpy = jest.spyOn(Element.prototype, "scrollTo").mockImplementation(() => {});
|
||||
const user = userEvent.setup();
|
||||
const view = render(<Logging/>);
|
||||
|
||||
@@ -175,7 +175,7 @@ describe("Logging component", () => {
|
||||
const logCell = makeCell({message: "Initial", firstRelativeCreated: 42});
|
||||
mockUseLogs.mockReturnValue({filteredLogs: [logCell], distinctNames: new Set()});
|
||||
|
||||
const scrollSpy = jest.spyOn(Element.prototype, "scrollIntoView").mockImplementation(() => {});
|
||||
const scrollSpy = jest.spyOn(Element.prototype, "scrollTo").mockImplementation(() => {});
|
||||
render(<Logging/>);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -209,7 +209,7 @@ describe("Logging component", () => {
|
||||
|
||||
const initialMap = firstProps.filterPredicates;
|
||||
expect(initialMap).toBeInstanceOf(Map);
|
||||
expect(initialMap.size).toBe(0);
|
||||
expect(initialMap.size).toBe(1); // Initially, only filter out experiment logs
|
||||
expect(mockUseLogs).toHaveBeenCalledWith(initialMap);
|
||||
|
||||
const updatedPredicate: LogFilterPredicate = {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
import { render, screen, act } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom";
|
||||
import {type LogRecord, useLogs} from "../../../src/components/Logging/useLogs.ts";
|
||||
import {type cell, useCell} from "../../../src/utils/cellStore.ts";
|
||||
import { StrictMode } from "react";
|
||||
import { API_BASE_URL } from "../../../src/config/api.ts";
|
||||
|
||||
jest.mock("../../../src/utils/priorityFiltering.ts", () => ({
|
||||
applyPriorityPredicates: jest.fn((_log, preds: any[]) =>
|
||||
@@ -80,7 +84,7 @@ describe("useLogs (unit)", () => {
|
||||
);
|
||||
const es = (globalThis as any).__es as MockEventSource;
|
||||
expect(es).toBeTruthy();
|
||||
expect(es.url).toBe("http://localhost:8000/logs/stream");
|
||||
expect(es.url).toBe(`${API_BASE_URL}/logs/stream`);
|
||||
|
||||
unmount();
|
||||
expect(es.close).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||
// University within the Software Project course.
|
||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||
@@ -1,104 +0,0 @@
|
||||
import { render, screen, act, cleanup, waitFor } from '@testing-library/react';
|
||||
import ConnectedRobots from '../../../src/pages/ConnectedRobots/ConnectedRobots';
|
||||
|
||||
// Mock event source
|
||||
const mockInstances: MockEventSource[] = [];
|
||||
class MockEventSource {
|
||||
url: string;
|
||||
onmessage: ((event: MessageEvent) => void) | null = null;
|
||||
closed = false;
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
mockInstances.push(this);
|
||||
}
|
||||
|
||||
sendMessage(data: string) {
|
||||
// Trigger whatever the component listens to
|
||||
this.onmessage?.({ data } as MessageEvent);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.closed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// mock event source generation with fake function that returns our fake mock source
|
||||
beforeAll(() => {
|
||||
// Cast globalThis to a type exposing EventSource and assign a mocked constructor.
|
||||
(globalThis as unknown as { EventSource?: typeof EventSource }).EventSource =
|
||||
jest.fn((url: string) => new MockEventSource(url)) as unknown as typeof EventSource;
|
||||
});
|
||||
|
||||
// clean after tests
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
jest.restoreAllMocks();
|
||||
mockInstances.length = 0;
|
||||
});
|
||||
|
||||
describe('ConnectedRobots', () => {
|
||||
test('renders initial state correctly', () => {
|
||||
render(<ConnectedRobots />);
|
||||
|
||||
// Check initial texts (before connection)
|
||||
expect(screen.getByText('Is robot currently connected?')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Robot is currently:\s*checking/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/If checking continues, make sure CB is properly loaded/i)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('updates to connected when message data is true', async () => {
|
||||
render(<ConnectedRobots />);
|
||||
const eventSource = mockInstances[0];
|
||||
expect(eventSource).toBeDefined();
|
||||
|
||||
// Check state after getting 'true' message
|
||||
await act(async () => {
|
||||
eventSource.sendMessage('true');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/connected! 🟢/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('updates to not connected when message data is false', async () => {
|
||||
render(<ConnectedRobots />);
|
||||
const eventSource = mockInstances[0];
|
||||
|
||||
// Check statew after getting 'false' message
|
||||
await act(async () => {
|
||||
eventSource.sendMessage('false');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/not connected.*🔴/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('handles invalid JSON gracefully', async () => {
|
||||
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
render(<ConnectedRobots />);
|
||||
const eventSource = mockInstances[0];
|
||||
|
||||
await act(async () => {
|
||||
eventSource.sendMessage('not-json');
|
||||
});
|
||||
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
'Ping message not in correct format:',
|
||||
'not-json'
|
||||
);
|
||||
});
|
||||
|
||||
test('closes EventSource on unmount', () => {
|
||||
render(<ConnectedRobots />);
|
||||
const eventSource = mockInstances[0];
|
||||
const closeSpy = jest.spyOn(eventSource, 'close');
|
||||
cleanup();
|
||||
expect(closeSpy).toHaveBeenCalled();
|
||||
expect(eventSource.closed).toBe(true);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user