13 Commits

Author SHA1 Message Date
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
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
133 changed files with 3232 additions and 745 deletions

515
package-lock.json generated
View File

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

View File

@@ -14,9 +14,11 @@
"dependencies": { "dependencies": {
"@neodrag/react": "^2.3.1", "@neodrag/react": "^2.3.1",
"@xyflow/react": "^12.8.6", "@xyflow/react": "^12.8.6",
"clsx": "^2.1.1",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-router": "^7.9.3", "react-router": "^7.9.3",
"reactflow": "^11.11.4",
"zustand": "^5.0.8" "zustand": "^5.0.8"
}, },
"devDependencies": { "devDependencies": {

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 { .logopepper {
height: 8em; height: 8em;
padding: 1.5em; padding: 1.5em;
@@ -161,7 +166,13 @@ input[type="checkbox"] {
.margin-0 { .margin-0 {
margin: 0; margin: 0;
} }
.margin-lg {
margin: 1rem;
}
.padding-0 {
padding: 0;
}
.padding-sm { .padding-sm {
padding: .25rem; padding: .25rem;
} }
@@ -171,11 +182,9 @@ input[type="checkbox"] {
.padding-lg { .padding-lg {
padding: 1rem; padding: 1rem;
} }
.padding-b-sm { .padding-h-lg {
padding-bottom: .25rem; padding-left: 1rem;
} padding-right: 1rem;
.padding-b-md {
padding-bottom: .5rem;
} }
.padding-b-lg { .padding-b-lg {
padding-bottom: 1rem; padding-bottom: 1rem;
@@ -204,6 +213,27 @@ input[type="checkbox"] {
border: 3px solid canvastext; border: 3px solid canvastext;
} }
.shadow-sm {
box-shadow: 0 0 0.25rem rgba(0, 0, 0, 0.25);
}
.shadow-md {
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.25);
}
.shadow-lg {
box-shadow: 0 0 1rem rgba(0, 0, 0, 0.25);
}
@media (prefers-color-scheme: dark) {
.shadow-sm {
box-shadow: 0 0 0.25rem rgba(0, 0, 0, 0.5);
}
.shadow-md {
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.5);
}
.shadow-lg {
box-shadow: 0 0 1rem rgba(0, 0, 0, 0.5);
}
}
.font-small { .font-small {
font-size: .75rem; font-size: .75rem;
} }
@@ -220,6 +250,9 @@ input[type="checkbox"] {
font-weight: bold; font-weight: bold;
} }
.relative {
position: relative;
}
.clickable { .clickable {
cursor: pointer; cursor: pointer;

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 { Routes, Route, Link } from 'react-router'
import './App.css' import './App.css'
import TemplatePage from './pages/TemplatePage/Template.tsx'
import Home from './pages/Home/Home.tsx' import Home from './pages/Home/Home.tsx'
import Robot from './pages/Robot/Robot.tsx'; import Robot from './pages/Robot/Robot.tsx';
import ConnectedRobots from './pages/ConnectedRobots/ConnectedRobots.tsx' import ConnectedRobots from './pages/ConnectedRobots/ConnectedRobots.tsx'
@@ -15,14 +17,14 @@ function App(){
return ( return (
<> <>
<header> <header>
<span>© Utrecht University (ICS)</span>
<Link to={"/"}>Home</Link> <Link to={"/"}>Home</Link>
<button onClick={() => setShowLogs(!showLogs)}>Toggle Logging</button> <button onClick={() => setShowLogs(!showLogs)}>Developer Logs</button>
</header> </header>
<div className={"flex-row justify-center flex-1 min-height-0"}> <div className={"flex-row justify-center flex-1 min-height-0"}>
<main className={"flex-col align-center flex-1 scroll-y"}> <main className={"flex-col align-center flex-1 scroll-y"}>
<Routes> <Routes>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/template" element={<TemplatePage />} />
<Route path="/editor" element={<VisProg />} /> <Route path="/editor" element={<VisProg />} />
<Route path="/robot" element={<Robot />} /> <Route path="/robot" element={<Robot />} />
<Route path="/ConnectedRobots" element={<ConnectedRobots />} /> <Route path="/ConnectedRobots" element={<ConnectedRobots />} />

48
src/components/Dialog.tsx Normal file
View File

@@ -0,0 +1,48 @@
import {type ReactNode, type RefObject, useEffect, useRef} from "react";
export default function Dialog({
open,
close,
classname,
children,
}: {
open: boolean;
close: () => void;
classname?: string;
children: ReactNode;
}) {
const ref: RefObject<HTMLDialogElement | null> = useRef(null);
useEffect(() => {
if (open) {
ref.current?.showModal();
} else {
ref.current?.close();
}
}, [open]);
function handleClickOutside(event: React.MouseEvent) {
if (!ref.current) return;
const dialogDimensions = ref.current.getBoundingClientRect()
if (
event.clientX < dialogDimensions.left ||
event.clientX > dialogDimensions.right ||
event.clientY < dialogDimensions.top ||
event.clientY > dialogDimensions.bottom
) {
close();
}
}
return (
<dialog
ref={ref}
onCancel={close}
onPointerDown={handleClickOutside}
className={classname}
>
{children}
</dialog>
);
}

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

@@ -0,0 +1,34 @@
// 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";
/**
* Zustand store definition for managing user preferences related to logging.
*
* Includes flags for toggling relative timestamps and automatic scroll behavior.
*/
export type LoggingSettings = {
/** Whether to display log timestamps as relative (e.g., "2m 15s ago") instead of absolute. */
showRelativeTime: boolean;
/** Updates the `showRelativeTime` setting. */
setShowRelativeTime: (showRelativeTime: boolean) => void;
};
/**
* Props for any component that renders a single log message entry.
*
* @param recordCell - A reactive `Cell` containing a single `LogRecord`.
* @param onUpdate - Optional callback triggered when the log entry updates.
*/
export type MessageComponentProps = {
recordCell: Cell<LogRecord>,
onUpdate?: () => void,
};
/**
* Key used for the experiment filter predicate in the filter map, to exclude experiment logs from the developer logs.
*/
export const EXPERIMENT_FILTER_KEY = "experiment_filter";
export const EXPERIMENT_LOGGER_NAME = "experiment";

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 { .filter-root {
position: relative; position: relative;
display: flex; 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 {useEffect, useRef, useState} from "react";
import type {LogFilterPredicate} from "./useLogs.ts"; import type {LogFilterPredicate} from "./useLogs.ts";
@@ -13,9 +16,8 @@ type Setter<T> = (value: T | ((prev: T) => T)) => void;
* Mapping of log level names to their corresponding numeric severity. * Mapping of log level names to their corresponding numeric severity.
* Used for comparison in log filtering predicates. * Used for comparison in log filtering predicates.
*/ */
const optionMapping = new Map([ const optionMapping: Map<string, number> = new Map([
["ALL", 0], ["ALL", 0],
["LLM", 9],
["DEBUG", 10], ["DEBUG", 10],
["INFO", 20], ["INFO", 20],
["WARNING", 30], ["WARNING", 30],
@@ -93,7 +95,7 @@ function GlobalLevelFilter({
filterPredicates: Map<string, LogFilterPredicate>; filterPredicates: Map<string, LogFilterPredicate>;
setFilterPredicates: Setter<Map<string, LogFilterPredicate>>; setFilterPredicates: Setter<Map<string, LogFilterPredicate>>;
}) { }) {
const selected = filterPredicates.get(GLOBAL_LOG_LEVEL_PREDICATE_KEY)?.value || "ALL"; const selected = filterPredicates.get(GLOBAL_LOG_LEVEL_PREDICATE_KEY)?.value ?? "ALL";
const setSelected = (selected: string | null) => { const setSelected = (selected: string | null) => {
if (!selected || !optionMapping.has(selected)) return; if (!selected || !optionMapping.has(selected)) return;

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 { .logging-container {
box-sizing: border-box; box-sizing: border-box;
@@ -5,7 +10,6 @@
flex-shrink: 0; flex-shrink: 0;
box-shadow: 0 0 1rem black; box-shadow: 0 0 1rem black;
padding: 1rem 1rem 0 1rem;
} }
.no-numbers { .no-numbers {
@@ -15,8 +19,6 @@
} }
.log-container { .log-container {
margin-bottom: .5rem;
.accented-0, .accented-10 { .accented-0, .accented-10 {
background-color: color-mix(in oklab, canvas, rgb(159, 159, 159) 35%) background-color: color-mix(in oklab, canvas, rgb(159, 159, 159) 35%)
} }
@@ -32,7 +34,7 @@
} }
.floating-button { .floating-button {
position: fixed; position: absolute;
bottom: 1rem; bottom: 1rem;
right: 1rem; right: 1rem;
box-shadow: 0 0 1rem rgba(0, 0, 0, 0.5); box-shadow: 0 0 1rem rgba(0, 0, 0, 0.5);

View File

@@ -1,38 +1,26 @@
import {useEffect, useRef, useState} from "react"; // This program has been developed by students from the bachelor Computer Science at Utrecht
import {create} from "zustand"; // 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 formatDuration from "../../utils/formatDuration.ts";
import {type LogFilterPredicate, type LogRecord, useLogs} from "./useLogs.ts"; import {type LogFilterPredicate, type LogRecord, useLogs} from "./useLogs.ts";
import Filters from "./Filters.tsx"; import Filters from "./Filters.tsx";
import {type Cell, useCell} from "../../utils/cellStore.ts"; import {type Cell, useCell} from "../../utils/cellStore.ts";
import styles from "./Logging.module.css"; import styles from "./Logging.module.css";
import {
EXPERIMENT_FILTER_KEY,
EXPERIMENT_LOGGER_NAME,
type LoggingSettings,
type MessageComponentProps
} from "./Definitions.ts";
import {create} from "zustand";
/** /**
* Zustand store definition for managing user preferences related to logging. * Local Zustand store for logging UI preferences.
*
* Includes flags for toggling relative timestamps and automatic scroll behavior.
*/
type LoggingSettings = {
/** Whether to display log timestamps as relative (e.g., "2m 15s ago") instead of absolute. */
showRelativeTime: boolean;
/** Updates the `showRelativeTime` setting. */
setShowRelativeTime: (showRelativeTime: boolean) => void;
/** Whether the log view should automatically scroll to the newest entry. */
scrollToBottom: boolean;
/** Updates the `scrollToBottom` setting. */
setScrollToBottom: (scrollToBottom: boolean) => void;
};
/**
* Global Zustand store for logging UI preferences.
*/ */
const useLoggingSettings = create<LoggingSettings>((set) => ({ const useLoggingSettings = create<LoggingSettings>((set) => ({
showRelativeTime: false, showRelativeTime: false,
setShowRelativeTime: (showRelativeTime: boolean) => set({ showRelativeTime }), setShowRelativeTime: (showRelativeTime: boolean) => set({ showRelativeTime }),
scrollToBottom: true,
setScrollToBottom: (scrollToBottom: boolean) => set({ scrollToBottom }),
})); }));
/** /**
@@ -45,13 +33,7 @@ const useLoggingSettings = create<LoggingSettings>((set) => ({
* @param onUpdate - Optional callback triggered when the log entry updates. * @param onUpdate - Optional callback triggered when the log entry updates.
* @returns A JSX element displaying a formatted log message. * @returns A JSX element displaying a formatted log message.
*/ */
function LogMessage({ function LogMessage({ recordCell, onUpdate }: MessageComponentProps) {
recordCell,
onUpdate,
}: {
recordCell: Cell<LogRecord>,
onUpdate?: () => void,
}) {
const { showRelativeTime, setShowRelativeTime } = useLoggingSettings(); const { showRelativeTime, setShowRelativeTime } = useLoggingSettings();
const record = useCell(recordCell); const record = useCell(recordCell);
@@ -69,7 +51,7 @@ function LogMessage({
/** Simplifies the logger name by showing only the last path segment. */ /** Simplifies the logger name by showing only the last path segment. */
const normalizedName = record.name.split(".").pop() || record.name; const normalizedName = record.name.split(".").pop() || record.name;
// Notify parent component (e.g. for scroll updates) when this record changes. // Notify the parent component (e.g., for scroll updates) when this record changes.
useEffect(() => { useEffect(() => {
if (onUpdate) onUpdate(); if (onUpdate) onUpdate();
}, [record, onUpdate]); }, [record, onUpdate]);
@@ -77,11 +59,10 @@ function LogMessage({
return <div className={`${styles.logContainer} round-md border-lg flex-row gap-md`}> return <div className={`${styles.logContainer} round-md border-lg flex-row gap-md`}>
<div className={`${styles[`accented${normalizedLevelNo}`]} flex-col padding-sm justify-between`}> <div className={`${styles[`accented${normalizedLevelNo}`]} flex-col padding-sm justify-between`}>
<span className={"mono bold"}>{record.levelname}</span> <span className={"mono bold"}>{record.levelname}</span>
<span className={"mono clickable font-small"} <span className={"mono clickable font-small"} onClick={() => setShowRelativeTime(!showRelativeTime)}>{
onClick={() => setShowRelativeTime(!showRelativeTime)} showRelativeTime
>{showRelativeTime ? formatDuration(record.relativeCreated)
? formatDuration(record.relativeCreated) : new Date(record.created * 1000).toLocaleTimeString()
: new Date(record.created * 1000).toLocaleTimeString()
}</span> }</span>
</div> </div>
<div className={"flex-col flex-1 padding-sm"}> <div className={"flex-col flex-1 padding-sm"}>
@@ -100,12 +81,18 @@ function LogMessage({
* - A floating "Scroll to bottom" button when not at the bottom. * - A floating "Scroll to bottom" button when not at the bottom.
* *
* @param recordCells - Array of reactive log records to display. * @param recordCells - Array of reactive log records to display.
* @param MessageComponent - A component to use to render each log message entry.
* @returns A scrollable log list component. * @returns A scrollable log list component.
*/ */
function LogMessages({ recordCells }: { recordCells: Cell<LogRecord>[] }) { export function LogMessages({
recordCells,
MessageComponent,
}: {
recordCells: Cell<LogRecord>[],
MessageComponent: ComponentType<MessageComponentProps>,
}) {
const scrollableRef = useRef<HTMLDivElement>(null); const scrollableRef = useRef<HTMLDivElement>(null);
const lastElementRef = useRef<HTMLLIElement>(null) const [scrollToBottom, setScrollToBottom] = useState(true);
const { scrollToBottom, setScrollToBottom } = useLoggingSettings();
// Disable auto-scroll if the user manually scrolls. // Disable auto-scroll if the user manually scrolls.
useEffect(() => { useEffect(() => {
@@ -124,30 +111,28 @@ function LogMessages({ recordCells }: { recordCells: Cell<LogRecord>[] }) {
}, [scrollableRef, setScrollToBottom]); }, [scrollableRef, setScrollToBottom]);
/** /**
* Scrolls the last log message into view if auto-scroll is enabled, * Scrolls the log messages to the bottom, making the latest messages visible.
* or if forced (e.g., user clicks "Scroll to bottom").
* *
* @param force - If true, forces scrolling even if `scrollToBottom` is false. * @param force - If true, forces scrolling even if `scrollToBottom` is false.
*/ */
function scrollLastElementIntoView(force = false) { function showBottom(force = false) {
if ((!scrollToBottom && !force) || !lastElementRef.current) return; if ((!scrollToBottom && !force) || !scrollableRef.current) return;
lastElementRef.current.scrollIntoView({ behavior: "smooth" }); scrollableRef.current.scrollTo({top: scrollableRef.current.scrollHeight, left: 0, behavior: "smooth"});
} }
return <div ref={scrollableRef} className={"min-height-0 scroll-y padding-b-md"}> return <div ref={scrollableRef} className={"min-height-0 scroll-y padding-h-lg padding-b-lg flex-1"}>
<ol className={`${styles.noNumbers} margin-0 flex-col gap-md`}> <ol className={`${styles.noNumbers} margin-0 flex-col gap-md`}>
{recordCells.map((recordCell, i) => ( {recordCells.map((recordCell, i) => (
<li key={`${i}_${recordCell.get().firstRelativeCreated}`}> <li key={`${i}_${recordCell.get().firstRelativeCreated}`}>
<LogMessage recordCell={recordCell} onUpdate={scrollLastElementIntoView} /> <MessageComponent recordCell={recordCell} onUpdate={showBottom} />
</li> </li>
))} ))}
<li ref={lastElementRef}></li>
</ol> </ol>
{!scrollToBottom && <button {!scrollToBottom && <button
className={styles.floatingButton} className={styles.floatingButton}
onClick={() => { onClick={() => {
setScrollToBottom(true); setScrollToBottom(true);
scrollLastElementIntoView(true); showBottom(true);
}} }}
> >
Scroll to bottom Scroll to bottom
@@ -164,16 +149,27 @@ function LogMessages({ recordCells }: { recordCells: Cell<LogRecord>[] }) {
* - Zustand-managed UI settings (auto-scroll, timestamp display). * - Zustand-managed UI settings (auto-scroll, timestamp display).
* *
* This component uses the `useLogs` hook to fetch and filter logs based on * This component uses the `useLogs` hook to fetch and filter logs based on
* active predicates, and re-renders automatically as new logs arrive. * active predicates and re-renders automatically as new logs arrive.
* *
* @returns The complete logging UI as a React element. * @returns The complete logging UI as a React element.
*/ */
export default function Logging() { export default function Logging() {
const [filterPredicates, setFilterPredicates] = useState(new Map<string, LogFilterPredicate>()); // By default, filter experiment logs from this debug logger
const [filterPredicates, setFilterPredicates] = useState(new Map<string, LogFilterPredicate>([
[
EXPERIMENT_FILTER_KEY,
{
predicate: (r) => r.name == EXPERIMENT_LOGGER_NAME ? false : null,
priority: 999,
value: null,
} as LogFilterPredicate,
],
]));
const { filteredLogs, distinctNames } = useLogs(filterPredicates) const { filteredLogs, distinctNames } = useLogs(filterPredicates)
distinctNames.delete(EXPERIMENT_LOGGER_NAME);
return <div className={`flex-col gap-lg min-height-0 ${styles.loggingContainer}`}> return <div className={`flex-col min-height-0 relative ${styles.loggingContainer}`}>
<div className={"flex-row gap-lg justify-between align-center"}> <div className={"flex-row gap-lg justify-between align-center padding-lg"}>
<h2 className={"margin-0"}>Logs</h2> <h2 className={"margin-0"}>Logs</h2>
<Filters <Filters
filterPredicates={filterPredicates} filterPredicates={filterPredicates}
@@ -181,6 +177,6 @@ export default function Logging() {
agentNames={distinctNames} agentNames={distinctNames}
/> />
</div> </div>
<LogMessages recordCells={filteredLogs} /> <LogMessages recordCells={filteredLogs} MessageComponent={LogMessage} />
</div>; </div>;
} }

View File

@@ -1,8 +1,26 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {useCallback, useEffect, useRef, useState} from "react"; import {useCallback, useEffect, useRef, useState} from "react";
import {applyPriorityPredicates, type PriorityFilterPredicate} from "../../utils/priorityFiltering.ts"; import {applyPriorityPredicates, type PriorityFilterPredicate} from "../../utils/priorityFiltering.ts";
import {cell, type Cell} from "../../utils/cellStore.ts"; import {cell, type Cell} from "../../utils/cellStore.ts";
type ExtraLevelName = 'LLM' | 'OBSERVATION' | 'ACTION' | 'CHAT';
export type LevelName = ExtraLevelName | 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL' | string;
/**
* Extra fields that are added to log records in the backend but are not part of the standard `LogRecord` type.
*
* @property reference - (Optional) A reference identifier linking related log messages.
* @property role - (Optional) For chat log messages, the role of the agent that generated the message.
*/
type ExtraLogRecordFields = {
reference?: string;
role?: string;
}
/** /**
* Represents a single log record emitted by the backend logging system. * Represents a single log record emitted by the backend logging system.
* *
@@ -12,21 +30,19 @@ import {cell, type Cell} from "../../utils/cellStore.ts";
* @property levelno - The numeric severity value corresponding to `levelname`. * @property levelno - The numeric severity value corresponding to `levelname`.
* @property created - The UNIX timestamp (in seconds) when this record was created. * @property created - The UNIX timestamp (in seconds) when this record was created.
* @property relativeCreated - The time (in milliseconds) since the logging system started. * @property relativeCreated - The time (in milliseconds) since the logging system started.
* @property reference - (Optional) A reference identifier linking related log messages.
* @property firstCreated - Timestamp of the first log in this reference group. * @property firstCreated - Timestamp of the first log in this reference group.
* @property firstRelativeCreated - Relative timestamp of the first log in this reference group. * @property firstRelativeCreated - Relative timestamp of the first log in this reference group.
*/ */
export type LogRecord = { export type LogRecord = {
name: string; name: string;
message: string; message: string;
levelname: 'LLM' | 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL' | string; levelname: LevelName;
levelno: number; levelno: number;
created: number; created: number;
relativeCreated: number; relativeCreated: number;
reference?: string;
firstCreated: number; firstCreated: number;
firstRelativeCreated: number; firstRelativeCreated: number;
}; } & ExtraLogRecordFields;
/** /**
* A log filter predicate with priority support, used to determine whether * A log filter predicate with priority support, used to determine whether
@@ -37,7 +53,7 @@ export type LogRecord = {
* *
* @template T - The type of record being filtered (here, `LogRecord`). * @template T - The type of record being filtered (here, `LogRecord`).
*/ */
export type LogFilterPredicate = PriorityFilterPredicate<LogRecord> & { export type LogFilterPredicate = PriorityFilterPredicate<LogRecord> & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any }; value: any };

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 { useEffect, useRef, useState } from "react";
import styles from "./TextField.module.css"; 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"; 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 { .text-field {
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 5pt; 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 {useEffect, useState} from "react";
import styles from "./TextField.module.css"; 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' 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 { :root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5; line-height: 1.5;
@@ -52,7 +63,7 @@ button {
font-size: 1em; font-size: 1em;
font-weight: 500; font-weight: 500;
font-family: inherit; font-family: inherit;
background-color: #1a1a1a; background-color: canvas;
cursor: pointer; cursor: pointer;
transition: border-color 0.25s; transition: border-color 0.25s;
} }
@@ -75,9 +86,6 @@ button:focus-visible {
--dropdown-menu-background-color: rgb(247, 247, 247); --dropdown-menu-background-color: rgb(247, 247, 247);
--dropdown-menu-border: rgba(207, 207, 207, 0.986); --dropdown-menu-border: rgba(207, 207, 207, 0.986);
} }
button {
background-color: #f9f9f9;
}
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {

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 { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router' 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' 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 { .read_the_docs {
color: #888; 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 { Link } from 'react-router'
import pepperLogo from '../../assets/pepper_transp2_small.svg' import pepperLogo from '../../assets/pepper_transp2_small.svg'
import styles from './Home.module.css' import styles from './Home.module.css'
@@ -21,7 +24,6 @@ function Home() {
<div className={styles.links}> <div className={styles.links}>
<Link to={"/robot"}>Robot Interaction </Link> <Link to={"/robot"}>Robot Interaction </Link>
<Link to={"/editor"}>Editor </Link> <Link to={"/editor"}>Editor </Link>
<Link to={"/template"}>Template </Link>
<Link to={"/ConnectedRobots"}>Connected Robots </Link> <Link to={"/ConnectedRobots"}>Connected Robots </Link>
</div> </div>
</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 { .dashboardContainer {
display: grid; display: grid;
grid-template-columns: 2fr 1fr; /* Left = content, Right = logs */ grid-template-columns: 2fr 1fr; /* Left = content, Right = logs */
@@ -28,6 +33,22 @@
position: static; /* ensures it scrolls away */ 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 { .phaseProgress {
margin-top: 0.5rem; margin-top: 0.5rem;
} }
@@ -77,11 +98,6 @@
color: white; color: white;
} }
.restartPhase{
background-color: rgb(255, 123, 0);
color: white;
}
.restartExperiment{ .restartExperiment{
background-color: red; background-color: red;
color: white; color: white;
@@ -194,32 +210,6 @@
line-height: 1.4; line-height: 1.4;
} }
/* LOGS */
.logs {
grid-area: logs;
background: var(--bg-surface);
color: var(--text-main);
box-shadow: var(--panel-shadow);
padding: 1rem;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
.logs textarea {
width: 100%;
height: 83%;
margin-top: 0.5rem;
background-color: Canvas;
color: CanvasText;
border: 1px solid var(--border-color);
}
.logs button {
background: var(--bg-surface);
box-shadow: var(--panel-shadow);
margin-top: 0.5rem;
margin-left: 0.5rem;
}
/* FOOTER */ /* FOOTER */
.controlsSection { .controlsSection {
grid-area: footer; grid-area: footer;

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, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import styles from './MonitoringPage.module.css'; import styles from './MonitoringPage.module.css';
@@ -15,7 +18,7 @@ import {
type CondNormsStateUpdate, type CondNormsStateUpdate,
type PhaseUpdate type PhaseUpdate
} from "./MonitoringPageAPI"; } from "./MonitoringPageAPI";
import { graphReducer, runProgramm } from '../VisProgPage/VisProgLogic.ts'; import { graphReducer, runProgram } from '../VisProgPage/VisProgLogic.ts';
// Types // Types
import type { NormNodeData } from '../VisProgPage/visualProgrammingUI/nodes/NormNode'; import type { NormNodeData } from '../VisProgPage/visualProgrammingUI/nodes/NormNode';
@@ -30,6 +33,7 @@ import {
StatusList, StatusList,
RobotConnected RobotConnected
} from './MonitoringPageComponents'; } from './MonitoringPageComponents';
import ExperimentLogs from "./components/ExperimentLogs.tsx";
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// 1. State management // 1. State management
@@ -101,7 +105,6 @@ function useExperimentLogic() {
}, [getPhaseIds, getGoalsInPhase, phaseIds, phaseIndex, phaseNames]); }, [getPhaseIds, getGoalsInPhase, phaseIds, phaseIndex, phaseNames]);
const handleStatusUpdate = useCallback((data: unknown) => { const handleStatusUpdate = useCallback((data: unknown) => {
const payload = data as CondNormsStateUpdate; const payload = data as CondNormsStateUpdate;
if (payload.type !== 'cond_norms_state_update') return; if (payload.type !== 'cond_norms_state_update') return;
@@ -132,7 +135,7 @@ function useExperimentLogic() {
setGoalIndex(0); setGoalIndex(0);
setIsFinished(false); setIsFinished(false);
await runProgramm(); await runProgram();
console.log("Experiment & UI successfully reset."); console.log("Experiment & UI successfully reset.");
} catch (err) { } catch (err) {
console.error("Failed to reset program:", err); console.error("Failed to reset program:", err);
@@ -141,7 +144,7 @@ function useExperimentLogic() {
} }
}, [setProgramState]); }, [setProgramState]);
const handleControlAction = async (action: "pause" | "play" | "nextPhase" | "resetPhase") => { const handleControlAction = async (action: "pause" | "play" | "nextPhase") => {
try { try {
setLoading(true); setLoading(true);
switch (action) { switch (action) {
@@ -156,7 +159,6 @@ function useExperimentLogic() {
case "nextPhase": case "nextPhase":
await nextPhase(); await nextPhase();
break; break;
// Case for resetPhase if implemented in API
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@@ -224,7 +226,7 @@ function ControlPanel({
}: { }: {
loading: boolean, loading: boolean,
isPlaying: boolean, isPlaying: boolean,
onAction: (a: "pause" | "play" | "nextPhase" | "resetPhase") => void, onAction: (a: "pause" | "play" | "nextPhase") => void,
onReset: () => void onReset: () => void
}) { }) {
return ( return (
@@ -249,12 +251,6 @@ function ControlPanel({
disabled={loading} disabled={loading}
></button> ></button>
<button
className={styles.restartPhase}
onClick={() => onAction("resetPhase")}
disabled={loading}
></button>
<button <button
className={styles.restartExperiment} className={styles.restartExperiment}
onClick={onReset} onClick={onReset}
@@ -279,14 +275,17 @@ function PhaseDashboard({
setActiveIds: React.Dispatch<React.SetStateAction<Record<string, boolean>>>, setActiveIds: React.Dispatch<React.SetStateAction<Record<string, boolean>>>,
goalIndex: number goalIndex: number
}) { }) {
const getGoals = useProgramStore((s) => s.getGoalsInPhase); const getGoalsWithDepth = useProgramStore((s) => s.getGoalsWithDepth);
const getTriggers = useProgramStore((s) => s.getTriggersInPhase); const getTriggers = useProgramStore((s) => s.getTriggersInPhase);
const getNorms = useProgramStore((s) => s.getNormsInPhase); const getNorms = useProgramStore((s) => s.getNormsInPhase);
// Prepare data view models // Prepare data view models
const goals = (getGoals(phaseId) as GoalNode[]).map(g => ({ const goals = getGoalsWithDepth(phaseId).map((g) => ({
...g, ...g,
achieved: activeIds[g.id] ?? false, id: g.id as string,
name: g.name as string,
achieved: activeIds[g.id as string] ?? false,
level: g.level, // Pass this new property to the UI
})); }));
const triggers = (getTriggers(phaseId) as TriggerNode[]).map(t => ({ const triggers = (getTriggers(phaseId) as TriggerNode[]).map(t => ({
@@ -386,17 +385,8 @@ const MonitoringPage: React.FC = () => {
)} )}
</main> </main>
{/* LOGS TODO: add actual logs */} {/* LOGS */}
<aside className={styles.logs}> <ExperimentLogs />
<h3>Logs</h3>
<div className={styles.logHeader}>
<span>Global:</span>
<button>ALL</button>
<button>Add</button>
<button className={styles.live}>Live</button>
</div>
<textarea defaultValue="Example Log: much log"></textarea>
</aside>
{/* FOOTER */} {/* FOOTER */}
<footer className={styles.controlsSection}> <footer className={styles.controlsSection}>

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'; import React, { useEffect } from 'react';
const API_BASE = "http://localhost:8000"; 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 * 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 React, { useEffect, useState } from 'react';
import styles from './MonitoringPage.module.css'; import styles from './MonitoringPage.module.css';
import { sendAPICall } from './MonitoringPageAPI'; import { sendAPICall } from './MonitoringPageAPI';
@@ -91,13 +94,14 @@ export const DirectSpeechInput: React.FC = () => {
}; };
// --- interface for goals/triggers/norms/conditional norms --- // --- interface for goals/triggers/norms/conditional norms ---
type StatusItem = { export type StatusItem = {
id?: string | number; id?: string | number;
achieved?: boolean; achieved?: boolean;
description?: string; description?: string;
label?: string; label?: string;
norm?: string; norm?: string;
name?: string; name?: string;
level?: number;
}; };
interface StatusListProps { interface StatusListProps {
@@ -129,7 +133,7 @@ export const StatusList: React.FC<StatusListProps> = ({
const isCurrentGoal = type === 'goal' && idx === currentGoalIndex; const isCurrentGoal = type === 'goal' && idx === currentGoalIndex;
const canOverride = (showIndicator && !isActive) || (type === 'cond_norm' && isActive); const canOverride = (showIndicator && !isActive) || (type === 'cond_norm' && isActive);
const indentation = (item.level || 0) * 20;
const handleOverrideClick = () => { const handleOverrideClick = () => {
if (!canOverride) return; if (!canOverride) return;
@@ -147,7 +151,10 @@ export const StatusList: React.FC<StatusListProps> = ({
}; };
return ( return (
<li key={item.id ?? idx} className={styles.statusItem}> <li key={item.id ?? idx}
className={styles.statusItem}
style={{ paddingLeft: `${indentation}px` }}
>
{showIndicator && ( {showIndicator && (
<span <span
className={`${styles.statusIndicator} ${isActive ? styles.active : styles.inactive} ${canOverride ? styles.clickable : ''}`} className={`${styles.statusIndicator} ${isActive ? styles.active : styles.inactive} ${canOverride ? styles.clickable : ''}`}

View File

@@ -0,0 +1,39 @@
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/
.logs {
/* grid-area used in MonitoringPage.module.css */
grid-area: logs;
box-shadow: var(--panel-shadow);
height: 900px;
width: 450px;
.live {
width: .5rem;
height: .5rem;
left: .5rem;
background: red;
border-radius: 50%;
}
}
.chat-message.alternate {
align-items: end;
text-align: end;
background-color: color-mix(in oklab, canvas, 75% #86c4fa);
.message-head {
flex-direction: row-reverse;
}
}
.download-list {
box-sizing: border-box;
list-style: none;
height: 50dvh;
min-width: 300px;
}

View File

@@ -0,0 +1,189 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import styles from "./ExperimentLogs.module.css";
import {LogMessages} from "../../../components/Logging/Logging.tsx";
import {useEffect, useMemo, useState} from "react";
import {type LogFilterPredicate, type LogRecord, useLogs} from "../../../components/Logging/useLogs.ts";
import capitalize from "../../../utils/capitalize.ts";
import {useCell} from "../../../utils/cellStore.ts";
import {
EXPERIMENT_FILTER_KEY,
EXPERIMENT_LOGGER_NAME,
type LoggingSettings,
type MessageComponentProps,
} from "../../../components/Logging/Definitions.ts";
import formatDuration from "../../../utils/formatDuration.ts";
import {create} from "zustand";
import Dialog from "../../../components/Dialog.tsx";
import delayedResolve from "../../../utils/delayedResolve.ts";
/**
* Local Zustand store for logging UI preferences.
*/
const useLoggingSettings = create<LoggingSettings>((set) => ({
showRelativeTime: false,
setShowRelativeTime: (showRelativeTime: boolean) => set({ showRelativeTime }),
}));
/**
* A dedicated component for rendering chat messages.
*
* @param record The chat record to render.
*/
function ChatMessage({ record }: { record: LogRecord }) {
const { showRelativeTime, setShowRelativeTime } = useLoggingSettings();
const reverse = record.role === "user" ? styles.alternate : "";
return <div className={`${styles.chatMessage} ${reverse} flex-col padding-md padding-h-lg shadow-md round-md`}>
<div className={`${styles.messageHead} flex-row gap-md align-center`}>
<span className={"bold"}>{capitalize(record.role ?? "unknown")}</span>
<span className={"font-small"}></span>
<span className={"mono clickable font-small"} onClick={() => setShowRelativeTime(!showRelativeTime)}>{
showRelativeTime
? formatDuration(record.relativeCreated)
: new Date(record.created * 1000).toLocaleTimeString()
}</span>
</div>
<span>{record.message}</span>
</div>
}
/**
* A generic log message component showing the log level, time, and message text.
*
* @param record The log record to render.
*/
function DefaultMessage({ record }: { record: LogRecord }) {
const { showRelativeTime, setShowRelativeTime } = useLoggingSettings();
return <div>
<div className={"flex-row gap-md align-center"}>
<span className={"font-small"}>{record.levelname}</span>
<span className={"font-small"}></span>
<span className={"mono clickable font-small"} onClick={() => setShowRelativeTime(!showRelativeTime)}>{
showRelativeTime
? formatDuration(record.relativeCreated)
: new Date(record.created * 1000).toLocaleTimeString()
}</span>
</div>
<span>{record.message}</span>
</div>;
}
/**
* A custom component for rendering experiment messages, which might include chat messages.
*
* @param recordCell The cell containing the log record to render.
* @param onUpdate A callback to notify the parent component when the record changes.
*/
function ExperimentMessage({recordCell, onUpdate}: MessageComponentProps) {
const record = useCell(recordCell);
// Notify the parent component (e.g., for scroll updates) when this record changes.
useEffect(() => {
if (onUpdate) onUpdate();
}, [record, onUpdate]);
if (record.levelname == "CHAT") {
return <ChatMessage record={record} />
} else {
return <DefaultMessage record={record} />
}
}
/**
* A download dialog listing experiment logs to download.
*
* @param filenames The list of available experiment logs to download.
* @param refresh A callback to refresh the list of available experiment logs.
*/
function DownloadScreen({filenames, refresh}: {filenames: string[] | null, refresh: () => void}) {
const list = (() => {
if (filenames == null) return <div className={`${styles.downloadList} flex-col align-center justify-center`}>
<p>Loading...</p>
</div>;
if (filenames.length === 0) return <div className={`${styles.downloadList} flex-col align-center justify-center`}>
<p>No files available.</p>
</div>
return <ol className={`${styles.downloadList} margin-0 padding-h-lg scroll-y`}>
{filenames!.map((filename) => (
<li><a key={filename} href={`http://localhost:8000/api/logs/files/${filename}`} download={filename}>{filename}</a></li>
))}
</ol>;
})();
return <div className={"flex-col"}>
<p className={"margin-lg"}>Select a file to download:</p>
{list}
<button onClick={refresh} className={"margin-lg shadow-sm"}>Refresh</button>
</div>;
}
/**
* A button that opens a download dialog for experiment logs when pressed.
*/
function DownloadButton() {
const [showModal, setShowModal] = useState(false);
const [filenames, setFilenames] = useState<string[] | null>(null);
async function getFiles(): Promise<string[]> {
const response = await fetch("http://localhost:8000/api/logs/files");
const files = await response.json();
files.sort();
return files;
}
useEffect(() => {
getFiles().then(setFilenames);
}, [showModal]);
return <>
<button className={"shadow-sm"} onClick={() => setShowModal((curr) => !curr)}>Download...</button>
<Dialog open={showModal} close={() => setShowModal(false)} classname={"padding-0 round-lg"}>
<DownloadScreen filenames={filenames} refresh={async () => {
setFilenames(null);
const files = await delayedResolve(getFiles(), 250);
setFilenames(files);
}} />
</Dialog>
</>;
}
/**
* A component for rendering experiment logs. This component uses the `useLogs` hook with a filter to show only
* experiment logs.
*/
export default function ExperimentLogs() {
// Show only experiment logs in this logger
const filters = useMemo(() => new Map<string, LogFilterPredicate>([
[
EXPERIMENT_FILTER_KEY,
{
predicate: (r) => r.name == EXPERIMENT_LOGGER_NAME,
priority: 999,
value: null,
} as LogFilterPredicate,
],
]), []);
const { filteredLogs } = useLogs(filters);
return <aside className={`${styles.logs} flex-col relative`}>
<div className={`${styles.head} padding-lg`}>
<div className={"flex-row align-center justify-between"}>
<h3>Logs</h3>
<div className={"flex-row gap-md align-center"}>
<div className={`flex-row align-center gap-md relative padding-md shadow-sm round-md`}>
<div className={styles.live}></div>
<span>Live</span>
</div>
<DownloadButton />
</div>
</div>
</div>
<LogMessages recordCells={filteredLogs} MessageComponent={ExperimentMessage} />
</aside>;
}

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' 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 */ /* editor UI */
.inner-editor-container { .inner-editor-container {

View File

@@ -1,23 +1,29 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { import {
Background, Background,
Controls, Controls,
Panel, Panel,
ReactFlow, ReactFlow,
ReactFlowProvider, ReactFlowProvider,
MarkerType, MarkerType, getOutgoers
} from '@xyflow/react'; } from '@xyflow/react';
import '@xyflow/react/dist/style.css'; import '@xyflow/react/dist/style.css';
import warningStyles from './visualProgrammingUI/components/WarningSidebar.module.css'
import {type CSSProperties, useEffect, useState} from "react"; import {type CSSProperties, useEffect, useState} from "react";
import {useShallow} from 'zustand/react/shallow'; import {useShallow} from 'zustand/react/shallow';
import useProgramStore from "../../utils/programStore.ts"; import useProgramStore from "../../utils/programStore.ts";
import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx'; import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx';
import {type EditorWarning, globalWarning} from "./visualProgrammingUI/components/EditorWarnings.tsx";
import {WarningsSidebar} from "./visualProgrammingUI/components/WarningSidebar.tsx";
import useFlowStore from './visualProgrammingUI/VisProgStores.tsx'; import useFlowStore from './visualProgrammingUI/VisProgStores.tsx';
import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx'; import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx';
import styles from './VisProg.module.css' import styles from './VisProg.module.css'
import {NodeTypes} from './visualProgrammingUI/NodeRegistry.ts'; import {NodeTypes} from './visualProgrammingUI/NodeRegistry.ts';
import SaveLoadPanel from './visualProgrammingUI/components/SaveLoadPanel.tsx'; import SaveLoadPanel from './visualProgrammingUI/components/SaveLoadPanel.tsx';
import MonitoringPage from '../MonitoringPage/MonitoringPage.tsx'; import MonitoringPage from '../MonitoringPage/MonitoringPage.tsx';
import { graphReducer, runProgramm } from './VisProgLogic.ts'; import {graphReducer, runProgram} from './VisProgLogic.ts';
// --| config starting params for flow |-- // --| config starting params for flow |--
@@ -42,6 +48,7 @@ const selector = (state: FlowState) => ({
nodes: state.nodes, nodes: state.nodes,
edges: state.edges, edges: state.edges,
onNodesChange: state.onNodesChange, onNodesChange: state.onNodesChange,
onNodesDelete: state.onNodesDelete,
onEdgesDelete: state.onEdgesDelete, onEdgesDelete: state.onEdgesDelete,
onEdgesChange: state.onEdgesChange, onEdgesChange: state.onEdgesChange,
onConnect: state.onConnect, onConnect: state.onConnect,
@@ -67,6 +74,7 @@ const VisProgUI = () => {
const { const {
nodes, edges, nodes, edges,
onNodesChange, onNodesChange,
onNodesDelete,
onEdgesDelete, onEdgesDelete,
onEdgesChange, onEdgesChange,
onConnect, onConnect,
@@ -89,15 +97,36 @@ const VisProgUI = () => {
window.addEventListener('keydown', handler); window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler);
}); });
const {unregisterWarning, registerWarning} = useFlowStore();
useEffect(() => {
if (checkPhaseChain()) {
unregisterWarning(globalWarning,'INCOMPLETE_PROGRAM');
} else {
// create global warning for incomplete program chain
const incompleteProgramWarning : EditorWarning = {
scope: {
id: globalWarning,
handleId: undefined
},
type: 'INCOMPLETE_PROGRAM',
severity: "ERROR",
description: "there is no complete phase chain from the startNode to the EndNode"
}
registerWarning(incompleteProgramWarning);
}
},[edges, registerWarning, unregisterWarning])
return ( return (
<div className={`${styles.innerEditorContainer} round-lg border-lg`} style={({'--flow-zoom': zoom} as CSSProperties)}> <div className={`${styles.innerEditorContainer} round-lg border-lg flex-row`} style={({'--flow-zoom': zoom} as CSSProperties)}>
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
edges={edges} edges={edges}
defaultEdgeOptions={DEFAULT_EDGE_OPTIONS} defaultEdgeOptions={DEFAULT_EDGE_OPTIONS}
nodeTypes={NodeTypes} nodeTypes={NodeTypes}
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
onNodesDelete={onNodesDelete}
onEdgesDelete={onEdgesDelete} onEdgesDelete={onEdgesDelete}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
onReconnect={onReconnect} onReconnect={onReconnect}
@@ -108,9 +137,11 @@ const VisProgUI = () => {
onNodeDragStop={endBatchAction} onNodeDragStop={endBatchAction}
preventScrolling={scrollable} preventScrolling={scrollable}
onMove={(_, viewport) => setZoom(viewport.zoom)} onMove={(_, viewport) => setZoom(viewport.zoom)}
reconnectRadius={15}
snapToGrid snapToGrid
fitView fitView
proOptions={{hideAttribution: true}} proOptions={{hideAttribution: true}}
style={{flexGrow: 3}}
> >
<Panel position="top-center" className={styles.dndPanel}> <Panel position="top-center" className={styles.dndPanel}>
<DndToolbar/> {/* contains the drag and drop panel for nodes */} <DndToolbar/> {/* contains the drag and drop panel for nodes */}
@@ -119,12 +150,16 @@ const VisProgUI = () => {
<SaveLoadPanel></SaveLoadPanel> <SaveLoadPanel></SaveLoadPanel>
</Panel> </Panel>
<Panel position="bottom-center"> <Panel position="bottom-center">
<button onClick={() => undo()}>undo</button> <button onClick={() => undo()}>Undo</button>
<button onClick={() => redo()}>Redo</button> <button onClick={() => redo()}>Redo</button>
</Panel> </Panel>
<Panel position="center-right" className={warningStyles.warningsSidebar}>
<WarningsSidebar/>
</Panel>
<Controls/> <Controls/>
<Background/> <Background/>
</ReactFlow> </ReactFlow>
</div> </div>
); );
}; };
@@ -143,7 +178,24 @@ function VisualProgrammingUI() {
</ReactFlowProvider> </ReactFlowProvider>
); );
} }
const checkPhaseChain = (): boolean => {
const {nodes, edges} = useFlowStore.getState();
function checkForCompleteChain(currentNodeId: string): boolean {
const outgoingPhases = getOutgoers({id: currentNodeId}, nodes, edges)
.filter(node => ["end", "phase"].includes(node.type!));
if (outgoingPhases.length === 0) return false;
if (outgoingPhases.some(node => node.type === "end" )) return true;
const next = outgoingPhases.map(node => checkForCompleteChain(node.id))
.find(result => result);
return !!next;
}
return checkForCompleteChain('start');
};
/** /**
* houses the entire page, so also UI elements * houses the entire page, so also UI elements
@@ -152,30 +204,43 @@ function VisualProgrammingUI() {
*/ */
function VisProgPage() { function VisProgPage() {
const [showSimpleProgram, setShowSimpleProgram] = useState(false); const [showSimpleProgram, setShowSimpleProgram] = useState(false);
const [programValidity, setProgramValidity] = useState<boolean>(true);
const {isProgramValid, severityIndex} = useFlowStore();
const setProgramState = useProgramStore((state) => state.setProgramState); 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 const phases = graphReducer(); // reduce graph
setProgramState({ phases }); // <-- save to store setProgramState({ phases }); // <-- save to store
setShowSimpleProgram(true); // show SimpleProgram setShowSimpleProgram(true); // show SimpleProgram
runProgramm(); // send to backend if needed runProgram(); // send to backend if needed
}; };
if (showSimpleProgram) { if (showSimpleProgram) {
return ( return (
<div> <div>
<button className={styles.backButton} onClick={() => setShowSimpleProgram(false)}> <button className={styles.backButton} onClick={() => setShowSimpleProgram(false)}>
Back to Editor Back to Editor
</button> </button>
<MonitoringPage/> <MonitoringPage/>
</div> </div>
); );
} }
return ( return (
<> <>
<VisualProgrammingUI/> <VisualProgrammingUI/>
<button onClick={runProgram}>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 useProgramStore from "../../utils/programStore";
import orderPhaseNodeArray from "../../utils/orderPhaseNodes"; import orderPhaseNodeArray from "../../utils/orderPhaseNodes";
import useFlowStore from './visualProgrammingUI/VisProgStores'; 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 * Outputs the prepared program to the console and sends it to the backend
*/ */
export function runProgramm() { export function runProgram() {
const phases = graphReducer(); const phases = graphReducer();
const program = {phases} const program = {phases}
console.log(JSON.stringify(program, null, 2)); 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 {Edge, Node} from "@xyflow/react";
import type {StateCreator, StoreApi } from 'zustand/vanilla'; import type {StateCreator, StoreApi } from 'zustand/vanilla';
import type {
SeverityIndex,
WarningRegistry
} from "./components/EditorWarnings.tsx";
import type {FlowState} from "./VisProgTypes.tsx"; import type {FlowState} from "./VisProgTypes.tsx";
export type FlowSnapshot = { export type FlowSnapshot = {
nodes: Node[]; nodes: Node[];
edges: Edge[]; edges: Edge[];
warnings: {
warningRegistry: WarningRegistry;
severityIndex: SeverityIndex;
}
} }
/** /**
@@ -41,7 +52,11 @@ export const UndoRedo = (
*/ */
const getSnapshot = (state : BaseFlowState) : FlowSnapshot => (structuredClone({ const getSnapshot = (state : BaseFlowState) : FlowSnapshot => (structuredClone({
nodes: state.nodes, nodes: state.nodes,
edges: state.edges edges: state.edges,
warnings: {
warningRegistry: state.editorWarningRegistry,
severityIndex: state.severityIndex,
}
})); }));
const initialState = config(set, get, api); const initialState = config(set, get, api);
@@ -78,6 +93,8 @@ export const UndoRedo = (
set({ set({
nodes: snapshot.nodes, nodes: snapshot.nodes,
edges: snapshot.edges, edges: snapshot.edges,
editorWarningRegistry: snapshot.warnings.warningRegistry,
severityIndex: snapshot.warnings.severityIndex,
}); });
state.future.push(currentSnapshot); // push current to redo state.future.push(currentSnapshot); // push current to redo
@@ -97,6 +114,8 @@ export const UndoRedo = (
set({ set({
nodes: snapshot.nodes, nodes: snapshot.nodes,
edges: snapshot.edges, edges: snapshot.edges,
editorWarningRegistry: snapshot.warnings.warningRegistry,
severityIndex: snapshot.warnings.severityIndex,
}); });
state.past.push(currentSnapshot); // push current to undo state.past.push(currentSnapshot); // push current to undo

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 {type Connection} from "@xyflow/react";
import {useEffect} from "react"; import {useEffect} from "react";
import useFlowStore from "./VisProgStores.tsx"; import useFlowStore from "./VisProgStores.tsx";
@@ -107,4 +110,16 @@ export function useHandleRules(
// finally we return a function that evaluates all rules using the created context // finally we return a function that evaluates all rules using the created context
return evaluateRules(targetRules, connection, context); return evaluateRules(targetRules, connection, context);
}; };
}
export function validateConnectionWithRules(
connection: Connection,
context: ConnectionContext
): RuleResult {
const rules = useFlowStore.getState().getTargetRules(
connection.target!,
connection.targetHandle!
);
return evaluateRules(rules,connection, context);
} }

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 { import {
type HandleRule, type HandleRule,
ruleResult 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, { import EndNode, {
EndConnectionTarget, EndConnectionTarget,
EndConnectionSource, 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 { create } from 'zustand';
import { import {
applyNodeChanges, applyNodeChanges,
@@ -9,6 +12,8 @@ import {
type XYPosition, type XYPosition,
} from '@xyflow/react'; } from '@xyflow/react';
import '@xyflow/react/dist/style.css'; import '@xyflow/react/dist/style.css';
import {type ConnectionContext, validateConnectionWithRules} from "./HandleRuleLogic.ts";
import {editorWarningRegistry} from "./components/EditorWarnings.tsx";
import type { FlowState } from './VisProgTypes'; import type { FlowState } from './VisProgTypes';
import { import {
NodeDefaults, NodeDefaults,
@@ -43,19 +48,18 @@ function createNode(id: string, type: string, position: XYPosition, data: Record
} }
} }
//* Initial nodes, created by using createNode. */ //* Initial nodes, created by using createNode. */
// Start and End don't need to apply the UUID, since they are technically never compiled into a program. // Start and End don't need to apply the UUID, since they are technically never compiled into a program.
const startNode = createNode('start', 'start', {x: 110, y: 100}, {label: "Start"}, false) const startNode = createNode('start', 'start', {x: 110, y: 100}, {label: "Start"}, false)
const endNode = createNode('end', 'end', {x: 590, y: 100}, {label: "End"}, false) const endNode = createNode('end', 'end', {x: 590, y: 100}, {label: "End"}, false)
const initialPhaseNode = createNode(crypto.randomUUID(), 'phase', {x:235, y:100}, {label: "Phase 1", children : [], isFirstPhase: false, nextPhaseId: null}) const initialPhaseNode = createNode(crypto.randomUUID(), 'phase', {x:235, y:100}, {label: "Phase 1", children : [], isFirstPhase: false, nextPhaseId: null})
const initialNodes : Node[] = [startNode, endNode, initialPhaseNode,]; const initialNodes : Node[] = [startNode, endNode, initialPhaseNode];
// Initial edges, leave empty as setting initial edges... // Initial edges, leave empty as setting initial edges...
// ...breaks logic that is dependent on connection events // ...breaks logic that is dependent on connection events
const initialEdges: Edge[] = []; const initialEdges: Edge[] = [];
/** /**
* useFlowStore contains the implementation for all editor functionality * useFlowStore contains the implementation for all editor functionality
* and stores the current state of the visual programming editor * and stores the current state of the visual programming editor
@@ -86,7 +90,9 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
*/ */
onNodesChange: (changes) => set({nodes: applyNodeChanges(changes, get().nodes)}), onNodesChange: (changes) => set({nodes: applyNodeChanges(changes, get().nodes)}),
onNodesDelete: (nodes) => nodes.forEach(node => get().unregisterNodeRules(node.id)), onNodesDelete: (nodes) => nodes.forEach((_node) => {
return;
}),
onEdgesDelete: (edges) => { onEdgesDelete: (edges) => {
// we make sure any affected nodes get updated to reflect removal of edges // we make sure any affected nodes get updated to reflect removal of edges
@@ -129,7 +135,41 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
* Handles reconnecting an edge between nodes. * Handles reconnecting an edge between nodes.
*/ */
onReconnect: (oldEdge, newConnection) => { onReconnect: (oldEdge, newConnection) => {
get().edgeReconnectSuccessful = true;
function createContext(
source: {id: string, handleId: string},
target: {id: string, handleId: string}
) : ConnectionContext {
const edges = get().edges;
const targetConnections = edges.filter(edge => edge.target === target.id && edge.targetHandle === target.handleId).length
return {
connectionCount: targetConnections,
source: source,
target: target
}
}
// connection validation
const context: ConnectionContext = oldEdge.source === newConnection.source
? createContext({id: newConnection.source, handleId: newConnection.sourceHandle!}, {id: newConnection.target, handleId: newConnection.targetHandle!})
: createContext({id: newConnection.target, handleId: newConnection.targetHandle!}, {id: newConnection.source, handleId: newConnection.sourceHandle!});
const result = validateConnectionWithRules(
newConnection,
context
);
if (!result.isSatisfied) {
set({
edges: get().edges.map(e =>
e.id === oldEdge.id ? oldEdge : e
),
});
return;
}
// further reconnect logic
set({ edgeReconnectSuccessful: true });
set({ edges: reconnectEdge(oldEdge, newConnection, get().edges) }); set({ edges: reconnectEdge(oldEdge, newConnection, get().edges) });
// We make sure to perform any required data updates on the newly reconnected nodes // We make sure to perform any required data updates on the newly reconnected nodes
@@ -182,19 +222,32 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
* Deletes a node by ID, respecting NodeDeletes rules. * Deletes a node by ID, respecting NodeDeletes rules.
* Also removes all edges connected to that node. * Also removes all edges connected to that node.
*/ */
deleteNode: (nodeId) => { deleteNode: (nodeId, deleteElements) => {
get().pushSnapshot(); get().pushSnapshot();
// Let's find our node to check if they have a special deletion function // Let's find our node to check if they have a special deletion function
const ourNode = get().nodes.find((n)=>n.id==nodeId); const ourNode = get().nodes.find((n)=>n.id==nodeId);
const ourFunction = Object.entries(NodeDeletes).find(([t])=>t==ourNode?.type)?.[1] const ourFunction = Object.entries(NodeDeletes).find(([t])=>t==ourNode?.type)?.[1]
// If there's no function, OR, our function tells us we can delete it, let's do so... // If there's no function, OR, our function tells us we can delete it, let's do so...
if (ourFunction == undefined || ourFunction()) { if (ourFunction == undefined || ourFunction()) {
set({ if (deleteElements){
nodes: get().nodes.filter((n) => n.id !== nodeId), deleteElements({
edges: get().edges.filter((e) => e.source !== nodeId && e.target !== nodeId), 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),
})
}
}
}, },
/** /**
@@ -306,8 +359,12 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
}) })
return { ruleRegistry: registry }; return { ruleRegistry: registry };
}) })
} },
...editorWarningRegistry(get, set),
})) }))
); );
export default useFlowStore; export default useFlowStore;

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 // VisProgTypes.ts
import type { import type {
Edge, Edge,
@@ -7,8 +10,9 @@ import type {
OnReconnect, OnReconnect,
Node, Node,
OnEdgesDelete, OnEdgesDelete,
OnNodesDelete OnNodesDelete, DeleteElementsOptions
} from '@xyflow/react'; } from '@xyflow/react';
import type {EditorWarningRegistry} from "./components/EditorWarnings.tsx";
import type {HandleRule} from "./HandleRuleLogic.ts"; import type {HandleRule} from "./HandleRuleLogic.ts";
import type { NodeTypes } from './NodeRegistry'; import type { NodeTypes } from './NodeRegistry';
import type {FlowSnapshot} from "./EditorUndoRedo.ts"; import type {FlowSnapshot} from "./EditorUndoRedo.ts";
@@ -68,7 +72,10 @@ export type FlowState = {
* Deletes a node and any connected edges. * Deletes a node and any connected edges.
* @param nodeId - the ID of the node to delete * @param nodeId - the ID of the node to delete
*/ */
deleteNode: (nodeId: string) => void; deleteNode: (nodeId: string, deleteElements?: (params: DeleteElementsOptions) => Promise<{
deletedNodes: Node[]
deletedEdges: Edge[]
}>) => void;
/** /**
* Replaces the current nodes array in the store. * Replaces the current nodes array in the store.
@@ -94,7 +101,7 @@ export type FlowState = {
* @param node - the Node object to add * @param node - the Node object to add
*/ */
addNode: (node: Node) => void; addNode: (node: Node) => void;
} & UndoRedoState & HandleRuleRegistry; } & UndoRedoState & HandleRuleRegistry & EditorWarningRegistry;
export type UndoRedoState = { export type UndoRedoState = {
// UndoRedo Types // UndoRedo Types
@@ -129,4 +136,7 @@ export type HandleRuleRegistry = {
// cleans up all registered rules of all handles of the provided node // cleans up all registered rules of all handles of the provided node
unregisterNodeRules: (nodeId: string) => void unregisterNodeRules: (nodeId: string) => void
} }

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 { useDraggable } from '@neodrag/react';
import { useReactFlow, type XYPosition } from '@xyflow/react'; import { useReactFlow, type XYPosition } from '@xyflow/react';
import { type ReactNode, useCallback, useRef, useState } from 'react'; import { type ReactNode, useCallback, useRef, useState } from 'react';

View File

@@ -0,0 +1,248 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
/* contains all logic for the VisProgEditor warning system
*
* Missing but desirable features:
* - Warning filtering:
* - if there is no completely connected chain of startNode-[PhaseNodes]-EndNode
* then hide any startNode, phaseNode, or endNode specific warnings
*/
import useFlowStore from "../VisProgStores.tsx";
import type {FlowState} from "../VisProgTypes.tsx";
// --| Type definitions |--
export type WarningId = NodeId | "GLOBAL_WARNINGS";
export type NodeId = string;
export type WarningType =
| 'MISSING_INPUT'
| 'MISSING_OUTPUT'
| 'PLAN_IS_UNDEFINED'
| 'INCOMPLETE_PROGRAM'
| 'NOT_CONNECTED_TO_PROGRAM'
| string
export type WarningSeverity =
| 'INFO' // Acceptable, but important to be aware of
| 'WARNING' // Acceptable, but probably undesirable behavior
| 'ERROR' // Prevents running program, should be fixed before running program is allowed
/**
* warning scope, include a handleId if the warning is handle specific
*/
export type WarningScope = {
id: string;
handleId?: string;
}
export type EditorWarning = {
scope: WarningScope;
type: WarningType;
severity: WarningSeverity;
description: string;
};
/**
* a scoped WarningKey,
* the handleId scoping is only needed for handle specific errors
*
* "`WarningType`:`handleId`"
*/
export type WarningKey = string; // for warnings that can occur on a per-handle basis
/**
* a composite key used in the severityIndex
*
* "`WarningId`|`WarningKey`"
*/
export type CompositeWarningKey = string;
export type WarningRegistry = Map<WarningId , Map<WarningKey, EditorWarning>>;
export type SeverityIndex = Map<WarningSeverity, Set<CompositeWarningKey>>;
type ZustandSet = (partial: Partial<FlowState> | ((state: FlowState) => Partial<FlowState>)) => void;
type ZustandGet = () => FlowState;
export type EditorWarningRegistry = {
/**
* stores all editor warnings
*/
editorWarningRegistry: WarningRegistry;
/**
* index of warnings by severity
*/
severityIndex: SeverityIndex;
/**
* gets all warnings and returns them as a list of warnings
* @returns {EditorWarning[]}
*/
getWarnings: () => EditorWarning[];
/**
* gets all warnings with the current severity
* @param {WarningSeverity} warningSeverity
* @returns {EditorWarning[]}
*/
getWarningsBySeverity: (warningSeverity: WarningSeverity) => EditorWarning[];
/**
* checks if there are no warnings of breaking severity
* @returns {boolean}
*/
isProgramValid: () => boolean;
/**
* registers a warning to the warningRegistry and the SeverityIndex
* @param {EditorWarning} warning
*/
registerWarning: (warning: EditorWarning) => void;
/**
* unregisters a warning from the warningRegistry and the SeverityIndex
* @param {EditorWarning} warning
*/
unregisterWarning: (id: WarningId, warningKey: WarningKey) => void
/**
* unregisters warnings from the warningRegistry and the SeverityIndex
* @param {WarningId} warning
*/
unregisterWarningsForId: (id: WarningId) => void;
}
// --| implemented logic |--
/**
* the id to use for global editor warnings
* @type {string}
*/
export const globalWarning = "GLOBAL_WARNINGS";
export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : EditorWarningRegistry { return {
editorWarningRegistry: new Map<NodeId, Map<WarningKey, EditorWarning>>(),
severityIndex: new Map([
['INFO', new Set<CompositeWarningKey>()],
['WARNING', new Set<CompositeWarningKey>()],
['ERROR', new Set<CompositeWarningKey>()],
]),
getWarningsBySeverity: (warningSeverity) => {
const wRegistry = new Map([...get().editorWarningRegistry].map(([k, v]) => [k, new Map(v)]));
const sIndex = new Map(get().severityIndex);
const warningKeys = sIndex.get(warningSeverity);
const warnings: EditorWarning[] = [];
warningKeys?.forEach(
(compositeKey) => {
const [id, warningKey] = compositeKey.split('|');
const warning = wRegistry.get(id)?.get(warningKey);
if (warning) {
warnings.push(warning);
}
}
)
return warnings;
},
isProgramValid: () => {
const sIndex = get().severityIndex;
return (sIndex.get("ERROR")!.size === 0);
},
getWarnings: () => Array.from(get().editorWarningRegistry.values())
.flatMap(innerMap => Array.from(innerMap.values())),
registerWarning: (warning) => {
const { scope: {id, handleId}, type, severity } = warning;
const warningKey = handleId ? `${type}:${handleId}` : type;
const compositeKey = `${id}|${warningKey}`;
const wRegistry = new Map([...get().editorWarningRegistry].map(([k, v]) => [k, new Map(v)]));
const sIndex = new Map(get().severityIndex);
// add to warning registry
if (!wRegistry.has(id)) {
wRegistry.set(id, new Map());
}
wRegistry.get(id)!.set(warningKey, warning);
// add to severityIndex
if (!sIndex.get(severity)!.has(compositeKey)) {
sIndex.get(severity)!.add(compositeKey);
}
set({
editorWarningRegistry: wRegistry,
severityIndex: sIndex
})
},
unregisterWarning: (id, warningKey) => {
const wRegistry = new Map([...get().editorWarningRegistry].map(([k, v]) => [k, new Map(v)]));
const sIndex = new Map(get().severityIndex);
// verify if the warning was created already
const warning = wRegistry.get(id)?.get(warningKey);
if (!warning) return;
// remove from warning registry
wRegistry.get(id)!.delete(warningKey);
// remove from severityIndex
sIndex.get(warning.severity)!.delete(`${id}|${warningKey}`);
set({
editorWarningRegistry: wRegistry,
severityIndex: sIndex
})
},
unregisterWarningsForId: (id) => {
const wRegistry = new Map([...get().editorWarningRegistry].map(([k, v]) => [k, new Map(v)]));
const sIndex = new Map(get().severityIndex);
const nodeWarnings = wRegistry.get(id);
// remove from severity index
if (nodeWarnings) {
nodeWarnings.forEach((warning) => {
const warningKey = warning.scope.handleId
? `${warning.type}:${warning.scope.handleId}`
: warning.type;
sIndex.get(warning.severity)?.delete(`${id}|${warningKey}`);
});
}
// remove from warning registry
wRegistry.delete(id);
set({
editorWarningRegistry: wRegistry,
severityIndex: sIndex
})
},
}}
/**
* returns a summary of the warningRegistry
* @returns {{info: number, warning: number, error: number, isValid: boolean}}
*/
export function warningSummary() {
const {severityIndex, isProgramValid} = useFlowStore.getState();
return {
info: severityIndex.get('INFO')!.size,
warning: severityIndex.get('WARNING')!.size,
error: severityIndex.get('ERROR')!.size,
isValid: isProgramValid(),
};
}

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 { .gestureEditor {
display: flex; display: flex;
flex-direction: column; 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 { useState, useRef } from "react";
import styles from './GestureValueEditor.module.css' import styles from './GestureValueEditor.module.css'

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 '@xyflow/react/dist/style.css';
import {type JSX, useState} from "react"; import {type JSX, useState} from "react";
import {createPortal} from "react-dom"; import {createPortal} from "react-dom";
@@ -30,10 +33,11 @@ type ToolbarProps = {
*/ */
export function Toolbar({nodeId, allowDelete}: ToolbarProps) { export function Toolbar({nodeId, allowDelete}: ToolbarProps) {
const {nodes, deleteNode} = useFlowStore(); const {nodes, deleteNode} = useFlowStore();
const { deleteElements } = useReactFlow();
const deleteParentNode = () => { const deleteParentNode = () => {
deleteNode(nodeId);
deleteNode(nodeId, deleteElements);
}; };
const nodeType = nodes.find((node) => node.id === nodeId)?.type as keyof typeof NodeTooltips; const nodeType = nodes.find((node) => node.id === nodeId)?.type as keyof typeof NodeTooltips;

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 { Plan, PlanElement } from "./Plan"; import type { Plan, PlanElement } from "./Plan";
export const defaultPlan: Plan = { export const defaultPlan: Plan = {

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 Node } from "@xyflow/react" import { type Node } from "@xyflow/react"
import { GoalReduce } from "../nodes/GoalNode" import { GoalReduce } from "../nodes/GoalNode"

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)
// This file is to avoid sharing both functions and components which eslint dislikes. :) // 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" import type { Goal, Plan } from "./Plan"

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 { .planDialog {
overflow:visible; overflow:visible;
width: 80vw; width: 80vw;

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 {useRef, useState} from "react"; import {useRef, useState} from "react";
import useFlowStore from "../VisProgStores.tsx"; import useFlowStore from "../VisProgStores.tsx";
import styles from './PlanEditor.module.css'; import styles from './PlanEditor.module.css';

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) { :global(.react-flow__handle.connected) {
background: lightgray; background: lightgray;
border-color: green; border-color: green;
filter: drop-shadow(0 0 0.25rem green); filter: drop-shadow(0 0 0.15rem green);
} }
:global(.singleConnectionHandle.connected) { :global(.singleConnectionHandle.connected) {
@@ -16,19 +30,19 @@
:global(.singleConnectionHandle.unconnected){ :global(.singleConnectionHandle.unconnected){
background: lightsalmon; background: lightsalmon;
border-color: #ff6060; border-color: #ff6060;
filter: drop-shadow(0 0 0.25rem #ff6060); filter: drop-shadow(0 0 0.15rem #ff6060);
} }
:global(.react-flow__handle.connectingto) { :global(.react-flow__handle.connectingto) {
background: #ff6060; background: #ff6060;
border-color: coral; border-color: coral;
filter: drop-shadow(0 0 0.25rem coral); filter: drop-shadow(0 0 0.15rem coral);
} }
:global(.react-flow__handle.valid) { :global(.react-flow__handle.valid) {
background: #55dd99; background: #55dd99;
border-color: green; border-color: green;
filter: drop-shadow(0 0 0.25rem green); filter: drop-shadow(0 0 0.15rem green);
} }
:global(.react-flow__handle) { :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 { import {
Handle, Handle,
type HandleProps, type HandleProps,
type Connection, type Connection,
useNodeId, useNodeConnections useNodeId, useNodeConnections
} from '@xyflow/react'; } from '@xyflow/react';
import {useState} from 'react';
import { type HandleRule, useHandleRules} from "../HandleRuleLogic.ts"; import { type HandleRule, useHandleRules} from "../HandleRuleLogic.ts";
import "./RuleBasedHandle.module.css"; import "./RuleBasedHandle.module.css";
@@ -29,21 +31,16 @@ export function MultiConnectionHandle({
handleId: id! handleId: id!
}) })
// initialise the handles state with { isValid: true } to show that connections are possible
const [handleState, setHandleState] = useState<{ isSatisfied: boolean, message?: string }>({ isSatisfied: true });
return ( return (
<Handle <Handle
{...otherProps} {...otherProps}
id={id} id={id}
type={type} type={type}
className={"multiConnectionHandle" + (connections.length === 0 ? " unconnected" : " connected")} className={"multiConnectionHandle" + (connections.length === 0 ? " unconnected" : " connected") + ` ${type}`}
isValidConnection={(connection) => { isValidConnection={(connection) => {
const result = validate(connection as Connection); const result = validate(connection as Connection);
setHandleState(result);
return result.isSatisfied; return result.isSatisfied;
}} }}
title={handleState.message}
/> />
); );
} }
@@ -66,22 +63,18 @@ export function SingleConnectionHandle({
handleId: id! handleId: id!
}) })
// initialise the handles state with { isValid: true } to show that connections are possible
const [handleState, setHandleState] = useState<{ isSatisfied: boolean, message?: string }>({ isSatisfied: true });
return ( return (
<Handle <Handle
{...otherProps} {...otherProps}
id={id} id={id}
type={type} type={type}
className={"singleConnectionHandle" + (connections.length === 0 ? " unconnected" : " connected")} className={"singleConnectionHandle" + (connections.length === 0 ? " unconnected" : " connected") + ` ${type}`}
isConnectable={connections.length === 0} isConnectable={connections.length === 0}
isValidConnection={(connection) => { isValidConnection={(connection) => {
const result = validate(connection as Connection); const result = validate(connection as Connection);
setHandleState(result);
return result.isSatisfied; return result.isSatisfied;
}} }}
title={handleState.message}
/> />
); );
} }

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 { .save-load-panel {
border-radius: 0 0 5pt 5pt; border-radius: 0 0 5pt 5pt;
background-color: canvas; 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 {type ChangeEvent, useRef, useState} from "react";
import useFlowStore from "../VisProgStores"; import useFlowStore from "../VisProgStores";
import visProgStyles from "../../VisProg.module.css"; import visProgStyles from "../../VisProg.module.css";
@@ -29,6 +32,8 @@ export default function SaveLoadPanel() {
const text = await file.text(); const text = await file.text();
const parsed = JSON.parse(text) as SavedProject; const parsed = JSON.parse(text) as SavedProject;
if (!parsed.nodes || !parsed.edges) throw new Error("Invalid file format"); 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); setNodes(parsed.nodes);
setEdges(parsed.edges); setEdges(parsed.edges);
} catch (e) { } catch (e) {

View File

@@ -0,0 +1,208 @@
/*
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/
.warnings-sidebar {
min-width: auto;
max-width: 340px;
margin-right: 0;
height: 100%;
background: canvas;
display: flex;
flex-direction: row;
}
.warnings-toggle-bar {
background-color: ButtonFace;
justify-items: center;
align-content: center;
width: 1rem;
cursor: pointer;
}
.warnings-toggle-bar.error:first-child:has(.arrow-right){
background-color: hsl(from red h s 75%);
}
.warnings-toggle-bar.warning:first-child:has(.arrow-right) {
background-color: hsl(from orange h s 75%);
}
.warnings-toggle-bar.info:first-child:has(.arrow-right) {
background-color: hsl(from steelblue h s 75%);
}
.warnings-toggle-bar:hover {
background-color: GrayText !important ;
.arrow-left {
border-right-color: ButtonFace;
transition: transform 0.15s ease-in-out;
transform: rotateY(180deg);
}
.arrow-right {
border-left-color: ButtonFace;
transition: transform 0.15s ease-in-out;
transform: rotateY(180deg);
}
}
.warnings-content {
width: 320px;
flex: 1;
flex-direction: column;
border-left: 2px solid CanvasText;
}
.warnings-header {
padding: 12px;
border-bottom: 2px solid CanvasText;
}
.severity-tabs {
display: flex;
gap: 4px;
}
.severity-tab {
flex: 1;
padding: 4px;
background: ButtonFace;
color: GrayText;
border: none;
cursor: pointer;
}
.count {
padding: 4px;
color: GrayText;
border: none;
cursor: pointer;
}
.severity-tab.active {
color: ButtonText;
border: 2px solid currentColor;
.count {
color: ButtonText;
}
}
.warning-group-header {
background: ButtonFace;
padding: 6px;
font-weight: bold;
}
.warnings-list {
flex: 1;
min-height: 0;
overflow-y: scroll;
}
.warnings-empty {
margin: auto;
}
.warning-item {
display: flex;
flex-direction: column;
margin: 5px;
gap: 2px;
padding: 0;
border-radius: 5px;
cursor: pointer;
color: GrayText;
}
.warning-item:hover {
background: ButtonFace;
}
.warning-item--error {
border: 2px solid red;
background-color: hsl(from red h s 96%);
.item-header{
background-color: red;
.type{
color: hsl(from red h s 96%);
}
}
}
.warning-item--error:hover {
background-color: hsl(from red h s 75%);
}
.warning-item--warning {
border: 2px solid orange;
background-color: hsl(from orange h s 96%);
.item-header{
background-color: orange;
.type{
color: hsl(from orange h s 96%);
}
}
}
.warning-item--warning:hover {
background-color: hsl(from orange h s 75%);
}
.warning-item--info {
border: 2px solid steelblue;
background-color: hsl(from steelblue h s 96%);
.item-header{
background-color: steelblue;
.type{
color: hsl(from steelblue h s 96%);
}
}
}
.warning-item--info:hover {
background-color: hsl(from steelblue h s 75%);
}
.warning-item .item-header {
padding: 8px 8px;
opacity: 1;
font-weight: bolder;
}
.warning-item .item-header .type{
padding: 2px 8px;
font-size: 0.9rem;
}
.warning-item .description {
padding: 5px 10px;
font-size: 0.8rem;
}
.auto-hide {
background-color: Canvas;
border-top: 2px solid CanvasText;
margin-top: auto;
width: 100%;
height: 2.5rem;
display: flex;
align-items: center;
padding: 0 12px;
}
/* arrows for toggleBar */
.arrow-right {
width: 0;
height: 0;
border-top: 0.5rem solid transparent;
border-bottom: 0.5rem solid transparent;
border-left: 0.6rem solid GrayText;
}
.arrow-left {
width: 0;
height: 0;
border-top: 0.5rem solid transparent;
border-bottom: 0.5rem solid transparent;
border-right: 0.6rem solid GrayText;
}

View File

@@ -0,0 +1,228 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {useReactFlow, useStoreApi} from "@xyflow/react";
import clsx from "clsx";
import {useEffect, useState} from "react";
import useFlowStore from "../VisProgStores.tsx";
import {
warningSummary,
type WarningSeverity,
type EditorWarning, globalWarning
} from "./EditorWarnings.tsx";
import styles from "./WarningSidebar.module.css";
/**
* the warning sidebar, shows all warnings
*
* @returns {React.JSX.Element}
* @constructor
*/
export function WarningsSidebar() {
const warnings = useFlowStore.getState().getWarnings();
const [hide, setHide] = useState(false);
const [severityFilter, setSeverityFilter] = useState<WarningSeverity | 'ALL'>('ALL');
const [autoHide, setAutoHide] = useState(false);
// let autohide change hide status only when autohide is toggled
// and allow for user to change the hide state even if autohide is enabled
const hasWarnings = warnings.length > 0;
useEffect(() => {
if (autoHide) {
setHide(!hasWarnings);
}
}, [autoHide, hasWarnings]);
const filtered = severityFilter === 'ALL'
? warnings
: warnings.filter(w => w.severity === severityFilter);
const summary = warningSummary();
// Finds the first key where the count > 0
const getHighestSeverity = () => {
if (summary.error > 0) return styles.error;
if (summary.warning > 0) return styles.warning;
if (summary.info > 0) return styles.info;
return '';
};
return (
<aside className={`flex-row`} >
<div
className={`${styles.warningsToggleBar} ${getHighestSeverity()}`}
onClick={() => setHide(!hide)}
title={"toggle warnings"}
>
<div className={`${hide ? styles.arrowRight : styles.arrowLeft}`}></div>
</div>
<div
id="warningSidebar"
className={styles.warningsContent}
style={hide ? {display: "none"} : {display: "flex"}}
>
<WarningsHeader
severityFilter={severityFilter}
onChange={setSeverityFilter}
/>
<WarningsList warnings={filtered} />
<div className={styles.autoHide}>
<input
id="autoHideSwitch"
type={"checkbox"}
checked={autoHide}
onChange={(e) => setAutoHide(e.target.checked)}
/><label>Hide if there are no warnings</label>
</div>
</div>
</aside>
);
}
/**
* the header of the warning sidebar, contains severity filtering buttons
*
* @param {WarningSeverity | "ALL"} severityFilter
* @param {(severity: (WarningSeverity | "ALL")) => void} onChange
* @returns {React.JSX.Element}
* @constructor
*/
function WarningsHeader({
severityFilter,
onChange,
}: {
severityFilter: WarningSeverity | 'ALL';
onChange: (severity: WarningSeverity | 'ALL') => void;
}) {
const summary = warningSummary();
return (
<div className={styles.warningsHeader}>
<h3>Warnings</h3>
<div className={styles.severityTabs}>
{(['ALL', 'ERROR', 'WARNING', 'INFO'] as const).map(severity => (
<button
key={severity}
className={clsx(styles.severityTab, severityFilter === severity && styles.active)}
onClick={() => onChange(severity)}
>
{severity}
{severity !== 'ALL' && (
<span className={styles.count}>
{summary[severity.toLowerCase() as keyof typeof summary]}
</span>
)}
</button>
))}
</div>
</div>
);
}
/**
* the list of warnings in the warning sidebar
*
* @param {{warnings: EditorWarning[]}} props
* @returns {React.JSX.Element}
* @constructor
*/
function WarningsList(props: { warnings: EditorWarning[] }) {
const splitWarnings = {
global: props.warnings.filter(w => w.scope.id === globalWarning),
other: props.warnings.filter(w => w.scope.id !== globalWarning),
}
if (props.warnings.length === 0) {
return (
<div className={styles.warningsEmpty}>
No warnings!
</div>
)
}
return (
<div className={styles.warningsList}>
<div className={styles.warningGroupHeader}>global:</div>
<div className={styles.warningsGroup}>
{splitWarnings.global.map((warning) => (
<WarningListItem warning={warning} key={`${warning.scope.id}|${warning.type}` + (warning.scope.handleId
? `:${warning.scope.handleId}`
: "")}
/>
))}
{splitWarnings.global.length === 0 && "No global warnings!"}
</div>
<div className={styles.warningGroupHeader}>other:</div>
<div className={styles.warningsGroup}>
{splitWarnings.other.map((warning) => (
<WarningListItem warning={warning} key={`${warning.scope.id}|${warning.type}` + (warning.scope.handleId
? `:${warning.scope.handleId}`
: "")}
/>
))}
{splitWarnings.other.length === 0 && "No other warnings!"}
</div>
</div>
);
}
/**
* a single warning in the warning sidebar
*
* @param {{warning: EditorWarning, key: string}} props
* @returns {React.JSX.Element}
* @constructor
*/
function WarningListItem(props: { warning: EditorWarning, key: string}) {
const jumpToNode = useJumpToNode();
return (
<div
className={clsx(styles.warningItem, styles[`warning-item--${props.warning.severity.toLowerCase()}`],)}
onClick={() => jumpToNode(props.warning.scope.id)}
>
<div className={styles.itemHeader}>
<span className={styles.type}>{props.warning.type}</span>
</div>
<div className={styles.description}>
{props.warning.description}
</div>
</div>
);
}
/**
* moves the editor to the provided node
* @returns {(nodeId: string) => void}
*/
function useJumpToNode() {
const { getNode, setCenter, getViewport } = useReactFlow();
const { addSelectedNodes } = useStoreApi().getState();
return (nodeId: string) => {
// user can't jump to global warning, so prevent further logic from running if the warning is a globalWarning
if (nodeId === globalWarning) return;
const node = getNode(nodeId);
if (!node) return;
const nodeElement = document.querySelector(`.react-flow__node[data-id="${nodeId}"]`) as HTMLElement;
const { position } = node;
const viewport = getViewport();
const { width, height } = nodeElement.getBoundingClientRect();
//move to node
setCenter(
position!.x + ((width / viewport.zoom) / 2),
position!.y + ((height / viewport.zoom) / 2),
{duration: 300, interpolate: "smooth" }
).then(() => {
addSelectedNodes([nodeId]);
});
};
}

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"; 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 { import {
type NodeProps, type NodeProps,
Position, Position,
@@ -10,6 +13,7 @@ import {allowOnlyConnectionsFromHandle} from "../HandleRules.ts";
import useFlowStore from '../VisProgStores.tsx'; import useFlowStore from '../VisProgStores.tsx';
import { TextField } from '../../../../components/TextField.tsx'; import { TextField } from '../../../../components/TextField.tsx';
import { MultilineTextField } from '../../../../components/MultilineTextField.tsx'; import { MultilineTextField } from '../../../../components/MultilineTextField.tsx';
import {noMatchingLeftRightBelief} from "./BeliefGlobals.ts";
/** /**
* The default data structure for a BasicBelief node * The default data structure for a BasicBelief node
@@ -112,9 +116,7 @@ export default function BasicBeliefNode(props: NodeProps<BasicBeliefNode>) {
updateNodeData(props.id, {...data, belief: {...data.belief, description: value}}); updateNodeData(props.id, {...data, belief: {...data.belief, description: value}});
} }
// Use this const emotionOptions = ["sad", "angry", "surprise", "fear", "happy", "disgust", "neutral"];
const emotionOptions = ["Happy", "Angry", "Sad", "Cheerful"]
let placeholder = "" let placeholder = ""
let wrapping = "" let wrapping = ""
@@ -189,8 +191,9 @@ export default function BasicBeliefNode(props: NodeProps<BasicBeliefNode>) {
</div> </div>
)} )}
<MultiConnectionHandle type="source" position={Position.Right} id="source" rules={[ <MultiConnectionHandle type="source" position={Position.Right} id="source" rules={[
allowOnlyConnectionsFromHandle([{nodeType:"trigger",handleId:"TriggerBeliefs"}, {nodeType:"norm",handleId:"NormBeliefs"},{nodeType:"InferredBelief",handleId:"inferred_belief"}]), noMatchingLeftRightBelief,
]}/> allowOnlyConnectionsFromHandle([{nodeType:"trigger",handleId:"TriggerBeliefs"}, {nodeType:"norm",handleId:"NormBeliefs"},{nodeType:"InferredBelief",handleId:"inferred_belief"}]),
]} title="Connect to any number of trigger and/or normNode(-s)"/>
</div> </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 {getOutgoers, type Node} from '@xyflow/react';
import {type HandleRule, type RuleResult, ruleResult} from "../HandleRuleLogic.ts"; import {type HandleRule, type RuleResult, ruleResult} from "../HandleRuleLogic.ts";
import useFlowStore from "../VisProgStores.tsx"; 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"; import type { EndNodeData } from "./EndNode";
/** /**

View File

@@ -1,12 +1,18 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { import {
type NodeProps, type NodeProps,
Position, Position,
type Node, type Node, useNodeConnections
} from '@xyflow/react'; } from '@xyflow/react';
import {useEffect} from "react";
import type {EditorWarning} from "../components/EditorWarnings.tsx";
import { Toolbar } from '../components/NodeComponents'; import { Toolbar } from '../components/NodeComponents';
import styles from '../../VisProg.module.css'; import styles from '../../VisProg.module.css';
import {SingleConnectionHandle} from "../components/RuleBasedHandle.tsx"; import {SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
import {allowOnlyConnectionsFromType} from "../HandleRules.ts"; import {allowOnlyConnectionsFromType} from "../HandleRules.ts";
import useFlowStore from "../VisProgStores.tsx";
@@ -27,6 +33,27 @@ export type EndNode = Node<EndNodeData>
* @returns React.JSX.Element * @returns React.JSX.Element
*/ */
export default function EndNode(props: NodeProps<EndNode>) { export default function EndNode(props: NodeProps<EndNode>) {
const {registerWarning, unregisterWarning} = useFlowStore.getState();
const connections = useNodeConnections({
id: props.id,
handleId: 'target'
})
useEffect(() => {
const noConnectionWarning : EditorWarning = {
scope: {
id: props.id,
handleId: 'target'
},
type: 'MISSING_INPUT',
severity: "ERROR",
description: "the endNode does not have an incoming connection from a phaseNode"
}
if (connections.length === 0) { registerWarning(noConnectionWarning); }
else { unregisterWarning(props.id, `${noConnectionWarning.type}:target`); }
}, [connections.length, props.id, registerWarning, unregisterWarning]);
return ( return (
<> <>
<Toolbar nodeId={props.id} allowDelete={false}/> <Toolbar nodeId={props.id} allowDelete={false}/>
@@ -36,7 +63,7 @@ export default function EndNode(props: NodeProps<EndNode>) {
</div> </div>
<SingleConnectionHandle type="target" position={Position.Left} id="target" rules={[ <SingleConnectionHandle type="target" position={Position.Left} id="target" rules={[
allowOnlyConnectionsFromType(["phase"]) allowOnlyConnectionsFromType(["phase"])
]}/> ]} title="Connect to a phaseNode"/>
</div> </div>
</> </>
); );

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"; import type { GoalNodeData } from "./GoalNode";
/** /**

View File

@@ -1,8 +1,13 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { import {
type NodeProps, type NodeProps,
Position, Position,
type Node, type Node
} from '@xyflow/react'; } from '@xyflow/react';
import {useEffect} from "react";
import type {EditorWarning} from "../components/EditorWarnings.tsx";
import { Toolbar } from '../components/NodeComponents'; import { Toolbar } from '../components/NodeComponents';
import styles from '../../VisProg.module.css'; import styles from '../../VisProg.module.css';
import { TextField } from '../../../../components/TextField'; import { TextField } from '../../../../components/TextField';
@@ -44,7 +49,7 @@ export type GoalNode = Node<GoalNodeData>
* @returns React.JSX.Element * @returns React.JSX.Element
*/ */
export default function GoalNode({id, data}: NodeProps<GoalNode>) { export default function GoalNode({id, data}: NodeProps<GoalNode>) {
const {updateNodeData} = useFlowStore(); const {updateNodeData, registerWarning, unregisterWarning} = useFlowStore();
const _nodes = useFlowStore().nodes; const _nodes = useFlowStore().nodes;
const text_input_id = `goal_${id}_text_input`; const text_input_id = `goal_${id}_text_input`;
@@ -64,6 +69,24 @@ export default function GoalNode({id, data}: NodeProps<GoalNode>) {
updateNodeData(id, {...data, can_fail: value}); updateNodeData(id, {...data, can_fail: value});
} }
useEffect(() => {
const noPlanWarning : EditorWarning = {
scope: {
id: id,
handleId: undefined
},
type: 'PLAN_IS_UNDEFINED',
severity: 'ERROR',
description: "This goalNode is missing a plan, please make sure to create a plan by using the create plan button"
};
if (!data.plan){
registerWarning(noPlanWarning);
return;
}
unregisterWarning(id, noPlanWarning.type);
},[data.plan, id, registerWarning, unregisterWarning])
return <> return <>
<Toolbar nodeId={id} allowDelete={true}/> <Toolbar nodeId={id} allowDelete={true}/>
<div className={`${styles.defaultNode} ${styles.nodeGoal} flex-col gap-sm`}> <div className={`${styles.defaultNode} ${styles.nodeGoal} flex-col gap-sm`}>
@@ -118,9 +141,11 @@ export default function GoalNode({id, data}: NodeProps<GoalNode>) {
</div> </div>
<MultiConnectionHandle type="source" position={Position.Right} id="GoalSource" rules={[ <MultiConnectionHandle type="source" position={Position.Right} id="GoalSource" rules={[
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}]), allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}]),
]}/> ]} title="Connect to any number of phase and/or goalNode(-s)"/>
<MultiConnectionHandle type="target" position={Position.Bottom} id="GoalTarget" rules={[allowOnlyConnectionsFromType(["goal"])]}/> <MultiConnectionHandle type="target" position={Position.Bottom} id="GoalTarget" rules={[
allowOnlyConnectionsFromType(["goal"])]
} title="Connect to any number of goalNode(-s)"/>
</div> </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"; import type { InferredBeliefNodeData } from "./InferredBeliefNode.tsx";
@@ -5,7 +8,7 @@ import type { InferredBeliefNodeData } from "./InferredBeliefNode.tsx";
* Default data for this node * Default data for this node
*/ */
export const InferredBeliefNodeDefaults: InferredBeliefNodeData = { export const InferredBeliefNodeDefaults: InferredBeliefNodeData = {
label: "Inferred Belief", label: "AND/OR",
droppable: true, droppable: true,
inferredBelief: { inferredBelief: {
left: undefined, 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 { .operator-switch {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;

View File

@@ -1,6 +1,10 @@
import {getConnectedEdges, type Node, type NodeProps, Position} from '@xyflow/react'; // This program has been developed by students from the bachelor Computer Science at Utrecht
import {useState} from "react"; // 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 styles from '../../VisProg.module.css';
import type {EditorWarning} from "../components/EditorWarnings.tsx";
import {Toolbar} from '../components/NodeComponents.tsx'; import {Toolbar} from '../components/NodeComponents.tsx';
import {MultiConnectionHandle, SingleConnectionHandle} from "../components/RuleBasedHandle.tsx"; import {MultiConnectionHandle, SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
import {allowOnlyConnectionsFromType} from "../HandleRules.ts"; import {allowOnlyConnectionsFromType} from "../HandleRules.ts";
@@ -91,7 +95,7 @@ export const InferredBeliefTooltip = `
*/ */
export default function InferredBeliefNode(props: NodeProps<InferredBeliefNode>) { export default function InferredBeliefNode(props: NodeProps<InferredBeliefNode>) {
const data = props.data; const data = props.data;
const { updateNodeData } = useFlowStore(); const { updateNodeData, registerWarning, unregisterWarning } = useFlowStore();
// start of as an AND operator, true: "AND", false: "OR" // start of as an AND operator, true: "AND", false: "OR"
const [enforceAllBeliefs, setEnforceAllBeliefs] = useState(true); 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 ( return (
<> <>
<Toolbar nodeId={props.id} allowDelete={true}/> <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"; 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 { import {
type NodeProps, type NodeProps,
Position, Position,
@@ -79,10 +82,10 @@ export default function NormNode(props: NodeProps<NormNode>) {
<MultiConnectionHandle type="source" position={Position.Right} id="norms" rules={[ <MultiConnectionHandle type="source" position={Position.Right} id="norms" rules={[
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}]) allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}])
]}/> ]} title="Connect to any number of phaseNode(-s)"/>
<SingleConnectionHandle type="target" position={Position.Bottom} id="NormBeliefs" rules={[ <SingleConnectionHandle type="target" position={Position.Bottom} id="NormBeliefs" rules={[
allowOnlyConnectionsFromType(["basic_belief", "inferred_belief"]) allowOnlyConnectionsFromType(["basic_belief", "inferred_belief"])
]}/> ]} title="Connect to a beliefNode or a set of beliefs combined using the AND/OR node"/>
</div> </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"; import type { PhaseNodeData } from "./PhaseNode";
/** /**

View File

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

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"; import type { StartNodeData } from "./StartNode";
/** /**

View File

@@ -1,12 +1,18 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { import {
type NodeProps, type NodeProps,
Position, Position,
type Node, type Node, useNodeConnections
} from '@xyflow/react'; } from '@xyflow/react';
import {useEffect} from "react";
import { Toolbar } from '../components/NodeComponents'; import { Toolbar } from '../components/NodeComponents';
import styles from '../../VisProg.module.css'; import styles from '../../VisProg.module.css';
import {SingleConnectionHandle} from "../components/RuleBasedHandle.tsx"; import {SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
import {type EditorWarning} from "../components/EditorWarnings.tsx";
import {allowOnlyConnectionsFromHandle} from "../HandleRules.ts"; import {allowOnlyConnectionsFromHandle} from "../HandleRules.ts";
import useFlowStore from "../VisProgStores.tsx";
export type StartNodeData = { export type StartNodeData = {
@@ -25,6 +31,27 @@ export type StartNode = Node<StartNodeData>
* @returns React.JSX.Element * @returns React.JSX.Element
*/ */
export default function StartNode(props: NodeProps<StartNode>) { export default function StartNode(props: NodeProps<StartNode>) {
const {registerWarning, unregisterWarning} = useFlowStore.getState();
const connections = useNodeConnections({
id: props.id,
handleId: 'source'
})
useEffect(() => {
const noConnectionWarning : EditorWarning = {
scope: {
id: props.id,
handleId: 'source'
},
type: 'MISSING_OUTPUT',
severity: "ERROR",
description: "the startNode does not have an outgoing connection to a phaseNode"
}
if (connections.length === 0) { registerWarning(noConnectionWarning); }
else { unregisterWarning(props.id, `${noConnectionWarning.type}:source`); }
}, [connections.length, props.id, registerWarning, unregisterWarning]);
return ( return (
<> <>
<Toolbar nodeId={props.id} allowDelete={false}/> <Toolbar nodeId={props.id} allowDelete={false}/>
@@ -34,7 +61,7 @@ export default function StartNode(props: NodeProps<StartNode>) {
</div> </div>
<SingleConnectionHandle type="source" position={Position.Right} id="source" rules={[ <SingleConnectionHandle type="source" position={Position.Right} id="source" rules={[
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"target"}]) allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"target"}])
]}/> ]} title="Connect to a phaseNode"/>
</div> </div>
</> </>
); );

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"; import type { TriggerNodeData } from "./TriggerNode";
/** /**

View File

@@ -1,8 +1,13 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { import {
type NodeProps, type NodeProps,
Position, Position,
type Node, type Node, useNodeConnections
} from '@xyflow/react'; } from '@xyflow/react';
import {useEffect} from "react";
import type {EditorWarning} from "../components/EditorWarnings.tsx";
import { Toolbar } from '../components/NodeComponents'; import { Toolbar } from '../components/NodeComponents';
import styles from '../../VisProg.module.css'; import styles from '../../VisProg.module.css';
import {MultiConnectionHandle, SingleConnectionHandle} from "../components/RuleBasedHandle.tsx"; import {MultiConnectionHandle, SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
@@ -45,14 +50,79 @@ export type TriggerNode = Node<TriggerNodeData>
*/ */
export default function TriggerNode(props: NodeProps<TriggerNode>) { export default function TriggerNode(props: NodeProps<TriggerNode>) {
const data = props.data; const data = props.data;
const {updateNodeData} = useFlowStore(); const {updateNodeData, registerWarning, unregisterWarning} = useFlowStore();
const setName= (value: string) => { const setName= (value: string) => {
updateNodeData(props.id, {...data, name: value}) updateNodeData(props.id, {...data, name: value})
} }
const beliefInput = useNodeConnections({
id: props.id,
handleType: "target",
handleId: "TriggerBeliefs"
})
const outputCons = useNodeConnections({
id: props.id,
handleType: "source",
handleId: "TriggerSource"
})
useEffect(() => {
const noPhaseConnectionWarning : EditorWarning = {
scope: {
id: props.id,
handleId: 'TriggerSource'
},
type: 'MISSING_OUTPUT',
severity: 'INFO',
description: "This triggerNode is missing a condition/belief, please make sure to connect a belief node to "
};
if (outputCons.length === 0){
registerWarning(noPhaseConnectionWarning);
return;
}
unregisterWarning(props.id, `${noPhaseConnectionWarning.type}:${noPhaseConnectionWarning.scope.handleId}`);
},[outputCons.length, props.id, registerWarning, unregisterWarning])
useEffect(() => {
const noBeliefWarning : EditorWarning = {
scope: {
id: props.id,
handleId: 'TriggerBeliefs'
},
type: 'MISSING_INPUT',
severity: 'ERROR',
description: "This triggerNode is missing a condition/belief, please make sure to connect a belief node to "
};
if (beliefInput.length === 0 && outputCons.length !== 0){
registerWarning(noBeliefWarning);
return;
}
unregisterWarning(props.id, `${noBeliefWarning.type}:${noBeliefWarning.scope.handleId}`);
},[beliefInput.length, outputCons.length, props.id, registerWarning, unregisterWarning])
useEffect(() => {
const noPlanWarning : EditorWarning = {
scope: {
id: props.id,
handleId: undefined
},
type: 'PLAN_IS_UNDEFINED',
severity: 'ERROR',
description: "This triggerNode is missing a plan, please make sure to create a plan by using the create plan button"
};
if (!data.plan && outputCons.length !== 0){
registerWarning(noPlanWarning);
return;
}
unregisterWarning(props.id, noPlanWarning.type);
},[data.plan, outputCons.length, props.id, registerWarning, unregisterWarning])
return <> return <>
<Toolbar nodeId={props.id} allowDelete={true}/> <Toolbar nodeId={props.id} allowDelete={true}/>
<div className={`${styles.defaultNode} ${styles.nodeTrigger} flex-col gap-sm`}> <div className={`${styles.defaultNode} ${styles.nodeTrigger} flex-col gap-sm`}>
<TextField <TextField
@@ -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> <div className={"flex-row gap-md"}>Plan{data.plan ? (": " + data.plan.name) : ""} is currently {data.plan ? "" : "not"} set. {data.plan ? "🟢" : "🔴"}</div>
<MultiConnectionHandle type="source" position={Position.Right} id="TriggerSource" rules={[ <MultiConnectionHandle type="source" position={Position.Right} id="TriggerSource" rules={[
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}]), allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}]),
]}/> ]} title="Connect to any number of phaseNodes"/>
<SingleConnectionHandle <SingleConnectionHandle
type="target" type="target"
position={Position.Bottom} position={Position.Bottom}
id="TriggerBeliefs" id="TriggerBeliefs"
style={{ left: '40%' }} style={{ left: '40%' }}
rules={[ rules={[
allowOnlyConnectionsFromType(['basic_belief', "inferred_belief"]), allowOnlyConnectionsFromType(['basic_belief','inferred_belief']),
]} ]}
title="Connect to a beliefNode or a set of beliefs combined using the AND/OR node"
/> />
<MultiConnectionHandle <MultiConnectionHandle
@@ -84,6 +155,7 @@ export default function TriggerNode(props: NodeProps<TriggerNode>) {
rules={[ rules={[
allowOnlyConnectionsFromType(['goal']), allowOnlyConnectionsFromType(['goal']),
]} ]}
title="Connect to any number of goalNodes"
/> />
<PlanEditorDialog <PlanEditorDialog
@@ -136,7 +208,7 @@ export function TriggerConnectionTarget(_thisNode: Node, _sourceNodeId: string)
const otherNode = nodes.find((x) => x.id === _sourceNodeId) const otherNode = nodes.find((x) => x.id === _sourceNodeId)
if (!otherNode) return; if (!otherNode) return;
if (otherNode.type === 'basic_belief'|| otherNode.type ==='inferred_belief') { if (['basic_belief', 'inferred_belief'].includes(otherNode.type!)) {
data.condition = _sourceNodeId; data.condition = _sourceNodeId;
} }
@@ -172,7 +244,7 @@ export function TriggerDisconnectionTarget(_thisNode: Node, _sourceNodeId: strin
const data = _thisNode.data as TriggerNodeData; const data = _thisNode.data as TriggerNodeData;
// remove if the target of disconnection was our condition // remove if the target of disconnection was our condition
if (_sourceNodeId == data.condition) data.condition = undefined if (_sourceNodeId == data.condition) data.condition = undefined
data.plan = deleteGoalInPlanByID(structuredClone(data.plan) as Plan, _sourceNodeId) data.plan = deleteGoalInPlanByID(structuredClone(data.plan) as Plan, _sourceNodeId)
} }

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"; import {type Edge, type Node } from "@xyflow/react";
export type SavedProject = { export type SavedProject = {

3
src/utils/capitalize.ts Normal file
View File

@@ -0,0 +1,3 @@
export default function (s: string) {
return s.charAt(0).toUpperCase() + s.slice(1);
}

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"; import {useSyncExternalStore} from "react";
type Unsub = () => void; type Unsub = () => void;

View File

@@ -0,0 +1,7 @@
export default async function <T>(promise: Promise<T>, minDelayMs: number): Promise<T> {
const [result] = await Promise.all([
promise,
new Promise(resolve => setTimeout(resolve, minDelayMs))
]);
return result;
}

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. * 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`. * 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"; import type {PhaseNode} from "../pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx";
/** /**

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)
export type PriorityFilterPredicate<T> = { export type PriorityFilterPredicate<T> = {
priority: number; priority: number;
predicate: (element: T) => boolean | null; // The predicate and its priority are ignored if it returns null. predicate: (element: T) => boolean | null; // The predicate and its priority are ignored if it returns null.
} }
/** /**
* Applies a list of priority predicates to an element. For all predicates that don't return null, if the ones with the highest level return true, then this function returns true. * Applies a list of priority predicates to an element. For all predicates that don't return null, if the ones with the highest level return true, then this function returns true. Or conversely, if the one with the highest level returns false, then this function returns false.
* @param element The element to apply the predicates to. * @param element The element to apply the predicates to.
* @param predicates The list of predicates to apply. * @param predicates The list of predicates to apply.
*/ */

View File

@@ -1,8 +1,13 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {create} from "zustand"; import {create} from "zustand";
// the type of a reduced program // the type of a reduced program
export type ReducedProgram = { phases: Record<string, unknown>[] }; export type ReducedProgram = { phases: Record<string, unknown>[] };
export type GoalWithDepth = Record<string, unknown> & { level: number };
/** /**
* the type definition of the programStore * the type definition of the programStore
*/ */
@@ -18,6 +23,7 @@ export type ProgramState = {
getPhaseNames: () => string[]; getPhaseNames: () => string[];
getNormsInPhase: (currentPhaseId: string) => Record<string, unknown>[]; getNormsInPhase: (currentPhaseId: string) => Record<string, unknown>[];
getGoalsInPhase: (currentPhaseId: string) => Record<string, unknown>[]; getGoalsInPhase: (currentPhaseId: string) => Record<string, unknown>[];
getGoalsWithDepth: (currentPhaseId: string) => GoalWithDepth[];
getTriggersInPhase: (currentPhaseId: string) => Record<string, unknown>[]; getTriggersInPhase: (currentPhaseId: string) => Record<string, unknown>[];
// if more specific utility functions are needed they can be added here: // if more specific utility functions are needed they can be added here:
} }
@@ -70,6 +76,50 @@ const useProgramStore = create<ProgramState>((set, get) => ({
} }
throw new Error(`phase with id:"${currentPhaseId}" not found`) throw new Error(`phase with id:"${currentPhaseId}" not found`)
}, },
getGoalsWithDepth: (currentPhaseId: string) => {
const program = get().currentProgram;
const phase = program.phases.find(val => val["id"] === currentPhaseId);
if (!phase) {
throw new Error(`phase with id:"${currentPhaseId}" not found`);
}
const rootGoals = phase["goals"] as Record<string, unknown>[];
const flatList: GoalWithDepth[] = [];
const isGoal = (item: Record<string, unknown>) => {
return item["plan"] !== undefined;
};
// Recursive helper function
const traverse = (goals: Record<string, unknown>[], depth: number) => {
goals.forEach((goal) => {
// 1. Add the current goal to the list
flatList.push({ ...goal, level: depth });
// 2. Check for children
const plan = goal["plan"] as Record<string, unknown> | undefined;
if (plan && Array.isArray(plan["steps"])) {
const steps = plan["steps"] as Record<string, unknown>[];
// 3. FILTER: Only recurse on steps that are actually goals
// If we just passed 'steps', we might accidentally add Actions/Speeches to the goal list
const childGoals = steps.filter(isGoal);
if (childGoals.length > 0) {
traverse(childGoals, depth + 1);
}
}
});
};
// Start traversal
traverse(rootGoals, 0);
return flatList;
},
/** /**
* gets the triggers for the provided phase * gets the triggers for the provided phase
*/ */

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 userEvent from '@testing-library/user-event';
import { render, screen} from '@testing-library/react'; import { render, screen} from '@testing-library/react';
import Counter from '../src/components/components'; 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 {render, screen, waitFor, fireEvent} from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import * as React from "react"; 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 {render, screen, fireEvent, act, waitFor} from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
@@ -11,8 +14,6 @@ const loggingStoreRef: { current: null | { setState: (state: Partial<LoggingSett
type LoggingSettingsState = { type LoggingSettingsState = {
showRelativeTime: boolean; showRelativeTime: boolean;
setShowRelativeTime: (show: boolean) => void; setShowRelativeTime: (show: boolean) => void;
scrollToBottom: boolean;
setScrollToBottom: (scroll: boolean) => void;
}; };
jest.mock("zustand", () => { jest.mock("zustand", () => {
@@ -59,8 +60,8 @@ type LoggingComponent = typeof import("../../../src/components/Logging/Logging.t
let Logging: LoggingComponent; let Logging: LoggingComponent;
beforeAll(async () => { beforeAll(async () => {
if (!Element.prototype.scrollIntoView) { if (!Element.prototype.scrollTo) {
Object.defineProperty(Element.prototype, "scrollIntoView", { Object.defineProperty(Element.prototype, "scrollTo", {
configurable: true, configurable: true,
writable: true, writable: true,
value: function () {}, value: function () {},
@@ -84,7 +85,6 @@ afterEach(() => {
function resetLoggingStore() { function resetLoggingStore() {
loggingStoreRef.current?.setState({ loggingStoreRef.current?.setState({
showRelativeTime: false, showRelativeTime: false,
scrollToBottom: true,
}); });
} }
@@ -151,7 +151,7 @@ describe("Logging component", () => {
]; ];
mockUseLogs.mockReturnValue({filteredLogs: logs, distinctNames: new Set()}); mockUseLogs.mockReturnValue({filteredLogs: logs, distinctNames: new Set()});
const scrollSpy = jest.spyOn(Element.prototype, "scrollIntoView").mockImplementation(() => {}); const scrollSpy = jest.spyOn(Element.prototype, "scrollTo").mockImplementation(() => {});
const user = userEvent.setup(); const user = userEvent.setup();
const view = render(<Logging/>); const view = render(<Logging/>);
@@ -175,7 +175,7 @@ describe("Logging component", () => {
const logCell = makeCell({message: "Initial", firstRelativeCreated: 42}); const logCell = makeCell({message: "Initial", firstRelativeCreated: 42});
mockUseLogs.mockReturnValue({filteredLogs: [logCell], distinctNames: new Set()}); mockUseLogs.mockReturnValue({filteredLogs: [logCell], distinctNames: new Set()});
const scrollSpy = jest.spyOn(Element.prototype, "scrollIntoView").mockImplementation(() => {}); const scrollSpy = jest.spyOn(Element.prototype, "scrollTo").mockImplementation(() => {});
render(<Logging/>); render(<Logging/>);
await waitFor(() => { await waitFor(() => {
@@ -209,7 +209,7 @@ describe("Logging component", () => {
const initialMap = firstProps.filterPredicates; const initialMap = firstProps.filterPredicates;
expect(initialMap).toBeInstanceOf(Map); expect(initialMap).toBeInstanceOf(Map);
expect(initialMap.size).toBe(0); expect(initialMap.size).toBe(1); // Initially, only filter out experiment logs
expect(mockUseLogs).toHaveBeenCalledWith(initialMap); expect(mockUseLogs).toHaveBeenCalledWith(initialMap);
const updatedPredicate: LogFilterPredicate = { const updatedPredicate: LogFilterPredicate = {

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 { render, screen, act } from "@testing-library/react";
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import {type LogRecord, useLogs} from "../../../src/components/Logging/useLogs.ts"; 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 { render, screen, act, cleanup, waitFor } from '@testing-library/react';
import ConnectedRobots from '../../../src/pages/ConnectedRobots/ConnectedRobots'; 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 { render, screen, fireEvent, act } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import MonitoringPage from '../../../src/pages/MonitoringPage/MonitoringPage'; import MonitoringPage from '../../../src/pages/MonitoringPage/MonitoringPage';
@@ -27,7 +30,7 @@ jest.mock('../../../src/pages/MonitoringPage/MonitoringPageAPI', () => ({
// Mock VisProg functionality // Mock VisProg functionality
jest.mock('../../../src/pages/VisProgPage/VisProgLogic', () => ({ jest.mock('../../../src/pages/VisProgPage/VisProgLogic', () => ({
graphReducer: jest.fn(), graphReducer: jest.fn(),
runProgramm: jest.fn(), runProgram: jest.fn(),
})); }));
// Mock Child Components to reduce noise (optional, but keeps unit test focused) // Mock Child Components to reduce noise (optional, but keeps unit test focused)
@@ -51,6 +54,7 @@ describe('MonitoringPage', () => {
const mockGetPhaseNames = jest.fn(); const mockGetPhaseNames = jest.fn();
const mockGetNorms = jest.fn(); const mockGetNorms = jest.fn();
const mockGetGoals = jest.fn(); const mockGetGoals = jest.fn();
const mockGetGoalsWithDepth = jest.fn();
const mockGetTriggers = jest.fn(); const mockGetTriggers = jest.fn();
const mockSetProgramState = jest.fn(); const mockSetProgramState = jest.fn();
@@ -65,6 +69,7 @@ describe('MonitoringPage', () => {
getNormsInPhase: mockGetNorms, getNormsInPhase: mockGetNorms,
getGoalsInPhase: mockGetGoals, getGoalsInPhase: mockGetGoals,
getTriggersInPhase: mockGetTriggers, getTriggersInPhase: mockGetTriggers,
getGoalsWithDepth: mockGetGoalsWithDepth,
setProgramState: mockSetProgramState, setProgramState: mockSetProgramState,
}; };
return selector(state); return selector(state);
@@ -81,7 +86,11 @@ describe('MonitoringPage', () => {
// Default mock return values // Default mock return values
mockGetPhaseIds.mockReturnValue(['phase-1', 'phase-2']); mockGetPhaseIds.mockReturnValue(['phase-1', 'phase-2']);
mockGetPhaseNames.mockReturnValue(['Intro', 'Main']); 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' }]); mockGetTriggers.mockReturnValue([{ id: 't1', name: 'Trigger 1' }]);
mockGetNorms.mockReturnValue([ mockGetNorms.mockReturnValue([
{ id: 'n1', norm: 'Norm 1', condition: null }, { id: 'n1', norm: 'Norm 1', condition: null },
@@ -154,12 +163,12 @@ describe('MonitoringPage', () => {
expect(VisProg.graphReducer).toHaveBeenCalled(); expect(VisProg.graphReducer).toHaveBeenCalled();
expect(mockSetProgramState).toHaveBeenCalledWith({ phases: [{ id: 'new-phase' }] }); expect(mockSetProgramState).toHaveBeenCalledWith({ phases: [{ id: 'new-phase' }] });
expect(VisProg.runProgramm).toHaveBeenCalled(); expect(VisProg.runProgram).toHaveBeenCalled();
}); });
test('Reset Experiment handles errors gracefully', async () => { test('Reset Experiment handles errors gracefully', async () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); 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 />); render(<MonitoringPage />);
await act(async () => { 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 { renderHook, act, cleanup } from '@testing-library/react';
import { import {
sendAPICall, sendAPICall,
nextPhase, nextPhase,
resetPhase,
pauseExperiment, pauseExperiment,
playExperiment, playExperiment,
useExperimentLogger, 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 () => { test('pauseExperiment sends correct params', async () => {
await pauseExperiment(); await pauseExperiment();
expect(globalThis.fetch).toHaveBeenCalledWith( 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 React from 'react';
import { render, screen, fireEvent, act } from '@testing-library/react'; import { render, screen, fireEvent, act } from '@testing-library/react';
import '@testing-library/jest-dom'; 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 { render, screen, act, cleanup, fireEvent } from '@testing-library/react';
import Robot from '../../../src/pages/Robot/Robot'; 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();
});
});

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 {act} from '@testing-library/react'; import {act} from '@testing-library/react';
import useFlowStore from '../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx'; import useFlowStore from '../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx';
import { mockReactFlow } from '../../../setupFlowTests.ts'; import { mockReactFlow } from '../../../setupFlowTests.ts';
@@ -34,10 +37,17 @@ describe("UndoRedo Middleware", () => {
type: 'default', type: 'default',
position: {x: 0, y: 0}, position: {x: 0, y: 0},
data: {label: 'A'} data: {label: 'A'}
}, }
], ],
edges: [] edges: [],
warnings: {
warningRegistry: new Map(),
severityIndex: new Map()
}
}], }],
ruleRegistry: new Map(),
editorWarningRegistry: new Map(),
severityIndex: new Map()
}); });
act(() => { act(() => {
@@ -53,7 +63,11 @@ describe("UndoRedo Middleware", () => {
position: {x: 0, y: 0}, position: {x: 0, y: 0},
data: {label: 'A'} data: {label: 'A'}
}], }],
edges: [] edges: [],
warnings: {
warningRegistry: {},
severityIndex: {}
}
}); });
expect(state.future).toEqual([]); expect(state.future).toEqual([]);
}); });
@@ -80,7 +94,9 @@ describe("UndoRedo Middleware", () => {
position: {x: 0, y: 0}, position: {x: 0, y: 0},
data: {label: 'A'} data: {label: 'A'}
}], }],
edges: [] edges: [],
editorWarningRegistry: new Map(),
severityIndex: new Map()
}); });
act(() => { act(() => {
@@ -114,7 +130,11 @@ describe("UndoRedo Middleware", () => {
position: {x: 0, y: 0}, position: {x: 0, y: 0},
data: {label: 'B'} data: {label: 'B'}
}], }],
edges: [] edges: [],
warnings: {
warningRegistry: {},
severityIndex: {}
}
}); });
}); });
@@ -140,7 +160,9 @@ describe("UndoRedo Middleware", () => {
position: {x: 0, y: 0}, position: {x: 0, y: 0},
data: {label: 'A'} data: {label: 'A'}
}], }],
edges: [] edges: [],
editorWarningRegistry: new Map(),
severityIndex: new Map()
}); });
act(() => { act(() => {
@@ -176,7 +198,11 @@ describe("UndoRedo Middleware", () => {
position: {x: 0, y: 0}, position: {x: 0, y: 0},
data: {label: 'A'} data: {label: 'A'}
}], }],
edges: [] edges: [],
warnings: {
warningRegistry: {},
severityIndex: {}
}
}); });
}); });
@@ -199,7 +225,9 @@ describe("UndoRedo Middleware", () => {
position: {x: 0, y: 0}, position: {x: 0, y: 0},
data: {label: 'A'} data: {label: 'A'}
}], }],
edges: [] edges: [],
editorWarningRegistry: new Map(),
severityIndex: new Map()
}); });
act(() => { store.getState().beginBatchAction(); }); act(() => { store.getState().beginBatchAction(); });

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