17 Commits

Author SHA1 Message Date
Björn Otgaar
26894cc2d2 Merge branch 'dev' into chore/clean-visprog-code 2026-01-28 12:27:27 +01:00
Björn Otgaar
dcf4f01e68 chore: replace plan editor functionality into own folder with own files. 2026-01-28 12:26:57 +01:00
Gerla, J. (Justin)
c84088dd9d Merge branch 'chore/editing-css-strings' into 'dev'
chore: updated css comments

See merge request ics/sp/2025/n25b/pepperplus-ui!51
2026-01-28 11:19:16 +00:00
JGerla
dfe793e04a chore: updated css comments 2026-01-28 12:07:33 +01:00
Björn Otgaar
1876138fe2 Merge branch 'dev' into chore/clean-visprog-code 2026-01-28 11:36:21 +01:00
Björn Otgaar
5930e24bf4 chore: intitial commit, starting seperating concerns for plan editor. 2026-01-28 11:35:34 +01:00
Gerla, J. (Justin)
9e06bf079b Merge branch 'chore/adding-uu-strings' into 'dev'
chore: added copyright strings and removed template page

See merge request ics/sp/2025/n25b/pepperplus-ui!50
2026-01-28 10:34:36 +00:00
Gerla, J. (Justin)
eb5a6cddd7 chore: added copyright strings and removed template page 2026-01-28 10:34:36 +00:00
Björn Otgaar
60f7bad5d1 Merge branch 'feat/experiment-logs' into 'dev'
Add experiment logs to the monitoring page

See merge request ics/sp/2025/n25b/pepperplus-ui!48
2026-01-28 10:15:59 +00:00
Twirre
835de03a29 Add experiment logs to the monitoring page 2026-01-28 10:15:58 +00:00
Pim Hutting
78901ee25b Merge branch 'temp_screenshot_manual' into 'dev'
feat: The Big One UI

See merge request ics/sp/2025/n25b/pepperplus-ui!47
2026-01-28 08:27:30 +00:00
Gerla, J. (Justin)
82785dc8cb feat: The Big One UI 2026-01-28 08:27:30 +00:00
Twirre
f9e0eb95f8 Merge branch 'feat/add-inferred-belief-node' into 'dev'
feat: added an inferred belief node to the editor

See merge request ics/sp/2025/n25b/pepperplus-ui!42
2026-01-23 12:57:35 +00:00
Gerla, J. (Justin)
47c5e94b8f feat: added an inferred belief node to the editor 2026-01-23 12:57:34 +00:00
Pim Hutting
b17d1e7618 Merge branch 'fix/correct-capitalization' into 'dev'
chore: fix the capitalization of 3 characters to make sure they match. :)

See merge request ics/sp/2025/n25b/pepperplus-ui!46
2026-01-23 10:42:36 +00:00
Björn Otgaar
ec211ccbc3 chore: fix the capitalization of 3 characters to make sure they match. :) 2026-01-23 11:25:30 +01:00
Gerla, J. (Justin)
6f4471ce6f Merge branch 'demo' into 'dev'
feat: merged demo into dev

See merge request ics/sp/2025/n25b/pepperplus-ui!43
2026-01-20 11:10:58 +00:00
132 changed files with 2421 additions and 986 deletions

499
package-lock.json generated
View File

