Compare commits
38 Commits
feat/edito
...
feat/step-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47e7207c32 | ||
|
|
5e245a00da | ||
|
|
1e951968dd | ||
|
|
108fdeeedc | ||
|
|
79d889c10e | ||
|
|
bac94d5f8c | ||
|
|
f95b1148d9 | ||
|
|
46d900305a | ||
|
|
c4a4c52ecc | ||
|
|
96242fa6b0 | ||
|
|
c2486f5f43 | ||
|
|
f0c67c00dc | ||
|
|
a0a4687aeb | ||
|
|
71443c7fb6 | ||
|
|
39f013c47f | ||
|
|
6e1eb25bbc | ||
|
|
f2c01f67ac | ||
|
|
14cfc2bf15 | ||
|
|
a2b4847ca4 | ||
|
|
a1e242e391 | ||
|
|
4356f201ab | ||
|
|
c9df87929b | ||
|
|
57ebe724db | ||
|
|
794e638081 | ||
|
|
12ef2ef86e | ||
|
|
0fefefe7f0 | ||
|
|
9601f56ea9 | ||
|
|
873b1cfb0b | ||
|
|
4bd67debf3 | ||
|
|
e53e1a3958 | ||
|
|
7a89b0aedd | ||
|
|
7b05c7344c | ||
|
|
d80ced547c | ||
|
|
cd1aa84f89 | ||
|
|
469a6c7a69 | ||
|
|
b0a5e4770c | ||
|
|
f0fe520ea0 | ||
|
|
b10dbae488 |
515
package-lock.json
generated
515
package-lock.json
generated
@@ -10,11 +10,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@neodrag/react": "^2.3.1",
|
"@neodrag/react": "^2.3.1",
|
||||||
"@xyflow/react": "^12.8.6",
|
"@xyflow/react": "^12.8.6",
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-router": "^7.9.3",
|
"react-router": "^7.9.3",
|
||||||
"reactflow": "^11.11.4",
|
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -2055,276 +2053,6 @@
|
|||||||
"url": "https://opencollective.com/pkgr"
|
"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": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-beta.35",
|
"version": "1.0.0-beta.35",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.35.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.35.tgz",
|
||||||
@@ -2919,102 +2647,12 @@
|
|||||||
"@babel/types": "^7.28.2"
|
"@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": {
|
"node_modules/@types/d3-color": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/d3-drag": {
|
||||||
"version": "3.0.7",
|
"version": "3.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
||||||
@@ -3024,54 +2662,6 @@
|
|||||||
"@types/d3-selection": "*"
|
"@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": {
|
"node_modules/@types/d3-interpolate": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||||
@@ -3081,78 +2671,12 @@
|
|||||||
"@types/d3-color": "*"
|
"@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": {
|
"node_modules/@types/d3-selection": {
|
||||||
"version": "3.0.11",
|
"version": "3.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
||||||
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
|
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/d3-transition": {
|
||||||
"version": "3.0.9",
|
"version": "3.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
||||||
@@ -3179,12 +2703,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/hast": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||||
@@ -4453,15 +3971,6 @@
|
|||||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
"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": {
|
"node_modules/co": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||||
@@ -7436,9 +6945,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "7.12.0",
|
"version": "7.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.3.tgz",
|
||||||
"integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==",
|
"integrity": "sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookie": "^1.0.1",
|
"cookie": "^1.0.1",
|
||||||
@@ -7457,24 +6966,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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": {
|
"node_modules/redent": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||||
|
|||||||
@@ -14,11 +14,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@neodrag/react": "^2.3.1",
|
"@neodrag/react": "^2.3.1",
|
||||||
"@xyflow/react": "^12.8.6",
|
"@xyflow/react": "^12.8.6",
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-router": "^7.9.3",
|
"react-router": "^7.9.3",
|
||||||
"reactflow": "^11.11.4",
|
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import VisProg from "./pages/VisProgPage/VisProg.tsx";
|
|||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import Logging from "./components/Logging/Logging.tsx";
|
import Logging from "./components/Logging/Logging.tsx";
|
||||||
|
|
||||||
|
|
||||||
function App(){
|
function App(){
|
||||||
const [showLogs, setShowLogs] = useState(false);
|
const [showLogs, setShowLogs] = useState(false);
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
background-color: #242424;
|
background-color: #242424;
|
||||||
|
|
||||||
--accent-color: #008080;
|
--accent-color: #008080;
|
||||||
|
--panel-shadow:
|
||||||
|
0 1px 2px white,
|
||||||
|
0 8px 24px rgba(190, 186, 186, 0.253);
|
||||||
|
|
||||||
font-synthesis: none;
|
font-synthesis: none;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
@@ -15,6 +18,14 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-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 {
|
html, body, #root {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
224
src/pages/MonitoringPage/Components.tsx
Normal file
224
src/pages/MonitoringPage/Components.tsx
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import styles from './MonitoringPage.module.css';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HELPER: Unified sender function
|
||||||
|
*/
|
||||||
|
const sendUserInterrupt = async (type: string, context: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("http://localhost:8000/button_pressed", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ type, context }),
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error("Backend response error");
|
||||||
|
console.log(`Interrupt Sent - Type: ${type}, Context: ${context}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to send interrupt:`, err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- GESTURE COMPONENT ---
|
||||||
|
export const GestureControls: React.FC = () => {
|
||||||
|
const [selectedGesture, setSelectedGesture] = useState("animations/Stand/BodyTalk/Speaking/BodyTalk_1");
|
||||||
|
|
||||||
|
const gestures = [
|
||||||
|
{ label: "Body Talk 1", value: "animations/Stand/BodyTalk/Speaking/BodyTalk_1" },
|
||||||
|
{ label: "Thinking 8", value: "animations/Stand/Gestures/Thinking_8" },
|
||||||
|
{ label: "Thinking 1", value: "animations/Stand/Gestures/Thinking_1" },
|
||||||
|
{ label: "Happy", value: "animations/Stand/Emotions/Positive/Happy_1" },
|
||||||
|
];
|
||||||
|
|
||||||
|
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={() => sendUserInterrupt("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={() => sendUserInterrupt("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;
|
||||||
|
sendUserInterrupt("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 ---
|
||||||
|
type StatusItem = {
|
||||||
|
id?: string | number;
|
||||||
|
achieved?: boolean;
|
||||||
|
description?: string;
|
||||||
|
label?: string;
|
||||||
|
norm?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface StatusListProps {
|
||||||
|
title: string;
|
||||||
|
items: StatusItem[];
|
||||||
|
type: 'goal' | 'trigger' | 'norm'| 'cond_norm';
|
||||||
|
activeIds: Record<string, boolean>;
|
||||||
|
currentGoalIndex?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- STATUS LIST COMPONENT ---
|
||||||
|
export const StatusList: React.FC<StatusListProps> = ({
|
||||||
|
title,
|
||||||
|
items,
|
||||||
|
type,
|
||||||
|
activeIds,
|
||||||
|
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 canOverride = showIndicator && !isActive;
|
||||||
|
|
||||||
|
const isCurrentGoal = type === 'goal' && idx === currentGoalIndex;
|
||||||
|
|
||||||
|
const handleOverrideClick = () => {
|
||||||
|
if (!canOverride) return;
|
||||||
|
sendUserInterrupt("override", String(item.id));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={item.id ?? idx} className={styles.statusItem}>
|
||||||
|
{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.description || item.label || 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("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>
|
||||||
|
<h3>Connection:</h3>
|
||||||
|
<p className={connected ? styles.connected : styles.disconnected }>{connected ? "● Robot is connected" : "● Robot is disconnected"}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
274
src/pages/MonitoringPage/MonitoringPage.module.css
Normal file
274
src/pages/MonitoringPage/MonitoringPage.module.css
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
.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 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restartPhase{
|
||||||
|
background-color: rgb(255, 123, 0);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemDescription {
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LOGS */
|
||||||
|
.logs {
|
||||||
|
grid-area: logs;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 83%;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
background-color: Canvas;
|
||||||
|
color: CanvasText;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs button {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
box-shadow: var(--panel-shadow);
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
347
src/pages/MonitoringPage/MonitoringPage.tsx
Normal file
347
src/pages/MonitoringPage/MonitoringPage.tsx
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './MonitoringPage.module.css';
|
||||||
|
import useProgramStore from "../../utils/programStore.ts";
|
||||||
|
import { GestureControls, SpeechPresets, DirectSpeechInput, StatusList, RobotConnected } from './Components';
|
||||||
|
import { nextPhase, useExperimentLogger, pauseExperiment, playExperiment, resetExperiment, resetPhase, type ExperimentStreamData, type GoalUpdate, type TriggerUpdate, type CondNormsStateUpdate, type PhaseUpdate } from ".//MonitoringPageAPI.ts"
|
||||||
|
|
||||||
|
import type { NormNodeData } from '../VisProgPage/visualProgrammingUI/nodes/NormNode.tsx';
|
||||||
|
|
||||||
|
// Stream message types are defined in MonitoringPageAPI as `ExperimentStreamData`.
|
||||||
|
// Types for reduced program items (output from node reducers):
|
||||||
|
export type ReducedPlanStep = {
|
||||||
|
id: string;
|
||||||
|
text?: string;
|
||||||
|
gesture?: { type: string; name?: string };
|
||||||
|
goal?: string;
|
||||||
|
} & Record<string, unknown>;
|
||||||
|
|
||||||
|
export type ReducedPlan = { id: string; steps: ReducedPlanStep[] } | "";
|
||||||
|
|
||||||
|
export type ReducedGoal = { id: string; name: string; description?: string; can_fail?: boolean; plan?: ReducedPlan };
|
||||||
|
|
||||||
|
export type ReducedCondition = {
|
||||||
|
id: string;
|
||||||
|
keyword?: string;
|
||||||
|
emotion?: string;
|
||||||
|
object?: string;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
} & Record<string, unknown>;
|
||||||
|
|
||||||
|
export type ReducedTrigger = { id: string; name: string; condition?: ReducedCondition | ""; plan?: ReducedPlan };
|
||||||
|
|
||||||
|
export type ReducedNorm = { id: string; label?: string; norm?: string; condition?: ReducedCondition | "" };
|
||||||
|
|
||||||
|
|
||||||
|
const MonitoringPage: React.FC = () => {
|
||||||
|
const getPhaseIds = useProgramStore((s) => s.getPhaseIds);
|
||||||
|
const getNormsInPhase = useProgramStore((s) => s.getNormsInPhase);
|
||||||
|
const getGoalsInPhase = useProgramStore((s) => s.getGoalsInPhase);
|
||||||
|
const getTriggersInPhase = useProgramStore((s) => s.getTriggersInPhase);
|
||||||
|
|
||||||
|
// Can be used to block actions until feedback from CB.
|
||||||
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const [activeIds, setActiveIds] = React.useState<Record<string, boolean>>({});
|
||||||
|
const [goalIndex, setGoalIndex] = React.useState(0);
|
||||||
|
const [isPlaying, setIsPlaying] = React.useState(false);
|
||||||
|
|
||||||
|
const phaseIds = getPhaseIds();
|
||||||
|
|
||||||
|
const [phaseIndex, setPhaseIndex] = React.useState(0);
|
||||||
|
|
||||||
|
//see if we reached end node
|
||||||
|
const [isFinished, setIsFinished] = React.useState(false);
|
||||||
|
|
||||||
|
const handleStreamUpdate = React.useCallback((data: ExperimentStreamData) => {
|
||||||
|
// Check for phase updates
|
||||||
|
if (data.type === 'phase_update' && data.id) {
|
||||||
|
const payload = data as PhaseUpdate;
|
||||||
|
if (payload.id === "end") {
|
||||||
|
setIsFinished(true);
|
||||||
|
} else {
|
||||||
|
setIsFinished(false);
|
||||||
|
|
||||||
|
const allIds = getPhaseIds();
|
||||||
|
const newIndex = allIds.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 ReducedGoal[];
|
||||||
|
const gIndex = currentPhaseGoals.findIndex((g: ReducedGoal) => g.id === payload.id);
|
||||||
|
|
||||||
|
if (gIndex !== -1) {
|
||||||
|
//set current goal to the goal that is just started
|
||||||
|
setGoalIndex(gIndex);
|
||||||
|
|
||||||
|
// All previous goals are set to "active" which means they are achieved
|
||||||
|
setActiveIds((prev) => {
|
||||||
|
const nextState = { ...prev };
|
||||||
|
|
||||||
|
// We loop until i is LESS than gIndex.
|
||||||
|
// This leaves currentPhaseGoals[gIndex] as isActive: false.
|
||||||
|
for (let i = 0; i < gIndex; i++) {
|
||||||
|
nextState[currentPhaseGoals[i].id ] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextState;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Now pursuing goal: ${payload.id}. Previous goals marked achieved.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (data.type === 'trigger_update') {
|
||||||
|
const payload = data as TriggerUpdate;
|
||||||
|
setActiveIds((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[payload.id]: payload.achieved
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
else if (data.type === 'cond_norms_state_update') {
|
||||||
|
const payload = data as CondNormsStateUpdate;
|
||||||
|
setActiveIds((prev) => {
|
||||||
|
const nextState = { ...prev };
|
||||||
|
// payload.norms is typed on the union, so safe to use directly
|
||||||
|
payload.norms.forEach((normUpdate) => {
|
||||||
|
nextState[normUpdate.id] = normUpdate.active;
|
||||||
|
console.log(`Conditional norm ${normUpdate.id} set to active: ${normUpdate.active}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return nextState;
|
||||||
|
});
|
||||||
|
console.log("Updated conditional norms state:", payload.norms);
|
||||||
|
}
|
||||||
|
}, [getPhaseIds, getGoalsInPhase, phaseIds, phaseIndex]);
|
||||||
|
|
||||||
|
useExperimentLogger(handleStreamUpdate);
|
||||||
|
|
||||||
|
if (phaseIds.length === 0) {
|
||||||
|
return <p className={styles.empty}>No program loaded.</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const phaseId = phaseIds[phaseIndex];
|
||||||
|
|
||||||
|
const goals = (getGoalsInPhase(phaseId) as ReducedGoal[]).map(g => ({
|
||||||
|
...g,
|
||||||
|
label: g.name,
|
||||||
|
achieved: activeIds[g.id] ?? false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
const triggers = (getTriggersInPhase(phaseId) as ReducedTrigger[]).map(t => ({
|
||||||
|
...t,
|
||||||
|
label: (() => {
|
||||||
|
|
||||||
|
let prefix = "";
|
||||||
|
if (t.condition && typeof t.condition !== "string" && "keyword" in t.condition && typeof t.condition.keyword === "string") {
|
||||||
|
prefix = `if keywords said: "${t.condition.keyword}"`;
|
||||||
|
} else if (t.condition && typeof t.condition !== "string" && "name" in t.condition && typeof t.condition.name === "string") {
|
||||||
|
prefix = `if LLM belief: ${t.condition.name}`;
|
||||||
|
} else { //fallback
|
||||||
|
prefix = t.name || "Trigger"; // use typed `name` as a reliable fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const stepLabels = (t.plan && typeof t.plan !== "string" ? t.plan.steps : []).map((step: ReducedPlanStep) => {
|
||||||
|
if ("text" in step && typeof step.text === "string") {
|
||||||
|
return `say: "${step.text}"`;
|
||||||
|
}
|
||||||
|
if ("gesture" in step && step.gesture) {
|
||||||
|
const g = step.gesture;
|
||||||
|
return `perform gesture: ${g.name || g.type}`;
|
||||||
|
}
|
||||||
|
if ("goal" in step && typeof step.goal === "string") {
|
||||||
|
return `perform LLM: ${step.goal}`;
|
||||||
|
}
|
||||||
|
return "Action"; // Fallback
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
const planText = stepLabels.length > 0
|
||||||
|
? `➔ Do: ${stepLabels.join(", ")}`
|
||||||
|
: "➔ (No actions set)";
|
||||||
|
|
||||||
|
return `${prefix} ${planText}`;
|
||||||
|
})(),
|
||||||
|
isActive: activeIds[t.id] ?? false
|
||||||
|
}));
|
||||||
|
|
||||||
|
const norms = (getNormsInPhase(phaseId) as NormNodeData[])
|
||||||
|
.filter(n => !n.condition)
|
||||||
|
.map(n => ({
|
||||||
|
...n,
|
||||||
|
label: n.norm,
|
||||||
|
}));
|
||||||
|
const conditionalNorms = (getNormsInPhase(phaseId) as ReducedNorm[])
|
||||||
|
.filter(n => !!n.condition) // Only items with a condition
|
||||||
|
.map(n => ({
|
||||||
|
...n,
|
||||||
|
label: (() => {
|
||||||
|
let prefix = "";
|
||||||
|
if (n.condition && typeof n.condition !== "string" && "keyword" in n.condition && typeof n.condition.keyword === "string") {
|
||||||
|
prefix = `if keywords said: "${n.condition.keyword}"`;
|
||||||
|
} else if (n.condition && typeof n.condition !== "string" && "name" in n.condition && typeof n.condition.name === "string") {
|
||||||
|
prefix = `if LLM belief: ${n.condition.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return `${prefix} ➔ Norm: ${n.norm}`;
|
||||||
|
})(),
|
||||||
|
achieved: activeIds[n.id] ?? false
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Handle logic of 'next' button.
|
||||||
|
|
||||||
|
const handleButton = async (button: string, _context?: string, _endpoint?: string) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
switch (button) {
|
||||||
|
case "pause":
|
||||||
|
await pauseExperiment();
|
||||||
|
break;
|
||||||
|
case "play":
|
||||||
|
await playExperiment();
|
||||||
|
break;
|
||||||
|
case "nextPhase":
|
||||||
|
await nextPhase();
|
||||||
|
break;
|
||||||
|
case "resetPhase":
|
||||||
|
await resetPhase();
|
||||||
|
break;
|
||||||
|
case "resetExperiment":
|
||||||
|
await resetExperiment();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={styles.dashboardContainer}>
|
||||||
|
{/* HEADER */}
|
||||||
|
<header className={styles.experimentOverview}>
|
||||||
|
<div className={styles.phaseName}>
|
||||||
|
<h2>Experiment Overview</h2>
|
||||||
|
<p><strong>Phase</strong> {` ${phaseIndex + 1}`} </p>
|
||||||
|
<div className={styles.phaseProgress}>
|
||||||
|
{phaseIds.map((id, index) => (
|
||||||
|
<span
|
||||||
|
key={id}
|
||||||
|
className={`${styles.phase} ${
|
||||||
|
index < phaseIndex ? styles.completed :
|
||||||
|
index === phaseIndex ? styles.current : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.experimentControls}>
|
||||||
|
<h3>Experiment Controls</h3>
|
||||||
|
<div className={styles.controlsButtons}>
|
||||||
|
{/*Pause button*/}
|
||||||
|
<button
|
||||||
|
className={`${!isPlaying ? styles.pausePlayActive : styles.pausePlayInactive}`}
|
||||||
|
onClick={() => {
|
||||||
|
setIsPlaying(false);
|
||||||
|
handleButton("pause");}
|
||||||
|
}
|
||||||
|
disabled={loading}
|
||||||
|
>❚❚</button>
|
||||||
|
|
||||||
|
{/*Play button*/}
|
||||||
|
<button
|
||||||
|
className={`${isPlaying ? styles.pausePlayActive : styles.pausePlayInactive}`}
|
||||||
|
onClick={() => {
|
||||||
|
setIsPlaying(true);
|
||||||
|
handleButton("play");}
|
||||||
|
}
|
||||||
|
disabled={loading}
|
||||||
|
>▶</button>
|
||||||
|
|
||||||
|
{/*Next button*/}
|
||||||
|
<button
|
||||||
|
className={styles.next}
|
||||||
|
onClick={() => handleButton("nextPhase")}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
⏭
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/*Restart Phase button*/}
|
||||||
|
<button
|
||||||
|
className={styles.restartPhase}
|
||||||
|
onClick={() => handleButton("resetPhase")}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
↩
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/*Restart Experiment button*/}
|
||||||
|
<button
|
||||||
|
className={styles.restartExperiment}
|
||||||
|
onClick={() => handleButton("resetExperiment")}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
⟲
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<StatusList title="Goals" items={goals} type="goal" activeIds={activeIds} 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} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* LOGS */}
|
||||||
|
<aside className={styles.logs}>
|
||||||
|
<h3>Logs</h3>
|
||||||
|
<div className={styles.logHeader}>
|
||||||
|
<span>Global:</span>
|
||||||
|
<button>ALL</button>
|
||||||
|
<button>Add</button>
|
||||||
|
<button className={styles.live}>Live</button>
|
||||||
|
</div>
|
||||||
|
<textarea defaultValue="Example Log: much log"></textarea>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* FOOTER */}
|
||||||
|
<footer className={styles.controlsSection}>
|
||||||
|
<GestureControls />
|
||||||
|
<SpeechPresets />
|
||||||
|
<DirectSpeechInput />
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MonitoringPage;
|
||||||
107
src/pages/MonitoringPage/MonitoringPageAPI.ts
Normal file
107
src/pages/MonitoringPage/MonitoringPageAPI.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
const API_BASE_BP = "http://localhost:8000/button_pressed"; // Change depending on Pims interup agent/ correct endpoint
|
||||||
|
const API_BASE = "http://localhost:8000";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HELPER: Unified sender function
|
||||||
|
* In a real app, you might move this to a /services or /hooks folder
|
||||||
|
*/
|
||||||
|
const sendAPICall = async (type: string, context: string, endpoint?: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_BP}${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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an API call to the CB for going to reset the currect phase
|
||||||
|
* In case we can't go to the next phase, the function will throw an error.
|
||||||
|
*/
|
||||||
|
export async function resetPhase(): Promise<void> {
|
||||||
|
const type = "reset_phase"
|
||||||
|
const context = ""
|
||||||
|
sendAPICall(type, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an API call to the CB for going to reset the experiment
|
||||||
|
* In case we can't go to the next phase, the function will throw an error.
|
||||||
|
*/
|
||||||
|
export async function resetExperiment(): Promise<void> {
|
||||||
|
const type = "reset_experiment"
|
||||||
|
const context = ""
|
||||||
|
sendAPICall(type, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function pauseExperiment(): Promise<void> {
|
||||||
|
const type = "pause"
|
||||||
|
const context = "true"
|
||||||
|
sendAPICall(type, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 and logs data to the console.
|
||||||
|
* It does not render anything.
|
||||||
|
*/
|
||||||
|
export function useExperimentLogger(onUpdate?: (data: ExperimentStreamData) => void) {
|
||||||
|
useEffect(() => {
|
||||||
|
const eventSource = new EventSource(`${API_BASE}/experiment_stream`);
|
||||||
|
|
||||||
|
eventSource.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const parsedData = JSON.parse(event.data) as ExperimentStreamData;
|
||||||
|
if (onUpdate) {
|
||||||
|
console.log(event.data);
|
||||||
|
onUpdate(parsedData);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Stream parse error:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = (err) => {
|
||||||
|
console.error("SSE Connection Error:", err);
|
||||||
|
eventSource.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
eventSource.close();
|
||||||
|
};
|
||||||
|
}, [onUpdate]);
|
||||||
|
}
|
||||||
167
src/pages/SimpleProgram/SimpleProgram.module.css
Normal file
167
src/pages/SimpleProgram/SimpleProgram.module.css
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
/* ---------- Layout ---------- */
|
||||||
|
|
||||||
|
.container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: clamp(0.75rem, 2vw, 1.25rem);
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-bottom: 1px solid #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h2 {
|
||||||
|
font-size: clamp(1rem, 2.2vw, 1.4rem);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls button {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
padding: 0.4rem 0.9rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: none;
|
||||||
|
background: #111;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls button:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Content ---------- */
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 2%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Grid ---------- */
|
||||||
|
|
||||||
|
.phaseGrid {
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
grid-template-rows: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 2%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Box ---------- */
|
||||||
|
|
||||||
|
.box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #1e1e1e;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.boxHeader {
|
||||||
|
padding: 0.6rem 0.9rem;
|
||||||
|
background: linear-gradient(135deg, #dcdcdc, #e9e9e9);
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: clamp(0.9rem, 1.5vw, 1.05rem);
|
||||||
|
border-bottom: 1px solid #cfcfcf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boxContent {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.8rem 1rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Lists ---------- */
|
||||||
|
|
||||||
|
.iconList {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconList li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: clamp(0.85rem, 1.3vw, 1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulletList {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulletList li {
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Icons ---------- */
|
||||||
|
|
||||||
|
.successIcon,
|
||||||
|
.failIcon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.successIcon {
|
||||||
|
background: #3cb371;
|
||||||
|
}
|
||||||
|
|
||||||
|
.failIcon {
|
||||||
|
background: #e5533d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Empty ---------- */
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
opacity: 0.55;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Responsive ---------- */
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.phaseGrid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftControls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backButton {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #555;
|
||||||
|
color: #ddd;
|
||||||
|
padding: 0.35rem 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backButton:hover {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
192
src/pages/SimpleProgram/SimpleProgram.tsx
Normal file
192
src/pages/SimpleProgram/SimpleProgram.tsx
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styles from "./SimpleProgram.module.css";
|
||||||
|
import useProgramStore from "../../utils/programStore.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic container box with a header and content area.
|
||||||
|
*/
|
||||||
|
type BoxProps = {
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Box: React.FC<BoxProps> = ({ title, children }) => (
|
||||||
|
<div className={styles.box}>
|
||||||
|
<div className={styles.boxHeader}>{title}</div>
|
||||||
|
<div className={styles.boxContent}>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a list of goals for a phase.
|
||||||
|
* Expects goal-like objects from the program store.
|
||||||
|
*/
|
||||||
|
const GoalList: React.FC<{ goals: unknown[] }> = ({ goals }) => {
|
||||||
|
if (!goals.length) {
|
||||||
|
return <p className={styles.empty}>No goals defined.</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className={styles.iconList}>
|
||||||
|
{goals.map((g, idx) => {
|
||||||
|
const goal = g as {
|
||||||
|
id?: string;
|
||||||
|
description?: string;
|
||||||
|
achieved?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={goal.id ?? idx}>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
goal.achieved ? styles.successIcon : styles.failIcon
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{goal.achieved ? "✔" : "✖"}
|
||||||
|
</span>
|
||||||
|
{goal.description ?? "Unnamed goal"}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a list of triggers for a phase.
|
||||||
|
*/
|
||||||
|
const TriggerList: React.FC<{ triggers: unknown[] }> = ({ triggers }) => {
|
||||||
|
if (!triggers.length) {
|
||||||
|
return <p className={styles.empty}>No triggers defined.</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className={styles.iconList}>
|
||||||
|
{triggers.map((t, idx) => {
|
||||||
|
const trigger = t as {
|
||||||
|
id?: string;
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={trigger.id ?? idx}>
|
||||||
|
<span className={styles.failIcon}>✖</span>
|
||||||
|
{trigger.label ?? "Unnamed trigger"}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a list of norms for a phase.
|
||||||
|
*/
|
||||||
|
const NormList: React.FC<{ norms: unknown[] }> = ({ norms }) => {
|
||||||
|
if (!norms.length) {
|
||||||
|
return <p className={styles.empty}>No norms defined.</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className={styles.bulletList}>
|
||||||
|
{norms.map((n, idx) => {
|
||||||
|
const norm = n as {
|
||||||
|
id?: string;
|
||||||
|
norm?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
return <li key={norm.id ?? idx}>{norm.norm ?? "Unnamed norm"}</li>;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays all phase-related information in a grid layout.
|
||||||
|
*/
|
||||||
|
type PhaseGridProps = {
|
||||||
|
norms: unknown[];
|
||||||
|
goals: unknown[];
|
||||||
|
triggers: unknown[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const PhaseGrid: React.FC<PhaseGridProps> = ({
|
||||||
|
norms,
|
||||||
|
goals,
|
||||||
|
triggers,
|
||||||
|
}) => (
|
||||||
|
<div className={styles.phaseGrid}>
|
||||||
|
<Box title="Norms">
|
||||||
|
<NormList norms={norms} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box title="Triggers">
|
||||||
|
<TriggerList triggers={triggers} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box title="Goals">
|
||||||
|
<GoalList goals={goals} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box title="Conditional Norms">
|
||||||
|
<p className={styles.empty}>No conditional norms defined.</p>
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main program viewer.
|
||||||
|
* Reads all data from the program store and allows
|
||||||
|
* navigating between phases.
|
||||||
|
*/
|
||||||
|
const SimpleProgram: React.FC = () => {
|
||||||
|
const getPhaseIds = useProgramStore((s) => s.getPhaseIds);
|
||||||
|
const getNormsInPhase = useProgramStore((s) => s.getNormsInPhase);
|
||||||
|
const getGoalsInPhase = useProgramStore((s) => s.getGoalsInPhase);
|
||||||
|
const getTriggersInPhase = useProgramStore((s) => s.getTriggersInPhase);
|
||||||
|
|
||||||
|
const phaseIds = getPhaseIds();
|
||||||
|
const [phaseIndex, setPhaseIndex] = React.useState(0);
|
||||||
|
|
||||||
|
if (phaseIds.length === 0) {
|
||||||
|
return <p className={styles.empty}>No program loaded.</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const phaseId = phaseIds[phaseIndex];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<header className={styles.header}>
|
||||||
|
<h2>
|
||||||
|
Phase {phaseIndex + 1} / {phaseIds.length}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className={styles.controls}>
|
||||||
|
<button
|
||||||
|
disabled={phaseIndex === 0}
|
||||||
|
onClick={() => setPhaseIndex((i) => i - 1)}
|
||||||
|
>
|
||||||
|
◀ Prev
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
disabled={phaseIndex === phaseIds.length - 1}
|
||||||
|
onClick={() => setPhaseIndex((i) => i + 1)}
|
||||||
|
>
|
||||||
|
Next ▶
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className={styles.content}>
|
||||||
|
<PhaseGrid
|
||||||
|
norms={getNormsInPhase(phaseId)}
|
||||||
|
goals={getGoalsInPhase(phaseId)}
|
||||||
|
triggers={getTriggersInPhase(phaseId)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SimpleProgram;
|
||||||
@@ -84,10 +84,7 @@
|
|||||||
filter: drop-shadow(0 0 0.25rem plum);
|
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 {
|
.draggable-node {
|
||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
@@ -161,14 +158,6 @@
|
|||||||
filter: drop-shadow(0 0 0.25rem plum);
|
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 {
|
.planNoIterate {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
@@ -183,6 +172,18 @@
|
|||||||
left: 60% !important;
|
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 {
|
.node-toolbar-tooltip {
|
||||||
background-color: darkgray;
|
background-color: darkgray;
|
||||||
color: white;
|
color: white;
|
||||||
|
|||||||
@@ -4,22 +4,21 @@ import {
|
|||||||
Panel,
|
Panel,
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
ReactFlowProvider,
|
ReactFlowProvider,
|
||||||
MarkerType, getOutgoers
|
MarkerType,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import {graphReducer, runProgram} from "./VisProgLogic.tsx";
|
|
||||||
import warningStyles from './visualProgrammingUI/components/WarningSidebar.module.css'
|
|
||||||
import {type CSSProperties, useEffect, useState} from "react";
|
import {type CSSProperties, useEffect, useState} from "react";
|
||||||
import {useShallow} from 'zustand/react/shallow';
|
import {useShallow} from 'zustand/react/shallow';
|
||||||
|
import orderPhaseNodeArray from "../../utils/orderPhaseNodes.ts";
|
||||||
import useProgramStore from "../../utils/programStore.ts";
|
import useProgramStore from "../../utils/programStore.ts";
|
||||||
import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx';
|
import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx';
|
||||||
import {type EditorWarning, globalWarning} from "./visualProgrammingUI/components/EditorWarnings.tsx";
|
import type {PhaseNode} from "./visualProgrammingUI/nodes/PhaseNode.tsx";
|
||||||
import {WarningsSidebar} from "./visualProgrammingUI/components/WarningSidebar.tsx";
|
|
||||||
import useFlowStore from './visualProgrammingUI/VisProgStores.tsx';
|
import useFlowStore from './visualProgrammingUI/VisProgStores.tsx';
|
||||||
import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx';
|
import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx';
|
||||||
import styles from './VisProg.module.css'
|
import styles from './VisProg.module.css'
|
||||||
import { NodeTypes } from './visualProgrammingUI/NodeRegistry.ts';
|
import { NodeReduces, NodeTypes } from './visualProgrammingUI/NodeRegistry.ts';
|
||||||
import SaveLoadPanel from './visualProgrammingUI/components/SaveLoadPanel.tsx';
|
import SaveLoadPanel from './visualProgrammingUI/components/SaveLoadPanel.tsx';
|
||||||
|
import MonitoringPage from '../MonitoringPage/MonitoringPage.tsx';
|
||||||
|
|
||||||
// --| config starting params for flow |--
|
// --| config starting params for flow |--
|
||||||
|
|
||||||
@@ -44,7 +43,6 @@ const selector = (state: FlowState) => ({
|
|||||||
nodes: state.nodes,
|
nodes: state.nodes,
|
||||||
edges: state.edges,
|
edges: state.edges,
|
||||||
onNodesChange: state.onNodesChange,
|
onNodesChange: state.onNodesChange,
|
||||||
onNodesDelete: state.onNodesDelete,
|
|
||||||
onEdgesDelete: state.onEdgesDelete,
|
onEdgesDelete: state.onEdgesDelete,
|
||||||
onEdgesChange: state.onEdgesChange,
|
onEdgesChange: state.onEdgesChange,
|
||||||
onConnect: state.onConnect,
|
onConnect: state.onConnect,
|
||||||
@@ -70,7 +68,6 @@ const VisProgUI = () => {
|
|||||||
const {
|
const {
|
||||||
nodes, edges,
|
nodes, edges,
|
||||||
onNodesChange,
|
onNodesChange,
|
||||||
onNodesDelete,
|
|
||||||
onEdgesDelete,
|
onEdgesDelete,
|
||||||
onEdgesChange,
|
onEdgesChange,
|
||||||
onConnect,
|
onConnect,
|
||||||
@@ -93,36 +90,15 @@ const VisProgUI = () => {
|
|||||||
window.addEventListener('keydown', handler);
|
window.addEventListener('keydown', handler);
|
||||||
return () => window.removeEventListener('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 (
|
return (
|
||||||
<div className={`${styles.innerEditorContainer} round-lg border-lg flex-row`} style={({'--flow-zoom': zoom} as CSSProperties)}>
|
<div className={`${styles.innerEditorContainer} round-lg border-lg`} style={({'--flow-zoom': zoom} as CSSProperties)}>
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
defaultEdgeOptions={DEFAULT_EDGE_OPTIONS}
|
defaultEdgeOptions={DEFAULT_EDGE_OPTIONS}
|
||||||
nodeTypes={NodeTypes}
|
nodeTypes={NodeTypes}
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
onNodesDelete={onNodesDelete}
|
|
||||||
onEdgesDelete={onEdgesDelete}
|
onEdgesDelete={onEdgesDelete}
|
||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
onReconnect={onReconnect}
|
onReconnect={onReconnect}
|
||||||
@@ -133,11 +109,9 @@ const VisProgUI = () => {
|
|||||||
onNodeDragStop={endBatchAction}
|
onNodeDragStop={endBatchAction}
|
||||||
preventScrolling={scrollable}
|
preventScrolling={scrollable}
|
||||||
onMove={(_, viewport) => setZoom(viewport.zoom)}
|
onMove={(_, viewport) => setZoom(viewport.zoom)}
|
||||||
reconnectRadius={15}
|
|
||||||
snapToGrid
|
snapToGrid
|
||||||
fitView
|
fitView
|
||||||
proOptions={{hideAttribution: true}}
|
proOptions={{hideAttribution: true}}
|
||||||
style={{flexGrow: 3}}
|
|
||||||
>
|
>
|
||||||
<Panel position="top-center" className={styles.dndPanel}>
|
<Panel position="top-center" className={styles.dndPanel}>
|
||||||
<DndToolbar/> {/* contains the drag and drop panel for nodes */}
|
<DndToolbar/> {/* contains the drag and drop panel for nodes */}
|
||||||
@@ -146,16 +120,12 @@ const VisProgUI = () => {
|
|||||||
<SaveLoadPanel></SaveLoadPanel>
|
<SaveLoadPanel></SaveLoadPanel>
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel position="bottom-center">
|
<Panel position="bottom-center">
|
||||||
<button onClick={() => undo()}>Undo</button>
|
<button onClick={() => undo()}>undo</button>
|
||||||
<button onClick={() => redo()}>Redo</button>
|
<button onClick={() => redo()}>Redo</button>
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel position="center-right" className={warningStyles.warningsSidebar}>
|
|
||||||
<WarningsSidebar/>
|
|
||||||
</Panel>
|
|
||||||
<Controls/>
|
<Controls/>
|
||||||
<Background/>
|
<Background/>
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -175,23 +145,41 @@ function VisualProgrammingUI() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkPhaseChain = (): boolean => {
|
// currently outputs the prepared program to the console
|
||||||
const {nodes, edges} = useFlowStore.getState();
|
function runProgramm() {
|
||||||
|
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.");
|
||||||
|
|
||||||
function checkForCompleteChain(currentNodeId: string): boolean {
|
// store reduced program in global program store for further use in the UI
|
||||||
const outgoingPhases = getOutgoers({id: currentNodeId}, nodes, edges)
|
// when the program was sent to the backend successfully:
|
||||||
.filter(node => ["end", "phase"].includes(node.type!));
|
useProgramStore.getState().setProgramState(structuredClone(program));
|
||||||
|
}).catch(() => console.log("Failed to send program to the backend."));
|
||||||
|
}
|
||||||
|
|
||||||
if (outgoingPhases.length === 0) return false;
|
/**
|
||||||
if (outgoingPhases.some(node => node.type === "end" )) return true;
|
* 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)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const next = outgoingPhases.map(node => checkForCompleteChain(node.id))
|
|
||||||
.find(result => result);
|
|
||||||
return !!next;
|
|
||||||
}
|
|
||||||
|
|
||||||
return checkForCompleteChain('start');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* houses the entire page, so also UI elements
|
* houses the entire page, so also UI elements
|
||||||
@@ -199,29 +187,31 @@ const checkPhaseChain = (): boolean => {
|
|||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
function VisProgPage() {
|
function VisProgPage() {
|
||||||
const [programValidity, setProgramValidity] = useState<boolean>(true);
|
const [showSimpleProgram, setShowSimpleProgram] = useState(false);
|
||||||
const {isProgramValid, severityIndex} = useFlowStore();
|
|
||||||
|
|
||||||
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 setProgramState = useProgramStore((state) => state.setProgramState);
|
const setProgramState = useProgramStore((state) => state.setProgramState);
|
||||||
|
|
||||||
const processProgram = () => {
|
const runProgram = () => {
|
||||||
const phases = graphReducer(); // reduce graph
|
const phases = graphReducer(); // reduce graph
|
||||||
setProgramState({ phases }); // <-- save to store
|
setProgramState({ phases }); // <-- save to store
|
||||||
runProgram(); // send to backend if needed
|
setShowSimpleProgram(true); // show SimpleProgram
|
||||||
|
runProgramm(); // send to backend if needed
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (showSimpleProgram) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button className={styles.backButton} onClick={() => setShowSimpleProgram(false)}>
|
||||||
|
Back to Editor ◀
|
||||||
|
</button>
|
||||||
|
<MonitoringPage/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<VisualProgrammingUI/>
|
<VisualProgrammingUI/>
|
||||||
<button onClick={processProgram} disabled={!programValidity}>Run Program</button>
|
<button onClick={runProgram}>run program</button>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
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";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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(
|
|
||||||
"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.");
|
|
||||||
|
|
||||||
// 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,18 +1,10 @@
|
|||||||
import type {Edge, Node} from "@xyflow/react";
|
import type {Edge, Node} from "@xyflow/react";
|
||||||
import type {StateCreator, StoreApi } from 'zustand/vanilla';
|
import type {StateCreator, StoreApi } from 'zustand/vanilla';
|
||||||
import type {
|
|
||||||
SeverityIndex,
|
|
||||||
WarningRegistry
|
|
||||||
} from "./components/EditorWarnings.tsx";
|
|
||||||
import type {FlowState} from "./VisProgTypes.tsx";
|
import type {FlowState} from "./VisProgTypes.tsx";
|
||||||
|
|
||||||
export type FlowSnapshot = {
|
export type FlowSnapshot = {
|
||||||
nodes: Node[];
|
nodes: Node[];
|
||||||
edges: Edge[];
|
edges: Edge[];
|
||||||
warnings: {
|
|
||||||
warningRegistry: WarningRegistry;
|
|
||||||
severityIndex: SeverityIndex;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,11 +41,7 @@ export const UndoRedo = (
|
|||||||
*/
|
*/
|
||||||
const getSnapshot = (state : BaseFlowState) : FlowSnapshot => (structuredClone({
|
const getSnapshot = (state : BaseFlowState) : FlowSnapshot => (structuredClone({
|
||||||
nodes: state.nodes,
|
nodes: state.nodes,
|
||||||
edges: state.edges,
|
edges: state.edges
|
||||||
warnings: {
|
|
||||||
warningRegistry: state.editorWarningRegistry,
|
|
||||||
severityIndex: state.severityIndex,
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const initialState = config(set, get, api);
|
const initialState = config(set, get, api);
|
||||||
@@ -90,8 +78,6 @@ export const UndoRedo = (
|
|||||||
set({
|
set({
|
||||||
nodes: snapshot.nodes,
|
nodes: snapshot.nodes,
|
||||||
edges: snapshot.edges,
|
edges: snapshot.edges,
|
||||||
editorWarningRegistry: snapshot.warnings.warningRegistry,
|
|
||||||
severityIndex: snapshot.warnings.severityIndex,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
state.future.push(currentSnapshot); // push current to redo
|
state.future.push(currentSnapshot); // push current to redo
|
||||||
@@ -111,8 +97,6 @@ export const UndoRedo = (
|
|||||||
set({
|
set({
|
||||||
nodes: snapshot.nodes,
|
nodes: snapshot.nodes,
|
||||||
edges: snapshot.edges,
|
edges: snapshot.edges,
|
||||||
editorWarningRegistry: snapshot.warnings.warningRegistry,
|
|
||||||
severityIndex: snapshot.warnings.severityIndex,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
state.past.push(currentSnapshot); // push current to undo
|
state.past.push(currentSnapshot); // push current to undo
|
||||||
|
|||||||
@@ -107,16 +107,4 @@ export function useHandleRules(
|
|||||||
// finally we return a function that evaluates all rules using the created context
|
// finally we return a function that evaluates all rules using the created context
|
||||||
return evaluateRules(targetRules, connection, 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);
|
|
||||||
}
|
}
|
||||||
@@ -43,4 +43,3 @@ export const noSelfConnections : HandleRule =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -52,24 +52,15 @@ import TriggerNode, {
|
|||||||
TriggerTooltip
|
TriggerTooltip
|
||||||
} from "./nodes/TriggerNode";
|
} from "./nodes/TriggerNode";
|
||||||
import { TriggerNodeDefaults } from "./nodes/TriggerNode.default";
|
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, {
|
import BasicBeliefNode, {
|
||||||
BasicBeliefConnectionSource,
|
BasicBeliefConnectionSource,
|
||||||
BasicBeliefConnectionTarget,
|
BasicBeliefConnectionTarget,
|
||||||
BasicBeliefDisconnectionSource,
|
BasicBeliefDisconnectionSource,
|
||||||
BasicBeliefDisconnectionTarget,
|
BasicBeliefDisconnectionTarget,
|
||||||
BasicBeliefReduce
|
BasicBeliefReduce,
|
||||||
,
|
|
||||||
BasicBeliefTooltip
|
BasicBeliefTooltip
|
||||||
} from "./nodes/BasicBeliefNode.tsx";
|
} from "./nodes/BasicBeliefNode";
|
||||||
import { BasicBeliefNodeDefaults } from "./nodes/BasicBeliefNode.default.ts";
|
import { BasicBeliefNodeDefaults } from "./nodes/BasicBeliefNode.default";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registered node types in the visual programming system.
|
* Registered node types in the visual programming system.
|
||||||
@@ -85,7 +76,6 @@ export const NodeTypes = {
|
|||||||
goal: GoalNode,
|
goal: GoalNode,
|
||||||
trigger: TriggerNode,
|
trigger: TriggerNode,
|
||||||
basic_belief: BasicBeliefNode,
|
basic_belief: BasicBeliefNode,
|
||||||
inferred_belief: InferredBeliefNode,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -101,7 +91,6 @@ export const NodeDefaults = {
|
|||||||
goal: GoalNodeDefaults,
|
goal: GoalNodeDefaults,
|
||||||
trigger: TriggerNodeDefaults,
|
trigger: TriggerNodeDefaults,
|
||||||
basic_belief: BasicBeliefNodeDefaults,
|
basic_belief: BasicBeliefNodeDefaults,
|
||||||
inferred_belief: InferredBeliefNodeDefaults,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -119,7 +108,6 @@ export const NodeReduces = {
|
|||||||
goal: GoalReduce,
|
goal: GoalReduce,
|
||||||
trigger: TriggerReduce,
|
trigger: TriggerReduce,
|
||||||
basic_belief: BasicBeliefReduce,
|
basic_belief: BasicBeliefReduce,
|
||||||
inferred_belief: InferredBeliefReduce,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -138,7 +126,6 @@ export const NodeConnections = {
|
|||||||
goal: GoalConnectionTarget,
|
goal: GoalConnectionTarget,
|
||||||
trigger: TriggerConnectionTarget,
|
trigger: TriggerConnectionTarget,
|
||||||
basic_belief: BasicBeliefConnectionTarget,
|
basic_belief: BasicBeliefConnectionTarget,
|
||||||
inferred_belief: InferredBeliefConnectionTarget,
|
|
||||||
},
|
},
|
||||||
Sources: {
|
Sources: {
|
||||||
start: StartConnectionSource,
|
start: StartConnectionSource,
|
||||||
@@ -147,8 +134,7 @@ export const NodeConnections = {
|
|||||||
norm: NormConnectionSource,
|
norm: NormConnectionSource,
|
||||||
goal: GoalConnectionSource,
|
goal: GoalConnectionSource,
|
||||||
trigger: TriggerConnectionSource,
|
trigger: TriggerConnectionSource,
|
||||||
basic_belief: BasicBeliefConnectionSource,
|
basic_belief: BasicBeliefConnectionSource
|
||||||
inferred_belief: InferredBeliefConnectionSource,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +153,6 @@ export const NodeDisconnections = {
|
|||||||
goal: GoalDisconnectionTarget,
|
goal: GoalDisconnectionTarget,
|
||||||
trigger: TriggerDisconnectionTarget,
|
trigger: TriggerDisconnectionTarget,
|
||||||
basic_belief: BasicBeliefDisconnectionTarget,
|
basic_belief: BasicBeliefDisconnectionTarget,
|
||||||
inferred_belief: InferredBeliefDisconnectionTarget,
|
|
||||||
},
|
},
|
||||||
Sources: {
|
Sources: {
|
||||||
start: StartDisconnectionSource,
|
start: StartDisconnectionSource,
|
||||||
@@ -177,7 +162,6 @@ export const NodeDisconnections = {
|
|||||||
goal: GoalDisconnectionSource,
|
goal: GoalDisconnectionSource,
|
||||||
trigger: TriggerDisconnectionSource,
|
trigger: TriggerDisconnectionSource,
|
||||||
basic_belief: BasicBeliefDisconnectionSource,
|
basic_belief: BasicBeliefDisconnectionSource,
|
||||||
inferred_belief: InferredBeliefDisconnectionSource,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +186,6 @@ export const NodesInPhase = {
|
|||||||
end: () => false,
|
end: () => false,
|
||||||
phase: () => false,
|
phase: () => false,
|
||||||
basic_belief: () => false,
|
basic_belief: () => false,
|
||||||
inferred_belief: () => false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -216,5 +199,4 @@ export const NodeTooltips = {
|
|||||||
goal: GoalTooltip,
|
goal: GoalTooltip,
|
||||||
trigger: TriggerTooltip,
|
trigger: TriggerTooltip,
|
||||||
basic_belief: BasicBeliefTooltip,
|
basic_belief: BasicBeliefTooltip,
|
||||||
inferred_belief: InferredBeliefTooltip,
|
|
||||||
}
|
}
|
||||||
@@ -9,8 +9,6 @@ import {
|
|||||||
type XYPosition,
|
type XYPosition,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import '@xyflow/react/dist/style.css';
|
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 type { FlowState } from './VisProgTypes';
|
||||||
import {
|
import {
|
||||||
NodeDefaults,
|
NodeDefaults,
|
||||||
@@ -45,18 +43,19 @@ function createNode(id: string, type: string, position: XYPosition, data: Record
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//* Initial nodes, created by using createNode. */
|
//* 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.
|
// 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 startNode = createNode('start', 'start', {x: 110, y: 100}, {label: "Start"}, false)
|
||||||
const endNode = createNode('end', 'end', {x: 590, y: 100}, {label: "End"}, 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 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...
|
// Initial edges, leave empty as setting initial edges...
|
||||||
// ...breaks logic that is dependent on connection events
|
// ...breaks logic that is dependent on connection events
|
||||||
const initialEdges: Edge[] = [];
|
const initialEdges: Edge[] = [];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* useFlowStore contains the implementation for all editor functionality
|
* useFlowStore contains the implementation for all editor functionality
|
||||||
* and stores the current state of the visual programming editor
|
* and stores the current state of the visual programming editor
|
||||||
@@ -87,9 +86,7 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
|
|||||||
*/
|
*/
|
||||||
onNodesChange: (changes) => set({nodes: applyNodeChanges(changes, get().nodes)}),
|
onNodesChange: (changes) => set({nodes: applyNodeChanges(changes, get().nodes)}),
|
||||||
|
|
||||||
onNodesDelete: (nodes) => nodes.forEach((_node) => {
|
onNodesDelete: (nodes) => nodes.forEach(node => get().unregisterNodeRules(node.id)),
|
||||||
return;
|
|
||||||
}),
|
|
||||||
|
|
||||||
onEdgesDelete: (edges) => {
|
onEdgesDelete: (edges) => {
|
||||||
// we make sure any affected nodes get updated to reflect removal of edges
|
// we make sure any affected nodes get updated to reflect removal of edges
|
||||||
@@ -132,41 +129,7 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
|
|||||||
* Handles reconnecting an edge between nodes.
|
* Handles reconnecting an edge between nodes.
|
||||||
*/
|
*/
|
||||||
onReconnect: (oldEdge, newConnection) => {
|
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) });
|
set({ edges: reconnectEdge(oldEdge, newConnection, get().edges) });
|
||||||
|
|
||||||
// We make sure to perform any required data updates on the newly reconnected nodes
|
// We make sure to perform any required data updates on the newly reconnected nodes
|
||||||
@@ -219,32 +182,19 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
|
|||||||
* Deletes a node by ID, respecting NodeDeletes rules.
|
* Deletes a node by ID, respecting NodeDeletes rules.
|
||||||
* Also removes all edges connected to that node.
|
* Also removes all edges connected to that node.
|
||||||
*/
|
*/
|
||||||
deleteNode: (nodeId, deleteElements) => {
|
deleteNode: (nodeId) => {
|
||||||
get().pushSnapshot();
|
get().pushSnapshot();
|
||||||
|
|
||||||
// Let's find our node to check if they have a special deletion function
|
// Let's find our node to check if they have a special deletion function
|
||||||
const ourNode = get().nodes.find((n)=>n.id==nodeId);
|
const ourNode = get().nodes.find((n)=>n.id==nodeId);
|
||||||
const ourFunction = Object.entries(NodeDeletes).find(([t])=>t==ourNode?.type)?.[1]
|
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 there's no function, OR, our function tells us we can delete it, let's do so...
|
||||||
if (ourFunction == undefined || ourFunction()) {
|
if (ourFunction == undefined || ourFunction()) {
|
||||||
if (deleteElements){
|
set({
|
||||||
deleteElements({
|
nodes: get().nodes.filter((n) => n.id !== nodeId),
|
||||||
nodes: get().nodes.filter((n) => n.id === nodeId),
|
edges: get().edges.filter((e) => e.source !== nodeId && e.target !== nodeId),
|
||||||
edges: get().edges.filter((e) => e.source !== nodeId && e.target === nodeId)}
|
})}
|
||||||
).then(() => {
|
|
||||||
get().unregisterNodeRules(nodeId);
|
|
||||||
get().unregisterWarningsForId(nodeId);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
set({
|
|
||||||
nodes: get().nodes.filter((n) => n.id !== nodeId),
|
|
||||||
edges: get().edges.filter((e) => e.source !== nodeId && e.target !== nodeId),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -356,12 +306,8 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
|
|||||||
})
|
})
|
||||||
return { ruleRegistry: registry };
|
return { ruleRegistry: registry };
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
...editorWarningRegistry(get, set),
|
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default useFlowStore;
|
export default useFlowStore;
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,8 @@ import type {
|
|||||||
OnReconnect,
|
OnReconnect,
|
||||||
Node,
|
Node,
|
||||||
OnEdgesDelete,
|
OnEdgesDelete,
|
||||||
OnNodesDelete, DeleteElementsOptions
|
OnNodesDelete
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import type {EditorWarningRegistry} from "./components/EditorWarnings.tsx";
|
|
||||||
import type {HandleRule} from "./HandleRuleLogic.ts";
|
import type {HandleRule} from "./HandleRuleLogic.ts";
|
||||||
import type { NodeTypes } from './NodeRegistry';
|
import type { NodeTypes } from './NodeRegistry';
|
||||||
import type {FlowSnapshot} from "./EditorUndoRedo.ts";
|
import type {FlowSnapshot} from "./EditorUndoRedo.ts";
|
||||||
@@ -69,10 +68,7 @@ export type FlowState = {
|
|||||||
* Deletes a node and any connected edges.
|
* Deletes a node and any connected edges.
|
||||||
* @param nodeId - the ID of the node to delete
|
* @param nodeId - the ID of the node to delete
|
||||||
*/
|
*/
|
||||||
deleteNode: (nodeId: string, deleteElements?: (params: DeleteElementsOptions) => Promise<{
|
deleteNode: (nodeId: string) => void;
|
||||||
deletedNodes: Node[]
|
|
||||||
deletedEdges: Edge[]
|
|
||||||
}>) => void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replaces the current nodes array in the store.
|
* Replaces the current nodes array in the store.
|
||||||
@@ -98,7 +94,7 @@ export type FlowState = {
|
|||||||
* @param node - the Node object to add
|
* @param node - the Node object to add
|
||||||
*/
|
*/
|
||||||
addNode: (node: Node) => void;
|
addNode: (node: Node) => void;
|
||||||
} & UndoRedoState & HandleRuleRegistry & EditorWarningRegistry;
|
} & UndoRedoState & HandleRuleRegistry;
|
||||||
|
|
||||||
export type UndoRedoState = {
|
export type UndoRedoState = {
|
||||||
// UndoRedo Types
|
// UndoRedo Types
|
||||||
@@ -133,7 +129,4 @@ export type HandleRuleRegistry = {
|
|||||||
|
|
||||||
// cleans up all registered rules of all handles of the provided node
|
// cleans up all registered rules of all handles of the provided node
|
||||||
unregisterNodeRules: (nodeId: string) => void
|
unregisterNodeRules: (nodeId: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
/* 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'
|
|
||||||
| 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,4 @@
|
|||||||
import {NodeToolbar, useReactFlow} from '@xyflow/react';
|
import {NodeToolbar} from '@xyflow/react';
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import {type JSX, useState} from "react";
|
import {type JSX, useState} from "react";
|
||||||
import {createPortal} from "react-dom";
|
import {createPortal} from "react-dom";
|
||||||
@@ -30,11 +30,10 @@ type ToolbarProps = {
|
|||||||
*/
|
*/
|
||||||
export function Toolbar({nodeId, allowDelete}: ToolbarProps) {
|
export function Toolbar({nodeId, allowDelete}: ToolbarProps) {
|
||||||
const {nodes, deleteNode} = useFlowStore();
|
const {nodes, deleteNode} = useFlowStore();
|
||||||
const { deleteElements } = useReactFlow();
|
|
||||||
|
|
||||||
const deleteParentNode = () => {
|
const deleteParentNode = () => {
|
||||||
|
deleteNode(nodeId);
|
||||||
deleteNode(nodeId, deleteElements);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const nodeType = nodes.find((node) => node.id === nodeId)?.type as keyof typeof NodeTooltips;
|
const nodeType = nodes.find((node) => node.id === nodeId)?.type as keyof typeof NodeTooltips;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.planDialog::backdrop {
|
.planDialog::backdrop {
|
||||||
background: rgba(0, 0, 0, 0.4);
|
background: rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
@@ -68,4 +67,17 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dragHandle {
|
||||||
|
margin-left: auto;
|
||||||
|
cursor: grab;
|
||||||
|
opacity: 0.5;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragHandle:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.planStepDragging {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
@@ -6,16 +6,26 @@ import { defaultPlan } from "../components/Plan.default";
|
|||||||
import { TextField } from "../../../../components/TextField";
|
import { TextField } from "../../../../components/TextField";
|
||||||
import GestureValueEditor from "./GestureValueEditor";
|
import GestureValueEditor from "./GestureValueEditor";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties of a plan editor.
|
||||||
|
* @property plan: The optional plan loaded into this editor.
|
||||||
|
* @property onSave: The function that will be called upon save.
|
||||||
|
* @property description: Optional description which is already set.
|
||||||
|
* @property onlyEditPhasing: Optional boolean to toggle
|
||||||
|
* whether or not this editor is part of the phase editing.
|
||||||
|
*/
|
||||||
type PlanEditorDialogProps = {
|
type PlanEditorDialogProps = {
|
||||||
plan?: Plan;
|
plan?: Plan;
|
||||||
onSave: (plan: Plan | undefined) => void;
|
onSave: (plan: Plan | undefined) => void;
|
||||||
description? : string;
|
description? : string;
|
||||||
|
onlyEditPhasing? : boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PlanEditorDialog({
|
export default function PlanEditorDialog({
|
||||||
plan,
|
plan,
|
||||||
onSave,
|
onSave,
|
||||||
description,
|
description,
|
||||||
|
onlyEditPhasing = false,
|
||||||
}: PlanEditorDialogProps) {
|
}: PlanEditorDialogProps) {
|
||||||
// UseStates and references
|
// UseStates and references
|
||||||
const dialogRef = useRef<HTMLDialogElement | null>(null);
|
const dialogRef = useRef<HTMLDialogElement | null>(null);
|
||||||
@@ -24,10 +34,11 @@ export default function PlanEditorDialog({
|
|||||||
const [newActionGestureType, setNewActionGestureType] = useState<boolean>(true);
|
const [newActionGestureType, setNewActionGestureType] = useState<boolean>(true);
|
||||||
const [newActionValue, setNewActionValue] = useState("");
|
const [newActionValue, setNewActionValue] = useState("");
|
||||||
const [hasInteractedWithPlan, setHasInteractedWithPlan] = useState<boolean>(false)
|
const [hasInteractedWithPlan, setHasInteractedWithPlan] = useState<boolean>(false)
|
||||||
|
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
|
||||||
const { setScrollable } = useFlowStore();
|
const { setScrollable } = useFlowStore();
|
||||||
const nodes = useFlowStore().nodes;
|
const nodes = useFlowStore().nodes;
|
||||||
|
|
||||||
//Button Actions
|
// Button Actions
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setScrollable(false);
|
setScrollable(false);
|
||||||
setDraftPlan({...structuredClone(defaultPlan), id: crypto.randomUUID()});
|
setDraftPlan({...structuredClone(defaultPlan), id: crypto.randomUUID()});
|
||||||
@@ -89,9 +100,9 @@ export default function PlanEditorDialog({
|
|||||||
data-testid={"PlanEditorDialogTestID"}
|
data-testid={"PlanEditorDialogTestID"}
|
||||||
>
|
>
|
||||||
<form method="dialog" className="flex-col gap-md">
|
<form method="dialog" className="flex-col gap-md">
|
||||||
<h3> {draftPlan?.id === plan?.id ? "Edit Plan" : "Create Plan"} </h3>
|
<h3> {onlyEditPhasing ? "Editing Phase Ordering" : (draftPlan?.id === plan?.id ? "Edit Plan" : "Create Plan")} </h3>
|
||||||
{/* Plan name text field */}
|
{/* Plan name text field */}
|
||||||
{draftPlan && (
|
{(draftPlan && !onlyEditPhasing) && (
|
||||||
<TextField
|
<TextField
|
||||||
value={draftPlan.name}
|
value={draftPlan.name}
|
||||||
setValue={(name) =>
|
setValue={(name) =>
|
||||||
@@ -104,12 +115,14 @@ export default function PlanEditorDialog({
|
|||||||
{draftPlan && (<div className={styles.planEditor}>
|
{draftPlan && (<div className={styles.planEditor}>
|
||||||
<div className={styles.planEditorLeft}>
|
<div className={styles.planEditorLeft}>
|
||||||
{/* Left Side (Action Adder) */}
|
{/* Left Side (Action Adder) */}
|
||||||
<h4>Add Action</h4>
|
<h4>{onlyEditPhasing ? "You can't add any actions, only rearrange the steps." : "Add Action"}</h4>
|
||||||
{(!plan && description && draftPlan.steps.length === 0 && !hasInteractedWithPlan) && (<div className={styles.stepSuggestion}>
|
{(!plan && description && draftPlan.steps.length === 0 && !hasInteractedWithPlan) && (<div className={styles.stepSuggestion}>
|
||||||
<label> Filled in as a suggestion! </label>
|
<label> Filled in as a suggestion! </label>
|
||||||
<label> Feel free to change! </label>
|
<label> Feel free to change! </label>
|
||||||
</div>)}
|
</div>)}
|
||||||
<label>
|
|
||||||
|
|
||||||
|
{(!onlyEditPhasing) && (<label>
|
||||||
Action Type <wbr />
|
Action Type <wbr />
|
||||||
{/* Type selection */}
|
{/* Type selection */}
|
||||||
<select
|
<select
|
||||||
@@ -123,10 +136,10 @@ export default function PlanEditorDialog({
|
|||||||
<option value="gesture">Gesture Action</option>
|
<option value="gesture">Gesture Action</option>
|
||||||
<option value="llm">LLM Action</option>
|
<option value="llm">LLM Action</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>)}
|
||||||
|
|
||||||
{/* Action value editor*/}
|
{/* Action value editor*/}
|
||||||
{newActionType === "gesture" ? (
|
{!onlyEditPhasing && newActionType === "gesture" ? (
|
||||||
// Gesture get their own editor component
|
// Gesture get their own editor component
|
||||||
<GestureValueEditor
|
<GestureValueEditor
|
||||||
value={newActionValue}
|
value={newActionValue}
|
||||||
@@ -134,17 +147,20 @@ export default function PlanEditorDialog({
|
|||||||
setType={setNewActionGestureType}
|
setType={setNewActionGestureType}
|
||||||
placeholder="Gesture name"
|
placeholder="Gesture name"
|
||||||
/>
|
/>
|
||||||
) : (
|
)
|
||||||
<TextField
|
:
|
||||||
|
// Only show the text field if we're not just rearranging.
|
||||||
|
(!onlyEditPhasing &&
|
||||||
|
(<TextField
|
||||||
value={newActionValue}
|
value={newActionValue}
|
||||||
setValue={setNewActionValue}
|
setValue={setNewActionValue}
|
||||||
placeholder={
|
placeholder={
|
||||||
newActionType === "speech" ? "Speech text"
|
newActionType === "speech" ? "Speech text"
|
||||||
: "LLM goal"
|
: "LLM goal"
|
||||||
}
|
}
|
||||||
/>
|
/>)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Adding steps */}
|
{/* Adding steps */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
:global(.react-flow__handle.source){
|
|
||||||
border-radius: 100%;
|
|
||||||
}
|
|
||||||
:global(.react-flow__handle.target){
|
|
||||||
border-radius: 15%;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
:global(.react-flow__handle.connected) {
|
:global(.react-flow__handle.connected) {
|
||||||
background: lightgray;
|
background: lightgray;
|
||||||
border-color: green;
|
border-color: green;
|
||||||
filter: drop-shadow(0 0 0.15rem green);
|
filter: drop-shadow(0 0 0.25rem green);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.singleConnectionHandle.connected) {
|
:global(.singleConnectionHandle.connected) {
|
||||||
@@ -25,19 +16,19 @@
|
|||||||
:global(.singleConnectionHandle.unconnected){
|
:global(.singleConnectionHandle.unconnected){
|
||||||
background: lightsalmon;
|
background: lightsalmon;
|
||||||
border-color: #ff6060;
|
border-color: #ff6060;
|
||||||
filter: drop-shadow(0 0 0.15rem #ff6060);
|
filter: drop-shadow(0 0 0.25rem #ff6060);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.react-flow__handle.connectingto) {
|
:global(.react-flow__handle.connectingto) {
|
||||||
background: #ff6060;
|
background: #ff6060;
|
||||||
border-color: coral;
|
border-color: coral;
|
||||||
filter: drop-shadow(0 0 0.15rem coral);
|
filter: drop-shadow(0 0 0.25rem coral);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.react-flow__handle.valid) {
|
:global(.react-flow__handle.valid) {
|
||||||
background: #55dd99;
|
background: #55dd99;
|
||||||
border-color: green;
|
border-color: green;
|
||||||
filter: drop-shadow(0 0 0.15rem green);
|
filter: drop-shadow(0 0 0.25rem green);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.react-flow__handle) {
|
:global(.react-flow__handle) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
type Connection,
|
type Connection,
|
||||||
useNodeId, useNodeConnections
|
useNodeId, useNodeConnections
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
|
import {useState} from 'react';
|
||||||
import { type HandleRule, useHandleRules} from "../HandleRuleLogic.ts";
|
import { type HandleRule, useHandleRules} from "../HandleRuleLogic.ts";
|
||||||
import "./RuleBasedHandle.module.css";
|
import "./RuleBasedHandle.module.css";
|
||||||
|
|
||||||
@@ -28,16 +29,21 @@ export function MultiConnectionHandle({
|
|||||||
handleId: id!
|
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 (
|
return (
|
||||||
<Handle
|
<Handle
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
id={id}
|
id={id}
|
||||||
type={type}
|
type={type}
|
||||||
className={"multiConnectionHandle" + (connections.length === 0 ? " unconnected" : " connected") + ` ${type}`}
|
className={"multiConnectionHandle" + (connections.length === 0 ? " unconnected" : " connected")}
|
||||||
isValidConnection={(connection) => {
|
isValidConnection={(connection) => {
|
||||||
const result = validate(connection as Connection);
|
const result = validate(connection as Connection);
|
||||||
|
setHandleState(result);
|
||||||
return result.isSatisfied;
|
return result.isSatisfied;
|
||||||
}}
|
}}
|
||||||
|
title={handleState.message}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -60,18 +66,22 @@ export function SingleConnectionHandle({
|
|||||||
handleId: id!
|
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 (
|
return (
|
||||||
<Handle
|
<Handle
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
id={id}
|
id={id}
|
||||||
type={type}
|
type={type}
|
||||||
className={"singleConnectionHandle" + (connections.length === 0 ? " unconnected" : " connected") + ` ${type}`}
|
className={"singleConnectionHandle" + (connections.length === 0 ? " unconnected" : " connected")}
|
||||||
isConnectable={connections.length === 0}
|
isConnectable={connections.length === 0}
|
||||||
isValidConnection={(connection) => {
|
isValidConnection={(connection) => {
|
||||||
const result = validate(connection as Connection);
|
const result = validate(connection as Connection);
|
||||||
|
setHandleState(result);
|
||||||
return result.isSatisfied;
|
return result.isSatisfied;
|
||||||
}}
|
}}
|
||||||
|
title={handleState.message}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,203 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
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,4 @@
|
|||||||
import type { BasicBeliefNodeData } from "./BasicBeliefNode.tsx";
|
import type { BasicBeliefNodeData } from "./BasicBeliefNode";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ import {
|
|||||||
Position,
|
Position,
|
||||||
type Node,
|
type Node,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import { Toolbar } from '../components/NodeComponents.tsx';
|
import { Toolbar } from '../components/NodeComponents';
|
||||||
import styles from '../../VisProg.module.css';
|
import styles from '../../VisProg.module.css';
|
||||||
import {MultiConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
import {MultiConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
||||||
import {allowOnlyConnectionsFromHandle} from "../HandleRules.ts";
|
import {allowOnlyConnectionsFromHandle} from "../HandleRules.ts";
|
||||||
import useFlowStore from '../VisProgStores.tsx';
|
import useFlowStore from '../VisProgStores';
|
||||||
import { TextField } from '../../../../components/TextField.tsx';
|
import { TextField } from '../../../../components/TextField';
|
||||||
import { MultilineTextField } from '../../../../components/MultilineTextField.tsx';
|
import { MultilineTextField } from '../../../../components/MultilineTextField';
|
||||||
import {noMatchingLeftRightBelief} from "./BeliefGlobals.ts";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default data structure for a BasicBelief node
|
* The default data structure for a BasicBelief node
|
||||||
@@ -32,7 +31,7 @@ export type BasicBeliefNodeData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// These are all the types a basic belief could be.
|
// These are all the types a basic belief could be.
|
||||||
export type BasicBeliefType = Keyword | Semantic | DetectedObject | Emotion
|
type BasicBeliefType = Keyword | Semantic | DetectedObject | Emotion
|
||||||
type Keyword = { type: "keyword", id: string, value: string, label: "Keyword said:"};
|
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 Semantic = { type: "semantic", id: string, value: string, description: string, label: "Detected with LLM:"};
|
||||||
type DetectedObject = { type: "object", id: string, value: string, label: "Object found:"};
|
type DetectedObject = { type: "object", id: string, value: string, label: "Object found:"};
|
||||||
@@ -190,9 +189,8 @@ export default function BasicBeliefNode(props: NodeProps<BasicBeliefNode>) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<MultiConnectionHandle type="source" position={Position.Right} id="source" rules={[
|
<MultiConnectionHandle type="source" position={Position.Right} id="source" rules={[
|
||||||
noMatchingLeftRightBelief,
|
allowOnlyConnectionsFromHandle([{nodeType:"trigger",handleId:"TriggerBeliefs"}, {nodeType:"norm",handleId:"NormBeliefs"}]),
|
||||||
allowOnlyConnectionsFromHandle([{nodeType:"trigger",handleId:"TriggerBeliefs"}, {nodeType:"norm",handleId:"NormBeliefs"}]),
|
]}/>
|
||||||
]} title="Connect to any number of trigger and/or normNode(-s)"/>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
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,15 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
type NodeProps,
|
type NodeProps,
|
||||||
Position,
|
Position,
|
||||||
type Node, useNodeConnections
|
type Node,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import {useEffect} from "react";
|
|
||||||
import type {EditorWarning} from "../components/EditorWarnings.tsx";
|
|
||||||
import { Toolbar } from '../components/NodeComponents';
|
import { Toolbar } from '../components/NodeComponents';
|
||||||
import styles from '../../VisProg.module.css';
|
import styles from '../../VisProg.module.css';
|
||||||
import {SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
import {SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
||||||
import {allowOnlyConnectionsFromType} from "../HandleRules.ts";
|
import {allowOnlyConnectionsFromType} from "../HandleRules.ts";
|
||||||
import useFlowStore from "../VisProgStores.tsx";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -30,27 +27,6 @@ export type EndNode = Node<EndNodeData>
|
|||||||
* @returns React.JSX.Element
|
* @returns React.JSX.Element
|
||||||
*/
|
*/
|
||||||
export default function EndNode(props: NodeProps<EndNode>) {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Toolbar nodeId={props.id} allowDelete={false}/>
|
<Toolbar nodeId={props.id} allowDelete={false}/>
|
||||||
@@ -60,7 +36,7 @@ export default function EndNode(props: NodeProps<EndNode>) {
|
|||||||
</div>
|
</div>
|
||||||
<SingleConnectionHandle type="target" position={Position.Left} id="target" rules={[
|
<SingleConnectionHandle type="target" position={Position.Left} id="target" rules={[
|
||||||
allowOnlyConnectionsFromType(["phase"])
|
allowOnlyConnectionsFromType(["phase"])
|
||||||
]} title="Connect to a phaseNode"/>
|
]}/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
type NodeProps,
|
type NodeProps,
|
||||||
Position,
|
Position,
|
||||||
type Node
|
type Node,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import {useEffect} from "react";
|
|
||||||
import type {EditorWarning} from "../components/EditorWarnings.tsx";
|
|
||||||
import { Toolbar } from '../components/NodeComponents';
|
import { Toolbar } from '../components/NodeComponents';
|
||||||
import styles from '../../VisProg.module.css';
|
import styles from '../../VisProg.module.css';
|
||||||
import { TextField } from '../../../../components/TextField';
|
import { TextField } from '../../../../components/TextField';
|
||||||
@@ -46,7 +44,7 @@ export type GoalNode = Node<GoalNodeData>
|
|||||||
* @returns React.JSX.Element
|
* @returns React.JSX.Element
|
||||||
*/
|
*/
|
||||||
export default function GoalNode({id, data}: NodeProps<GoalNode>) {
|
export default function GoalNode({id, data}: NodeProps<GoalNode>) {
|
||||||
const {updateNodeData, registerWarning, unregisterWarning} = useFlowStore();
|
const {updateNodeData} = useFlowStore();
|
||||||
const _nodes = useFlowStore().nodes;
|
const _nodes = useFlowStore().nodes;
|
||||||
|
|
||||||
const text_input_id = `goal_${id}_text_input`;
|
const text_input_id = `goal_${id}_text_input`;
|
||||||
@@ -66,24 +64,6 @@ export default function GoalNode({id, data}: NodeProps<GoalNode>) {
|
|||||||
updateNodeData(id, {...data, can_fail: value});
|
updateNodeData(id, {...data, can_fail: value});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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){
|
|
||||||
registerWarning(noPlanWarning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
unregisterWarning(id, noPlanWarning.type);
|
|
||||||
},[data.plan, id, registerWarning, unregisterWarning])
|
|
||||||
return <>
|
return <>
|
||||||
<Toolbar nodeId={id} allowDelete={true}/>
|
<Toolbar nodeId={id} allowDelete={true}/>
|
||||||
<div className={`${styles.defaultNode} ${styles.nodeGoal} flex-col gap-sm`}>
|
<div className={`${styles.defaultNode} ${styles.nodeGoal} flex-col gap-sm`}>
|
||||||
@@ -138,11 +118,9 @@ export default function GoalNode({id, data}: NodeProps<GoalNode>) {
|
|||||||
</div>
|
</div>
|
||||||
<MultiConnectionHandle type="source" position={Position.Right} id="GoalSource" rules={[
|
<MultiConnectionHandle type="source" position={Position.Right} id="GoalSource" rules={[
|
||||||
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}]),
|
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={[
|
<MultiConnectionHandle type="target" position={Position.Bottom} id="GoalTarget" rules={[allowOnlyConnectionsFromType(["goal"])]}/>
|
||||||
allowOnlyConnectionsFromType(["goal"])]
|
|
||||||
} title="Connect to any number of goalNode(-s)"/>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@ import { TextField } from '../../../../components/TextField';
|
|||||||
import {MultiConnectionHandle, SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
import {MultiConnectionHandle, SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
||||||
import {allowOnlyConnectionsFromHandle, allowOnlyConnectionsFromType} from "../HandleRules.ts";
|
import {allowOnlyConnectionsFromHandle, allowOnlyConnectionsFromType} from "../HandleRules.ts";
|
||||||
import useFlowStore from '../VisProgStores';
|
import useFlowStore from '../VisProgStores';
|
||||||
import {BeliefGlobalReduce} from "./BeliefGlobals.ts";
|
import { BasicBeliefReduce } from './BasicBeliefNode';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default data dot a phase node
|
* The default data dot a phase node
|
||||||
@@ -79,10 +79,10 @@ export default function NormNode(props: NodeProps<NormNode>) {
|
|||||||
|
|
||||||
<MultiConnectionHandle type="source" position={Position.Right} id="norms" rules={[
|
<MultiConnectionHandle type="source" position={Position.Right} id="norms" rules={[
|
||||||
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}])
|
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}])
|
||||||
]} title="Connect to any number of phaseNode(-s)"/>
|
]}/>
|
||||||
<SingleConnectionHandle type="target" position={Position.Bottom} id="NormBeliefs" rules={[
|
<SingleConnectionHandle type="target" position={Position.Bottom} id="NormBeliefs" rules={[
|
||||||
allowOnlyConnectionsFromType(["basic_belief", "inferred_belief"])
|
allowOnlyConnectionsFromType(["basic_belief"])
|
||||||
]} title="Connect to a beliefNode or a set of beliefs combined using the AND/OR node"/>
|
]}/>
|
||||||
</div>
|
</div>
|
||||||
</>;
|
</>;
|
||||||
};
|
};
|
||||||
@@ -105,10 +105,11 @@ export function NormReduce(node: Node, nodes: Node[]) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (data.condition) {
|
if (data.condition) {
|
||||||
|
const reducer = BasicBeliefReduce; // TODO: also add inferred.
|
||||||
const conditionNode = nodes.find((node) => node.id === data.condition);
|
const conditionNode = nodes.find((node) => node.id === data.condition);
|
||||||
// In case something went wrong, and our condition doesn't actually exist;
|
// In case something went wrong, and our condition doesn't actually exist;
|
||||||
if (conditionNode == undefined) return result;
|
if (conditionNode == undefined) return result;
|
||||||
result["condition"] = BeliefGlobalReduce(conditionNode, nodes)
|
result["condition"] = reducer(conditionNode, nodes)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -125,7 +126,7 @@ export const NormTooltip = `
|
|||||||
export function NormConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
export function NormConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
||||||
const data = _thisNode.data as NormNodeData;
|
const data = _thisNode.data as NormNodeData;
|
||||||
// If we got a belief connected, this is the condition for the norm.
|
// If we got a belief connected, this is the condition for the norm.
|
||||||
if ((useFlowStore.getState().nodes.find((node) => node.id === _sourceNodeId && ['basic_belief', 'inferred_belief'].includes(node.type!)))) {
|
if ((useFlowStore.getState().nodes.find((node) => node.id === _sourceNodeId && node.type === 'basic_belief' /* TODO: Add the option for an inferred belief */))) {
|
||||||
data.condition = _sourceNodeId;
|
data.condition = _sourceNodeId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ export const PhaseNodeDefaults: PhaseNodeData = {
|
|||||||
hasReduce: true,
|
hasReduce: true,
|
||||||
nextPhaseId: null,
|
nextPhaseId: null,
|
||||||
isFirstPhase: false,
|
isFirstPhase: false,
|
||||||
|
plan: undefined,
|
||||||
};
|
};
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
type NodeProps,
|
type NodeProps,
|
||||||
Position,
|
Position,
|
||||||
type Node, useNodeConnections
|
type Node
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import {useEffect, useRef} from "react";
|
|
||||||
import {type EditorWarning} from "../components/EditorWarnings.tsx";
|
|
||||||
import { Toolbar } from '../components/NodeComponents';
|
import { Toolbar } from '../components/NodeComponents';
|
||||||
import styles from '../../VisProg.module.css';
|
import styles from '../../VisProg.module.css';
|
||||||
import {SingleConnectionHandle, MultiConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
import {SingleConnectionHandle, MultiConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
||||||
@@ -12,6 +10,11 @@ import {allowOnlyConnectionsFromType, noSelfConnections} from "../HandleRules.ts
|
|||||||
import { NodeReduces, NodesInPhase, NodeTypes} from '../NodeRegistry';
|
import { NodeReduces, NodesInPhase, NodeTypes} from '../NodeRegistry';
|
||||||
import useFlowStore from '../VisProgStores';
|
import useFlowStore from '../VisProgStores';
|
||||||
import { TextField } from '../../../../components/TextField';
|
import { TextField } from '../../../../components/TextField';
|
||||||
|
import PlanEditorDialog from '../components/PlanEditor.tsx';
|
||||||
|
import type { Plan } from '../components/Plan.tsx';
|
||||||
|
import { insertGoalInPlan } from '../components/PlanEditingFunctions.tsx';
|
||||||
|
import { defaultPlan } from '../components/Plan.default.ts';
|
||||||
|
import type { GoalNode } from './GoalNode.tsx';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default data dot a phase node
|
* The default data dot a phase node
|
||||||
@@ -28,6 +31,7 @@ export type PhaseNodeData = {
|
|||||||
hasReduce: boolean;
|
hasReduce: boolean;
|
||||||
nextPhaseId: string | "end" | null;
|
nextPhaseId: string | "end" | null;
|
||||||
isFirstPhase: boolean;
|
isFirstPhase: boolean;
|
||||||
|
plan?: Plan;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PhaseNode = Node<PhaseNodeData>
|
export type PhaseNode = Node<PhaseNodeData>
|
||||||
@@ -39,107 +43,10 @@ export type PhaseNode = Node<PhaseNodeData>
|
|||||||
*/
|
*/
|
||||||
export default function PhaseNode(props: NodeProps<PhaseNode>) {
|
export default function PhaseNode(props: NodeProps<PhaseNode>) {
|
||||||
const data = props.data;
|
const data = props.data;
|
||||||
const {updateNodeData, registerWarning, unregisterWarning} = useFlowStore();
|
const {updateNodeData} = useFlowStore();
|
||||||
const updateLabel = (value: string) => updateNodeData(props.id, {...data, label: value});
|
const updateLabel = (value: string) => updateNodeData(props.id, {...data, label: value});
|
||||||
const label_input_id = `phase_${props.id}_label_input`;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Toolbar nodeId={props.id} allowDelete={true}/>
|
<Toolbar nodeId={props.id} allowDelete={true}/>
|
||||||
@@ -153,17 +60,35 @@ export default function PhaseNode(props: NodeProps<PhaseNode>) {
|
|||||||
placeholder={"Phase ..."}
|
placeholder={"Phase ..."}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{(data.plan && data.plan.steps.length > 0) && (<div>
|
||||||
|
<PlanEditorDialog
|
||||||
|
plan={data.plan}
|
||||||
|
onSave={(plan) => {
|
||||||
|
updateNodeData(props.id, {
|
||||||
|
...data,
|
||||||
|
plan,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
description={props.data.label}
|
||||||
|
onlyEditPhasing={true}
|
||||||
|
/>
|
||||||
|
</div>)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<SingleConnectionHandle type="target" position={Position.Left} id="target" rules={[
|
<SingleConnectionHandle type="target" position={Position.Left} id="target" rules={[
|
||||||
noSelfConnections,
|
noSelfConnections,
|
||||||
allowOnlyConnectionsFromType(["phase", "start"]),
|
allowOnlyConnectionsFromType(["phase", "start"]),
|
||||||
]} title="Connect to a phase or the startNode"/>
|
]}/>
|
||||||
<MultiConnectionHandle type="target" position={Position.Bottom} id="data" rules={[
|
<MultiConnectionHandle type="target" position={Position.Bottom} id="data" rules={[
|
||||||
allowOnlyConnectionsFromType(["norm", "goal", "trigger"])
|
allowOnlyConnectionsFromType(["norm", "goal", "trigger"])
|
||||||
]} title="Connect to any number of norm, goal, and TriggerNode(-s)"/>
|
]}/>
|
||||||
<SingleConnectionHandle type="source" position={Position.Right} id="source" rules={[
|
<SingleConnectionHandle type="source" position={Position.Right} id="source" rules={[
|
||||||
noSelfConnections,
|
noSelfConnections,
|
||||||
allowOnlyConnectionsFromType(["phase", "end"]),
|
allowOnlyConnectionsFromType(["phase", "end"]),
|
||||||
]} title="Connect to a phase or the endNode"/>
|
]}/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -228,9 +153,9 @@ export const PhaseTooltip = `
|
|||||||
*/
|
*/
|
||||||
export function PhaseConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
export function PhaseConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
||||||
const data = _thisNode.data as PhaseNodeData
|
const data = _thisNode.data as PhaseNodeData
|
||||||
|
|
||||||
const nodes = useFlowStore.getState().nodes;
|
const nodes = useFlowStore.getState().nodes;
|
||||||
const sourceNode = nodes.find((node) => node.id === _sourceNodeId)!
|
const sourceNode = nodes.find((node) => node.id === _sourceNodeId)!
|
||||||
|
|
||||||
switch (sourceNode.type) {
|
switch (sourceNode.type) {
|
||||||
case "phase": break;
|
case "phase": break;
|
||||||
case "start": data.isFirstPhase = true; break;
|
case "start": data.isFirstPhase = true; break;
|
||||||
@@ -238,6 +163,18 @@ export function PhaseConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
|||||||
// endNodes cannot be the source of an outgoing connection
|
// endNodes cannot be the source of an outgoing connection
|
||||||
// so we don't need to cover them with a special case
|
// so we don't need to cover them with a special case
|
||||||
// before handling the default behavior
|
// before handling the default behavior
|
||||||
|
case "goal": {
|
||||||
|
// First, let's see if we have a plan currently. If not, let's create a default plan with this goal inside.:)
|
||||||
|
if (!data.plan) {
|
||||||
|
data.plan = insertGoalInPlan({...structuredClone(defaultPlan), id: crypto.randomUUID()} as Plan, sourceNode as GoalNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else, lets just insert this goal into our current plan.
|
||||||
|
else {
|
||||||
|
data.plan = insertGoalInPlan(structuredClone(data.plan), sourceNode as GoalNode)
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: data.children.push(_sourceNodeId); break;
|
default: data.children.push(_sourceNodeId); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
type NodeProps,
|
type NodeProps,
|
||||||
Position,
|
Position,
|
||||||
type Node, useNodeConnections
|
type Node,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import {useEffect} from "react";
|
|
||||||
import { Toolbar } from '../components/NodeComponents';
|
import { Toolbar } from '../components/NodeComponents';
|
||||||
import styles from '../../VisProg.module.css';
|
import styles from '../../VisProg.module.css';
|
||||||
import {SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
import {SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
||||||
import {type EditorWarning} from "../components/EditorWarnings.tsx";
|
|
||||||
import {allowOnlyConnectionsFromHandle} from "../HandleRules.ts";
|
import {allowOnlyConnectionsFromHandle} from "../HandleRules.ts";
|
||||||
import useFlowStore from "../VisProgStores.tsx";
|
|
||||||
|
|
||||||
|
|
||||||
export type StartNodeData = {
|
export type StartNodeData = {
|
||||||
@@ -28,27 +25,6 @@ export type StartNode = Node<StartNodeData>
|
|||||||
* @returns React.JSX.Element
|
* @returns React.JSX.Element
|
||||||
*/
|
*/
|
||||||
export default function StartNode(props: NodeProps<StartNode>) {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Toolbar nodeId={props.id} allowDelete={false}/>
|
<Toolbar nodeId={props.id} allowDelete={false}/>
|
||||||
@@ -58,7 +34,7 @@ export default function StartNode(props: NodeProps<StartNode>) {
|
|||||||
</div>
|
</div>
|
||||||
<SingleConnectionHandle type="source" position={Position.Right} id="source" rules={[
|
<SingleConnectionHandle type="source" position={Position.Right} id="source" rules={[
|
||||||
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"target"}])
|
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"target"}])
|
||||||
]} title="Connect to a phaseNode"/>
|
]}/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import {
|
import {
|
||||||
type NodeProps,
|
type NodeProps,
|
||||||
Position,
|
Position,
|
||||||
type Node, useNodeConnections
|
type Node,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import {useEffect} from "react";
|
|
||||||
import type {EditorWarning} from "../components/EditorWarnings.tsx";
|
|
||||||
import { Toolbar } from '../components/NodeComponents';
|
import { Toolbar } from '../components/NodeComponents';
|
||||||
import styles from '../../VisProg.module.css';
|
import styles from '../../VisProg.module.css';
|
||||||
import {MultiConnectionHandle, SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
import {MultiConnectionHandle, SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
||||||
import {allowOnlyConnectionsFromHandle, allowOnlyConnectionsFromType} from "../HandleRules.ts";
|
import {allowOnlyConnectionsFromHandle, allowOnlyConnectionsFromType} from "../HandleRules.ts";
|
||||||
import useFlowStore from '../VisProgStores';
|
import useFlowStore from '../VisProgStores';
|
||||||
import {PlanReduce, type Plan } from '../components/Plan';
|
import {PlanReduce, type Plan } from '../components/Plan';
|
||||||
import PlanEditorDialog from '../components/PlanEditor';
|
import PlanEditorDialog from '../components/PlanEditor';
|
||||||
import {BeliefGlobalReduce} from "./BeliefGlobals.ts";
|
import { BasicBeliefReduce } from './BasicBeliefNode';
|
||||||
import type { GoalNode } from './GoalNode.tsx';
|
import type { GoalNode } from './GoalNode.tsx';
|
||||||
import { defaultPlan } from '../components/Plan.default.ts';
|
import { defaultPlan } from '../components/Plan.default.ts';
|
||||||
import { deleteGoalInPlanByID, insertGoalInPlan } from '../components/PlanEditingFunctions.tsx';
|
import { deleteGoalInPlanByID, insertGoalInPlan } from '../components/PlanEditingFunctions.tsx';
|
||||||
@@ -47,79 +45,14 @@ export type TriggerNode = Node<TriggerNodeData>
|
|||||||
*/
|
*/
|
||||||
export default function TriggerNode(props: NodeProps<TriggerNode>) {
|
export default function TriggerNode(props: NodeProps<TriggerNode>) {
|
||||||
const data = props.data;
|
const data = props.data;
|
||||||
const {updateNodeData, registerWarning, unregisterWarning} = useFlowStore();
|
const {updateNodeData} = useFlowStore();
|
||||||
|
|
||||||
const setName= (value: string) => {
|
const setName= (value: string) => {
|
||||||
updateNodeData(props.id, {...data, name: value})
|
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 && outputCons.length !== 0){
|
|
||||||
registerWarning(noPlanWarning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
unregisterWarning(props.id, noPlanWarning.type);
|
|
||||||
},[data.plan, outputCons.length, props.id, registerWarning, unregisterWarning])
|
|
||||||
return <>
|
return <>
|
||||||
|
|
||||||
<Toolbar nodeId={props.id} allowDelete={true}/>
|
<Toolbar nodeId={props.id} allowDelete={true}/>
|
||||||
<div className={`${styles.defaultNode} ${styles.nodeTrigger} flex-col gap-sm`}>
|
<div className={`${styles.defaultNode} ${styles.nodeTrigger} flex-col gap-sm`}>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -132,16 +65,15 @@ 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>
|
<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={[
|
<MultiConnectionHandle type="source" position={Position.Right} id="TriggerSource" rules={[
|
||||||
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}]),
|
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}]),
|
||||||
]} title="Connect to any number of phaseNodes"/>
|
]}/>
|
||||||
<SingleConnectionHandle
|
<SingleConnectionHandle
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Bottom}
|
position={Position.Bottom}
|
||||||
id="TriggerBeliefs"
|
id="TriggerBeliefs"
|
||||||
style={{ left: '40%' }}
|
style={{ left: '40%' }}
|
||||||
rules={[
|
rules={[
|
||||||
allowOnlyConnectionsFromType(["basic_belief","inferred_belief"]),
|
allowOnlyConnectionsFromType(['basic_belief']),
|
||||||
]}
|
]}
|
||||||
title="Connect to a beliefNode or a set of beliefs combined using the AND/OR node"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MultiConnectionHandle
|
<MultiConnectionHandle
|
||||||
@@ -152,7 +84,6 @@ export default function TriggerNode(props: NodeProps<TriggerNode>) {
|
|||||||
rules={[
|
rules={[
|
||||||
allowOnlyConnectionsFromType(['goal']),
|
allowOnlyConnectionsFromType(['goal']),
|
||||||
]}
|
]}
|
||||||
title="Connect to any number of goalNodes"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PlanEditorDialog
|
<PlanEditorDialog
|
||||||
@@ -171,13 +102,13 @@ export default function TriggerNode(props: NodeProps<TriggerNode>) {
|
|||||||
/**
|
/**
|
||||||
* Reduces each Trigger, including its children down into its core data.
|
* Reduces each Trigger, including its children down into its core data.
|
||||||
* @param node - The Trigger node to reduce.
|
* @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.
|
* @returns A simplified object containing the node label and its list of triggers.
|
||||||
*/
|
*/
|
||||||
export function TriggerReduce(node: Node, nodes: Node[]) {
|
export function TriggerReduce(node: Node, nodes: Node[]) {
|
||||||
const data = node.data as TriggerNodeData;
|
const data = node.data as TriggerNodeData;
|
||||||
const conditionNode = data.condition ? nodes.find((n)=>n.id===data.condition) : undefined
|
const conditionNode = data.condition ? nodes.find((n)=>n.id===data.condition) : undefined
|
||||||
const conditionData = conditionNode ? BeliefGlobalReduce(conditionNode, nodes) : ""
|
const conditionData = conditionNode ? BasicBeliefReduce(conditionNode, nodes) : ""
|
||||||
return {
|
return {
|
||||||
id: node.id,
|
id: node.id,
|
||||||
name: node.data.name,
|
name: node.data.name,
|
||||||
@@ -205,7 +136,7 @@ export function TriggerConnectionTarget(_thisNode: Node, _sourceNodeId: string)
|
|||||||
const otherNode = nodes.find((x) => x.id === _sourceNodeId)
|
const otherNode = nodes.find((x) => x.id === _sourceNodeId)
|
||||||
if (!otherNode) return;
|
if (!otherNode) return;
|
||||||
|
|
||||||
if (['basic_belief', 'inferred_belief'].includes(otherNode.type!)) {
|
if (otherNode.type === 'basic_belief' /* TODO: Add the option for an inferred belief */) {
|
||||||
data.condition = _sourceNodeId;
|
data.condition = _sourceNodeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +172,7 @@ export function TriggerDisconnectionTarget(_thisNode: Node, _sourceNodeId: strin
|
|||||||
const data = _thisNode.data as TriggerNodeData;
|
const data = _thisNode.data as TriggerNodeData;
|
||||||
// remove if the target of disconnection was our condition
|
// remove if the target of disconnection was our condition
|
||||||
if (_sourceNodeId == data.condition) data.condition = undefined
|
if (_sourceNodeId == data.condition) data.condition = undefined
|
||||||
|
|
||||||
data.plan = deleteGoalInPlanByID(structuredClone(data.plan) as Plan, _sourceNodeId)
|
data.plan = deleteGoalInPlanByID(structuredClone(data.plan) as Plan, _sourceNodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
83
test/pages/simpleProgram/SimpleProgram.tsx
Normal file
83
test/pages/simpleProgram/SimpleProgram.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { render, screen, fireEvent } from "@testing-library/react";
|
||||||
|
import SimpleProgram from "../../../src/pages/SimpleProgram/SimpleProgram";
|
||||||
|
import useProgramStore from "../../../src/utils/programStore";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to preload the program store before rendering.
|
||||||
|
*/
|
||||||
|
function loadProgram(phases: Record<string, unknown>[]) {
|
||||||
|
useProgramStore.getState().setProgramState({ phases });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("SimpleProgram", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
loadProgram([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows empty state when no program is loaded", () => {
|
||||||
|
render(<SimpleProgram />);
|
||||||
|
expect(screen.getByText("No program loaded.")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders first phase content", () => {
|
||||||
|
loadProgram([
|
||||||
|
{
|
||||||
|
id: "phase-1",
|
||||||
|
norms: [{ id: "n1", norm: "Be polite" }],
|
||||||
|
goals: [{ id: "g1", description: "Finish task", achieved: true }],
|
||||||
|
triggers: [{ id: "t1", label: "Keyword trigger" }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
render(<SimpleProgram />);
|
||||||
|
|
||||||
|
expect(screen.getByText("Phase 1 / 1")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Be polite")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Finish task")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Keyword trigger")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("allows navigating between phases", () => {
|
||||||
|
loadProgram([
|
||||||
|
{
|
||||||
|
id: "phase-1",
|
||||||
|
norms: [],
|
||||||
|
goals: [],
|
||||||
|
triggers: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "phase-2",
|
||||||
|
norms: [{ id: "n2", norm: "Be careful" }],
|
||||||
|
goals: [],
|
||||||
|
triggers: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
render(<SimpleProgram />);
|
||||||
|
|
||||||
|
expect(screen.getByText("Phase 1 / 2")).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Next ▶"));
|
||||||
|
|
||||||
|
expect(screen.getByText("Phase 2 / 2")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Be careful")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("prev button is disabled on first phase", () => {
|
||||||
|
loadProgram([
|
||||||
|
{ id: "phase-1", norms: [], goals: [], triggers: [] },
|
||||||
|
]);
|
||||||
|
|
||||||
|
render(<SimpleProgram />);
|
||||||
|
expect(screen.getByText("◀ Prev")).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("next button is disabled on last phase", () => {
|
||||||
|
loadProgram([
|
||||||
|
{ id: "phase-1", norms: [], goals: [], triggers: [] },
|
||||||
|
]);
|
||||||
|
|
||||||
|
render(<SimpleProgram />);
|
||||||
|
expect(screen.getByText("Next ▶")).toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -34,17 +34,10 @@ describe("UndoRedo Middleware", () => {
|
|||||||
type: 'default',
|
type: 'default',
|
||||||
position: {x: 0, y: 0},
|
position: {x: 0, y: 0},
|
||||||
data: {label: 'A'}
|
data: {label: 'A'}
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
edges: [],
|
edges: []
|
||||||
warnings: {
|
|
||||||
warningRegistry: new Map(),
|
|
||||||
severityIndex: new Map()
|
|
||||||
}
|
|
||||||
}],
|
}],
|
||||||
ruleRegistry: new Map(),
|
|
||||||
editorWarningRegistry: new Map(),
|
|
||||||
severityIndex: new Map()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -60,11 +53,7 @@ describe("UndoRedo Middleware", () => {
|
|||||||
position: {x: 0, y: 0},
|
position: {x: 0, y: 0},
|
||||||
data: {label: 'A'}
|
data: {label: 'A'}
|
||||||
}],
|
}],
|
||||||
edges: [],
|
edges: []
|
||||||
warnings: {
|
|
||||||
warningRegistry: {},
|
|
||||||
severityIndex: {}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
expect(state.future).toEqual([]);
|
expect(state.future).toEqual([]);
|
||||||
});
|
});
|
||||||
@@ -91,9 +80,7 @@ describe("UndoRedo Middleware", () => {
|
|||||||
position: {x: 0, y: 0},
|
position: {x: 0, y: 0},
|
||||||
data: {label: 'A'}
|
data: {label: 'A'}
|
||||||
}],
|
}],
|
||||||
edges: [],
|
edges: []
|
||||||
editorWarningRegistry: new Map(),
|
|
||||||
severityIndex: new Map()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -127,11 +114,7 @@ describe("UndoRedo Middleware", () => {
|
|||||||
position: {x: 0, y: 0},
|
position: {x: 0, y: 0},
|
||||||
data: {label: 'B'}
|
data: {label: 'B'}
|
||||||
}],
|
}],
|
||||||
edges: [],
|
edges: []
|
||||||
warnings: {
|
|
||||||
warningRegistry: {},
|
|
||||||
severityIndex: {}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -157,9 +140,7 @@ describe("UndoRedo Middleware", () => {
|
|||||||
position: {x: 0, y: 0},
|
position: {x: 0, y: 0},
|
||||||
data: {label: 'A'}
|
data: {label: 'A'}
|
||||||
}],
|
}],
|
||||||
edges: [],
|
edges: []
|
||||||
editorWarningRegistry: new Map(),
|
|
||||||
severityIndex: new Map()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -195,11 +176,7 @@ describe("UndoRedo Middleware", () => {
|
|||||||
position: {x: 0, y: 0},
|
position: {x: 0, y: 0},
|
||||||
data: {label: 'A'}
|
data: {label: 'A'}
|
||||||
}],
|
}],
|
||||||
edges: [],
|
edges: []
|
||||||
warnings: {
|
|
||||||
warningRegistry: {},
|
|
||||||
severityIndex: {}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -222,9 +199,7 @@ describe("UndoRedo Middleware", () => {
|
|||||||
position: {x: 0, y: 0},
|
position: {x: 0, y: 0},
|
||||||
data: {label: 'A'}
|
data: {label: 'A'}
|
||||||
}],
|
}],
|
||||||
edges: [],
|
edges: []
|
||||||
editorWarningRegistry: new Map(),
|
|
||||||
severityIndex: new Map()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => { store.getState().beginBatchAction(); });
|
act(() => { store.getState().beginBatchAction(); });
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import {act} from '@testing-library/react';
|
import {act} from '@testing-library/react';
|
||||||
import {
|
import type {Connection, Edge, Node} from "@xyflow/react";
|
||||||
type Connection,
|
|
||||||
type Edge,
|
|
||||||
type Node,
|
|
||||||
} from "@xyflow/react";
|
|
||||||
import type {HandleRule, RuleResult} from "../../../../src/pages/VisProgPage/visualProgrammingUI/HandleRuleLogic.ts";
|
import type {HandleRule, RuleResult} from "../../../../src/pages/VisProgPage/visualProgrammingUI/HandleRuleLogic.ts";
|
||||||
import { NodeDisconnections } from "../../../../src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts";
|
import { NodeDisconnections } from "../../../../src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts";
|
||||||
import type {PhaseNodeData} from "../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx";
|
import type {PhaseNodeData} from "../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx";
|
||||||
@@ -402,7 +398,6 @@ describe('FlowStore Functionality', () => {
|
|||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
act(()=> {
|
act(()=> {
|
||||||
deleteNode(nodeId);
|
deleteNode(nodeId);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
import { describe, it, expect} from '@jest/globals';
|
|
||||||
import {
|
|
||||||
type EditorWarning, warningSummary
|
|
||||||
} from "../../../../../src/pages/VisProgPage/visualProgrammingUI/components/EditorWarnings.tsx";
|
|
||||||
import useFlowStore from "../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx";
|
|
||||||
|
|
||||||
|
|
||||||
function makeWarning(
|
|
||||||
overrides?: Partial<EditorWarning>
|
|
||||||
): EditorWarning {
|
|
||||||
return {
|
|
||||||
scope: { id: 'node-1' },
|
|
||||||
type: 'MISSING_INPUT',
|
|
||||||
severity: 'ERROR',
|
|
||||||
description: 'Missing input',
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("editorWarnings", () => {
|
|
||||||
describe('registerWarning', () => {
|
|
||||||
it('registers a node-level warning', () => {
|
|
||||||
const warning = makeWarning();
|
|
||||||
const {registerWarning, getWarnings} = useFlowStore.getState()
|
|
||||||
registerWarning(warning);
|
|
||||||
|
|
||||||
const warnings = getWarnings();
|
|
||||||
expect(warnings).toHaveLength(1);
|
|
||||||
expect(warnings[0]).toEqual(warning);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('registers a handle-level warning with scoped key', () => {
|
|
||||||
const warning = makeWarning({
|
|
||||||
scope: { id: 'node-1', handleId: 'input-1' },
|
|
||||||
});
|
|
||||||
const {registerWarning} = useFlowStore.getState()
|
|
||||||
registerWarning(warning);
|
|
||||||
const nodeWarnings = useFlowStore.getState().editorWarningRegistry.get('node-1');
|
|
||||||
expect(nodeWarnings?.has('MISSING_INPUT:input-1') === true).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates severityIndex correctly', () => {
|
|
||||||
const {registerWarning, severityIndex} = useFlowStore.getState()
|
|
||||||
registerWarning(makeWarning());
|
|
||||||
expect(severityIndex.get('ERROR')!.size).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getWarningsBySeverity', () => {
|
|
||||||
it('returns only warnings of requested severity', () => {
|
|
||||||
const {registerWarning, getWarningsBySeverity} = useFlowStore.getState()
|
|
||||||
registerWarning(
|
|
||||||
makeWarning({ severity: 'ERROR' })
|
|
||||||
);
|
|
||||||
|
|
||||||
registerWarning(
|
|
||||||
makeWarning({
|
|
||||||
severity: 'WARNING',
|
|
||||||
type: 'MISSING_OUTPUT',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const errors = getWarningsBySeverity('ERROR');
|
|
||||||
const warnings = getWarningsBySeverity('WARNING');
|
|
||||||
|
|
||||||
expect(errors).toHaveLength(1);
|
|
||||||
expect(warnings).toHaveLength(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('isProgramValid', () => {
|
|
||||||
it('returns true when no ERROR warnings exist', () => {
|
|
||||||
expect(useFlowStore.getState().isProgramValid()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns false when ERROR warnings exist', () => {
|
|
||||||
const {registerWarning, isProgramValid} = useFlowStore.getState()
|
|
||||||
registerWarning(makeWarning());
|
|
||||||
expect(isProgramValid()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('unregisterWarning', () => {
|
|
||||||
it('removes warning from registry and severityIndex', () => {
|
|
||||||
const warning = makeWarning();
|
|
||||||
const {
|
|
||||||
registerWarning,
|
|
||||||
getWarnings,
|
|
||||||
unregisterWarning,
|
|
||||||
severityIndex
|
|
||||||
} = useFlowStore.getState()
|
|
||||||
|
|
||||||
registerWarning(warning);
|
|
||||||
|
|
||||||
unregisterWarning('node-1', 'MISSING_INPUT');
|
|
||||||
|
|
||||||
expect(getWarnings()).toHaveLength(0);
|
|
||||||
expect(severityIndex.get('ERROR')!.size).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does nothing if warning does not exist', () => {
|
|
||||||
expect(() =>
|
|
||||||
useFlowStore.getState().unregisterWarning('node-1', 'DOES_NOT_EXIST')
|
|
||||||
).not.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('unregisterWarningsForId', () => {
|
|
||||||
it('removes all warnings for a node', () => {
|
|
||||||
const {registerWarning, unregisterWarningsForId, getWarnings, severityIndex} = useFlowStore.getState()
|
|
||||||
registerWarning(
|
|
||||||
makeWarning({
|
|
||||||
scope: { id: 'node-1', handleId: 'h1' },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
registerWarning(
|
|
||||||
makeWarning({
|
|
||||||
scope: { id: 'node-1' },
|
|
||||||
type: 'MISSING_OUTPUT',
|
|
||||||
severity: 'WARNING',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
unregisterWarningsForId('node-1');
|
|
||||||
|
|
||||||
expect(getWarnings()).toHaveLength(0);
|
|
||||||
expect(
|
|
||||||
severityIndex.get('ERROR')!.size
|
|
||||||
).toBe(0);
|
|
||||||
expect(
|
|
||||||
severityIndex.get('WARNING')!.size
|
|
||||||
).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('warningSummary', () => {
|
|
||||||
it('returns correct counts and validity', () => {
|
|
||||||
const {registerWarning} = useFlowStore.getState()
|
|
||||||
registerWarning(
|
|
||||||
makeWarning({ severity: 'ERROR' })
|
|
||||||
);
|
|
||||||
|
|
||||||
const summary = warningSummary();
|
|
||||||
|
|
||||||
expect(summary.error).toBe(1);
|
|
||||||
expect(summary.warning).toBe(0);
|
|
||||||
expect(summary.info).toBe(0);
|
|
||||||
expect(summary.isValid).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
import {fireEvent, render, screen} from '@testing-library/react';
|
|
||||||
import '@testing-library/jest-dom';
|
|
||||||
import {useReactFlow, useStoreApi} from "@xyflow/react";
|
|
||||||
import {
|
|
||||||
type EditorWarning,
|
|
||||||
globalWarning
|
|
||||||
} from "../../../../../src/pages/VisProgPage/visualProgrammingUI/components/EditorWarnings.tsx";
|
|
||||||
import {WarningsSidebar} from "../../../../../src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx";
|
|
||||||
import useFlowStore from "../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx";
|
|
||||||
|
|
||||||
|
|
||||||
jest.mock('@xyflow/react', () => ({
|
|
||||||
useReactFlow: jest.fn(),
|
|
||||||
useStoreApi: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx');
|
|
||||||
|
|
||||||
function makeWarning(
|
|
||||||
overrides?: Partial<EditorWarning>
|
|
||||||
): EditorWarning {
|
|
||||||
return {
|
|
||||||
scope: { id: 'node-1' },
|
|
||||||
type: 'MISSING_INPUT',
|
|
||||||
severity: 'ERROR',
|
|
||||||
description: 'Missing input',
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('WarningsSidebar', () => {
|
|
||||||
let getStateSpy: jest.SpyInstance;
|
|
||||||
|
|
||||||
const setCenter = jest.fn(() => Promise.resolve());
|
|
||||||
const getNode = jest.fn();
|
|
||||||
const addSelectedNodes = jest.fn();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
|
|
||||||
// React Flow hooks
|
|
||||||
(useReactFlow as jest.Mock).mockReturnValue({
|
|
||||||
getNode,
|
|
||||||
setCenter,
|
|
||||||
});
|
|
||||||
(useStoreApi as jest.Mock).mockReturnValue({
|
|
||||||
getState: () => ({ addSelectedNodes }),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use spyOn to override store
|
|
||||||
const mockWarnings = [
|
|
||||||
makeWarning({ description: 'Node warning', scope: { id: 'node-1' } }),
|
|
||||||
makeWarning({
|
|
||||||
description: 'Global warning',
|
|
||||||
scope: { id: globalWarning },
|
|
||||||
type: 'INCOMPLETE_PROGRAM',
|
|
||||||
severity: 'WARNING',
|
|
||||||
}),
|
|
||||||
makeWarning({
|
|
||||||
description: 'Info warning',
|
|
||||||
scope: { id: 'node-2' },
|
|
||||||
severity: 'INFO',
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
getStateSpy = jest
|
|
||||||
.spyOn(useFlowStore, 'getState')
|
|
||||||
.mockReturnValue({
|
|
||||||
getWarnings: () => mockWarnings,
|
|
||||||
} as any);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
getStateSpy.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders warnings header', () => {
|
|
||||||
render(<WarningsSidebar />);
|
|
||||||
expect(screen.getByText('Warnings')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders all warning descriptions', () => {
|
|
||||||
render(<WarningsSidebar />);
|
|
||||||
expect(screen.getByText('Node warning')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Global warning')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Info warning')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('splits global and other warnings correctly', () => {
|
|
||||||
render(<WarningsSidebar />);
|
|
||||||
expect(screen.getByText('global:')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('other:')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows empty state when no warnings exist', () => {
|
|
||||||
getStateSpy.mockReturnValueOnce({
|
|
||||||
getWarnings: () => [],
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
render(<WarningsSidebar />);
|
|
||||||
expect(screen.getByText('No warnings!')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('filters by severity', () => {
|
|
||||||
render(<WarningsSidebar />);
|
|
||||||
fireEvent.click(screen.getByText('ERROR'));
|
|
||||||
|
|
||||||
expect(screen.getByText('Node warning')).toBeInTheDocument();
|
|
||||||
expect(screen.queryByText('Global warning')).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByText('Info warning')).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('filters INFO severity correctly', () => {
|
|
||||||
render(<WarningsSidebar />);
|
|
||||||
fireEvent.click(screen.getByText('INFO'));
|
|
||||||
|
|
||||||
expect(screen.getByText('Info warning')).toBeInTheDocument();
|
|
||||||
expect(screen.queryByText('Node warning')).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByText('Global warning')).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('clicking global warning does NOT jump', () => {
|
|
||||||
render(<WarningsSidebar />);
|
|
||||||
fireEvent.click(screen.getByText('Global warning'));
|
|
||||||
|
|
||||||
expect(setCenter).not.toHaveBeenCalled();
|
|
||||||
expect(addSelectedNodes).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does nothing if node does not exist', () => {
|
|
||||||
getNode.mockReturnValue(undefined);
|
|
||||||
|
|
||||||
render(<WarningsSidebar />);
|
|
||||||
fireEvent.click(screen.getByText('Node warning'));
|
|
||||||
|
|
||||||
expect(setCenter).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
|
|
||||||
import {type Connection, getOutgoers, type Node} from '@xyflow/react';
|
|
||||||
import {ruleResult} from "../../../../../src/pages/VisProgPage/visualProgrammingUI/HandleRuleLogic.ts";
|
|
||||||
import {BasicBeliefReduce} from "../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx";
|
|
||||||
import {
|
|
||||||
BeliefGlobalReduce, noBeliefCycles,
|
|
||||||
noMatchingLeftRightBelief
|
|
||||||
} from "../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/BeliefGlobals.ts";
|
|
||||||
import { InferredBeliefReduce } from "../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/InferredBeliefNode.tsx";
|
|
||||||
import useFlowStore from "../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx";
|
|
||||||
import * as BasicModule from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode';
|
|
||||||
import * as InferredModule from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/InferredBeliefNode.tsx';
|
|
||||||
import * as FlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx';
|
|
||||||
|
|
||||||
|
|
||||||
describe('BeliefGlobalReduce', () => {
|
|
||||||
const nodes: Node[] = [];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('delegates to BasicBeliefReduce for basic_belief nodes', () => {
|
|
||||||
const spy = jest
|
|
||||||
.spyOn(BasicModule, 'BasicBeliefReduce')
|
|
||||||
.mockReturnValue('basic-result' as any);
|
|
||||||
|
|
||||||
const node = { id: '1', type: 'basic_belief' } as Node;
|
|
||||||
|
|
||||||
const result = BeliefGlobalReduce(node, nodes);
|
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledWith(node, nodes);
|
|
||||||
expect(result).toBe('basic-result');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('delegates to InferredBeliefReduce for inferred_belief nodes', () => {
|
|
||||||
const spy = jest
|
|
||||||
.spyOn(InferredModule, 'InferredBeliefReduce')
|
|
||||||
.mockReturnValue('inferred-result' as any);
|
|
||||||
|
|
||||||
const node = { id: '2', type: 'inferred_belief' } as Node;
|
|
||||||
|
|
||||||
const result = BeliefGlobalReduce(node, nodes);
|
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledWith(node, nodes);
|
|
||||||
expect(result).toBe('inferred-result');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns undefined for unknown node types', () => {
|
|
||||||
const node = { id: '3', type: 'other' } as Node;
|
|
||||||
|
|
||||||
const result = BeliefGlobalReduce(node, nodes);
|
|
||||||
|
|
||||||
expect(result).toBeUndefined();
|
|
||||||
expect(BasicBeliefReduce).not.toHaveBeenCalled();
|
|
||||||
expect(InferredBeliefReduce).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('noMatchingLeftRightBelief rule', () => {
|
|
||||||
let getStateSpy: ReturnType<typeof jest.spyOn>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
getStateSpy = jest.spyOn(FlowStore.default, 'getState');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('is satisfied when target node is not an inferred belief', () => {
|
|
||||||
getStateSpy.mockReturnValue({
|
|
||||||
nodes: [{ id: 't1', type: 'basic_belief' }],
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const result = noMatchingLeftRightBelief(
|
|
||||||
{ source: 's1', target: 't1' } as Connection,
|
|
||||||
null as any
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toBe(ruleResult.satisfied);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('is satisfied when inferred belief has no matching left/right', () => {
|
|
||||||
getStateSpy.mockReturnValue({
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
id: 't1',
|
|
||||||
type: 'inferred_belief',
|
|
||||||
data: {
|
|
||||||
inferredBelief: {
|
|
||||||
left: 'a',
|
|
||||||
right: 'b',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const result = noMatchingLeftRightBelief(
|
|
||||||
{ source: 'c', target: 't1' } as Connection,
|
|
||||||
null as any
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toBe(ruleResult.satisfied);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('is NOT satisfied when source matches left input', () => {
|
|
||||||
getStateSpy.mockReturnValue({
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
id: 't1',
|
|
||||||
type: 'inferred_belief',
|
|
||||||
data: {
|
|
||||||
inferredBelief: {
|
|
||||||
left: 's1',
|
|
||||||
right: 's2',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const result = noMatchingLeftRightBelief(
|
|
||||||
{ source: 's1', target: 't1' } as Connection,
|
|
||||||
null as any
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.isSatisfied).toBe(false);
|
|
||||||
if (!(result.isSatisfied)) {
|
|
||||||
expect(result.message).toContain(
|
|
||||||
'Connecting one belief to both input handles of an inferred belief node is not allowed'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('is NOT satisfied when source matches right input', () => {
|
|
||||||
getStateSpy.mockReturnValue({
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
id: 't1',
|
|
||||||
type: 'inferred_belief',
|
|
||||||
data: {
|
|
||||||
inferredBelief: {
|
|
||||||
left: 's1',
|
|
||||||
right: 's2',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const result = noMatchingLeftRightBelief(
|
|
||||||
{ source: 's2', target: 't1' } as Connection,
|
|
||||||
null as any
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.isSatisfied).toBe(false);
|
|
||||||
if (!(result.isSatisfied)) {
|
|
||||||
expect(result.message).toContain(
|
|
||||||
'Connecting one belief to both input handles of an inferred belief node is not allowed'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
jest.mock('@xyflow/react', () => ({
|
|
||||||
getOutgoers: jest.fn(),
|
|
||||||
getConnectedEdges: jest.fn(), // include if some tests require it
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('noBeliefCycles rule', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns notSatisfied when source === target', () => {
|
|
||||||
const result = noBeliefCycles({ source: 'n1', target: 'n1' } as any, null as any);
|
|
||||||
expect(result.isSatisfied).toBe(false);
|
|
||||||
if (!(result.isSatisfied)) {
|
|
||||||
expect(result.message).toContain('Cyclical connection exists');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns satisfied when there are no outgoing inferred beliefs', () => {
|
|
||||||
jest.spyOn(useFlowStore, 'getState').mockReturnValue({
|
|
||||||
nodes: [{ id: 'n1', type: 'inferred_belief' }],
|
|
||||||
edges: [],
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
(getOutgoers as jest.Mock).mockReturnValue([]);
|
|
||||||
|
|
||||||
const result = noBeliefCycles({ source: 'n1', target: 'n2' } as any, null as any);
|
|
||||||
expect(result).toBe(ruleResult.satisfied);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns notSatisfied for direct cycle', () => {
|
|
||||||
jest.spyOn(useFlowStore, 'getState').mockReturnValue({
|
|
||||||
nodes: [
|
|
||||||
{ id: 'n1', type: 'inferred_belief' },
|
|
||||||
{ id: 'n2', type: 'inferred_belief' },
|
|
||||||
],
|
|
||||||
edges: [{ source: 'n2', target: 'n1' }],
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
// @ts-expect-error is acting up
|
|
||||||
(getOutgoers as jest.Mock).mockImplementation(({ id }) => {
|
|
||||||
if (id === 'n2') return [{ id: 'n1', type: 'inferred_belief' }];
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = noBeliefCycles({ source: 'n1', target: 'n2' } as any, null as any);
|
|
||||||
expect(result.isSatisfied).toBe(false);
|
|
||||||
if (!(result.isSatisfied)) {
|
|
||||||
expect(result.message).toContain('Cyclical connection exists');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns notSatisfied for indirect cycle', () => {
|
|
||||||
jest.spyOn(useFlowStore, 'getState').mockReturnValue({
|
|
||||||
nodes: [
|
|
||||||
{ id: 'A', type: 'inferred_belief' },
|
|
||||||
{ id: 'B', type: 'inferred_belief' },
|
|
||||||
{ id: 'C', type: 'inferred_belief' },
|
|
||||||
],
|
|
||||||
edges: [
|
|
||||||
{ source: 'A', target: 'B' },
|
|
||||||
{ source: 'B', target: 'C' },
|
|
||||||
{ source: 'C', target: 'A' },
|
|
||||||
],
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
// @ts-expect-error is acting up
|
|
||||||
(getOutgoers as jest.Mock).mockImplementation(({ id }) => {
|
|
||||||
const mapping: Record<string, any[]> = {
|
|
||||||
A: [{ id: 'B', type: 'inferred_belief' }],
|
|
||||||
B: [{ id: 'C', type: 'inferred_belief' }],
|
|
||||||
C: [{ id: 'A', type: 'inferred_belief' }],
|
|
||||||
};
|
|
||||||
return mapping[id] || [];
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = noBeliefCycles({ source: 'A', target: 'B' } as any, null as any);
|
|
||||||
expect(result.isSatisfied).toBe(false);
|
|
||||||
if (!(result.isSatisfied)) {
|
|
||||||
expect(result.message).toContain('Cyclical connection exists');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns satisfied when no cycle exists in a multi-node graph', () => {
|
|
||||||
jest.spyOn(useFlowStore, 'getState').mockReturnValue({
|
|
||||||
nodes: [
|
|
||||||
{ id: 'A', type: 'inferred_belief' },
|
|
||||||
{ id: 'B', type: 'inferred_belief' },
|
|
||||||
{ id: 'C', type: 'inferred_belief' },
|
|
||||||
],
|
|
||||||
edges: [
|
|
||||||
{ source: 'A', target: 'B' },
|
|
||||||
{ source: 'B', target: 'C' },
|
|
||||||
],
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
// @ts-expect-error is acting up
|
|
||||||
(getOutgoers as jest.Mock).mockImplementation(({ id }) => {
|
|
||||||
const mapping: Record<string, any[]> = {
|
|
||||||
A: [{ id: 'B', type: 'inferred_belief' }],
|
|
||||||
B: [{ id: 'C', type: 'inferred_belief' }],
|
|
||||||
C: [],
|
|
||||||
};
|
|
||||||
return mapping[id] || [];
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = noBeliefCycles({ source: 'A', target: 'B' } as any, null as any);
|
|
||||||
expect(result).toBe(ruleResult.satisfied);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -3,7 +3,7 @@ import { describe, it, beforeEach } from '@jest/globals';
|
|||||||
import { screen, waitFor } from '@testing-library/react';
|
import { screen, waitFor } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { renderWithProviders } from '../.././/./../../test-utils/test-utils';
|
import { renderWithProviders } from '../.././/./../../test-utils/test-utils';
|
||||||
import BasicBeliefNode, { type BasicBeliefNodeData } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx';
|
import BasicBeliefNode, { type BasicBeliefNodeData } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode';
|
||||||
import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores';
|
import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores';
|
||||||
import type { Node } from '@xyflow/react';
|
import type { Node } from '@xyflow/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
|
|
||||||
import type {Node, Edge} from '@xyflow/react';
|
|
||||||
import * as FlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx';
|
|
||||||
import {
|
|
||||||
type InferredBelief,
|
|
||||||
InferredBeliefConnectionTarget,
|
|
||||||
InferredBeliefDisconnectionTarget,
|
|
||||||
InferredBeliefReduce,
|
|
||||||
} from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/InferredBeliefNode.tsx';
|
|
||||||
|
|
||||||
// helper functions
|
|
||||||
function inferredNode(overrides = {}): Node {
|
|
||||||
return {
|
|
||||||
id: 'i1',
|
|
||||||
type: 'inferred_belief',
|
|
||||||
position: {x: 0, y: 0},
|
|
||||||
data: {
|
|
||||||
inferredBelief: {
|
|
||||||
left: undefined,
|
|
||||||
operator: true,
|
|
||||||
right: undefined,
|
|
||||||
},
|
|
||||||
...overrides,
|
|
||||||
},
|
|
||||||
} as Node;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('InferredBelief connection logic', () => {
|
|
||||||
let getStateSpy: ReturnType<typeof jest.spyOn>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
getStateSpy = jest.spyOn(FlowStore.default, 'getState');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets left belief when connected on beliefLeft handle', () => {
|
|
||||||
const node = inferredNode();
|
|
||||||
|
|
||||||
getStateSpy.mockReturnValue({
|
|
||||||
nodes: [{ id: 'b1', type: 'basic_belief' }],
|
|
||||||
edges: [
|
|
||||||
{
|
|
||||||
source: 'b1',
|
|
||||||
target: 'i1',
|
|
||||||
targetHandle: 'beliefLeft',
|
|
||||||
} as Edge,
|
|
||||||
],
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
InferredBeliefConnectionTarget(node, 'b1');
|
|
||||||
|
|
||||||
expect((node.data.inferredBelief as InferredBelief).left).toBe('b1');
|
|
||||||
expect((node.data.inferredBelief as InferredBelief).right).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets right belief when connected on beliefRight handle', () => {
|
|
||||||
const node = inferredNode();
|
|
||||||
|
|
||||||
getStateSpy.mockReturnValue({
|
|
||||||
nodes: [{ id: 'b2', type: 'basic_belief' }],
|
|
||||||
edges: [
|
|
||||||
{
|
|
||||||
source: 'b2',
|
|
||||||
target: 'i1',
|
|
||||||
targetHandle: 'beliefRight',
|
|
||||||
} as Edge,
|
|
||||||
],
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
InferredBeliefConnectionTarget(node, 'b2');
|
|
||||||
|
|
||||||
expect((node.data.inferredBelief as InferredBelief).right).toBe('b2');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('ignores connections from unsupported node types', () => {
|
|
||||||
const node = inferredNode();
|
|
||||||
|
|
||||||
getStateSpy.mockReturnValue({
|
|
||||||
nodes: [{ id: 'x', type: 'norm' }],
|
|
||||||
edges: [],
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
InferredBeliefConnectionTarget(node, 'x');
|
|
||||||
|
|
||||||
expect((node.data.inferredBelief as InferredBelief).left).toBeUndefined();
|
|
||||||
expect((node.data.inferredBelief as InferredBelief).right).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('clears left or right belief on disconnection', () => {
|
|
||||||
const node = inferredNode({
|
|
||||||
inferredBelief: { left: 'a', right: 'b', operator: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
InferredBeliefDisconnectionTarget(node, 'a');
|
|
||||||
expect((node.data.inferredBelief as InferredBelief).left).toBeUndefined();
|
|
||||||
|
|
||||||
InferredBeliefDisconnectionTarget(node, 'b');
|
|
||||||
expect((node.data.inferredBelief as InferredBelief).right).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('InferredBeliefReduce', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws if left belief is missing', () => {
|
|
||||||
const node = inferredNode({
|
|
||||||
inferredBelief: { left: 'l', right: 'r', operator: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(() =>
|
|
||||||
InferredBeliefReduce(node, [{ id: 'r' } as Node])
|
|
||||||
).toThrow('No Left belief found');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws if right belief is missing', () => {
|
|
||||||
const node = inferredNode({
|
|
||||||
inferredBelief: { left: 'l', right: 'r', operator: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(() =>
|
|
||||||
InferredBeliefReduce(node, [{ id: 'l' } as Node])
|
|
||||||
).toThrow('No Right Belief found');
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import { cleanup } from '@testing-library/react';
|
import { cleanup } from '@testing-library/react';
|
||||||
import {
|
|
||||||
type CompositeWarningKey,
|
|
||||||
type SeverityIndex,
|
|
||||||
} from "../src/pages/VisProgPage/visualProgrammingUI/components/EditorWarnings.tsx";
|
|
||||||
import useFlowStore from '../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx';
|
import useFlowStore from '../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx';
|
||||||
|
|
||||||
if (!globalThis.structuredClone) {
|
if (!globalThis.structuredClone) {
|
||||||
@@ -73,6 +69,8 @@ export const mockReactFlow = () => {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
useFlowStore.setState({
|
useFlowStore.setState({
|
||||||
nodes: [],
|
nodes: [],
|
||||||
@@ -81,13 +79,7 @@ beforeAll(() => {
|
|||||||
future: [],
|
future: [],
|
||||||
isBatchAction: false,
|
isBatchAction: false,
|
||||||
edgeReconnectSuccessful: true,
|
edgeReconnectSuccessful: true,
|
||||||
ruleRegistry: new Map(),
|
ruleRegistry: new Map()
|
||||||
editorWarningRegistry: new Map(),
|
|
||||||
severityIndex: new Map([
|
|
||||||
['INFO', new Set<CompositeWarningKey>()],
|
|
||||||
['WARNING', new Set<CompositeWarningKey>()],
|
|
||||||
['ERROR', new Set<CompositeWarningKey>()],
|
|
||||||
]) as SeverityIndex,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -100,13 +92,7 @@ afterEach(() => {
|
|||||||
future: [],
|
future: [],
|
||||||
isBatchAction: false,
|
isBatchAction: false,
|
||||||
edgeReconnectSuccessful: true,
|
edgeReconnectSuccessful: true,
|
||||||
ruleRegistry: new Map(),
|
ruleRegistry: new Map()
|
||||||
editorWarningRegistry: new Map(),
|
|
||||||
severityIndex: new Map([
|
|
||||||
['INFO', new Set<CompositeWarningKey>()],
|
|
||||||
['WARNING', new Set<CompositeWarningKey>()],
|
|
||||||
['ERROR', new Set<CompositeWarningKey>()],
|
|
||||||
]) as SeverityIndex,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
import { render, type RenderOptions } from '@testing-library/react';
|
import { render, type RenderOptions } from '@testing-library/react';
|
||||||
import { type ReactElement, type ReactNode } from 'react';
|
import { type ReactElement, type ReactNode } from 'react';
|
||||||
import { ReactFlowProvider } from '@xyflow/react';
|
import { ReactFlowProvider } from '@xyflow/react';
|
||||||
import {mockReactFlow} from "../setupFlowTests.ts";
|
|
||||||
|
|
||||||
mockReactFlow();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom render function that wraps components with necessary providers
|
* Custom render function that wraps components with necessary providers
|
||||||
|
|||||||
Reference in New Issue
Block a user