@@ -14,6 +14,7 @@
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router": "^7.9.3",
"reactflow": "^11.11.4",
"zustand": "^5.0.8"
},
"devDependencies": {
@@ -2054,6 +2055,276 @@
"url": "https://opencollective.com/pkgr"
}
},
"node_modules/@reactflow/background": {
"version": "11.3.14",
"resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz",
"integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==",
"license": "MIT",
"dependencies": {
"@reactflow/core": "11.11.4",
"classcat": "^5.0.3",
"zustand": "^4.4.1"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@reactflow/background/node_modules/zustand": {
"version": "4.5.7",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@reactflow/controls": {
"version": "11.2.14",
"resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz",
"integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==",
"license": "MIT",
"dependencies": {
"@reactflow/core": "11.11.4",
"classcat": "^5.0.3",
"zustand": "^4.4.1"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@reactflow/controls/node_modules/zustand": {
"version": "4.5.7",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@reactflow/core": {
"version": "11.11.4",
"resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz",
"integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==",
"license": "MIT",
"dependencies": {
"@types/d3": "^7.4.0",
"@types/d3-drag": "^3.0.1",
"@types/d3-selection": "^3.0.3",
"@types/d3-zoom": "^3.0.1",
"classcat": "^5.0.3",
"d3-drag": "^3.0.0",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0",
"zustand": "^4.4.1"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@reactflow/core/node_modules/zustand": {
"version": "4.5.7",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@reactflow/minimap": {
"version": "11.7.14",
"resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz",
"integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==",
"license": "MIT",
"dependencies": {
"@reactflow/core": "11.11.4",
"@types/d3-selection": "^3.0.3",
"@types/d3-zoom": "^3.0.1",
"classcat": "^5.0.3",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0",
"zustand": "^4.4.1"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@reactflow/minimap/node_modules/zustand": {
"version": "4.5.7",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@reactflow/node-resizer": {
"version": "2.2.14",
"resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz",
"integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==",
"license": "MIT",
"dependencies": {
"@reactflow/core": "11.11.4",
"classcat": "^5.0.4",
"d3-drag": "^3.0.0",
"d3-selection": "^3.0.0",
"zustand": "^4.4.1"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@reactflow/node-resizer/node_modules/zustand": {
"version": "4.5.7",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@reactflow/node-toolbar": {
"version": "1.3.14",
"resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz",
"integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==",
"license": "MIT",
"dependencies": {
"@reactflow/core": "11.11.4",
"classcat": "^5.0.3",
"zustand": "^4.4.1"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@reactflow/node-toolbar/node_modules/zustand": {
"version": "4.5.7",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.35",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.35.tgz",
@@ -2648,12 +2919,102 @@
"@babel/types": "^7.28.2"
}
},
"node_modules/@types/d3": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
"integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
"license": "MIT",
"dependencies": {
"@types/d3-array": "*",
"@types/d3-axis": "*",
"@types/d3-brush": "*",
"@types/d3-chord": "*",
"@types/d3-color": "*",
"@types/d3-contour": "*",
"@types/d3-delaunay": "*",
"@types/d3-dispatch": "*",
"@types/d3-drag": "*",
"@types/d3-dsv": "*",
"@types/d3-ease": "*",
"@types/d3-fetch": "*",
"@types/d3-force": "*",
"@types/d3-format": "*",
"@types/d3-geo": "*",
"@types/d3-hierarchy": "*",
"@types/d3-interpolate": "*",
"@types/d3-path": "*",
"@types/d3-polygon": "*",
"@types/d3-quadtree": "*",
"@types/d3-random": "*",
"@types/d3-scale": "*",
"@types/d3-scale-chromatic": "*",
"@types/d3-selection": "*",
"@types/d3-shape": "*",
"@types/d3-time": "*",
"@types/d3-time-format": "*",
"@types/d3-timer": "*",
"@types/d3-transition": "*",
"@types/d3-zoom": "*"
}
},
"node_modules/@types/d3-array": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
"license": "MIT"
},
"node_modules/@types/d3-axis": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
"integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
"license": "MIT",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-brush": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
"integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
"license": "MIT",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-chord": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
"integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
"license": "MIT"
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"license": "MIT"
},
"node_modules/@types/d3-contour": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
"integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
"license": "MIT",
"dependencies": {
"@types/d3-array": "*",
"@types/geojson": "*"
}
},
"node_modules/@types/d3-delaunay": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
"integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
"license": "MIT"
},
"node_modules/@types/d3-dispatch": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
"integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
"license": "MIT"
},
"node_modules/@types/d3-drag": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
@@ -2663,6 +3024,54 @@
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-dsv": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
"integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
"license": "MIT"
},
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
"license": "MIT"
},
"node_modules/@types/d3-fetch": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
"integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
"license": "MIT",
"dependencies": {
"@types/d3-dsv": "*"
}
},
"node_modules/@types/d3-force": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
"integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
"license": "MIT"
},
"node_modules/@types/d3-format": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
"integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
"license": "MIT"
},
"node_modules/@types/d3-geo": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
"integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/d3-hierarchy": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
"integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
"license": "MIT"
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
@@ -2672,12 +3081,78 @@
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
"license": "MIT"
},
"node_modules/@types/d3-polygon": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
"integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
"license": "MIT"
},
"node_modules/@types/d3-quadtree": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
"integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
"license": "MIT"
},
"node_modules/@types/d3-random": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
"integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
"license": "MIT"
},
"node_modules/@types/d3-scale": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
"license": "MIT",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-scale-chromatic": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
"integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
"license": "MIT"
},
"node_modules/@types/d3-selection": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
"license": "MIT"
},
"node_modules/@types/d3-shape": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
"integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
"license": "MIT",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
"license": "MIT"
},
"node_modules/@types/d3-time-format": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
"integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
"license": "MIT"
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
"node_modules/@types/d3-transition": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
@@ -2704,6 +3179,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/geojson": {
"version": "7946.0.16",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"license": "MIT"
},
"node_modules/@types/hast": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
@@ -6976,6 +7457,24 @@
}
}
},
"node_modules/reactflow": {
"version": "11.11.4",
"resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz",
"integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==",
"license": "MIT",
"dependencies": {
"@reactflow/background": "11.3.14",
"@reactflow/controls": "11.2.14",
"@reactflow/core": "11.11.4",
"@reactflow/minimap": "11.7.14",
"@reactflow/node-resizer": "2.2.14",
"@reactflow/node-toolbar": "1.3.14"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",

View File

@@ -18,6 +18,7 @@
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router": "^7.9.3",
"reactflow": "^11.11.4",
"zustand": "^5.0.8"
},
"devDependencies": {

View File

@@ -1,3 +1,8 @@
{/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/}
.logopepper {
height: 8em;
padding: 1.5em;

View File

@@ -1,6 +1,8 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { Routes, Route, Link } from 'react-router'
import './App.css'
import TemplatePage from './pages/TemplatePage/Template.tsx'
import Home from './pages/Home/Home.tsx'
import Robot from './pages/Robot/Robot.tsx';
import ConnectedRobots from './pages/ConnectedRobots/ConnectedRobots.tsx'
@@ -15,6 +17,7 @@ function App(){
return (
<>
<header>
<span>© Utrecht University (ICS)</span>
<Link to={"/"}>Home</Link>
<button onClick={() => setShowLogs(!showLogs)}>Developer Logs</button>
</header>
@@ -22,7 +25,6 @@ function App(){
<main className={"flex-col align-center flex-1 scroll-y"}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/template" element={<TemplatePage />} />
<Route path="/editor" element={<VisProg />} />
<Route path="/robot" element={<Robot />} />
<Route path="/ConnectedRobots" element={<ConnectedRobots />} />

View File

@@ -0,0 +1,8 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
export default function Next({ fill }: { fill?: string }) {
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
<path d="M664.07-224.93v-510.14h91v510.14h-91Zm-459.14 0v-510.14L587.65-480 204.93-224.93Z"/>
</svg>;
}

View File

@@ -0,0 +1,8 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
export default function Pause({ fill }: { fill?: string }) {
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
<path d="M556.17-185.41v-589.18h182v589.18h-182Zm-334.34 0v-589.18h182v589.18h-182Z"/>
</svg>;
}

View File

@@ -0,0 +1,8 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
export default function Play({ fill }: { fill?: string }) {
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
<path d="M311.87-185.41v-589.18L775.07-480l-463.2 294.59Z"/>
</svg>;
}

View File

@@ -0,0 +1,8 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
export default function Redo({ fill }: { fill?: string }) {
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
<path d="M390.98-191.87q-98.44 0-168.77-65.27-70.34-65.27-70.34-161.43 0-96.15 70.34-161.54 70.33-65.39 168.77-65.39h244.11l-98.98-98.98 63.65-63.65L808.13-600 599.76-391.87l-63.65-63.65 98.98-98.98H390.98q-60.13 0-104.12 38.92-43.99 38.93-43.99 96.78 0 57.84 43.99 96.89 43.99 39.04 104.12 39.04h286.15v91H390.98Z"/>
</svg>;
}

View File

@@ -0,0 +1,8 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
export default function Replay({ fill }: { fill?: string }) {
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
<path d="M480.05-70.43q-76.72 0-143.78-29.1-67.05-29.1-116.75-78.8-49.69-49.69-78.79-116.75-29.1-67.05-29.1-143.49h91q0 115.81 80.73 196.47Q364.1-161.43 480-161.43q115.8 0 196.47-80.74 80.66-80.73 80.66-196.63 0-115.81-80.73-196.47-80.74-80.66-196.64-80.66h-6.24l60.09 60.08-58.63 60.63-166.22-166.21 166.22-166.22 58.63 60.87-59.85 59.85h6q76.74 0 143.76 29.09 67.02 29.1 116.84 78.8 49.81 49.69 78.91 116.64 29.1 66.95 29.1 143.61 0 76.66-29.1 143.71-29.1 67.06-78.79 116.75-49.7 49.7-116.7 78.8-67.01 29.1-143.73 29.1Z"/>
</svg>;
}

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import type {Cell} from "../../utils/cellStore.ts";
import type {LogRecord} from "./useLogs.ts";

View File

@@ -1,3 +1,8 @@
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/
.filter-root {
position: relative;
display: flex;

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {useEffect, useRef, useState} from "react";
import type {LogFilterPredicate} from "./useLogs.ts";

View File

@@ -1,3 +1,8 @@
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/
.logging-container {
box-sizing: border-box;

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {type ComponentType, useEffect, useRef, useState} from "react";
import formatDuration from "../../utils/formatDuration.ts";
import {type LogFilterPredicate, type LogRecord, useLogs} from "./useLogs.ts";

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {useCallback, useEffect, useRef, useState} from "react";
import {applyPriorityPredicates, type PriorityFilterPredicate} from "../../utils/priorityFiltering.ts";

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { useEffect, useRef, useState } from "react";
import styles from "./TextField.module.css";

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {useEffect, useRef} from "react";
/**

View File

@@ -1,3 +1,8 @@
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/
.text-field {
border: 1px solid transparent;
border-radius: 5pt;

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {useEffect, useState} from "react";
import styles from "./TextField.module.css";

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { useState } from 'react'
/**

View File

@@ -1,3 +1,14 @@
{/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/}
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router'

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { useEffect, useState } from 'react'
/**

View File

@@ -1,3 +1,8 @@
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/
.read_the_docs {
color: #888;
}

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { Link } from 'react-router'
import pepperLogo from '../../assets/pepper_transp2_small.svg'
import styles from './Home.module.css'
@@ -21,7 +24,6 @@ function Home() {
<div className={styles.links}>
<Link to={"/robot"}>Robot Interaction </Link>
<Link to={"/editor"}>Editor </Link>
<Link to={"/template"}>Template </Link>
<Link to={"/ConnectedRobots"}>Connected Robots </Link>
</div>
</div>

View File

@@ -1,3 +1,8 @@
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/
.dashboardContainer {
display: grid;
grid-template-columns: 2fr 1fr; /* Left = content, Right = logs */
@@ -28,6 +33,22 @@
position: static; /* ensures it scrolls away */
}
.controlsButtons {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: .25rem;
max-width: 260px;
flex-wrap: wrap;
button {
display: flex;
justify-content: center;
align-items: center;
}
}
.phaseProgress {
margin-top: 0.5rem;
}
@@ -77,11 +98,6 @@
color: white;
}
.restartPhase{
background-color: rgb(255, 123, 0);
color: white;
}
.restartExperiment{
background-color: red;
color: white;

View File

@@ -1,4 +1,7 @@
import React, { useCallback, useRef, useState } from 'react';
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import React, { useCallback, useState } from 'react';
import styles from './MonitoringPage.module.css';
// Store & API
@@ -15,7 +18,7 @@ import {
type CondNormsStateUpdate,
type PhaseUpdate
} from "./MonitoringPageAPI";
import { graphReducer, runProgramm } from '../VisProgPage/VisProgLogic.ts';
import { graphReducer, runProgram } from '../VisProgPage/VisProgLogic.ts';
// Types
import type { NormNodeData } from '../VisProgPage/visualProgrammingUI/nodes/NormNode';
@@ -53,16 +56,12 @@ function useExperimentLogic() {
const [phaseIndex, setPhaseIndex] = useState(0);
const [isFinished, setIsFinished] = useState(false);
// Ref to suppress stream updates during the "Reset Phase" fast-forward sequence
const suppressUpdates = useRef(false);
const phaseIds = getPhaseIds();
const phaseNames = getPhaseNames();
// --- Stream Handlers ---
const handleStreamUpdate = useCallback((data: ExperimentStreamData) => {
if (suppressUpdates.current) return;
if (data.type === 'phase_update' && data.id) {
const payload = data as PhaseUpdate;
console.log(`${data.type} received, id : ${data.id}`);
@@ -106,7 +105,6 @@ function useExperimentLogic() {
}, [getPhaseIds, getGoalsInPhase, phaseIds, phaseIndex, phaseNames]);
const handleStatusUpdate = useCallback((data: unknown) => {
if (suppressUpdates.current) return;
const payload = data as CondNormsStateUpdate;
if (payload.type !== 'cond_norms_state_update') return;
@@ -137,7 +135,7 @@ function useExperimentLogic() {
setGoalIndex(0);
setIsFinished(false);
await runProgramm();
await runProgram();
console.log("Experiment & UI successfully reset.");
} catch (err) {
console.error("Failed to reset program:", err);
@@ -146,7 +144,7 @@ function useExperimentLogic() {
}
}, [setProgramState]);
const handleControlAction = async (action: "pause" | "play" | "nextPhase" | "resetPhase") => {
const handleControlAction = async (action: "pause" | "play" | "nextPhase") => {
try {
setLoading(true);
switch (action) {
@@ -161,30 +159,6 @@ function useExperimentLogic() {
case "nextPhase":
await nextPhase();
break;
case "resetPhase":
//make sure you don't see the phases pass to arrive back at current phase
suppressUpdates.current = true;
const targetIndex = phaseIndex;
console.log(`Resetting phase: Restarting and skipping to index ${targetIndex}`);
const phases = graphReducer();
setProgramState({ phases });
setActiveIds({});
setPhaseIndex(0); // Visually reset to start
setGoalIndex(0);
setIsFinished(false);
// Restart backend
await runProgramm();
for (let i = 0; i < targetIndex; i++) {
console.log(`Skipping phase ${i}...`);
await nextPhase();
}
suppressUpdates.current = false;
setPhaseIndex(targetIndex);
setIsPlaying(true); //Maybe you pause and then reset
break;
}
} catch (err) {
console.error(err);
@@ -252,7 +226,7 @@ function ControlPanel({
}: {
loading: boolean,
isPlaying: boolean,
onAction: (a: "pause" | "play" | "nextPhase" | "resetPhase") => void,
onAction: (a: "pause" | "play" | "nextPhase") => void,
onReset: () => void
}) {
return (
@@ -277,12 +251,6 @@ function ControlPanel({
disabled={loading}
></button>
<button
className={styles.restartPhase}
onClick={() => onAction("resetPhase")}
disabled={loading}
></button>
<button
className={styles.restartExperiment}
onClick={onReset}

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import React, { useEffect } from 'react';
const API_BASE = "http://localhost:8000";
@@ -32,16 +35,6 @@ export async function nextPhase(): Promise<void> {
}
/**
* 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 pause experiment
*/

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import React, { useEffect, useState } from 'react';
import styles from './MonitoringPage.module.css';
import { sendAPICall } from './MonitoringPageAPI';

View File

@@ -1,3 +1,8 @@
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/
.logs {
/* grid-area used in MonitoringPage.module.css */
grid-area: logs;

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import styles from "./ExperimentLogs.module.css";
import {LogMessages} from "../../../components/Logging/Logging.tsx";
import {useEffect, useMemo, useState} from "react";

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { useState, useEffect, useRef } from 'react'
/**

View File

@@ -1,167 +0,0 @@
/* ---------- 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;
}

View File

@@ -1,192 +0,0 @@
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;

View File

@@ -1,11 +0,0 @@
import Counter from '../../components/components.tsx'
function TemplatePage() {
return (
<>
<Counter />
</>
)
}
export default TemplatePage

View File

@@ -1,3 +1,8 @@
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/
/* editor UI */
.inner-editor-container {

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {
Background,
Controls,
@@ -7,20 +10,20 @@ import {
MarkerType, getOutgoers
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import warningStyles from './visualProgrammingUI/components/WarningSidebar.module.css'
import {type CSSProperties, useEffect, useState} from "react";
import {useShallow} from 'zustand/react/shallow';
import useProgramStore from "../../utils/programStore.ts";
import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx';
import {type EditorWarning, globalWarning} from "./visualProgrammingUI/components/EditorWarnings.tsx";
import {WarningsSidebar} from "./visualProgrammingUI/components/WarningSidebar.tsx";
import type {PhaseNode} from "./visualProgrammingUI/nodes/PhaseNode.tsx";
import useFlowStore from './visualProgrammingUI/VisProgStores.tsx';
import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx';
import styles from './VisProg.module.css'
import {NodeTypes} from './visualProgrammingUI/NodeRegistry.ts';
import SaveLoadPanel from './visualProgrammingUI/components/SaveLoadPanel.tsx';
import MonitoringPage from '../MonitoringPage/MonitoringPage.tsx';
import { graphReducer, runProgramm } from './VisProgLogic.ts';
import {graphReducer, runProgram} from './VisProgLogic.ts';
// --| config starting params for flow |--
@@ -45,6 +48,7 @@ const selector = (state: FlowState) => ({
nodes: state.nodes,
edges: state.edges,
onNodesChange: state.onNodesChange,
onNodesDelete: state.onNodesDelete,
onEdgesDelete: state.onEdgesDelete,
onEdgesChange: state.onEdgesChange,
onConnect: state.onConnect,
@@ -70,6 +74,7 @@ const VisProgUI = () => {
const {
nodes, edges,
onNodesChange,
onNodesDelete,
onEdgesDelete,
onEdgesChange,
onConnect,
@@ -94,7 +99,7 @@ const VisProgUI = () => {
});
const {unregisterWarning, registerWarning} = useFlowStore();
useEffect(() => {
if (checkPhaseChain()) {
unregisterWarning(globalWarning,'INCOMPLETE_PROGRAM');
} else {
@@ -113,8 +118,6 @@ const VisProgUI = () => {
}
},[edges, registerWarning, unregisterWarning])
return (
<div className={`${styles.innerEditorContainer} round-lg border-lg flex-row`} style={({'--flow-zoom': zoom} as CSSProperties)}>
<ReactFlow
@@ -123,6 +126,7 @@ const VisProgUI = () => {
defaultEdgeOptions={DEFAULT_EDGE_OPTIONS}
nodeTypes={NodeTypes}
onNodesChange={onNodesChange}
onNodesDelete={onNodesDelete}
onEdgesDelete={onEdgesDelete}
onEdgesChange={onEdgesChange}
onReconnect={onReconnect}
@@ -133,6 +137,7 @@ const VisProgUI = () => {
onNodeDragStop={endBatchAction}
preventScrolling={scrollable}
onMove={(_, viewport) => setZoom(viewport.zoom)}
reconnectRadius={15}
snapToGrid
fitView
proOptions={{hideAttribution: true}}
@@ -145,14 +150,16 @@ const VisProgUI = () => {
<SaveLoadPanel></SaveLoadPanel>
</Panel>
<Panel position="bottom-center">
<button onClick={() => undo()}>undo</button>
<button onClick={() => undo()}>Undo</button>
<button onClick={() => redo()}>Redo</button>
</Panel>
<Panel position="center-right" className={warningStyles.warningsSidebar}>
<WarningsSidebar/>
</Panel>
<Controls/>
<Background/>
</ReactFlow>
<WarningsSidebar/>
</div>
);
};
@@ -184,14 +191,12 @@ const checkPhaseChain = (): boolean => {
const next = outgoingPhases.map(node => checkForCompleteChain(node.id))
.find(result => result);
console.log(next);
return !!next;
}
return checkForCompleteChain('start');
};
/**
* houses the entire page, so also UI elements
* that are not a part of the Visual Programming UI
@@ -199,39 +204,43 @@ const checkPhaseChain = (): boolean => {
*/
function VisProgPage() {
const [showSimpleProgram, setShowSimpleProgram] = useState(false);
const [programValidity, setProgramValidity] = useState<boolean>(true);
const {isProgramValid, severityIndex} = useFlowStore();
const setProgramState = useProgramStore((state) => state.setProgramState);
const runProgram = () => {
const validity = () => {return isProgramValid();}
useEffect(() => {
setProgramValidity(validity);
// the following eslint disable is required as it wants us to use all possible dependencies for the useEffect statement,
// however this would cause unneeded updates
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [severityIndex]);
const processProgram = () => {
const phases = graphReducer(); // reduce graph
setProgramState({ phases }); // <-- save to store
setShowSimpleProgram(true); // show SimpleProgram
runProgramm(); // send to backend if needed
runProgram(); // send to backend if needed
};
if (showSimpleProgram) {
return (
<div>
<button className={styles.backButton} onClick={() => setShowSimpleProgram(false)}>
Back to Editor
Back to Editor
</button>
<MonitoringPage/>
</div>
);
}
const [programValidity, setProgramValidity] = useState<boolean>(true);
const {isProgramValid, severityIndex} = useFlowStore();
useEffect(() => {
setProgramValidity(isProgramValid);
// 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]);
return (
<>
<VisualProgrammingUI/>
<button onClick={runProgram} disabled={!programValidity}>run program</button>
<button onClick={processProgram} disabled={!programValidity}>Run Program</button>
</>
)
}

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import useProgramStore from "../../utils/programStore";
import orderPhaseNodeArray from "../../utils/orderPhaseNodes";
import useFlowStore from './visualProgrammingUI/VisProgStores';
@@ -20,7 +23,7 @@ export function graphReducer() {
/**
* Outputs the prepared program to the console and sends it to the backend
*/
export function runProgramm() {
export function runProgram() {
const phases = graphReducer();
const program = {phases}
console.log(JSON.stringify(program, null, 2));

View File

@@ -1,10 +1,21 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import type {Edge, Node} from "@xyflow/react";
import type {StateCreator, StoreApi } from 'zustand/vanilla';
import type {
SeverityIndex,
WarningRegistry
} from "./components/EditorWarnings.tsx";
import type {FlowState} from "./VisProgTypes.tsx";
export type FlowSnapshot = {
nodes: Node[];
edges: Edge[];
warnings: {
warningRegistry: WarningRegistry;
severityIndex: SeverityIndex;
}
}
/**
@@ -41,7 +52,11 @@ export const UndoRedo = (
*/
const getSnapshot = (state : BaseFlowState) : FlowSnapshot => (structuredClone({
nodes: state.nodes,
edges: state.edges
edges: state.edges,
warnings: {
warningRegistry: state.editorWarningRegistry,
severityIndex: state.severityIndex,
}
}));
const initialState = config(set, get, api);
@@ -78,6 +93,8 @@ export const UndoRedo = (
set({
nodes: snapshot.nodes,
edges: snapshot.edges,
editorWarningRegistry: snapshot.warnings.warningRegistry,
severityIndex: snapshot.warnings.severityIndex,
});
state.future.push(currentSnapshot); // push current to redo
@@ -97,6 +114,8 @@ export const UndoRedo = (
set({
nodes: snapshot.nodes,
edges: snapshot.edges,
editorWarningRegistry: snapshot.warnings.warningRegistry,
severityIndex: snapshot.warnings.severityIndex,
});
state.past.push(currentSnapshot); // push current to undo

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {type Connection} from "@xyflow/react";
import {useEffect} from "react";
import useFlowStore from "./VisProgStores.tsx";

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {
type HandleRule,
ruleResult

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import EndNode, {
EndConnectionTarget,
EndConnectionSource,

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { create } from 'zustand';
import {
applyNodeChanges,
@@ -45,19 +48,18 @@ function createNode(id: string, type: string, position: XYPosition, data: Record
}
}
//* Initial nodes, created by using createNode. */
// Start and End don't need to apply the UUID, since they are technically never compiled into a program.
const startNode = createNode('start', 'start', {x: 110, y: 100}, {label: "Start"}, false)
const endNode = createNode('end', 'end', {x: 590, y: 100}, {label: "End"}, false)
const initialPhaseNode = createNode(crypto.randomUUID(), 'phase', {x:235, y:100}, {label: "Phase 1", children : [], isFirstPhase: false, nextPhaseId: null})
//* Initial nodes, created by using createNode. */
// Start and End don't need to apply the UUID, since they are technically never compiled into a program.
const startNode = createNode('start', 'start', {x: 110, y: 100}, {label: "Start"}, false)
const endNode = createNode('end', 'end', {x: 590, y: 100}, {label: "End"}, false)
const initialPhaseNode = createNode(crypto.randomUUID(), 'phase', {x:235, y:100}, {label: "Phase 1", children : [], isFirstPhase: false, nextPhaseId: null})
const initialNodes : Node[] = [startNode, endNode, initialPhaseNode];
const initialNodes : Node[] = [startNode, endNode, initialPhaseNode];
// Initial edges, leave empty as setting initial edges...
// ...breaks logic that is dependent on connection events
const initialEdges: Edge[] = [];
/**
* useFlowStore contains the implementation for all editor functionality
* and stores the current state of the visual programming editor
@@ -88,7 +90,9 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
*/
onNodesChange: (changes) => set({nodes: applyNodeChanges(changes, get().nodes)}),
onNodesDelete: (nodes) => nodes.forEach(node => get().unregisterNodeRules(node.id)),
onNodesDelete: (nodes) => nodes.forEach((_node) => {
return;
}),
onEdgesDelete: (edges) => {
// we make sure any affected nodes get updated to reflect removal of edges
@@ -218,19 +222,32 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
* Deletes a node by ID, respecting NodeDeletes rules.
* Also removes all edges connected to that node.
*/
deleteNode: (nodeId) => {
deleteNode: (nodeId, deleteElements) => {
get().pushSnapshot();
// Let's find our node to check if they have a special deletion function
const ourNode = get().nodes.find((n)=>n.id==nodeId);
const ourFunction = Object.entries(NodeDeletes).find(([t])=>t==ourNode?.type)?.[1]
// If there's no function, OR, our function tells us we can delete it, let's do so...
if (ourFunction == undefined || ourFunction()) {
set({
nodes: get().nodes.filter((n) => n.id !== nodeId),
edges: get().edges.filter((e) => e.source !== nodeId && e.target !== nodeId),
})}
if (deleteElements){
deleteElements({
nodes: get().nodes.filter((n) => n.id === nodeId),
edges: get().edges.filter((e) => e.source !== nodeId && e.target === nodeId)}
).then(() => {
get().unregisterNodeRules(nodeId);
get().unregisterWarningsForId(nodeId);
});
} else {
set({
nodes: get().nodes.filter((n) => n.id !== nodeId),
edges: get().edges.filter((e) => e.source !== nodeId && e.target !== nodeId),
})
}
}
},
/**

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
// VisProgTypes.ts
import type {
Edge,
@@ -7,7 +10,7 @@ import type {
OnReconnect,
Node,
OnEdgesDelete,
OnNodesDelete
OnNodesDelete, DeleteElementsOptions
} from '@xyflow/react';
import type {EditorWarningRegistry} from "./components/EditorWarnings.tsx";
import type {HandleRule} from "./HandleRuleLogic.ts";
@@ -69,7 +72,10 @@ export type FlowState = {
* Deletes a node and any connected edges.
* @param nodeId - the ID of the node to delete
*/
deleteNode: (nodeId: string) => void;
deleteNode: (nodeId: string, deleteElements?: (params: DeleteElementsOptions) => Promise<{
deletedNodes: Node[]
deletedEdges: Edge[]
}>) => void;
/**
* Replaces the current nodes array in the store.

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { useDraggable } from '@neodrag/react';
import { useReactFlow, type XYPosition } from '@xyflow/react';
import { type ReactNode, useCallback, useRef, useState } from 'react';

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
/* contains all logic for the VisProgEditor warning system
*
* Missing but desirable features:
@@ -19,6 +22,7 @@ export type WarningType =
| 'MISSING_OUTPUT'
| 'PLAN_IS_UNDEFINED'
| 'INCOMPLETE_PROGRAM'
| 'NOT_CONNECTED_TO_PROGRAM'
| string
export type WarningSeverity =
@@ -26,6 +30,9 @@ export type WarningSeverity =
| '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;
@@ -60,11 +67,26 @@ type ZustandSet = (partial: Partial<FlowState> | ((state: FlowState) => Partial<
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[];
/**
@@ -87,13 +109,17 @@ export type EditorWarningRegistry = {
/**
* unregisters warnings from the warningRegistry and the SeverityIndex
* @param {EditorWarning} warning
* @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 {
@@ -105,8 +131,8 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor
]),
getWarningsBySeverity: (warningSeverity) => {
const wRegistry = get().editorWarningRegistry;
const sIndex = get().severityIndex;
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[] = [];
@@ -137,9 +163,8 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor
const { scope: {id, handleId}, type, severity } = warning;
const warningKey = handleId ? `${type}:${handleId}` : type;
const compositeKey = `${id}|${warningKey}`;
const wRegistry = structuredClone(get().editorWarningRegistry);
const sIndex = structuredClone(get().severityIndex);
console.log("register")
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());
@@ -159,9 +184,8 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor
},
unregisterWarning: (id, warningKey) => {
const wRegistry = structuredClone(get().editorWarningRegistry);
const sIndex = structuredClone(get().severityIndex);
console.log("unregister")
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;
@@ -180,14 +204,17 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor
},
unregisterWarningsForId: (id) => {
const wRegistry = structuredClone(get().editorWarningRegistry);
const sIndex = structuredClone(get().severityIndex);
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, warningKey) => {
nodeWarnings.forEach((warning) => {
const warningKey = warning.scope.handleId
? `${warning.type}:${warning.scope.handleId}`
: warning.type;
sIndex.get(warning.severity)?.delete(`${id}|${warningKey}`);
});
}
@@ -203,7 +230,11 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor
}}
// returns a summary of the warningRegistry
/**
* returns a summary of the warningRegistry
* @returns {{info: number, warning: number, error: number, isValid: boolean}}
*/
export function warningSummary() {
const {severityIndex, isProgramValid} = useFlowStore.getState();
return {

View File

@@ -1,4 +1,7 @@
import {NodeToolbar} from '@xyflow/react';
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {NodeToolbar, useReactFlow} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import {type JSX, useState} from "react";
import {createPortal} from "react-dom";
@@ -30,10 +33,11 @@ type ToolbarProps = {
*/
export function Toolbar({nodeId, allowDelete}: ToolbarProps) {
const {nodes, deleteNode} = useFlowStore();
const { deleteElements } = useReactFlow();
const deleteParentNode = () => {
deleteNode(nodeId);
deleteNode(nodeId, deleteElements);
};
const nodeType = nodes.find((node) => node.id === nodeId)?.type as keyof typeof NodeTooltips;

View File

@@ -1,7 +0,0 @@
import type { Plan, PlanElement } from "./Plan";
export const defaultPlan: Plan = {
name: "Default Plan",
id: "-1",
steps: [] as PlanElement[],
}

View File

@@ -1,250 +0,0 @@
import {useRef, useState} from "react";
import useFlowStore from "../VisProgStores.tsx";
import styles from './PlanEditor.module.css';
import { GetActionValue, type Action, type ActionTypes, type Plan } from "../components/Plan";
import { defaultPlan } from "../components/Plan.default";
import { TextField } from "../../../../components/TextField";
import GestureValueEditor from "./GestureValueEditor";
type PlanEditorDialogProps = {
plan?: Plan;
onSave: (plan: Plan | undefined) => void;
description? : string;
};
export default function PlanEditorDialog({
plan,
onSave,
description,
}: PlanEditorDialogProps) {
// UseStates and references
const dialogRef = useRef<HTMLDialogElement | null>(null);
const [draftPlan, setDraftPlan] = useState<Plan | null>(null);
const [newActionType, setNewActionType] = useState<ActionTypes>("speech");
const [newActionGestureType, setNewActionGestureType] = useState<boolean>(true);
const [newActionValue, setNewActionValue] = useState("");
const [hasInteractedWithPlan, setHasInteractedWithPlan] = useState<boolean>(false)
const { setScrollable } = useFlowStore();
const nodes = useFlowStore().nodes;
//Button Actions
const openCreate = () => {
setScrollable(false);
setDraftPlan({...structuredClone(defaultPlan), id: crypto.randomUUID()});
dialogRef.current?.showModal();
};
const openCreateWithDescription = () => {
setScrollable(false);
setDraftPlan({...structuredClone(defaultPlan), id: crypto.randomUUID(), name: description!});
setNewActionType("llm")
setNewActionValue(description!)
dialogRef.current?.showModal();
}
const openEdit = () => {
setScrollable(false);
if (!plan) return;
setDraftPlan(structuredClone(plan));
dialogRef.current?.showModal();
};
const close = () => {
setScrollable(true);
dialogRef.current?.close();
setDraftPlan(null);
};
const buildAction = (): Action => {
const id = crypto.randomUUID();
setHasInteractedWithPlan(true)
switch (newActionType) {
case "speech":
return { id, text: newActionValue, type: "speech" };
case "gesture":
return { id, gesture: newActionValue, isTag: newActionGestureType, type: "gesture" };
case "llm":
return { id, goal: newActionValue, type: "llm" };
}
};
return (<>
{/* Create and edit buttons */}
{!plan && (
<button className={styles.nodeButton} onClick={description ? openCreateWithDescription : openCreate}>
Create Plan
</button>
)}
{plan && (
<button className={styles.nodeButton} onClick={openEdit}>
Edit Plan
</button>
)}
{/* Start of dialog (plan editor) */}
<dialog
ref={dialogRef}
className={`${styles.planDialog}`}
//onWheel={(e) => e.stopPropagation()}
data-testid={"PlanEditorDialogTestID"}
>
<form method="dialog" className="flex-col gap-md">
<h3> {draftPlan?.id === plan?.id ? "Edit Plan" : "Create Plan"} </h3>
{/* Plan name text field */}
{draftPlan && (
<TextField
value={draftPlan.name}
setValue={(name) =>
setDraftPlan({ ...draftPlan, name })}
placeholder="Plan name"
data-testid="name_text_field"/>
)}
{/* Entire "bottom" part (adder and steps) without cancel, confirm and reset */}
{draftPlan && (<div className={styles.planEditor}>
<div className={styles.planEditorLeft}>
{/* Left Side (Action Adder) */}
<h4>Add Action</h4>
{(!plan && description && draftPlan.steps.length === 0 && !hasInteractedWithPlan) && (<div className={styles.stepSuggestion}>
<label> Filled in as a suggestion! </label>
<label> Feel free to change! </label>
</div>)}
<label>
Action Type <wbr />
{/* Type selection */}
<select
value={newActionType}
onChange={(e) => {
setNewActionType(e.target.value as ActionTypes);
// Reset value when action type changes
setNewActionValue("");
}}>
<option value="speech">Speech Action</option>
<option value="gesture">Gesture Action</option>
<option value="llm">LLM Action</option>
</select>
</label>
{/* Action value editor*/}
{newActionType === "gesture" ? (
// Gesture get their own editor component
<GestureValueEditor
value={newActionValue}
setValue={setNewActionValue}
setType={setNewActionGestureType}
placeholder="Gesture name"
/>
) : (
<TextField
value={newActionValue}
setValue={setNewActionValue}
placeholder={
newActionType === "speech" ? "Speech text"
: "LLM goal"
}
/>
)}
{/* Adding steps */}
<button
type="button"
disabled={!newActionValue}
onClick={() => {
if (!draftPlan) return;
// Add action to steps
const action = buildAction();
setDraftPlan({
...draftPlan,
steps: [...draftPlan.steps, action],});
// Reset current action building
setNewActionValue("");
setNewActionType("speech");
}}>
Add Step
</button>
</div>
{/* Right Side (Steps shown) */}
<div className={styles.planEditorRight}>
<h4>Steps</h4>
{/* Show if there are no steps yet */}
{draftPlan.steps.length === 0 && (
<div className={styles.emptySteps}>
No steps yet
</div>
)}
{/* Map over all steps */}
{draftPlan.steps.map((step, index) => (
<div
role="button"
tabIndex={0}
key={step.id}
className={styles.planStep}
// Extra logic for screen readers to access using keyboard
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
setDraftPlan({
...draftPlan,
steps: draftPlan.steps.filter((s) => s.id !== step.id),});
}}}
onClick={() => {
setDraftPlan({
...draftPlan,
steps: draftPlan.steps.filter((s) => s.id !== step.id),});
}}>
<span className={styles.stepIndex}>{index + 1}.</span>
<span className={styles.stepType}>{step.type}:</span>
<span className={styles.stepName}>
{
// This just tries to find the goals name, i know it looks ugly:(
step.type === "goal"
? ((nodes.find(x => x.id === step.id)?.data.name as string) == "" ?
"unnamed goal": (nodes.find(x => x.id === step.id)?.data.name as string))
: (GetActionValue(step) ?? "")}
</span>
</div>
))}
</div>
</div>
)}
{/* Buttons */}
<div className="flex-row gap-md">
{/* Close button */}
<button type="button" onClick={close}>
Cancel
</button>
{/* Confirm/ Create button */}
<button
type="button"
disabled={!draftPlan}
onClick={() => {
if (!draftPlan) return;
onSave(draftPlan);
close();
}}>
{draftPlan?.id === plan?.id ? "Confirm" : "Create"}
</button>
{/* Reset button */}
<button
type="button"
disabled={!draftPlan}
onClick={() => {
onSave(undefined);
close();
}}>
Reset
</button>
</div>
</form>
</dialog>
</>
);
}

View File

@@ -0,0 +1,72 @@
import { TextField } from "../../../../../components/TextField";
import GestureValueEditor from "./GestureValueEditor";
import type { ActionTypes } from "./Plan";
import styles from './PlanEditor.module.css';
type ActionAdderProps = {
newActionType: ActionTypes;
setNewActionType: (t: ActionTypes) => void;
newActionValue: string;
setNewActionValue: (v: string) => void;
setNewActionGestureType: (b: boolean) => void;
onAdd: () => void;
showSuggestion: boolean;
};
export function ActionAdder({
newActionType,
setNewActionType,
newActionValue,
setNewActionValue,
setNewActionGestureType,
onAdd,
showSuggestion,
}: ActionAdderProps) {
return (
<div className={styles.planEditorLeft}>
<h4>Add Action</h4>
{showSuggestion && (
<div className={styles.stepSuggestion}>
<label>Filled in as a suggestion!</label>
<label>Feel free to change!</label>
</div>
)}
<label>
Action Type <wbr />
<select
value={newActionType}
onChange={(e) => {
setNewActionType(e.target.value as ActionTypes);
setNewActionValue("");
}}
>
<option value="speech">Speech Action</option>
<option value="gesture">Gesture Action</option>
<option value="llm">LLM Action</option>
</select>
</label>
{newActionType === "gesture" ? (
<GestureValueEditor
value={newActionValue}
setValue={setNewActionValue}
setType={setNewActionGestureType}
placeholder="Gesture name"
/>
) : (
<TextField
value={newActionValue}
setValue={setNewActionValue}
placeholder={newActionType === "speech" ? "Speech text" : "LLM goal"}
/>
)}
<button type="button" disabled={!newActionValue} onClick={onAdd}>
Add Step
</button>
</div>
);
}

View File

@@ -1,4 +1,8 @@
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/
.gestureEditor {
display: flex;
flex-direction: column;

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { useState, useRef } from "react";
import styles from './GestureValueEditor.module.css'

View File

@@ -0,0 +1,10 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import type { Plan, PlanElement } from "./Plan";
export const defaultPlan: Plan = {
name: "Default Plan",
id: "-1",
steps: [] as PlanElement[],
}

View File

@@ -1,5 +1,8 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { type Node } from "@xyflow/react"
import { GoalReduce } from "../nodes/GoalNode"
import { GoalReduce } from "../../nodes/GoalNode"
export type Plan = {
@@ -121,4 +124,6 @@ export function GetActionValue(action: Action) {
return returnAction.goal;
default:
}
}
}

View File

@@ -1,5 +1,8 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
// This file is to avoid sharing both functions and components which eslint dislikes. :)
import type { GoalNode } from "../nodes/GoalNode"
import type { GoalNode } from "../../nodes/GoalNode"
import type { Goal, Plan } from "./Plan"
/**
@@ -32,4 +35,5 @@ export function deleteGoalInPlanByID(plan: Plan, goalID: string) {
steps: plan.steps.filter((x) => x.id !== goalID)
}
return updatedPlan.steps.length == 0 ? undefined : updatedPlan
}
}

View File

@@ -1,3 +1,8 @@
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/
.planDialog {
overflow:visible;
width: 80vw;

View File

@@ -0,0 +1,214 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {useRef, useState} from "react";
import useFlowStore from "../../VisProgStores.tsx";
import styles from './PlanEditor.module.css';
import { type Action, type ActionTypes, type Plan } from "./Plan.tsx";
import { defaultPlan } from "./Plan.default.ts";
import { TextField } from "../../../../../components/TextField.tsx";
import { StepsList } from "./StepList.tsx";
import { ActionAdder } from "./ActionAdder.tsx";
type PlanEditorDialogProps = {
plan?: Plan;
onSave: (plan: Plan | undefined) => void;
description? : string;
};
/**
* Creates an action, as a step for a plan.
* @param type the type of action to build
* @param value the value of this action to build
* @param isGestureTag whether or not this action, restricted to gestures, is a tag.
* @returns An action
*/
function buildAction(
type: ActionTypes,
value: string,
isGestureTag: boolean
): Action {
const id = crypto.randomUUID();
switch (type) {
case "speech":
return { id, text: value, type: "speech" };
case "gesture":
return { id, gesture: value, isTag: isGestureTag, type: "gesture" };
case "llm":
return { id, goal: value, type: "llm" };
}
}
export default function PlanEditorDialog({
plan,
onSave,
description,
}: PlanEditorDialogProps) {
// UseStates and references
const dialogRef = useRef<HTMLDialogElement | null>(null);
const [draftPlan, setDraftPlan] = useState<Plan | null>(null);
const [newActionType, setNewActionType] = useState<ActionTypes>("speech");
const [newActionGestureType, setNewActionGestureType] = useState<boolean>(true);
const [newActionValue, setNewActionValue] = useState("");
const [hasInteractedWithPlan, setHasInteractedWithPlan] = useState<boolean>(false)
const { setScrollable } = useFlowStore();
const nodes = useFlowStore().nodes;
//Button Actions
const openCreate = () => {
setScrollable(false);
setDraftPlan({...structuredClone(defaultPlan), id: crypto.randomUUID()});
dialogRef.current?.showModal();
};
const openCreateWithDescription = () => {
setScrollable(false);
setDraftPlan({...structuredClone(defaultPlan), id: crypto.randomUUID(), name: description!});
setNewActionType("llm")
setNewActionValue(description!)
dialogRef.current?.showModal();
}
const openEdit = () => {
setScrollable(false);
if (!plan) return;
setDraftPlan(structuredClone(plan));
dialogRef.current?.showModal();
};
const close = () => {
setScrollable(true);
dialogRef.current?.close();
setDraftPlan(null);
};
const addAction = () => {
if (!draftPlan) return;
// Add action to steps
const action = buildAction(newActionType, newActionValue, newActionGestureType);
setDraftPlan({
...draftPlan,
steps: [...draftPlan.steps, action],});
// Reset current action building
setNewActionValue("");
setNewActionType("speech");
setHasInteractedWithPlan(true);
}
const showSuggestion : boolean = (
!plan &&
!!description &&
draftPlan !== null &&
draftPlan.steps.length === 0 &&
!hasInteractedWithPlan)
return (<>
{/* Create and edit buttons */}
{!plan && (
<button className={styles.nodeButton} onClick={description ? openCreateWithDescription : openCreate}>
Create Plan
</button>
)}
{plan && (
<button className={styles.nodeButton} onClick={openEdit}>
Edit Plan
</button>
)}
{/* Start of dialog (plan editor) */}
<dialog
ref={dialogRef}
className={`${styles.planDialog}`}
//onWheel={(e) => e.stopPropagation()}
data-testid={"PlanEditorDialogTestID"}
>
<form method="dialog" className="flex-col gap-md">
<h3> {draftPlan?.id === plan?.id ? "Edit Plan" : "Create Plan"} </h3>
{/* Plan name text field */}
{draftPlan && (
<TextField
value={draftPlan.name}
setValue={(name) =>
setDraftPlan({ ...draftPlan, name })}
placeholder="Plan name"
data-testid="name_text_field"/>
)}
{/* Entire "bottom" part (adder and steps) without cancel, confirm and reset */}
{draftPlan && (<div className={styles.planEditor}>
<div className={styles.planEditorLeft}>
{/* Left Side (Action Adder) */}
<ActionAdder
newActionType={newActionType}
setNewActionType={setNewActionType}
newActionValue={newActionValue}
setNewActionValue={setNewActionValue}
setNewActionGestureType={setNewActionGestureType}
onAdd={addAction}
showSuggestion={showSuggestion}
/>
</div>
{/* Right Side (Steps shown) */}
<div className={styles.planEditorRight}>
<h4>Steps</h4>
{/* Map over all steps */}
<StepsList
steps={draftPlan.steps}
nodes={nodes}
onRemove={(id) =>
setDraftPlan({
...draftPlan,
steps: draftPlan.steps.filter(s => s.id !== id),
})
}
/>
</div>
</div>
)}
{/* Buttons */}
<div className="flex-row gap-md">
{/* Close button */}
<button type="button" onClick={close}>
Cancel
</button>
{/* Confirm/ Create button */}
<button
type="button"
disabled={!draftPlan}
onClick={() => {
if (!draftPlan) return;
onSave(draftPlan);
close();
}}>
{draftPlan?.id === plan?.id ? "Confirm" : "Create"}
</button>
{/* Reset button */}
<button
type="button"
disabled={!draftPlan}
onClick={() => {
onSave(undefined);
close();
}}>
Reset
</button>
</div>
</form>
</dialog>
</>
);
}

View File

@@ -0,0 +1,54 @@
import { GetActionValue, type PlanElement } from "./Plan";
import styles from './PlanEditor.module.css';
import { type Node} from "@xyflow/react"
type StepsListProps = {
steps: PlanElement[];
onRemove: (id: string) => void;
nodes: Node[];
};
function getStepLabel(
step: PlanElement,
nodes: Node[],
): string {
if (step.type === "goal") {
// For goals, we lookup the value through the nodes in the diagram
const node = nodes.find(n => n.id === step.id);
return (node?.data?.name as string)?.trim() || "unnamed goal";
}
// Not a goal, we lookup the correct action value of the action
return GetActionValue(step) ?? "";
}
export function StepsList({ steps, onRemove, nodes }: StepsListProps) {
if (steps.length === 0) {
return <div className={styles.emptySteps}>No steps yet</div>;
}
return (
<>
{steps.map((step, index) => (
<div
key={step.id}
role="button"
tabIndex={0}
className={styles.planStep}
onClick={() => onRemove(step.id)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
onRemove(step.id);
}
}}
>
<span className={styles.stepIndex}>{index + 1}.</span>
<span className={styles.stepType}>{step.type}:</span>
<span className={styles.stepName}>
{getStepLabel(step, nodes)}
</span>
</div>
))}
</>
);
}

View File

@@ -1,7 +1,21 @@
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/
:global(.react-flow__handle.source){
border-radius: 100%;
}
:global(.react-flow__handle.target){
border-radius: 15%;
}
:global(.react-flow__handle.connected) {
background: lightgray;
border-color: green;
filter: drop-shadow(0 0 0.25rem green);
filter: drop-shadow(0 0 0.15rem green);
}
:global(.singleConnectionHandle.connected) {
@@ -16,19 +30,19 @@
:global(.singleConnectionHandle.unconnected){
background: lightsalmon;
border-color: #ff6060;
filter: drop-shadow(0 0 0.25rem #ff6060);
filter: drop-shadow(0 0 0.15rem #ff6060);
}
:global(.react-flow__handle.connectingto) {
background: #ff6060;
border-color: coral;
filter: drop-shadow(0 0 0.25rem coral);
filter: drop-shadow(0 0 0.15rem coral);
}
:global(.react-flow__handle.valid) {
background: #55dd99;
border-color: green;
filter: drop-shadow(0 0 0.25rem green);
filter: drop-shadow(0 0 0.15rem green);
}
:global(.react-flow__handle) {

View File

@@ -1,10 +1,12 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {
Handle,
type HandleProps,
type Connection,
useNodeId, useNodeConnections
} from '@xyflow/react';
import {useState} from 'react';
import { type HandleRule, useHandleRules} from "../HandleRuleLogic.ts";
import "./RuleBasedHandle.module.css";
@@ -29,21 +31,16 @@ export function MultiConnectionHandle({
handleId: id!
})
// initialise the handles state with { isValid: true } to show that connections are possible
const [handleState, setHandleState] = useState<{ isSatisfied: boolean, message?: string }>({ isSatisfied: true });
return (
<Handle
{...otherProps}
id={id}
type={type}
className={"multiConnectionHandle" + (connections.length === 0 ? " unconnected" : " connected")}
className={"multiConnectionHandle" + (connections.length === 0 ? " unconnected" : " connected") + ` ${type}`}
isValidConnection={(connection) => {
const result = validate(connection as Connection);
setHandleState(result);
return result.isSatisfied;
}}
title={handleState.message}
/>
);
}
@@ -66,22 +63,18 @@ export function SingleConnectionHandle({
handleId: id!
})
// initialise the handles state with { isValid: true } to show that connections are possible
const [handleState, setHandleState] = useState<{ isSatisfied: boolean, message?: string }>({ isSatisfied: true });
return (
<Handle
{...otherProps}
id={id}
type={type}
className={"singleConnectionHandle" + (connections.length === 0 ? " unconnected" : " connected")}
className={"singleConnectionHandle" + (connections.length === 0 ? " unconnected" : " connected") + ` ${type}`}
isConnectable={connections.length === 0}
isValidConnection={(connection) => {
const result = validate(connection as Connection);
setHandleState(result);
return result.isSatisfied;
}}
title={handleState.message}
/>
);
}

View File

@@ -1,3 +1,8 @@
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/
.save-load-panel {
border-radius: 0 0 5pt 5pt;
background-color: canvas;

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {type ChangeEvent, useRef, useState} from "react";
import useFlowStore from "../VisProgStores";
import visProgStyles from "../../VisProg.module.css";
@@ -29,6 +32,8 @@ export default function SaveLoadPanel() {
const text = await file.text();
const parsed = JSON.parse(text) as SavedProject;
if (!parsed.nodes || !parsed.edges) throw new Error("Invalid file format");
const {nodes, unregisterWarningsForId} = useFlowStore.getState();
nodes.forEach((node) => {unregisterWarningsForId(node.id);});
setNodes(parsed.nodes);
setEdges(parsed.edges);
} catch (e) {

View File

@@ -1,15 +1,61 @@
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/
.warnings-sidebar {
width: 320px;
min-width: auto;
max-width: 340px;
margin-right: 0;
height: 100%;
background: canvas;
border-left: 2px solid black;
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: 1px solid #2a2a2e;
border-bottom: 2px solid CanvasText;
}
.severity-tabs {
@@ -36,15 +82,21 @@
.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;
overflow-y: auto;
min-height: 0;
overflow-y: scroll;
}
.warnings-empty {
@@ -53,11 +105,13 @@
.warning-item {
display: flex;
flex-direction: column;
margin: 5px;
gap: 8px;
padding: 8px 12px;
gap: 2px;
padding: 0;
border-radius: 5px;
cursor: pointer;
color: GrayText;
}
.warning-item:hover {
@@ -66,17 +120,89 @@
.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: 3px solid orange;
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: 3px solid steelblue;
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 .meta {
font-size: 11px;
opacity: 0.6;
}
.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;
}

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {useReactFlow, useStoreApi} from "@xyflow/react";
import clsx from "clsx";
import {useEffect, useState} from "react";
@@ -9,31 +12,83 @@ import {
} 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]);
useEffect(() => {}, [warnings]);
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={styles.warningsSidebar}>
<WarningsHeader
severityFilter={severityFilter}
onChange={setSeverityFilter}
/>
<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} />
<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,
@@ -46,7 +101,6 @@ function WarningsHeader({
return (
<div className={styles.warningsHeader}>
<h3>Warnings</h3>
<div className={styles.severityTabs}>
{(['ALL', 'ERROR', 'WARNING', 'INFO'] as const).map(severity => (
<button
@@ -68,13 +122,18 @@ function WarningsHeader({
}
/**
* 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}>
@@ -83,30 +142,39 @@ function WarningsList(props: { warnings: EditorWarning[] }) {
)
}
return (
<div>
<div className={"warningGroup"}>
<div className={styles.warningsList}>
<div className={styles.warningGroupHeader}>global:</div>
<div className={styles.warningsList}>
<div className={styles.warningsGroup}>
{splitWarnings.global.map((warning) => (
<WarningListItem warning={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>
<div className={"warningGroup"}>
<div className={styles.warningGroupHeader}>other:</div>
<div className={styles.warningsList}>
<div className={styles.warningsGroup}>
{splitWarnings.other.map((warning) => (
<WarningListItem warning={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>
</div>
);
}
function WarningListItem(props: { warning: EditorWarning }) {
/**
* 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 (
@@ -114,41 +182,47 @@ function WarningListItem(props: { warning: EditorWarning }) {
className={clsx(styles.warningItem, styles[`warning-item--${props.warning.severity.toLowerCase()}`],)}
onClick={() => jumpToNode(props.warning.scope.id)}
>
<div className={styles.description}>
{props.warning.description}
<div className={styles.itemHeader}>
<span className={styles.type}>{props.warning.type}</span>
</div>
<div className={styles.meta}>
{props.warning.scope.id}
{props.warning.scope.handleId && (
<span className={styles.handle}>@{props.warning.scope.handleId}</span>
)}
<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 } = useReactFlow();
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
// 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 { position, width = 0, height = 0} = node;
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
//move to node
setCenter(
position!.x + width / 2,
position!.y + height / 2,
{ zoom: 2, duration: 300 }
position!.x + ((width / viewport.zoom) / 2),
position!.y + ((height / viewport.zoom) / 2),
{duration: 300, interpolate: "smooth" }
).then(() => {
// select the node
addSelectedNodes([nodeId]);
});
};
}

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import type { BasicBeliefNodeData } from "./BasicBeliefNode.tsx";

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {
type NodeProps,
Position,
@@ -113,9 +116,7 @@ export default function BasicBeliefNode(props: NodeProps<BasicBeliefNode>) {
updateNodeData(props.id, {...data, belief: {...data.belief, description: value}});
}
// These are the labels outputted by our emotion detection model
const emotionOptions = ["sad", "angry", "surprise", "fear", "happy", "disgust", "neutral"];
const emotionOptions = ["sad", "angry", "surprise", "fear", "happy", "disgust", "neutral"];
let placeholder = ""
let wrapping = ""
@@ -191,8 +192,8 @@ export default function BasicBeliefNode(props: NodeProps<BasicBeliefNode>) {
)}
<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"},{nodeType:"InferredBelief",handleId:"inferred_belief"}]),
]} title="Connect to any number of trigger and/or normNode(-s)"/>
</div>
</>
);

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {getOutgoers, type Node} from '@xyflow/react';
import {type HandleRule, type RuleResult, ruleResult} from "../HandleRuleLogic.ts";
import useFlowStore from "../VisProgStores.tsx";

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import type { EndNodeData } from "./EndNode";
/**

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {
type NodeProps,
Position,
@@ -60,7 +63,7 @@ export default function EndNode(props: NodeProps<EndNode>) {
</div>
<SingleConnectionHandle type="target" position={Position.Left} id="target" rules={[
allowOnlyConnectionsFromType(["phase"])
]}/>
]} title="Connect to a phaseNode"/>
</div>
</>
);

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import type { GoalNodeData } from "./GoalNode";
/**

View File

@@ -1,19 +1,24 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {
type NodeProps,
Position,
type Node,
type Node
} from '@xyflow/react';
import {useEffect} from "react";
import type {EditorWarning} from "../components/EditorWarnings.tsx";
import { Toolbar } from '../components/NodeComponents';
import styles from '../../VisProg.module.css';
import { TextField } from '../../../../components/TextField';
import {MultiConnectionHandle} from "../components/RuleBasedHandle.tsx";
import {allowOnlyConnectionsFromHandle, allowOnlyConnectionsFromType} from "../HandleRules.ts";
import useFlowStore from '../VisProgStores';
import {DoesPlanIterate, HasCheckingSubGoal, PlanReduce, type Plan } from '../components/Plan';
import PlanEditorDialog from '../components/PlanEditor';
import {DoesPlanIterate, HasCheckingSubGoal, PlanReduce, type Plan } from '../components/PlanEditor/Plan.tsx';
import PlanEditorDialog from '../components/PlanEditor/PlanEditor.tsx';
import { MultilineTextField } from '../../../../components/MultilineTextField';
import { defaultPlan } from '../components/Plan.default.ts';
import { deleteGoalInPlanByID, insertGoalInPlan } from '../components/PlanEditingFunctions.tsx';
import { defaultPlan } from '../components/PlanEditor/Plan.default.ts';
import { deleteGoalInPlanByID, insertGoalInPlan } from '../components/PlanEditor/PlanEditingFunctions.tsx';
/**
* The default data dot a phase node
@@ -44,7 +49,7 @@ export type GoalNode = Node<GoalNodeData>
* @returns React.JSX.Element
*/
export default function GoalNode({id, data}: NodeProps<GoalNode>) {
const {updateNodeData} = useFlowStore();
const {updateNodeData, registerWarning, unregisterWarning} = useFlowStore();
const _nodes = useFlowStore().nodes;
const text_input_id = `goal_${id}_text_input`;
@@ -64,6 +69,24 @@ export default function GoalNode({id, data}: NodeProps<GoalNode>) {
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 <>
<Toolbar nodeId={id} allowDelete={true}/>
<div className={`${styles.defaultNode} ${styles.nodeGoal} flex-col gap-sm`}>
@@ -118,9 +141,11 @@ export default function GoalNode({id, data}: NodeProps<GoalNode>) {
</div>
<MultiConnectionHandle type="source" position={Position.Right} id="GoalSource" rules={[
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}]),
]}/>
]} title="Connect to any number of phase and/or goalNode(-s)"/>
<MultiConnectionHandle type="target" position={Position.Bottom} id="GoalTarget" rules={[allowOnlyConnectionsFromType(["goal"])]}/>
<MultiConnectionHandle type="target" position={Position.Bottom} id="GoalTarget" rules={[
allowOnlyConnectionsFromType(["goal"])]
} title="Connect to any number of goalNode(-s)"/>
</div>

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import type { InferredBeliefNodeData } from "./InferredBeliefNode.tsx";
@@ -5,7 +8,7 @@ import type { InferredBeliefNodeData } from "./InferredBeliefNode.tsx";
* Default data for this node
*/
export const InferredBeliefNodeDefaults: InferredBeliefNodeData = {
label: "Inferred Belief",
label: "AND/OR",
droppable: true,
inferredBelief: {
left: undefined,

View File

@@ -1,3 +1,8 @@
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/
.operator-switch {
display: inline-flex;
align-items: center;

View File

@@ -1,6 +1,10 @@
import {getConnectedEdges, type Node, type NodeProps, Position} from '@xyflow/react';
import {useState} from "react";
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {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";
@@ -91,7 +95,7 @@ export const InferredBeliefTooltip = `
*/
export default function InferredBeliefNode(props: NodeProps<InferredBeliefNode>) {
const data = props.data;
const { updateNodeData } = useFlowStore();
const { updateNodeData, registerWarning, unregisterWarning } = useFlowStore();
// start of as an AND operator, true: "AND", false: "OR"
const [enforceAllBeliefs, setEnforceAllBeliefs] = useState(true);
@@ -109,6 +113,29 @@ export default function InferredBeliefNode(props: NodeProps<InferredBeliefNode>)
});
}
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}/>

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import type { NormNodeData } from "./NormNode";
/**

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {
type NodeProps,
Position,
@@ -79,10 +82,10 @@ export default function NormNode(props: NodeProps<NormNode>) {
<MultiConnectionHandle type="source" position={Position.Right} id="norms" rules={[
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}])
]}/>
]} title="Connect to any number of phaseNode(-s)"/>
<SingleConnectionHandle type="target" position={Position.Bottom} id="NormBeliefs" rules={[
allowOnlyConnectionsFromType(["basic_belief", "inferred_belief"])
]}/>
]} title="Connect to a beliefNode or a set of beliefs combined using the AND/OR node"/>
</div>
</>;
};

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import type { PhaseNodeData } from "./PhaseNode";
/**

View File

@@ -1,10 +1,13 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {
type NodeProps,
Position,
type Node, useNodeConnections
} from '@xyflow/react';
import {useEffect} from "react";
import type {EditorWarning} from "../components/EditorWarnings.tsx";
import {useEffect, useRef} from "react";
import {type EditorWarning} from "../components/EditorWarnings.tsx";
import { Toolbar } from '../components/NodeComponents';
import styles from '../../VisProg.module.css';
import {SingleConnectionHandle, MultiConnectionHandle} from "../components/RuleBasedHandle.tsx";
@@ -39,16 +42,29 @@ export type PhaseNode = Node<PhaseNodeData>
*/
export default function PhaseNode(props: NodeProps<PhaseNode>) {
const data = props.data;
const {updateNodeData} = useFlowStore();
const {updateNodeData, registerWarning, unregisterWarning} = useFlowStore();
const updateLabel = (value: string) => updateNodeData(props.id, {...data, label: value});
const label_input_id = `phase_${props.id}_label_input`;
const {registerWarning, unregisterWarning} = useFlowStore.getState();
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 = {
@@ -61,10 +77,72 @@ export default function PhaseNode(props: NodeProps<PhaseNode>) {
description: "the phaseNode has no incoming goals, norms, and/or triggers"
}
if (connections.length === 0) { registerWarning(noConnectionWarning); }
else { unregisterWarning(props.id, `${noConnectionWarning.type}:data`); }
if (connections.length === 0) { registerWarning(noConnectionWarning); return; }
unregisterWarning(props.id, `${noConnectionWarning.type}:data`);
}, [connections.length, props.id, registerWarning, unregisterWarning]);
useEffect(() => {
const notConnectedInfo : EditorWarning = {
scope: {
id: props.id,
handleId: undefined,
},
type: 'NOT_CONNECTED_TO_PROGRAM',
severity: "INFO",
description: "The PhaseNode is not connected to other nodes"
};
const noIncomingPhaseWarning : EditorWarning = {
scope: {
id: props.id,
handleId: 'target'
},
type: 'MISSING_INPUT',
severity: "WARNING",
description: "the phaseNode has no incoming connection from a phase or the startNode"
}
const noOutgoingPhaseWarning : EditorWarning = {
scope: {
id: props.id,
handleId: 'source'
},
type: 'MISSING_OUTPUT',
severity: "WARNING",
description: "the phaseNode has no outgoing connection to a phase or the endNode"
}
// register relevant warning and unregister others
if (phaseInCons.length === 0 && phaseOutCons.length === 0) {
registerWarning(notConnectedInfo);
unregisterWarning(props.id, `${noOutgoingPhaseWarning.type}:${noOutgoingPhaseWarning.scope.handleId}`);
unregisterWarning(props.id, `${noIncomingPhaseWarning.type}:${noIncomingPhaseWarning.scope.handleId}`);
return;
}
if (phaseOutCons.length === 0) {
registerWarning(noOutgoingPhaseWarning);
unregisterWarning(props.id, `${noIncomingPhaseWarning.type}:${noIncomingPhaseWarning.scope.handleId}`);
unregisterWarning(notConnectedInfo.scope.id, notConnectedInfo.type);
return;
}
if (phaseInCons.length === 0) {
registerWarning(noIncomingPhaseWarning);
unregisterWarning(props.id, `${noOutgoingPhaseWarning.type}:${noOutgoingPhaseWarning.scope.handleId}`);
unregisterWarning(notConnectedInfo.scope.id, notConnectedInfo.type);
return;
}
// unregister all warnings if none should be present
unregisterWarning(notConnectedInfo.scope.id, notConnectedInfo.type);
unregisterWarning(props.id, `${noOutgoingPhaseWarning.type}:${noOutgoingPhaseWarning.scope.handleId}`);
unregisterWarning(props.id, `${noIncomingPhaseWarning.type}:${noIncomingPhaseWarning.scope.handleId}`);
}, [phaseInCons.length, phaseOutCons.length, props.id, registerWarning, unregisterWarning]);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (ref.current) {
const { width, height } = ref.current.getBoundingClientRect();
console.log('Node width:', width, 'height:', height);
}
}, []);
return (
<>
<Toolbar nodeId={props.id} allowDelete={true}/>
@@ -81,14 +159,14 @@ export default function PhaseNode(props: NodeProps<PhaseNode>) {
<SingleConnectionHandle type="target" position={Position.Left} id="target" rules={[
noSelfConnections,
allowOnlyConnectionsFromType(["phase", "start"]),
]}/>
]} title="Connect to a phase or the startNode"/>
<MultiConnectionHandle type="target" position={Position.Bottom} id="data" rules={[
allowOnlyConnectionsFromType(["norm", "goal", "trigger"])
]}/>
]} title="Connect to any number of norm, goal, and TriggerNode(-s)"/>
<SingleConnectionHandle type="source" position={Position.Right} id="source" rules={[
noSelfConnections,
allowOnlyConnectionsFromType(["phase", "end"]),
]}/>
]} title="Connect to a phase or the endNode"/>
</div>
</>
);

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import type { StartNodeData } from "./StartNode";
/**

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {
type NodeProps,
Position,
@@ -58,7 +61,7 @@ export default function StartNode(props: NodeProps<StartNode>) {
</div>
<SingleConnectionHandle type="source" position={Position.Right} id="source" rules={[
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"target"}])
]}/>
]} title="Connect to a phaseNode"/>
</div>
</>
);

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import type { TriggerNodeData } from "./TriggerNode";
/**

View File

@@ -1,19 +1,24 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {
type NodeProps,
Position,
type Node,
type Node, useNodeConnections
} from '@xyflow/react';
import {useEffect} from "react";
import type {EditorWarning} from "../components/EditorWarnings.tsx";
import { Toolbar } from '../components/NodeComponents';
import styles from '../../VisProg.module.css';
import {MultiConnectionHandle, SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
import {allowOnlyConnectionsFromHandle, allowOnlyConnectionsFromType} from "../HandleRules.ts";
import useFlowStore from '../VisProgStores';
import {PlanReduce, type Plan } from '../components/Plan';
import PlanEditorDialog from '../components/PlanEditor';
import {PlanReduce, type Plan } from '../components/PlanEditor/Plan.tsx';
import PlanEditorDialog from '../components/PlanEditor/PlanEditor.tsx';
import {BeliefGlobalReduce} from "./BeliefGlobals.ts";
import type { GoalNode } from './GoalNode.tsx';
import { defaultPlan } from '../components/Plan.default.ts';
import { deleteGoalInPlanByID, insertGoalInPlan } from '../components/PlanEditingFunctions.tsx';
import { defaultPlan } from '../components/PlanEditor/Plan.default.ts';
import { deleteGoalInPlanByID, insertGoalInPlan } from '../components/PlanEditor/PlanEditingFunctions.tsx';
import { TextField } from '../../../../components/TextField.tsx';
/**
@@ -45,12 +50,77 @@ export type TriggerNode = Node<TriggerNodeData>
*/
export default function TriggerNode(props: NodeProps<TriggerNode>) {
const data = props.data;
const {updateNodeData} = useFlowStore();
const {updateNodeData, registerWarning, unregisterWarning} = useFlowStore();
const setName= (value: string) => {
updateNodeData(props.id, {...data, name: value})
}
const beliefInput = useNodeConnections({
id: props.id,
handleType: "target",
handleId: "TriggerBeliefs"
})
const outputCons = useNodeConnections({
id: props.id,
handleType: "source",
handleId: "TriggerSource"
})
useEffect(() => {
const noPhaseConnectionWarning : EditorWarning = {
scope: {
id: props.id,
handleId: 'TriggerSource'
},
type: 'MISSING_OUTPUT',
severity: 'INFO',
description: "This triggerNode is missing a condition/belief, please make sure to connect a belief node to "
};
if (outputCons.length === 0){
registerWarning(noPhaseConnectionWarning);
return;
}
unregisterWarning(props.id, `${noPhaseConnectionWarning.type}:${noPhaseConnectionWarning.scope.handleId}`);
},[outputCons.length, props.id, registerWarning, unregisterWarning])
useEffect(() => {
const noBeliefWarning : EditorWarning = {
scope: {
id: props.id,
handleId: 'TriggerBeliefs'
},
type: 'MISSING_INPUT',
severity: 'ERROR',
description: "This triggerNode is missing a condition/belief, please make sure to connect a belief node to "
};
if (beliefInput.length === 0 && outputCons.length !== 0){
registerWarning(noBeliefWarning);
return;
}
unregisterWarning(props.id, `${noBeliefWarning.type}:${noBeliefWarning.scope.handleId}`);
},[beliefInput.length, outputCons.length, props.id, registerWarning, unregisterWarning])
useEffect(() => {
const noPlanWarning : EditorWarning = {
scope: {
id: props.id,
handleId: undefined
},
type: 'PLAN_IS_UNDEFINED',
severity: 'ERROR',
description: "This triggerNode is missing a plan, please make sure to create a plan by using the create plan button"
};
if (!data.plan && outputCons.length !== 0){
registerWarning(noPlanWarning);
return;
}
unregisterWarning(props.id, noPlanWarning.type);
},[data.plan, outputCons.length, props.id, registerWarning, unregisterWarning])
return <>
<Toolbar nodeId={props.id} allowDelete={true}/>
@@ -65,15 +135,16 @@ export default function TriggerNode(props: NodeProps<TriggerNode>) {
<div className={"flex-row gap-md"}>Plan{data.plan ? (": " + data.plan.name) : ""} is currently {data.plan ? "" : "not"} set. {data.plan ? "🟢" : "🔴"}</div>
<MultiConnectionHandle type="source" position={Position.Right} id="TriggerSource" rules={[
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}]),
]}/>
]} title="Connect to any number of phaseNodes"/>
<SingleConnectionHandle
type="target"
position={Position.Bottom}
id="TriggerBeliefs"
style={{ left: '40%' }}
rules={[
allowOnlyConnectionsFromType(["basic_belief","inferred_belief"]),
allowOnlyConnectionsFromType(['basic_belief','inferred_belief']),
]}
title="Connect to a beliefNode or a set of beliefs combined using the AND/OR node"
/>
<MultiConnectionHandle
@@ -84,6 +155,7 @@ export default function TriggerNode(props: NodeProps<TriggerNode>) {
rules={[
allowOnlyConnectionsFromType(['goal']),
]}
title="Connect to any number of goalNodes"
/>
<PlanEditorDialog

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {type Edge, type Node } from "@xyflow/react";
export type SavedProject = {

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {useSyncExternalStore} from "react";
type Unsub = () => void;

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
/**
* Find the indices of all elements that occur more than once.
*

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
/**
* Format a time duration like `HH:MM:SS.mmm`.
*

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import type {PhaseNode} from "../pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx";
/**

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
export type PriorityFilterPredicate<T> = {
priority: number;
predicate: (element: T) => boolean | null; // The predicate and its priority are ignored if it returns null.

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {create} from "zustand";
// the type of a reduced program
@@ -85,9 +88,8 @@ const useProgramStore = create<ProgramState>((set, get) => ({
const rootGoals = phase["goals"] as Record<string, unknown>[];
const flatList: GoalWithDepth[] = [];
// Helper: Define this ONCE, outside the loop
const isGoal = (item: Record<string, unknown>) => {
return item["plan"] !== undefined && item["plan"] !== null;
return item["plan"] !== undefined;
};
// Recursive helper function

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import userEvent from '@testing-library/user-event';
import { render, screen} from '@testing-library/react';
import Counter from '../src/components/components';

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {render, screen, waitFor, fireEvent} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import * as React from "react";

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {render, screen, fireEvent, act, waitFor} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom";

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { render, screen, act } from "@testing-library/react";
import "@testing-library/jest-dom";
import {type LogRecord, useLogs} from "../../../src/components/Logging/useLogs.ts";

View File

@@ -0,0 +1,3 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { render, screen, act, cleanup, waitFor } from '@testing-library/react';
import ConnectedRobots from '../../../src/pages/ConnectedRobots/ConnectedRobots';

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { render, screen, fireEvent, act } from '@testing-library/react';
import '@testing-library/jest-dom';
import MonitoringPage from '../../../src/pages/MonitoringPage/MonitoringPage';
@@ -27,7 +30,7 @@ jest.mock('../../../src/pages/MonitoringPage/MonitoringPageAPI', () => ({
// Mock VisProg functionality
jest.mock('../../../src/pages/VisProgPage/VisProgLogic', () => ({
graphReducer: jest.fn(),
runProgramm: jest.fn(),
runProgram: jest.fn(),
}));
// Mock Child Components to reduce noise (optional, but keeps unit test focused)
@@ -51,6 +54,7 @@ describe('MonitoringPage', () => {
const mockGetPhaseNames = jest.fn();
const mockGetNorms = jest.fn();
const mockGetGoals = jest.fn();
const mockGetGoalsWithDepth = jest.fn();
const mockGetTriggers = jest.fn();
const mockSetProgramState = jest.fn();
@@ -65,6 +69,7 @@ describe('MonitoringPage', () => {
getNormsInPhase: mockGetNorms,
getGoalsInPhase: mockGetGoals,
getTriggersInPhase: mockGetTriggers,
getGoalsWithDepth: mockGetGoalsWithDepth,
setProgramState: mockSetProgramState,
};
return selector(state);
@@ -81,7 +86,11 @@ describe('MonitoringPage', () => {
// Default mock return values
mockGetPhaseIds.mockReturnValue(['phase-1', 'phase-2']);
mockGetPhaseNames.mockReturnValue(['Intro', 'Main']);
mockGetGoals.mockReturnValue([{ id: 'g1', name: 'Goal 1' }, { id: 'g2', name: 'Goal 2' }]);
mockGetGoals.mockReturnValue([{ id: 'g1', name: 'Goal 1'}, { id: 'g2', name: 'Goal 2'}]);
mockGetGoalsWithDepth.mockReturnValue([
{ id: 'g1', name: 'Goal 1', level: 0 },
{ id: 'g2', name: 'Goal 2', level: 0 }
]);
mockGetTriggers.mockReturnValue([{ id: 't1', name: 'Trigger 1' }]);
mockGetNorms.mockReturnValue([
{ id: 'n1', norm: 'Norm 1', condition: null },
@@ -154,12 +163,12 @@ describe('MonitoringPage', () => {
expect(VisProg.graphReducer).toHaveBeenCalled();
expect(mockSetProgramState).toHaveBeenCalledWith({ phases: [{ id: 'new-phase' }] });
expect(VisProg.runProgramm).toHaveBeenCalled();
expect(VisProg.runProgram).toHaveBeenCalled();
});
test('Reset Experiment handles errors gracefully', async () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
(VisProg.runProgramm as jest.Mock).mockRejectedValue(new Error('Fail'));
(VisProg.runProgram as jest.Mock).mockRejectedValue(new Error('Fail'));
render(<MonitoringPage />);
await act(async () => {

View File

@@ -1,8 +1,10 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { renderHook, act, cleanup } from '@testing-library/react';
import {
sendAPICall,
nextPhase,
resetPhase,
nextPhase,
pauseExperiment,
playExperiment,
useExperimentLogger,
@@ -116,14 +118,6 @@ describe('MonitoringPageAPI', () => {
);
});
test('resetPhase sends correct params', async () => {
await resetPhase();
expect(globalThis.fetch).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({ body: JSON.stringify({ type: 'reset_phase', context: '' }) })
);
});
test('pauseExperiment sends correct params', async () => {
await pauseExperiment();
expect(globalThis.fetch).toHaveBeenCalledWith(

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import React from 'react';
import { render, screen, fireEvent, act } from '@testing-library/react';
import '@testing-library/jest-dom';

View File

@@ -1,3 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { render, screen, act, cleanup, fireEvent } from '@testing-library/react';
import Robot from '../../../src/pages/Robot/Robot';

View File

@@ -1,83 +0,0 @@
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();
});
});

Some files were not shown because too many files have changed in this diff Show More