Compare commits

88 Commits

Author SHA1 Message Date
JGerla
48c800eb2f Merge branch 'feat/editor-user-feedback' into demo 2026-01-20 12:55:35 +01:00
JGerla
327d1de621 feat: Added visual separation between global and node or handle specific warnings
ref: N25B-450
2026-01-20 12:54:47 +01:00
JGerla
3f6d95683d feat: Added visual separation between global and node or handle specific warnings
ref: N25B-450
2026-01-20 12:50:29 +01:00
Twirre Meulenbelt
633590e247 Merge remote-tracking branch 'origin/demo' into demo 2026-01-20 12:42:01 +01:00
Pim Hutting
e4ac063322 chore: now it works 2026-01-20 12:41:27 +01:00
Twirre Meulenbelt
ca5d50b824 Merge branch 'feat/experiment-logs' into demo 2026-01-20 12:39:41 +01:00
Pim Hutting
61b4afa4de Merge branch 'feat/monitoringpage-pim' into demo 2026-01-20 12:37:34 +01:00
Pim Hutting
a8f9965391 chore: added recursive goals to monitor page 2026-01-20 12:31:34 +01:00
JGerla
6da7e12138 Merge branch 'feat/editor-user-feedback' into demo
# Conflicts:
#	src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx
2026-01-20 12:14:54 +01:00
JGerla
5d55ebaaa2 feat: Added global warning for incomplete program chain
ref: N25B-450
2026-01-20 11:53:51 +01:00
JGerla
23a02b2b4a Merge branch 'demo' into feat/editor-user-feedback 2026-01-20 11:13:22 +01:00
JGerla
487ee30923 feat: made jumpToNode select the node after focussing the editor
ref: N25B-450
2026-01-20 10:22:08 +01:00
Pim Hutting
2ca0c9c4c0 chore: added Storms change 2026-01-19 18:17:47 +01:00
JGerla
7c9c0e1581 Merge branch 'demo' into feat/add-inferred-belief-node
# Conflicts:
#	src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx
#	src/pages/VisProgPage/visualProgrammingUI/nodes/NormNode.tsx
#	src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.tsx
2026-01-19 17:19:28 +01:00
JGerla
0ba026092d fix: fixed reconnect being able to circumvent connection rules
the fix fixes the behavior, however user feedback does not reflect the fix as it driven by events that aren't triggered by onReconnect connection, and thus it would take a lot of time to fix the user feedback for onReconnect
2026-01-19 17:13:46 +01:00
Twirre Meulenbelt
6d5f1e33ef feat: add experiment logs to the monitoring page
ref: N25B-401
2026-01-19 12:57:01 +01:00
Pim Hutting
dcc50fd978 feat: implemented basic version of reset phase
right now reset phase also clears LLM

ref: N25B-400
2026-01-18 13:56:47 +01:00
Pim Hutting
2c4f24fbb6 chore: gesture test updated 2026-01-18 12:49:14 +01:00
Pim Hutting
0a243851b1 Merge branch 'feat/monitoringpage-pim' into feat/monitoringpage 2026-01-18 12:40:41 +01:00
Pim Hutting
cada85e253 chore: added tests for monitoringpage
also moved some functions from VisProg outside VisProg.tsx into
VisProgLogic.tsx so I can reuse it for the reset experiment function
of monitor page
Also fixed a small merge error in TriggerNodes.tsx

ref: N25B-400
2026-01-18 12:38:23 +01:00
JobvAlewijk
2aede38e4d feat: 10 basic gestures
ref: N25B-300
2026-01-17 16:09:13 +01:00
Pim Hutting
ab383d77c4 chore: fixed some tests 2026-01-17 16:04:47 +01:00
Pim Hutting
45ef5a353c Merge remote-tracking branch 'origin/feat/add-inferred-belief-node' into feat/monitoringpage 2026-01-16 15:39:20 +01:00
Pim Hutting
d8cae9f838 feat: added reset experiment (in UI)
ref:N25B-400
2026-01-16 14:25:26 +01:00
Pim Hutting
c4e3ab27b2 chore: among other things, fixed connection issue
fix: connection issue
conditional norm now able to undo
 and are updated via pings
goals are able to be achieved out of turn

ref: N25B-400
2026-01-16 12:57:22 +01:00
JGerla
5a9b78fdda feat: added jump to node on clicking the warning
ref: N25B-450
2026-01-15 16:24:08 +01:00
JGerla
a6f24b677f feat: added two new warnings
ref: N25B-450
2026-01-15 16:09:24 +01:00
JGerla
022a6708ea feat: added a Warnings sidebar
warnings are now displayed in the sidebar

ref: N25B-450
2026-01-15 16:04:09 +01:00
JGerla
f62f416af3 Merge remote-tracking branch 'origin/feat/editor-user-feedback' into feat/editor-user-feedback
# Conflicts:
#	src/pages/VisProgPage/VisProg.tsx
2026-01-15 14:23:55 +01:00
JGerla
385ec250cc feat: finished basic warning system
nodes can now register warnings to prevent running the program

ref: N25B-450
2026-01-15 14:23:35 +01:00
JGerla
35bf3ad9e5 feat: finished basic warning system
nodes can now register warnings to prevent running the program

ref: N25B-450
2026-01-15 14:22:50 +01:00
JGerla
66daafe1f0 feat: added disabling of runProgram button if program is not valid
ref: N25B-450
2026-01-15 12:21:09 +01:00
JGerla
5d650b36ce feat: implemented the rest of the warning registry
ref: N25B-450
2026-01-15 12:15:32 +01:00
Pim Hutting
a98a87f8ce refactor: renamed Components.tsx
renamed it to MonitoringPageComponents.tsx

ref: N25B-400
2026-01-15 09:26:40 +01:00
Pim Hutting
714ee34bbe refactor: move all ZMQ to API
also removed a lot of logs to the console to avoid cluttering

ref: N25B-400
2026-01-15 09:24:51 +01:00
Pim Hutting
7d00f35990 feat: made unachieve norm possible
also small fix of formatting keywords said -> keyword said in plan
as we are only able to detect single keywords
(multiple keywords are possible with inferred beliefs)

ref: N25B-400
2026-01-15 09:04:32 +01:00
JGerla
e9acab456e feat: implemented getWarningsBySeverity
ref: N25B-450
2026-01-14 16:59:28 +01:00
JGerla
1a8670ba13 feat: implemented delete all warnings for a node
ref: N25B-450
2026-01-14 16:52:43 +01:00
JGerla
f174623a4c feat: implemented basic add and remove functions
ref: N25B-450
2026-01-14 16:46:50 +01:00
JGerla
b3b77b94ad feat: slightly modified structure for better global logic
ref: N25B-450
2026-01-14 16:11:48 +01:00
JGerla
67558a7ac7 feat: added full definition of editor warning infrastructure
Everything is now defined architecturally, and can be implemented properly.

ref: N25B-450
2026-01-14 16:04:44 +01:00
JGerla
f99ad7ad2e feat: added tooltip and patched potential breaking point in mode toggle for the new node
ref: N25B-433
2026-01-14 13:57:58 +01:00
JGerla
33f520d310 Merge branch 'demo' into feat/add-inferred-belief-node
# Conflicts:
#	src/pages/VisProgPage/VisProg.module.css
#	src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts
2026-01-14 13:48:57 +01:00
JGerla
1bec74a078 test: wrote tests for inferred Belief node
ref: N25B-433
2026-01-14 13:47:54 +01:00
JGerla
1f0237baac refactor: moved the nobeliefCyclerule into BeliefGlobals, and fixed function and filenaming
ref: N25B-433
2026-01-14 13:44:59 +01:00
Storm
5e245a00da chore: remove useState import 2026-01-13 12:39:48 +01:00
Storm
1e951968dd Merge remote-tracking branch 'origin/demo' into feat/monitoringpage 2026-01-13 12:39:25 +01:00
Björn Otgaar
108fdeeedc chore: gptd data typing and tests fixing 2026-01-13 12:03:49 +01:00
Björn Otgaar
79d889c10e chore: revert components page, fixing the test 2026-01-13 11:43:24 +01:00
Björn Otgaar
bac94d5f8c chore: remove unused imports 2026-01-13 11:41:58 +01:00
JGerla
3d6e065dd5 feat: added rule to prevent one belief being connected to both inferredBelief inputs
ref: N25B-433
2026-01-13 11:32:23 +01:00
JGerla
f8f0f12128 style: added documentation to the InferredBelief Node
ref: N25B-433
2026-01-13 09:26:02 +01:00
JGerla
2d9f430a30 style: cleaned up code for InferredBeliefNode.tsx
ref: N25B-433
2026-01-13 08:59:34 +01:00
JGerla
f8acdda03c feat: added rule to prevent cyclical connections between inferred belief nodes
ref: N25B-433
2026-01-13 08:52:50 +01:00
Pim Hutting
f95b1148d9 chore: made last page access more sensible
still didnt work with CB

ref: N25B-400
2026-01-13 00:53:41 +01:00
Pim Hutting
46d900305a chore: made activatable elements clickable
ref: N25B-400
2026-01-12 19:41:05 +01:00
Pim Hutting
c4a4c52ecc Merge remote-tracking branch 'origin/feat/recursive-goal-making' into feat/monitoringpage 2026-01-12 17:16:52 +01:00
JGerla
b869f7c071 feat: added reduce and connection logic
A rule still needs to be added to the inferred belief handles to prevent cyclical inferredBeliefs, but aside from that logic ifs finished.

ref: N25B-433
2026-01-12 16:44:25 +01:00
JGerla
0a4940bdd0 feat: made a basic version of inferredBeliefNode without all functionality
full reduce and connection logic is still missing

ref: N25B-433
2026-01-12 15:34:44 +01:00
Pim Hutting
96242fa6b0 chore: fix connection error
was connected to wrong endpoint after merge

ref: N25B-400
2026-01-12 15:17:38 +01:00
Pim Hutting
c2486f5f43 Merge branch 'feat/monitoringpage-pim' into feat/monitoringpage 2026-01-12 13:04:15 +01:00
Pim Hutting
f0c67c00dc fix: update goals and trigger/norms.. correctly
feat: N25B-400
2026-01-12 12:49:00 +01:00
Björn Otgaar
a0a4687aeb chore: add support for dark mode in monitoring page 2026-01-10 12:14:37 +01:00
Pim Hutting
71443c7fb6 feat: added scroll bar to simple program
note that it will take quite a lot of items
for the simple prog to be filled up, only when it's
overflown the scroll bar will appear

ref: N25B-400
2026-01-08 15:19:38 +01:00
Pim Hutting
39f013c47f feat: goals now update in UI
ref: N25B-400
2026-01-08 14:51:02 +01:00
Björn Otgaar
6e1eb25bbc feat: add robot connection
ref: N25B-400
2026-01-08 14:01:42 +01:00
Pim Hutting
f2c01f67ac feat: small implementation change
ref: N25B-400
2026-01-08 11:26:31 +01:00
Pim Hutting
14cfc2bf15 fix: removed unused imports
ref: N25B-400
2026-01-08 11:01:19 +01:00
Pim Hutting
a2b4847ca4 Merge remote-tracking branch 'origin/demo' into feat/monitoringpage-pim 2026-01-08 09:54:48 +01:00
Björn Otgaar
a1e242e391 feat: added the functionality for the play, pause, next phase, reset phase, reset experiment buttons
ref: N25B-400
2026-01-07 18:31:56 +01:00
Pim Hutting
4356f201ab feat: added endpoint
ref:N25B-400
2026-01-07 17:39:20 +01:00
Björn Otgaar
c9df87929b feat: add the buttons for next, reset phase and reset experiment
ref: N25B-400
2026-01-07 15:09:44 +01:00
Björn Otgaar
57ebe724db Merge remote-tracking branch 'origin/feat/monitoringpage-pim' into feat/monitoringpage-bjorn 2026-01-07 11:55:20 +01:00
Björn Otgaar
794e638081 feat: start with functionality
ref: N25B-400
2026-01-07 11:54:29 +01:00
Pim Hutting
12ef2ef86e feat: added forced speech/gestures +overrides
ref: N25B-400
2026-01-06 15:15:36 +01:00
Tuurminator69
0fefefe7f0 feat: removed the temporary access to MP from Home
ref: N25B-398
2026-01-05 17:41:36 +01:00
Tuurminator69
9601f56ea9 feat: merged most of simpleprogram into MP
ref: N25B-398
2026-01-05 17:35:32 +01:00
Tuurminator69
873b1cfb0b Merge branch 'feat/simple-program-page' of git.science.uu.nl:ics/sp/2025/n25b/pepperplus-ui into feat/monitoringpage 2026-01-05 16:41:51 +01:00
Tuurminator69
4bd67debf3 feat: added and changed the monitoringpage a lot
ref: N25B-398
2026-01-04 20:07:44 +01:00
Tuurminator69
e53e1a3958 feat: can go to a skeleton monitoringpage
ref: N25B-398
2026-01-03 21:59:51 +01:00
Tuurminator69
7a89b0aedd feat: added a skeleton for the monitoringpage.
ref: N25B-398
2026-01-03 21:07:32 +01:00
JobvAlewijk
7b05c7344c feat: added tests
ref: N25B-399
2026-01-02 21:06:41 +01:00
JobvAlewijk
d80ced547c feat: SimpleProgram no longer relies on types
ref: N25B-399
2026-01-02 20:55:24 +01:00
JobvAlewijk
cd1aa84f89 feat: using programstore
ref: N25B-399
2026-01-02 20:43:20 +01:00
JobvAlewijk
469a6c7a69 build: merge
ref: N25B-402
2026-01-02 19:56:01 +01:00
JobvAlewijk
b0a5e4770c feat: improved visuals and structure
ref: N25B-402
2025-12-30 20:56:05 +01:00
JobvAlewijk
f0fe520ea0 feat: first version of simple program shown
shows up if you run the program

ref: N25B-405
2025-12-30 18:10:51 +01:00
JGerla
b10dbae488 test: added tests for the ProgramStore
also added documentation

ref: N25B-428
2025-12-20 22:58:22 +01:00
140 changed files with 1506 additions and 2517 deletions

View File

@@ -1,2 +0,0 @@
# The location of the backend
VITE_API_BASE_URL=http://localhost:8000

77
.githooks/check-branch-name.sh Executable file
View File

@@ -0,0 +1,77 @@
#!/usr/bin/env bash
# This script checks if the current branch name follows the specified format.
# It's designed to be used as a 'pre-commit' git hook.
# Format: <type>/<short-description>
# Example: feat/add-user-login
# --- Configuration ---
# An array of allowed commit types
ALLOWED_TYPES=(feat fix refactor perf style test docs build chore revert)
# An array of branches to ignore
IGNORED_BRANCHES=(main dev demo)
# --- Colors for Output ---
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# --- Helper Functions ---
error_exit() {
echo -e "${RED}ERROR: $1${NC}" >&2
echo -e "${YELLOW}Branch name format is incorrect. Aborting commit.${NC}" >&2
exit 1
}
# --- Main Logic ---
# 1. Get the current branch name
BRANCH_NAME=$(git symbolic-ref --short HEAD)
# 2. Check if the current branch is in the ignored list
for ignored_branch in "${IGNORED_BRANCHES[@]}"; do
if [ "$BRANCH_NAME" == "$ignored_branch" ]; then
echo -e "${GREEN}Branch check skipped for default branch: $BRANCH_NAME${NC}"
exit 0
fi
done
# 3. Validate the overall structure: <type>/<description>
if ! [[ "$BRANCH_NAME" =~ ^[a-z]+/.+$ ]]; then
error_exit "Branch name must be in the format: <type>/<short-description>\nExample: feat/add-user-login"
fi
# 4. Extract the type and description
TYPE=$(echo "$BRANCH_NAME" | cut -d'/' -f1)
DESCRIPTION=$(echo "$BRANCH_NAME" | cut -d'/' -f2-)
# 5. Validate the <type>
type_valid=false
for allowed_type in "${ALLOWED_TYPES[@]}"; do
if [ "$TYPE" == "$allowed_type" ]; then
type_valid=true
break
fi
done
if [ "$type_valid" == false ]; then
error_exit "Invalid type '$TYPE'.\nAllowed types are: ${ALLOWED_TYPES[*]}"
fi
# 6. Validate the <short-description>
# Regex breakdown:
# ^[a-z0-9]+ - Starts with one or more lowercase letters/numbers (the first word).
# (-[a-z0-9]+){0,5} - Followed by a group of (dash + word) 0 to 5 times.
# $ - End of the string.
# This entire pattern enforces 1 to 6 words total, separated by dashes.
DESCRIPTION_REGEX="^[a-z0-9]+(-[a-z0-9]+){0,5}$"
if ! [[ "$DESCRIPTION" =~ $DESCRIPTION_REGEX ]]; then
error_exit "Invalid short description '$DESCRIPTION'.\nIt must be a maximum of 6 words, all lowercase, separated by dashes.\nExample: add-new-user-authentication-feature"
fi
# If all checks pass, exit successfully
echo -e "${GREEN}Branch name '$BRANCH_NAME' is valid.${NC}"
exit 0

138
.githooks/check-commit-msg.sh Executable file
View File

@@ -0,0 +1,138 @@
#!/usr/bin/env bash
# This script checks if a commit message follows the specified format.
# It's designed to be used as a 'commit-msg' git hook.
# Format:
# <type>: <short description>
#
# [optional]<body>
#
# [ref/close]: <issue identifier>
# --- Configuration ---
# An array of allowed commit types
ALLOWED_TYPES=(feat fix refactor perf style test docs build chore revert)
# --- Colors for Output ---
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# The first argument to the hook is the path to the file containing the commit message
COMMIT_MSG_FILE=$1
# --- Automated Commit Detection ---
# Read the first line (header) for initial checks
HEADER=$(head -n 1 "$COMMIT_MSG_FILE")
echo 'Given commit message:'
echo $HEADER
# Check for Merge commits (covers 'git merge' and PR merges from GitHub/GitLab)
# Examples: "Merge branch 'main' into ...", "Merge pull request #123 from ..."
MERGE_PATTERN="^Merge (remote-tracking )?(branch|pull request|tag) .*"
if [[ "$HEADER" =~ $MERGE_PATTERN ]]; then
echo -e "${GREEN}Merge commit detected by message content. Skipping validation.${NC}"
exit 0
fi
# Check for Revert commits
# Example: "Revert "feat: add new feature""
REVERT_PATTERN="^Revert \".*\""
if [[ "$HEADER" =~ $REVERT_PATTERN ]]; then
echo -e "${GREEN}Revert commit detected by message content. Skipping validation.${NC}"
exit 0
fi
# Check for Cherry-pick commits (this pattern appears at the end of the message)
# Example: "(cherry picked from commit deadbeef...)"
# We use grep -q to search the whole file quietly.
CHERRY_PICK_PATTERN="\(cherry picked from commit [a-f0-9]{7,40}\)"
if grep -qE "$CHERRY_PICK_PATTERN" "$COMMIT_MSG_FILE"; then
echo -e "${GREEN}Cherry-pick detected by message content. Skipping validation.${NC}"
exit 0
fi
# Check for Squash
# Example: "Squash commits ..."
SQUASH_PATTERN="^Squash .+"
if [[ "$HEADER" =~ $SQUASH_PATTERN ]]; then
echo -e "${GREEN}Squash commit detected by message content. Skipping validation.${NC}"
exit 0
fi
# --- Validation Functions ---
# Function to print an error message and exit
# Usage: error_exit "Your error message here"
error_exit() {
# >&2 redirects echo to stderr
echo -e "${RED}ERROR: $1${NC}" >&2
echo -e "${YELLOW}Commit message format is incorrect. Aborting commit.${NC}" >&2
exit 1
}
# --- Main Logic ---
# 1. Read the header (first line) of the commit message
HEADER=$(head -n 1 "$COMMIT_MSG_FILE")
# 2. Validate the header format: <type>: <description>
# Regex breakdown:
# ^(type1|type2|...) - Starts with one of the allowed types
# : - Followed by a literal colon
# \s - Followed by a single space
# .+ - Followed by one or more characters for the description
# $ - End of the line
TYPES_REGEX=$(
IFS="|"
echo "${ALLOWED_TYPES[*]}"
)
HEADER_REGEX="^($TYPES_REGEX): .+$"
if ! [[ "$HEADER" =~ $HEADER_REGEX ]]; then
error_exit "Invalid header format.\n\nHeader must be in the format: <type>: <short description>\nAllowed types: ${ALLOWED_TYPES[*]}\nExample: feat: add new user authentication feature"
fi
# Only validate footer if commit type is not chore
TYPE=$(echo "$HEADER" | cut -d':' -f1)
if [ "$TYPE" != "chore" ]; then
# 3. Validate the footer (last line) of the commit message
FOOTER=$(tail -n 1 "$COMMIT_MSG_FILE")
# Regex breakdown:
# ^(ref|close) - Starts with 'ref' or 'close'
# : - Followed by a literal colon
# \s - Followed by a single space
# N25B- - Followed by the literal string 'N25B-'
# [0-9]+ - Followed by one or more digits
# $ - End of the line
FOOTER_REGEX="^(ref|close): N25B-[0-9]+$"
if ! [[ "$FOOTER" =~ $FOOTER_REGEX ]]; then
error_exit "Invalid footer format.\n\nFooter must be in the format: [ref/close]: <issue identifier>\nExample: ref: N25B-123"
fi
fi
# 4. If the message has more than 2 lines, validate the separator
# A blank line must exist between the header and the body.
LINE_COUNT=$(wc -l <"$COMMIT_MSG_FILE" | xargs) # xargs trims whitespace
# We only care if there is a body. Header + Footer = 2 lines.
# Header + Blank Line + Body... + Footer > 2 lines.
if [ "$LINE_COUNT" -gt 2 ]; then
# Get the second line
SECOND_LINE=$(sed -n '2p' "$COMMIT_MSG_FILE")
# Check if the second line is NOT empty. If it's not, it's an error.
if [ -n "$SECOND_LINE" ]; then
error_exit "Missing blank line between header and body.\n\nThe second line of your commit message must be empty if a body is present."
fi
fi
# If all checks pass, exit with success
echo -e "${GREEN}Commit message is valid.${NC}"
exit 0

1
.husky/commit-msg Normal file
View File

@@ -0,0 +1 @@
sh .githooks/check-commit-msg.sh $1

3
.husky/pre-commit Normal file
View File

@@ -0,0 +1,3 @@
sh .githooks/check-branch-name.sh
npm run lint

View File

@@ -28,14 +28,6 @@ npm run dev
It should automatically reload when you save changes.
## Environment
Copy `.env.example` to `.env.local` and adjust values as needed:
```shell
cp .env.example .env.local
```
## Git Hooks
To activate automatic linting, branch name checks and commit message checks, run:

516
package-lock.json generated
View File

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

View File

@@ -8,7 +8,8 @@
"build": "tsc -b && vite build",
"lint": "eslint src test",
"preview": "vite preview",
"test": "jest"
"test": "jest",
"prepare": "husky"
},
"dependencies": {
"@neodrag/react": "^2.3.1",
@@ -17,7 +18,6 @@
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router": "^7.9.3",
"reactflow": "^11.11.4",
"zustand": "^5.0.8"
},
"devDependencies": {
@@ -34,6 +34,7 @@
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.4.0",
"husky": "^9.1.7",
"identity-obj-proxy": "^3.0.0",
"jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0",

Binary file not shown.

Binary file not shown.

View File

@@ -1,8 +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)
*/
.logopepper {
height: 8em;
padding: 1.5em;

View File

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

View File

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

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

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

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

@@ -1,5 +0,0 @@
export default function Replay({ fill }: { fill?: string }) {
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
<path d="M480.05-70.43q-76.72 0-143.78-29.1-67.05-29.1-116.75-78.8-49.69-49.69-78.79-116.75-29.1-67.05-29.1-143.49h91q0 115.81 80.73 196.47Q364.1-161.43 480-161.43q115.8 0 196.47-80.74 80.66-80.73 80.66-196.63 0-115.81-80.73-196.47-80.74-80.66-196.64-80.66h-6.24l60.09 60.08-58.63 60.63-166.22-166.21 166.22-166.22 58.63 60.87-59.85 59.85h6q76.74 0 143.76 29.09 67.02 29.1 116.84 78.8 49.81 49.69 78.91 116.64 29.1 66.95 29.1 143.61 0 76.66-29.1 143.71-29.1 67.06-78.79 116.75-49.7 49.7-116.7 78.8-67.01 29.1-143.73 29.1Z"/>
</svg>;
}

View File

@@ -1,8 +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)
*/
.filter-root {
position: relative;
display: flex;

View File

@@ -1,6 +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)
import {useEffect, useRef, useState} from "react";
import type {LogFilterPredicate} from "./useLogs.ts";

View File

@@ -1,8 +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)
*/
.logging-container {
box-sizing: border-box;

View File

@@ -1,6 +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)
import {type ComponentType, useEffect, useRef, useState} from "react";
import formatDuration from "../../utils/formatDuration.ts";
import {type LogFilterPredicate, type LogRecord, useLogs} from "./useLogs.ts";

View File

@@ -1,11 +1,7 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {useCallback, useEffect, useRef, useState} from "react";
import {applyPriorityPredicates, type PriorityFilterPredicate} from "../../utils/priorityFiltering.ts";
import {cell, type Cell} from "../../utils/cellStore.ts";
import { API_BASE_URL } from "../../config/api.ts";
type ExtraLevelName = 'LLM' | 'OBSERVATION' | 'ACTION' | 'CHAT';
@@ -211,7 +207,7 @@ export function useLogs(filterPredicates: Map<string, LogFilterPredicate>) {
// Only create one SSE connection for the lifetime of the hook.
if (sseRef.current) return;
const es = new EventSource(`${API_BASE_URL}/logs/stream`);
const es = new EventSource("http://localhost:8000/logs/stream");
sseRef.current = es;
es.onmessage = (event) => {

View File

@@ -1,6 +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)
import { useEffect, useRef, useState } from "react";
import styles from "./TextField.module.css";

View File

@@ -1,6 +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)
import {useEffect, useRef} from "react";
/**

View File

@@ -1,8 +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)
*/
.text-field {
border: 1px solid transparent;
border-radius: 5pt;

View File

@@ -1,6 +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)
import {useEffect, useState} from "react";
import styles from "./TextField.module.css";

View File

@@ -1,6 +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)
import { useState } from 'react'
/**

View File

@@ -1,11 +0,0 @@
declare const __VITE_API_BASE_URL__: string | undefined;
const DEFAULT_API_BASE_URL = "http://localhost:8000";
const rawApiBaseUrl =
(typeof __VITE_API_BASE_URL__ !== "undefined" ? __VITE_API_BASE_URL__ : undefined) ??
DEFAULT_API_BASE_URL;
export const API_BASE_URL = rawApiBaseUrl.endsWith("/")
? rawApiBaseUrl.slice(0, -1)
: rawApiBaseUrl;

View File

@@ -1,9 +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)
*/
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;

View File

@@ -1,6 +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)
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router'

View File

@@ -0,0 +1,60 @@
import { useEffect, useState } from 'react'
/**
* Displays the current connection status of a robot in real time.
*
* Opens an SSE connection to the backend (`/robot/ping_stream`) that emits
* simple boolean JSON messages (`true` or `false`). Updates automatically when
* the robot connects or disconnects.
*
* @returns A React element showing the current robot connection status.
*/
export default function ConnectedRobots() {
/**
* The current connection state:
* - `true`: Robot is connected.
* - `false`: Robot is not connected.
* - `null`: Connection status is unknown (initial check in progress).
*/
const [connected, setConnected] = useState<boolean | null>(null);
useEffect(() => {
// Open a Server-Sent Events (SSE) connection to receive live ping updates.
// We're expecting a stream of data like that looks like this: `data = False` or `data = True`
const eventSource = new EventSource("http://localhost:8000/robot/ping_stream");
eventSource.onmessage = (event) => {
// Expecting messages in JSON format: `true` or `false`
console.log("received message:", event.data);
try {
const data = JSON.parse(event.data);
try {
setConnected(data)
}
catch {
console.log("couldnt extract connected from incoming ping data")
}
} catch {
console.log("Ping message not in correct format:", event.data);
}
};
// Clean up the SSE connection when the component unmounts.
return () => eventSource.close();
}, []);
return (
<div>
<h1>Is robot currently connected?</h1>
<div>
<h2>Robot is currently: {connected == null ? "checking..." : (connected ? "connected! 🟢" : "not connected... 🔴")} </h2>
<h3>
{connected == null ? "If checking continues, make sure CB is properly loaded with robot at least once." : ""}
</h3>
</div>
</div>
);
}

View File

@@ -1,8 +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)
*/
.read_the_docs {
color: #888;
}
@@ -27,51 +22,3 @@ University within the Software Project course.
flex-direction: column;
gap: 1em;
}
.links {
display: flex;
flex-direction: row; /* Horizontal layout looks more like a dashboard */
gap: 1.5rem;
justify-content: center;
margin-top: 2rem;
}
.navCard {
display: flex;
align-items: center;
justify-content: center;
padding: 1rem 2rem;
min-width: 180px;
background-color: #ffffff;
color: #333;
text-decoration: none;
font-weight: 600;
border-radius: 12px;
border: 1px solid #e0e0e0;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease-in-out;
}
/* Hover effects */
.navCard:hover {
transform: translateY(-4px);
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
border-color: #ffcd00; /* UU Yellow accent */
color: #000;
}
/* Specific styling for the logo container */
.logoPepperScaling {
display: flex;
justify-content: center;
transition: transform 0.3s ease;
}
.logoPepperScaling:hover {
transform: scale(1.05);
}
.logopepper {
height: 120px;
width: auto;
}

View File

@@ -1,6 +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)
import { Link } from 'react-router'
import pepperLogo from '../../assets/pepper_transp2_small.svg'
import styles from './Home.module.css'
@@ -16,20 +13,16 @@ import styles from './Home.module.css'
function Home() {
return (
<div className={`flex-col ${styles.gapXl}`}>
<div className={styles.logoPepperScaling}>
<a href="https://git.science.uu.nl/ics/sp/2025/n25b" target="_blank" rel="noreferrer">
<img src={pepperLogo} className={styles.logopepper} alt="Pepper logo" />
<div className="logoPepperScaling">
<a href="https://git.science.uu.nl/ics/sp/2025/n25b" target="_blank">
<img src={pepperLogo} className="logopepper" alt="Pepper logo" />
</a>
</div>
<div className={styles.links}>
{/* Program Editor is now first */}
<Link to="/editor" className={styles.navCard}>
Program Editor
</Link>
<Link to="/user_manual" className={styles.navCard}>
User and Developer Manual
</Link>
<Link to={"/robot"}>Robot Interaction </Link>
<Link to={"/editor"}>Editor </Link>
<Link to={"/template"}>Template </Link>
<Link to={"/ConnectedRobots"}>Connected Robots </Link>
</div>
</div>
)

View File

@@ -1,81 +0,0 @@
/* This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/
.manualContainer {
width: 100%;
max-width: 800px;
margin: 4rem auto;
padding: 2rem;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
text-align: center;
}
.manualHeader h1 {
font-size: 2.2rem;
margin-bottom: 0.5rem;
}
.buttonStack {
display: flex;
flex-direction: column; /* Stacks the manual sections vertically */
gap: 3rem;
margin-top: 3rem;
align-items: center;
}
.manualEntry {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.manualEntry h3 {
margin: 0;
font-size: 1.4rem;
}
.manualEntry p {
color: #666;
margin-bottom: 0.5rem;
}
.downloadBtn {
display: inline-block;
background-color: #ffffff; /* White background as requested */
color: #000;
padding: 1rem 2rem;
border-radius: 50px;
text-decoration: none;
font-weight: 700;
font-size: 1.1rem;
width: 280px; /* Fixed width for uniform appearance */
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
transition: transform 0.2s ease, background-color 0.2s ease, color 0.2s ease;
}
.downloadBtn:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
background-color: #247284; /* Teal hover as requested */
color: #ffffff; /* Text turns white on teal for better contrast */
}
.dateBadge {
display: inline-block;
margin-top: 1rem;
padding: 0.4rem 1rem;
background-color: #f0f0f0;
border-radius: 4px;
font-size: 0.85rem;
color: #888;
}
.divider {
margin-top: 4rem;
border: 0;
border-top: 1px solid #eee;
width: 60%;
}

View File

@@ -1,39 +0,0 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import styles from './Manuals.module.css';
export default function Manuals() {
const userManualPath = "/UserManual.pdf";
const developerManualPath = "/DeveloperManual.pdf";
return (
<div className={styles.manualContainer}>
<header className={styles.manualHeader}>
<h1>Documentation & Manuals</h1>
<span className={styles.dateBadge}>Last Updated: January 2026</span>
</header>
<div className={styles.buttonStack}>
<div className={styles.manualEntry}>
<h3>User Manual</h3>
<p>Manual for Users of the Pepper+ Software </p>
<a href={userManualPath} download="UserManual.pdf" className={styles.downloadBtn}>
Download User Manual
</a>
</div>
<div className={styles.manualEntry}>
<h3>Developer Manual</h3>
<p>Technical documentation for future developers.</p>
<a href={developerManualPath} download="DeveloperManual.pdf" className={styles.downloadBtn}>
Download Developer Manual
</a>
</div>
</div>
<hr className={styles.divider} />
</div>
);
}

View File

@@ -1,8 +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)
*/
.dashboardContainer {
display: grid;
grid-template-columns: 2fr 1fr; /* Left = content, Right = logs */
@@ -33,22 +28,6 @@ University within the Software Project course.
position: static; /* ensures it scrolls away */
}
.controlsButtons {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: .25rem;
max-width: 260px;
flex-wrap: wrap;
button {
display: flex;
justify-content: center;
align-items: center;
}
}
.phaseProgress {
margin-top: 0.5rem;
}
@@ -98,8 +77,8 @@ University within the Software Project course.
color: white;
}
.stop {
background-color: red;
.restartPhase{
background-color: rgb(255, 123, 0);
color: white;
}

View File

@@ -1,14 +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 React, { useCallback, useState } from 'react';
import React, { useCallback, useRef, useState } from 'react';
import styles from './MonitoringPage.module.css';
// Store & API
import useProgramStore from "../../utils/programStore";
import {
nextPhase,
stopExperiment,
useExperimentLogger,
useStatusLogger,
pauseExperiment,
@@ -19,7 +15,7 @@ import {
type CondNormsStateUpdate,
type PhaseUpdate
} from "./MonitoringPageAPI";
import { graphReducer, runProgram } from '../VisProgPage/VisProgLogic.ts';
import { graphReducer, runProgramm } from '../VisProgPage/VisProgLogic.ts';
// Types
import type { NormNodeData } from '../VisProgPage/visualProgrammingUI/nodes/NormNode';
@@ -57,12 +53,16 @@ function useExperimentLogic() {
const [phaseIndex, setPhaseIndex] = useState(0);
const [isFinished, setIsFinished] = useState(false);
// Ref to suppress stream updates during the "Reset Phase" fast-forward sequence
const suppressUpdates = useRef(false);
const phaseIds = getPhaseIds();
const phaseNames = getPhaseNames();
// --- Stream Handlers ---
const handleStreamUpdate = useCallback((data: ExperimentStreamData) => {
if (suppressUpdates.current) return;
if (data.type === 'phase_update' && data.id) {
const payload = data as PhaseUpdate;
console.log(`${data.type} received, id : ${data.id}`);
@@ -106,6 +106,7 @@ function useExperimentLogic() {
}, [getPhaseIds, getGoalsInPhase, phaseIds, phaseIndex, phaseNames]);
const handleStatusUpdate = useCallback((data: unknown) => {
if (suppressUpdates.current) return;
const payload = data as CondNormsStateUpdate;
if (payload.type !== 'cond_norms_state_update') return;
@@ -136,7 +137,7 @@ function useExperimentLogic() {
setGoalIndex(0);
setIsFinished(false);
await runProgram();
await runProgramm();
console.log("Experiment & UI successfully reset.");
} catch (err) {
console.error("Failed to reset program:", err);
@@ -145,7 +146,7 @@ function useExperimentLogic() {
}
}, [setProgramState]);
const handleControlAction = async (action: "pause" | "play" | "nextPhase" | "stop") => {
const handleControlAction = async (action: "pause" | "play" | "nextPhase" | "resetPhase") => {
try {
setLoading(true);
switch (action) {
@@ -160,8 +161,29 @@ function useExperimentLogic() {
case "nextPhase":
await nextPhase();
break;
case "stop":
await stopExperiment();
case "resetPhase":
//make sure you don't see the phases pass to arrive back at current phase
suppressUpdates.current = true;
const targetIndex = phaseIndex;
console.log(`Resetting phase: Restarting and skipping to index ${targetIndex}`);
const phases = graphReducer();
setProgramState({ phases });
setActiveIds({});
setPhaseIndex(0); // Visually reset to start
setGoalIndex(0);
setIsFinished(false);
// Restart backend
await runProgramm();
for (let i = 0; i < targetIndex; i++) {
console.log(`Skipping phase ${i}...`);
await nextPhase();
}
suppressUpdates.current = false;
setPhaseIndex(targetIndex);
setIsPlaying(true); //Maybe you pause and then reset
break;
}
} catch (err) {
@@ -230,7 +252,7 @@ function ControlPanel({
}: {
loading: boolean,
isPlaying: boolean,
onAction: (a: "pause" | "play" | "nextPhase" | "stop") => void,
onAction: (a: "pause" | "play" | "nextPhase" | "resetPhase") => void,
onReset: () => void
}) {
return (
@@ -255,17 +277,17 @@ function ControlPanel({
disabled={loading}
></button>
<button
className={styles.restartPhase}
onClick={() => onAction("resetPhase")}
disabled={loading}
></button>
<button
className={styles.restartExperiment}
onClick={onReset}
disabled={loading}
></button>
<button
className={styles.stop}
onClick={() => onAction("stop")}
disabled={loading}
></button>
</div>
</div>
);

View File

@@ -1,15 +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)
import React, { useEffect } from 'react';
import { API_BASE_URL } from '../../config/api.ts';
const API_BASE = "http://localhost:8000";
const API_BASE_BP = API_BASE + "/button_pressed"; //UserInterruptAgent endpoint
/**
* HELPER: Unified sender function
*/
export const sendAPICall = async (type: string, context: string, endpoint?: string) => {
try {
const response = await fetch(`${API_BASE_URL}/button_pressed${endpoint ?? ""}`, {
const response = await fetch(`${API_BASE_BP}${endpoint ?? ""}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ type, context }),
@@ -32,13 +31,17 @@ export async function nextPhase(): Promise<void> {
sendAPICall(type, context)
}
export async function stopExperiment(): Promise<void> {
const type = "stop"
/**
* 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
*/
@@ -80,7 +83,7 @@ export function useExperimentLogger(onUpdate?: (data: ExperimentStreamData) => v
useEffect(() => {
console.log("Connecting to Experiment Stream...");
const eventSource = new EventSource(`${API_BASE_URL}/experiment_stream`);
const eventSource = new EventSource(`${API_BASE}/experiment_stream`);
eventSource.onmessage = (event) => {
try {
@@ -116,7 +119,7 @@ export function useStatusLogger(onUpdate?: (data: ExperimentStreamData) => void)
}, [onUpdate]);
useEffect(() => {
const eventSource = new EventSource(`${API_BASE_URL}/status_stream`);
const eventSource = new EventSource(`${API_BASE}/status_stream`);
eventSource.onmessage = (event) => {
try {
const parsedData = JSON.parse(event.data);

View File

@@ -1,10 +1,6 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import React, { useEffect, useState } from 'react';
import styles from './MonitoringPage.module.css';
import { sendAPICall } from './MonitoringPageAPI';
import { API_BASE_URL } from '../../config/api.ts';
// --- GESTURE COMPONENT ---
export const GestureControls: React.FC = () => {
@@ -202,7 +198,7 @@ export const RobotConnected = () => {
useEffect(() => {
// Open a Server-Sent Events (SSE) connection to receive live ping updates.
// We're expecting a stream of data like that looks like this: `data = False` or `data = True`
const eventSource = new EventSource(`${API_BASE_URL}/robot/ping_stream`);
const eventSource = new EventSource("http://localhost:8000/robot/ping_stream");
eventSource.onmessage = (event) => {
// Expecting messages in JSON format: `true` or `false`

View File

@@ -1,8 +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)
*/
.logs {
/* grid-area used in MonitoringPage.module.css */
grid-area: logs;

View File

@@ -1,6 +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)
import styles from "./ExperimentLogs.module.css";
import {LogMessages} from "../../../components/Logging/Logging.tsx";
import {useEffect, useMemo, useState} from "react";
@@ -17,7 +14,6 @@ import formatDuration from "../../../utils/formatDuration.ts";
import {create} from "zustand";
import Dialog from "../../../components/Dialog.tsx";
import delayedResolve from "../../../utils/delayedResolve.ts";
import { API_BASE_URL } from "../../../config/api.ts";
/**
* Local Zustand store for logging UI preferences.
@@ -111,7 +107,7 @@ function DownloadScreen({filenames, refresh}: {filenames: string[] | null, refre
return <ol className={`${styles.downloadList} margin-0 padding-h-lg scroll-y`}>
{filenames!.map((filename) => (
<li><a key={filename} href={`${API_BASE_URL}/api/logs/files/${filename}`} download={filename}>{filename}</a></li>
<li><a key={filename} href={`http://localhost:8000/api/logs/files/${filename}`} download={filename}>{filename}</a></li>
))}
</ol>;
})();
@@ -131,7 +127,7 @@ function DownloadButton() {
const [filenames, setFilenames] = useState<string[] | null>(null);
async function getFiles(): Promise<string[]> {
const response = await fetch(`${API_BASE_URL}/api/logs/files`);
const response = await fetch("http://localhost:8000/api/logs/files");
const files = await response.json();
files.sort();
return files;

130
src/pages/Robot/Robot.tsx Normal file
View File

@@ -0,0 +1,130 @@
import { useState, useEffect, useRef } from 'react'
/**
* Displays a live robot interaction panel with user input, conversation history,
* and real-time updates from the robot backend via Server-Sent Events (SSE).
*
* @returns A React element rendering the interactive robot UI.
*/
export default function Robot() {
/** The text message currently entered by the user. */
const [message, setMessage] = useState('');
/** Whether the robots microphone or listening mode is currently active. */
const [listening, setListening] = useState(false);
/** The ongoing conversation history as a sequence of user/assistant messages. */
const [conversation, setConversation] = useState<
{"role": "user" | "assistant", "content": string}[]>([])
/** Reference to the scrollable conversation container for auto-scrolling. */
const conversationRef = useRef<HTMLDivElement | null>(null);
/**
* Index used to force refresh the SSE connection or clear conversation.
* Incrementing this value triggers a reset of the live data stream.
*/
const [conversationIndex, setConversationIndex] = useState(0);
/**
* Sends a message to the robot backend.
*
* Makes a POST request to `/message` with the users text.
* The backend may respond with confirmation or error information.
*/
const sendMessage = async () => {
try {
const response = await fetch("http://localhost:8000/message", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ message }),
});
const data = await response.json();
console.log(data);
} catch (error) {
console.error("Error sending message: ", error);
}
};
/**
* Establishes a persistent Server-Sent Events (SSE) connection
* to receive real-time updates from the robot backend.
*
* Handles three event types:
* - `voice_active`: whether the robot is currently listening.
* - `speech`: recognized user speech input.
* - `llm_response`: the robots language model-generated reply.
*
* The connection resets whenever `conversationIndex` changes.
*/
useEffect(() => {
const eventSource = new EventSource("http://localhost:8000/sse");
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if ("voice_active" in data) setListening(data.voice_active);
if ("speech" in data) setConversation(conversation => [...conversation, {"role": "user", "content": data.speech}]);
if ("llm_response" in data) setConversation(conversation => [...conversation, {"role": "assistant", "content": data.llm_response}]);
} catch {
console.log("Unparsable SSE message:", event.data);
}
};
return () => {
eventSource.close();
};
}, [conversationIndex]);
/**
* Automatically scrolls the conversation view to the bottom
* whenever a new message is added.
*/
useEffect(() => {
if (!conversationRef || !conversationRef.current) return;
conversationRef.current.scrollTop = conversationRef.current.scrollHeight;
}, [conversation]);
return (
<>
<h1>Robot interaction</h1>
<h2>Force robot speech</h2>
<div className={"flex-row gap-md justify-center"}>
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && sendMessage().then(() => setMessage(""))}
placeholder="Enter a message"
/>
<button onClick={sendMessage}>Speak</button>
</div>
<div className={"flex-col gap-lg align-center"}>
<h2>Conversation</h2>
<p>Listening {listening ? "🟢" : "🔴"}</p>
<div style={{ maxHeight: "200px", maxWidth: "600px", overflowY: "auto"}} ref={conversationRef}>
{conversation.map((item, i) => (
<p key={i}
style={{
backgroundColor: item["role"] == "user"
? "color-mix(in oklab, canvas, blue 20%)"
: "color-mix(in oklab, canvas, gray 20%)",
whiteSpace: "pre-line",
}}
className={"round-md padding-md"}
>{item["content"]}</p>
))}
</div>
<div className={"flex-row gap-md justify-center"}>
<button onClick={() => {
setConversationIndex((conversationIndex) => conversationIndex + 1)
setConversation([])
}}>Reset</button>
<button onClick={() => {
setConversationIndex((conversationIndex) => conversationIndex == -1 ? 0 : -1)
setConversation([])
}}>{conversationIndex == -1 ? "Start" : "Stop"}</button>
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,167 @@
/* ---------- Layout ---------- */
.container {
height: 100%;
display: flex;
flex-direction: column;
background: #1e1e1e;
color: #f5f5f5;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: clamp(0.75rem, 2vw, 1.25rem);
background: #2a2a2a;
border-bottom: 1px solid #3a3a3a;
}
.header h2 {
font-size: clamp(1rem, 2.2vw, 1.4rem);
font-weight: 600;
}
.controls button {
margin-left: 0.5rem;
padding: 0.4rem 0.9rem;
border-radius: 6px;
border: none;
background: #111;
color: white;
cursor: pointer;
}
.controls button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* ---------- Content ---------- */
.content {
flex: 1;
padding: 2%;
}
/* ---------- Grid ---------- */
.phaseGrid {
height: 100%;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-rows: repeat(2, minmax(0, 1fr));
gap: 2%;
}
/* ---------- Box ---------- */
.box {
display: flex;
flex-direction: column;
background: #ffffff;
color: #1e1e1e;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
}
.boxHeader {
padding: 0.6rem 0.9rem;
background: linear-gradient(135deg, #dcdcdc, #e9e9e9);
font-style: italic;
font-weight: 500;
font-size: clamp(0.9rem, 1.5vw, 1.05rem);
border-bottom: 1px solid #cfcfcf;
}
.boxContent {
flex: 1;
padding: 0.8rem 1rem;
overflow-y: auto;
}
/* ---------- Lists ---------- */
.iconList {
list-style: none;
padding: 0;
margin: 0;
}
.iconList li {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 0.5rem;
font-size: clamp(0.85rem, 1.3vw, 1rem);
}
.bulletList {
margin: 0;
padding-left: 1.2rem;
}
.bulletList li {
margin-bottom: 0.4rem;
}
/* ---------- Icons ---------- */
.successIcon,
.failIcon {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.5rem;
height: 1.5rem;
border-radius: 4px;
font-weight: bold;
color: white;
flex-shrink: 0;
}
.successIcon {
background: #3cb371;
}
.failIcon {
background: #e5533d;
}
/* ---------- Empty ---------- */
.empty {
opacity: 0.55;
font-style: italic;
font-size: 0.9rem;
}
/* ---------- Responsive ---------- */
@media (max-width: 900px) {
.phaseGrid {
grid-template-columns: 1fr;
grid-template-rows: repeat(4, minmax(0, 1fr));
gap: 1rem;
}
}
.leftControls {
display: flex;
align-items: center;
gap: 1rem;
}
.backButton {
background: transparent;
border: 1px solid #555;
color: #ddd;
padding: 0.35rem 0.75rem;
border-radius: 6px;
cursor: pointer;
}
.backButton:hover {
background: #333;
}

View File

@@ -0,0 +1,192 @@
import React from "react";
import styles from "./SimpleProgram.module.css";
import useProgramStore from "../../utils/programStore.ts";
/**
* Generic container box with a header and content area.
*/
type BoxProps = {
title: string;
children: React.ReactNode;
};
const Box: React.FC<BoxProps> = ({ title, children }) => (
<div className={styles.box}>
<div className={styles.boxHeader}>{title}</div>
<div className={styles.boxContent}>{children}</div>
</div>
);
/**
* Renders a list of goals for a phase.
* Expects goal-like objects from the program store.
*/
const GoalList: React.FC<{ goals: unknown[] }> = ({ goals }) => {
if (!goals.length) {
return <p className={styles.empty}>No goals defined.</p>;
}
return (
<ul className={styles.iconList}>
{goals.map((g, idx) => {
const goal = g as {
id?: string;
description?: string;
achieved?: boolean;
};
return (
<li key={goal.id ?? idx}>
<span
className={
goal.achieved ? styles.successIcon : styles.failIcon
}
>
{goal.achieved ? "✔" : "✖"}
</span>
{goal.description ?? "Unnamed goal"}
</li>
);
})}
</ul>
);
};
/**
* Renders a list of triggers for a phase.
*/
const TriggerList: React.FC<{ triggers: unknown[] }> = ({ triggers }) => {
if (!triggers.length) {
return <p className={styles.empty}>No triggers defined.</p>;
}
return (
<ul className={styles.iconList}>
{triggers.map((t, idx) => {
const trigger = t as {
id?: string;
label?: string;
};
return (
<li key={trigger.id ?? idx}>
<span className={styles.failIcon}></span>
{trigger.label ?? "Unnamed trigger"}
</li>
);
})}
</ul>
);
};
/**
* Renders a list of norms for a phase.
*/
const NormList: React.FC<{ norms: unknown[] }> = ({ norms }) => {
if (!norms.length) {
return <p className={styles.empty}>No norms defined.</p>;
}
return (
<ul className={styles.bulletList}>
{norms.map((n, idx) => {
const norm = n as {
id?: string;
norm?: string;
};
return <li key={norm.id ?? idx}>{norm.norm ?? "Unnamed norm"}</li>;
})}
</ul>
);
};
/**
* Displays all phase-related information in a grid layout.
*/
type PhaseGridProps = {
norms: unknown[];
goals: unknown[];
triggers: unknown[];
};
const PhaseGrid: React.FC<PhaseGridProps> = ({
norms,
goals,
triggers,
}) => (
<div className={styles.phaseGrid}>
<Box title="Norms">
<NormList norms={norms} />
</Box>
<Box title="Triggers">
<TriggerList triggers={triggers} />
</Box>
<Box title="Goals">
<GoalList goals={goals} />
</Box>
<Box title="Conditional Norms">
<p className={styles.empty}>No conditional norms defined.</p>
</Box>
</div>
);
/**
* Main program viewer.
* Reads all data from the program store and allows
* navigating between phases.
*/
const SimpleProgram: React.FC = () => {
const getPhaseIds = useProgramStore((s) => s.getPhaseIds);
const getNormsInPhase = useProgramStore((s) => s.getNormsInPhase);
const getGoalsInPhase = useProgramStore((s) => s.getGoalsInPhase);
const getTriggersInPhase = useProgramStore((s) => s.getTriggersInPhase);
const phaseIds = getPhaseIds();
const [phaseIndex, setPhaseIndex] = React.useState(0);
if (phaseIds.length === 0) {
return <p className={styles.empty}>No program loaded.</p>;
}
const phaseId = phaseIds[phaseIndex];
return (
<div className={styles.container}>
<header className={styles.header}>
<h2>
Phase {phaseIndex + 1} / {phaseIds.length}
</h2>
<div className={styles.controls}>
<button
disabled={phaseIndex === 0}
onClick={() => setPhaseIndex((i) => i - 1)}
>
Prev
</button>
<button
disabled={phaseIndex === phaseIds.length - 1}
onClick={() => setPhaseIndex((i) => i + 1)}
>
Next
</button>
</div>
</header>
<div className={styles.content}>
<PhaseGrid
norms={getNormsInPhase(phaseId)}
goals={getGoalsInPhase(phaseId)}
triggers={getTriggersInPhase(phaseId)}
/>
</div>
</div>
);
};
export default SimpleProgram;

View File

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

View File

@@ -1,8 +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)
*/
/* editor UI */
.inner-editor-container {

View File

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

View File

@@ -1,12 +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 useProgramStore from "../../utils/programStore";
import orderPhaseNodeArray from "../../utils/orderPhaseNodes";
import useFlowStore from './visualProgrammingUI/VisProgStores';
import { NodeReduces } from './visualProgrammingUI/NodeRegistry';
import type { PhaseNode } from "./visualProgrammingUI/nodes/PhaseNode";
import { API_BASE_URL } from "../../config/api.ts";
/**
* Reduces the graph into its phases' information and recursively calls their reducing function
@@ -24,12 +20,12 @@ export function graphReducer() {
/**
* Outputs the prepared program to the console and sends it to the backend
*/
export function runProgram() {
export function runProgramm() {
const phases = graphReducer();
const program = {phases}
console.log(JSON.stringify(program, null, 2));
fetch(
`${API_BASE_URL}/program`,
"http://localhost:8000/program",
{
method: "POST",
headers: {"Content-Type": "application/json"},

View File

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

View File

@@ -1,6 +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)
import {type Connection} from "@xyflow/react";
import {useEffect} from "react";
import useFlowStore from "./VisProgStores.tsx";

View File

@@ -1,6 +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)
import {
type HandleRule,
ruleResult

View File

@@ -1,6 +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)
import EndNode, {
EndConnectionTarget,
EndConnectionSource,

View File

@@ -1,6 +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)
import { create } from 'zustand';
import {
applyNodeChanges,
@@ -48,18 +45,19 @@ function createNode(id: string, type: string, position: XYPosition, data: Record
}
}
//* Initial nodes, created by using createNode. */
// Start and End don't need to apply the UUID, since they are technically never compiled into a program.
const startNode = createNode('start', 'start', {x: 110, y: 100}, {label: "Start"}, false)
const endNode = createNode('end', 'end', {x: 590, y: 100}, {label: "End"}, false)
const initialPhaseNode = createNode(crypto.randomUUID(), 'phase', {x:235, y:100}, {label: "Phase 1", children : [], isFirstPhase: false, nextPhaseId: null})
//* Initial nodes, created by using createNode. */
// Start and End don't need to apply the UUID, since they are technically never compiled into a program.
const startNode = createNode('start', 'start', {x: 110, y: 100}, {label: "Start"}, false)
const endNode = createNode('end', 'end', {x: 590, y: 100}, {label: "End"}, false)
const initialPhaseNode = createNode(crypto.randomUUID(), 'phase', {x:235, y:100}, {label: "Phase 1", children : [], isFirstPhase: false, nextPhaseId: null})
const initialNodes : Node[] = [startNode, endNode, initialPhaseNode];
const initialNodes : Node[] = [startNode, endNode, initialPhaseNode];
// Initial edges, leave empty as setting initial edges...
// ...breaks logic that is dependent on connection events
const initialEdges: Edge[] = [];
/**
* useFlowStore contains the implementation for all editor functionality
* and stores the current state of the visual programming editor
@@ -90,20 +88,7 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
*/
onNodesChange: (changes) => set({nodes: applyNodeChanges(changes, get().nodes)}),
onNodesDelete: (deletedNodes) => {
const allNodes = get().nodes;
const deletedIds = new Set(deletedNodes.map(n => n.id));
deletedNodes.forEach((node) => {
get().unregisterNodeRules(node.id);
get().unregisterWarningsForId(node.id);
});
const remainingNodes = allNodes.filter((node) => !deletedIds.has(node.id));
// Validate only the survivors
get().validateDuplicateNames(remainingNodes);
},
onNodesDelete: (nodes) => nodes.forEach(node => get().unregisterNodeRules(node.id)),
onEdgesDelete: (edges) => {
// we make sure any affected nodes get updated to reflect removal of edges
@@ -233,36 +218,19 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
* Deletes a node by ID, respecting NodeDeletes rules.
* Also removes all edges connected to that node.
*/
deleteNode: (nodeId, deleteElements) => {
deleteNode: (nodeId) => {
get().pushSnapshot();
// Let's find our node to check if they have a special deletion function
const ourNode = get().nodes.find((n)=>n.id==nodeId);
const ourFunction = Object.entries(NodeDeletes).find(([t])=>t==ourNode?.type)?.[1]
// If there's no function, OR, our function tells us we can delete it, let's do so...
if (ourFunction == undefined || ourFunction()) {
if (deleteElements){
deleteElements({
nodes: get().nodes.filter((n) => n.id === nodeId),
edges: get().edges.filter((e) => e.source !== nodeId && e.target === nodeId)}
).then(() => {
get().unregisterNodeRules(nodeId);
get().unregisterWarningsForId(nodeId);
// Re-validate after deletion is finished
get().validateDuplicateNames(get().nodes);
});
} else {
const remainingNodes = get().nodes.filter((n) => n.id !== nodeId);
get().validateDuplicateNames(remainingNodes); // Re-validate survivors
set({
nodes: remainingNodes,
nodes: get().nodes.filter((n) => n.id !== nodeId),
edges: get().edges.filter((e) => e.source !== nodeId && e.target !== nodeId),
})
}
}
})}
},
/**
@@ -280,50 +248,16 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
*/
updateNodeData: (nodeId, data) => {
get().pushSnapshot();
const updatedNodes = get().nodes.map((node) => {
set({
nodes: get().nodes.map((node) => {
if (node.id === nodeId) {
return { ...node, data: { ...node.data, ...data } };
node = { ...node, data: { ...node.data, ...data }};
}
return node;
});
get().validateDuplicateNames(updatedNodes); // Re-validate after update
set({ nodes: updatedNodes });
},
//helper function to see if any of the nodes have duplicate names
validateDuplicateNames: (nodes: Node[]) => {
const nameMap = new Map<string, string[]>();
// 1. Group IDs by their identifier (name, norm, or label)
nodes.forEach((n) => {
const name = (n.data.name || n.data.norm )?.toString().trim();
if (name) {
if (!nameMap.has(name)) nameMap.set(name, []);
nameMap.get(name)!.push(n.id);
}
});
// 2. Scan nodes and toggle the warning
nodes.forEach((n) => {
const name = (n.data.name || n.data.norm )?.toString().trim();
const isDuplicate = name ? (nameMap.get(name)?.length || 0) > 1 : false;
if (isDuplicate) {
get().registerWarning({
scope: { id: n.id },
type: 'DUPLICATE_ELEMENT_NAME',
severity: 'ERROR',
description: `The name "${name}" is already used by another element.`
});
} else {
// This clears the warning if the "twin" was deleted or renamed
get().unregisterWarning(n.id, 'DUPLICATE_ELEMENT_NAME');
}
}),
});
},
/**
* Adds a new node to the flow store.
*/

View File

@@ -1,6 +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)
// VisProgTypes.ts
import type {
Edge,
@@ -10,7 +7,7 @@ import type {
OnReconnect,
Node,
OnEdgesDelete,
OnNodesDelete, DeleteElementsOptions
OnNodesDelete
} from '@xyflow/react';
import type {EditorWarningRegistry} from "./components/EditorWarnings.tsx";
import type {HandleRule} from "./HandleRuleLogic.ts";
@@ -72,10 +69,7 @@ export type FlowState = {
* Deletes a node and any connected edges.
* @param nodeId - the ID of the node to delete
*/
deleteNode: (nodeId: string, deleteElements?: (params: DeleteElementsOptions) => Promise<{
deletedNodes: Node[]
deletedEdges: Edge[]
}>) => void;
deleteNode: (nodeId: string) => void;
/**
* Replaces the current nodes array in the store.
@@ -96,11 +90,6 @@ export type FlowState = {
*/
updateNodeData: (nodeId: string, data: object) => void;
/**
* Validates that all node names are unique across the workspace.
*/
validateDuplicateNames: (nodes: Node[]) => void;
/**
* Adds a new node to the flow.
* @param node - the Node object to add

View File

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

View File

@@ -1,6 +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)
/* contains all logic for the VisProgEditor warning system
*
* Missing but desirable features:
@@ -22,9 +19,6 @@ export type WarningType =
| 'MISSING_OUTPUT'
| 'PLAN_IS_UNDEFINED'
| 'INCOMPLETE_PROGRAM'
| 'NOT_CONNECTED_TO_PROGRAM'
| 'ELEMENT_STARTS_WITH_NUMBER' //(non-phase)elements are not allowed to be or start with a number
| 'DUPLICATE_ELEMENT_NAME' // elements are not allowed to have the same name as another element
| string
export type WarningSeverity =
@@ -32,9 +26,6 @@ export type WarningSeverity =
| 'WARNING' // Acceptable, but probably undesirable behavior
| 'ERROR' // Prevents running program, should be fixed before running program is allowed
/**
* warning scope, include a handleId if the warning is handle specific
*/
export type WarningScope = {
id: string;
handleId?: string;
@@ -69,26 +60,11 @@ type ZustandSet = (partial: Partial<FlowState> | ((state: FlowState) => Partial<
type ZustandGet = () => FlowState;
export type EditorWarningRegistry = {
/**
* stores all editor warnings
*/
editorWarningRegistry: WarningRegistry;
/**
* index of warnings by severity
*/
severityIndex: SeverityIndex;
/**
* gets all warnings and returns them as a list of warnings
* @returns {EditorWarning[]}
*/
getWarnings: () => EditorWarning[];
/**
* gets all warnings with the current severity
* @param {WarningSeverity} warningSeverity
* @returns {EditorWarning[]}
*/
getWarningsBySeverity: (warningSeverity: WarningSeverity) => EditorWarning[];
/**
@@ -111,17 +87,13 @@ export type EditorWarningRegistry = {
/**
* unregisters warnings from the warningRegistry and the SeverityIndex
* @param {WarningId} warning
* @param {EditorWarning} 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 {
@@ -133,8 +105,8 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor
]),
getWarningsBySeverity: (warningSeverity) => {
const wRegistry = new Map([...get().editorWarningRegistry].map(([k, v]) => [k, new Map(v)]));
const sIndex = new Map(get().severityIndex);
const wRegistry = get().editorWarningRegistry;
const sIndex = get().severityIndex;
const warningKeys = sIndex.get(warningSeverity);
const warnings: EditorWarning[] = [];
@@ -165,8 +137,9 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor
const { scope: {id, handleId}, type, severity } = warning;
const warningKey = handleId ? `${type}:${handleId}` : type;
const compositeKey = `${id}|${warningKey}`;
const wRegistry = new Map([...get().editorWarningRegistry].map(([k, v]) => [k, new Map(v)]));
const sIndex = new Map(get().severityIndex);
const wRegistry = structuredClone(get().editorWarningRegistry);
const sIndex = structuredClone(get().severityIndex);
console.log("register")
// add to warning registry
if (!wRegistry.has(id)) {
wRegistry.set(id, new Map());
@@ -186,8 +159,9 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor
},
unregisterWarning: (id, warningKey) => {
const wRegistry = new Map([...get().editorWarningRegistry].map(([k, v]) => [k, new Map(v)]));
const sIndex = new Map(get().severityIndex);
const wRegistry = structuredClone(get().editorWarningRegistry);
const sIndex = structuredClone(get().severityIndex);
console.log("unregister")
// verify if the warning was created already
const warning = wRegistry.get(id)?.get(warningKey);
if (!warning) return;
@@ -206,17 +180,14 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor
},
unregisterWarningsForId: (id) => {
const wRegistry = new Map([...get().editorWarningRegistry].map(([k, v]) => [k, new Map(v)]));
const sIndex = new Map(get().severityIndex);
const wRegistry = structuredClone(get().editorWarningRegistry);
const sIndex = structuredClone(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;
nodeWarnings.forEach((warning, warningKey) => {
sIndex.get(warning.severity)?.delete(`${id}|${warningKey}`);
});
}
@@ -232,11 +203,7 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor
}}
/**
* returns a summary of the warningRegistry
* @returns {{info: number, warning: number, error: number, isValid: boolean}}
*/
// returns a summary of the warningRegistry
export function warningSummary() {
const {severityIndex, isProgramValid} = useFlowStore.getState();
return {

View File

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

View File

@@ -1,6 +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)
import { useState, useRef } from "react";
import styles from './GestureValueEditor.module.css'

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +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)
*/
.planDialog {
overflow:visible;
width: 80vw;

View File

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

View File

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

View File

@@ -1,12 +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 {
Handle,
type HandleProps,
type Connection,
useNodeId, useNodeConnections
} from '@xyflow/react';
import {useState} from 'react';
import { type HandleRule, useHandleRules} from "../HandleRuleLogic.ts";
import "./RuleBasedHandle.module.css";
@@ -31,16 +29,21 @@ export function MultiConnectionHandle({
handleId: id!
})
// initialise the handles state with { isValid: true } to show that connections are possible
const [handleState, setHandleState] = useState<{ isSatisfied: boolean, message?: string }>({ isSatisfied: true });
return (
<Handle
{...otherProps}
id={id}
type={type}
className={"multiConnectionHandle" + (connections.length === 0 ? " unconnected" : " connected") + ` ${type}`}
className={"multiConnectionHandle" + (connections.length === 0 ? " unconnected" : " connected")}
isValidConnection={(connection) => {
const result = validate(connection as Connection);
setHandleState(result);
return result.isSatisfied;
}}
title={handleState.message}
/>
);
}
@@ -63,18 +66,22 @@ export function SingleConnectionHandle({
handleId: id!
})
// initialise the handles state with { isValid: true } to show that connections are possible
const [handleState, setHandleState] = useState<{ isSatisfied: boolean, message?: string }>({ isSatisfied: true });
return (
<Handle
{...otherProps}
id={id}
type={type}
className={"singleConnectionHandle" + (connections.length === 0 ? " unconnected" : " connected") + ` ${type}`}
className={"singleConnectionHandle" + (connections.length === 0 ? " unconnected" : " connected")}
isConnectable={connections.length === 0}
isValidConnection={(connection) => {
const result = validate(connection as Connection);
setHandleState(result);
return result.isSatisfied;
}}
title={handleState.message}
/>
);
}

View File

@@ -1,8 +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)
*/
.save-load-panel {
border-radius: 0 0 5pt 5pt;
background-color: canvas;

View File

@@ -1,6 +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)
import {type ChangeEvent, useRef, useState} from "react";
import useFlowStore from "../VisProgStores";
import visProgStyles from "../../VisProg.module.css";
@@ -32,8 +29,6 @@ export default function SaveLoadPanel() {
const text = await file.text();
const parsed = JSON.parse(text) as SavedProject;
if (!parsed.nodes || !parsed.edges) throw new Error("Invalid file format");
const {nodes, unregisterWarningsForId} = useFlowStore.getState();
nodes.forEach((node) => {unregisterWarningsForId(node.id);});
setNodes(parsed.nodes);
setEdges(parsed.edges);
} catch (e) {

View File

@@ -1,61 +1,15 @@
/*
This program has been developed 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;
width: 320px;
height: 100%;
background: canvas;
border-left: 2px solid black;
display: flex;
flex-direction: row;
}
.warnings-toggle-bar {
background-color: ButtonFace;
justify-items: center;
align-content: center;
width: 1rem;
cursor: pointer;
}
.warnings-toggle-bar.error:first-child:has(.arrow-right){
background-color: hsl(from red h s 75%);
}
.warnings-toggle-bar.warning:first-child:has(.arrow-right) {
background-color: hsl(from orange h s 75%);
}
.warnings-toggle-bar.info:first-child:has(.arrow-right) {
background-color: hsl(from steelblue h s 75%);
}
.warnings-toggle-bar:hover {
background-color: GrayText !important ;
.arrow-left {
border-right-color: ButtonFace;
transition: transform 0.15s ease-in-out;
transform: rotateY(180deg);
}
.arrow-right {
border-left-color: ButtonFace;
transition: transform 0.15s ease-in-out;
transform: rotateY(180deg);
}
}
.warnings-content {
width: 320px;
flex: 1;
flex-direction: column;
border-left: 2px solid CanvasText;
}
.warnings-header {
padding: 12px;
border-bottom: 2px solid CanvasText;
border-bottom: 1px solid #2a2a2e;
}
.severity-tabs {
@@ -82,21 +36,15 @@ University within the Software Project course.
.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;
overflow-y: auto;
}
.warnings-empty {
@@ -105,13 +53,11 @@ University within the Software Project course.
.warning-item {
display: flex;
flex-direction: column;
margin: 5px;
gap: 2px;
padding: 0;
gap: 8px;
padding: 8px 12px;
border-radius: 5px;
cursor: pointer;
color: GrayText;
}
.warning-item:hover {
@@ -120,89 +66,17 @@ University within the Software Project course.
.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%);
border: 3px solid orange;
}
.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%);
}
}
border: 3px solid steelblue;
}
.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;
.warning-item .meta {
font-size: 11px;
opacity: 0.6;
}

View File

@@ -1,6 +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)
import {useReactFlow, useStoreApi} from "@xyflow/react";
import clsx from "clsx";
import {useEffect, useState} from "react";
@@ -12,83 +9,31 @@ import {
} from "./EditorWarnings.tsx";
import styles from "./WarningSidebar.module.css";
/**
* the warning sidebar, shows all warnings
*
* @returns {React.JSX.Element}
* @constructor
*/
export function WarningsSidebar() {
const warnings = useFlowStore.getState().getWarnings();
const [hide, setHide] = useState(false);
const [severityFilter, setSeverityFilter] = useState<WarningSeverity | 'ALL'>('ALL');
const [autoHide, setAutoHide] = useState(false);
// let autohide change hide status only when autohide is toggled
// and allow for user to change the hide state even if autohide is enabled
const hasWarnings = warnings.length > 0;
useEffect(() => {
if (autoHide) {
setHide(!hasWarnings);
}
}, [autoHide, hasWarnings]);
useEffect(() => {}, [warnings]);
const filtered = severityFilter === 'ALL'
? warnings
: warnings.filter(w => w.severity === severityFilter);
const summary = warningSummary();
// Finds the first key where the count > 0
const getHighestSeverity = () => {
if (summary.error > 0) return styles.error;
if (summary.warning > 0) return styles.warning;
if (summary.info > 0) return styles.info;
return '';
};
return (
<aside className={`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"}}
>
<aside className={styles.warningsSidebar}>
<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,
@@ -101,6 +46,7 @@ function WarningsHeader({
return (
<div className={styles.warningsHeader}>
<h3>Warnings</h3>
<div className={styles.severityTabs}>
{(['ALL', 'ERROR', 'WARNING', 'INFO'] as const).map(severity => (
<button
@@ -122,18 +68,13 @@ function WarningsHeader({
}
/**
* the list of warnings in the warning sidebar
*
* @param {{warnings: EditorWarning[]}} props
* @returns {React.JSX.Element}
* @constructor
*/
function WarningsList(props: { warnings: EditorWarning[] }) {
const splitWarnings = {
global: props.warnings.filter(w => w.scope.id === globalWarning),
other: props.warnings.filter(w => w.scope.id !== globalWarning),
}
if (props.warnings.length === 0) {
return (
<div className={styles.warningsEmpty}>
@@ -142,39 +83,30 @@ function WarningsList(props: { warnings: EditorWarning[] }) {
)
}
return (
<div className={styles.warningsList}>
<div>
<div className={"warningGroup"}>
<div className={styles.warningGroupHeader}>global:</div>
<div className={styles.warningsGroup}>
<div className={styles.warningsList}>
{splitWarnings.global.map((warning) => (
<WarningListItem warning={warning} key={`${warning.scope.id}|${warning.type}` + (warning.scope.handleId
? `:${warning.scope.handleId}`
: "")}
/>
<WarningListItem warning={warning} />
))}
{splitWarnings.global.length === 0 && "No global warnings!"}
</div>
</div>
<div className={"warningGroup"}>
<div className={styles.warningGroupHeader}>other:</div>
<div className={styles.warningsGroup}>
<div className={styles.warningsList}>
{splitWarnings.other.map((warning) => (
<WarningListItem warning={warning} key={`${warning.scope.id}|${warning.type}` + (warning.scope.handleId
? `:${warning.scope.handleId}`
: "")}
/>
<WarningListItem warning={warning} />
))}
{splitWarnings.other.length === 0 && "No other warnings!"}
</div>
</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}) {
function WarningListItem(props: { warning: EditorWarning }) {
const jumpToNode = useJumpToNode();
return (
@@ -182,47 +114,41 @@ function WarningListItem(props: { warning: EditorWarning, key: string}) {
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 className={styles.meta}>
{props.warning.scope.id}
{props.warning.scope.handleId && (
<span className={styles.handle}>@{props.warning.scope.handleId}</span>
)}
</div>
</div>
);
}
/**
* moves the editor to the provided node
* @returns {(nodeId: string) => void}
*/
function useJumpToNode() {
const { getNode, setCenter, getViewport } = useReactFlow();
const { getNode, setCenter } = 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
// user can't jump to global warning, so prevent further logic from running
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();
const { position, width = 0, height = 0} = node;
//move to node
// move to node
setCenter(
position!.x + ((width / viewport.zoom) / 2),
position!.y + ((height / viewport.zoom) / 2),
{duration: 300, interpolate: "smooth" }
position!.x + width / 2,
position!.y + height / 2,
{ zoom: 2, duration: 300 }
).then(() => {
// select the node
addSelectedNodes([nodeId]);
});
};
}

View File

@@ -1,6 +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)
import type { BasicBeliefNodeData } from "./BasicBeliefNode.tsx";

View File

@@ -1,6 +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)
import {
type NodeProps,
Position,
@@ -116,8 +113,10 @@ export default function BasicBeliefNode(props: NodeProps<BasicBeliefNode>) {
updateNodeData(props.id, {...data, belief: {...data.belief, description: value}});
}
// These are the labels outputted by our emotion detection model
const emotionOptions = ["sad", "angry", "surprise", "fear", "happy", "disgust", "neutral"];
let placeholder = ""
let wrapping = ""
switch (props.data.belief.type) {
@@ -192,8 +191,8 @@ export default function BasicBeliefNode(props: NodeProps<BasicBeliefNode>) {
)}
<MultiConnectionHandle type="source" position={Position.Right} id="source" rules={[
noMatchingLeftRightBelief,
allowOnlyConnectionsFromHandle([{nodeType:"trigger",handleId:"TriggerBeliefs"}, {nodeType:"norm",handleId:"NormBeliefs"},{nodeType:"InferredBelief",handleId:"inferred_belief"}]),
]} title="Connect to any number of trigger and/or normNode(-s)"/>
allowOnlyConnectionsFromHandle([{nodeType:"trigger",handleId:"TriggerBeliefs"}, {nodeType:"norm",handleId:"NormBeliefs"}]),
]}/>
</div>
</>
);

View File

@@ -1,6 +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)
import {getOutgoers, type Node} from '@xyflow/react';
import {type HandleRule, type RuleResult, ruleResult} from "../HandleRuleLogic.ts";
import useFlowStore from "../VisProgStores.tsx";

View File

@@ -1,6 +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)
import type { EndNodeData } from "./EndNode";
/**

View File

@@ -1,6 +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)
import {
type NodeProps,
Position,
@@ -63,7 +60,7 @@ export default function EndNode(props: NodeProps<EndNode>) {
</div>
<SingleConnectionHandle type="target" position={Position.Left} id="target" rules={[
allowOnlyConnectionsFromType(["phase"])
]} title="Connect to a phaseNode"/>
]}/>
</div>
</>
);

View File

@@ -1,6 +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)
import type { GoalNodeData } from "./GoalNode";
/**

View File

@@ -1,13 +1,8 @@
// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {
type NodeProps,
Position,
type Node
type Node,
} from '@xyflow/react';
import {useEffect} from "react";
import type {EditorWarning} from "../components/EditorWarnings.tsx";
import { Toolbar } from '../components/NodeComponents';
import styles from '../../VisProg.module.css';
import { TextField } from '../../../../components/TextField';
@@ -49,7 +44,7 @@ export type GoalNode = Node<GoalNodeData>
* @returns React.JSX.Element
*/
export default function GoalNode({id, data}: NodeProps<GoalNode>) {
const {updateNodeData, registerWarning, unregisterWarning} = useFlowStore();
const {updateNodeData} = useFlowStore();
const _nodes = useFlowStore().nodes;
const text_input_id = `goal_${id}_text_input`;
@@ -69,43 +64,6 @@ export default function GoalNode({id, data}: NodeProps<GoalNode>) {
updateNodeData(id, {...data, can_fail: value});
}
//undefined plan warning
useEffect(() => {
const noPlanWarning : EditorWarning = {
scope: {
id: id,
handleId: undefined
},
type: 'PLAN_IS_UNDEFINED',
severity: 'ERROR',
description: "This goalNode is missing a plan, please make sure to create a plan by using the create plan button"
};
if (!data.plan || data.plan.steps?.length === 0){
registerWarning(noPlanWarning);
return;
}
unregisterWarning(id, noPlanWarning.type);
},[data.plan, id, registerWarning, unregisterWarning])
//starts with number warning
useEffect(() => {
const name = data.name || "";
const startsWithNumberWarning: EditorWarning = {
scope: { id: id },
type: 'ELEMENT_STARTS_WITH_NUMBER',
severity: 'ERROR',
description: "Norms are not allowed to start with a number."
};
if (/^\d/.test(name)) {
registerWarning(startsWithNumberWarning);
} else {
unregisterWarning(id, 'ELEMENT_STARTS_WITH_NUMBER');
}
}, [data.name, id, registerWarning, unregisterWarning]);
return <>
<Toolbar nodeId={id} allowDelete={true}/>
<div className={`${styles.defaultNode} ${styles.nodeGoal} flex-col gap-sm`}>
@@ -160,11 +118,9 @@ export default function GoalNode({id, data}: NodeProps<GoalNode>) {
</div>
<MultiConnectionHandle type="source" position={Position.Right} id="GoalSource" rules={[
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}]),
]} title="Connect to any number of phase and/or goalNode(-s)"/>
]}/>
<MultiConnectionHandle type="target" position={Position.Bottom} id="GoalTarget" rules={[
allowOnlyConnectionsFromType(["goal"])]
} title="Connect to any number of goalNode(-s)"/>
<MultiConnectionHandle type="target" position={Position.Bottom} id="GoalTarget" rules={[allowOnlyConnectionsFromType(["goal"])]}/>
</div>

View File

@@ -1,6 +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)
import type { InferredBeliefNodeData } from "./InferredBeliefNode.tsx";
@@ -8,7 +5,7 @@ import type { InferredBeliefNodeData } from "./InferredBeliefNode.tsx";
* Default data for this node
*/
export const InferredBeliefNodeDefaults: InferredBeliefNodeData = {
label: "AND/OR",
label: "Inferred Belief",
droppable: true,
inferredBelief: {
left: undefined,

View File

@@ -1,8 +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)
*/
.operator-switch {
display: inline-flex;
align-items: center;

View File

@@ -1,10 +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 {getConnectedEdges, type Node, type NodeProps, Position, useNodeConnections} from '@xyflow/react';
import {useEffect, useState} from "react";
import {getConnectedEdges, type Node, type NodeProps, Position} from '@xyflow/react';
import {useState} from "react";
import styles from '../../VisProg.module.css';
import type {EditorWarning} from "../components/EditorWarnings.tsx";
import {Toolbar} from '../components/NodeComponents.tsx';
import {MultiConnectionHandle, SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
import {allowOnlyConnectionsFromType} from "../HandleRules.ts";
@@ -95,7 +91,7 @@ export const InferredBeliefTooltip = `
*/
export default function InferredBeliefNode(props: NodeProps<InferredBeliefNode>) {
const data = props.data;
const { updateNodeData, registerWarning, unregisterWarning } = useFlowStore();
const { updateNodeData } = useFlowStore();
// start of as an AND operator, true: "AND", false: "OR"
const [enforceAllBeliefs, setEnforceAllBeliefs] = useState(true);
@@ -113,29 +109,6 @@ export default function InferredBeliefNode(props: NodeProps<InferredBeliefNode>)
});
}
const beliefConnections = useNodeConnections({
id: props.id,
handleType: "target",
})
useEffect(() => {
const noBeliefsWarning : EditorWarning = {
scope: {
id: props.id,
handleId: undefined
},
type: 'MISSING_INPUT',
severity: 'ERROR',
description: `This AND/OR node is missing one or more beliefs,
please make sure to use both inputs of an AND/OR node`
};
if (beliefConnections.length < 2){
registerWarning(noBeliefsWarning);
return;
}
unregisterWarning(props.id, noBeliefsWarning.type);
},[beliefConnections.length, props.id, registerWarning, unregisterWarning])
return (
<>
<Toolbar nodeId={props.id} allowDelete={true}/>

View File

@@ -1,6 +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)
import type { NormNodeData } from "./NormNode";
/**

View File

@@ -1,8 +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)
import { useEffect } from "react";
import type { EditorWarning } from "../components/EditorWarnings.tsx";
import {
type NodeProps,
Position,
@@ -41,7 +36,7 @@ export type NormNode = Node<NormNodeData>
*/
export default function NormNode(props: NodeProps<NormNode>) {
const data = props.data;
const {updateNodeData, registerWarning, unregisterWarning} = useFlowStore();
const {updateNodeData} = useFlowStore();
const text_input_id = `norm_${props.id}_text_input`;
const checkbox_id = `goal_${props.id}_checkbox`;
@@ -49,44 +44,10 @@ export default function NormNode(props: NodeProps<NormNode>) {
const setValue = (value: string) => {
updateNodeData(props.id, {norm: value});
}
//this function is commented out, because of lack of backend implementation.
//If you wish to set critical norms, in the UI side, you can uncomment and use this function.
// const setCritical = (value: boolean) => {
// updateNodeData(props.id, {...data, critical: value});
// }
useEffect(() => {
const normText = data.norm || "";
const startsWithNumberWarning: EditorWarning = {
scope: { id: props.id },
type: 'ELEMENT_STARTS_WITH_NUMBER',
severity: 'ERROR',
description: "Norms are not allowed to start with a number."
};
if (/^\d/.test(normText)) {
registerWarning(startsWithNumberWarning);
} else {
unregisterWarning(props.id, 'ELEMENT_STARTS_WITH_NUMBER');
const setCritical = (value: boolean) => {
updateNodeData(props.id, {...data, critical: value});
}
}, [data.norm, props.id, registerWarning, unregisterWarning]);
useEffect(() => {
const normText = data.norm || "";
const startsWithNumberWarning: EditorWarning = {
scope: { id: props.id },
type: 'ELEMENT_STARTS_WITH_NUMBER',
severity: 'ERROR',
description: "Norms are not allowed to start with a number."
};
if (/^\d/.test(normText)) {
registerWarning(startsWithNumberWarning);
} else {
unregisterWarning(props.id, 'ELEMENT_STARTS_WITH_NUMBER');
}
}, [data.norm, props.id, registerWarning, unregisterWarning]);
return <>
<Toolbar nodeId={props.id} allowDelete={true}/>
@@ -100,10 +61,7 @@ export default function NormNode(props: NodeProps<NormNode>) {
placeholder={"Pepper should ..."}
/>
</div>
{/*There is no backend implementation yet of how critical norms would
be treated differently than normal norms. The commented code below shows
how you could add the UI side, if you wish to implement */}
{/* <div className={"flex-row gap-md align-center"}>
<div className={"flex-row gap-md align-center"}>
<label htmlFor={checkbox_id}>Critical:</label>
<input
id={checkbox_id}
@@ -111,7 +69,7 @@ export default function NormNode(props: NodeProps<NormNode>) {
checked={data.critical || false}
onChange={(e) => setCritical(e.target.checked)}
/>
</div> */}
</div>
{data.condition && (<div className={"flex-row gap-md align-center"} data-testid="norm-condition-information">
@@ -121,10 +79,10 @@ export default function NormNode(props: NodeProps<NormNode>) {
<MultiConnectionHandle type="source" position={Position.Right} id="norms" rules={[
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}])
]} title="Connect to any number of phaseNode(-s)"/>
]}/>
<SingleConnectionHandle type="target" position={Position.Bottom} id="NormBeliefs" rules={[
allowOnlyConnectionsFromType(["basic_belief", "inferred_belief"])
]} title="Connect to a beliefNode or a set of beliefs combined using the AND/OR node"/>
]}/>
</div>
</>;
};

View File

@@ -1,6 +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)
import type { PhaseNodeData } from "./PhaseNode";
/**

View File

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

View File

@@ -1,6 +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)
import type { StartNodeData } from "./StartNode";
/**

View File

@@ -1,6 +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)
import {
type NodeProps,
Position,
@@ -61,7 +58,7 @@ export default function StartNode(props: NodeProps<StartNode>) {
</div>
<SingleConnectionHandle type="source" position={Position.Right} id="source" rules={[
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"target"}])
]} title="Connect to a phaseNode"/>
]}/>
</div>
</>
);

View File

@@ -1,6 +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)
import type { TriggerNodeData } from "./TriggerNode";
/**

View File

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

View File

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

View File

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

View File

@@ -1,6 +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)
/**
* Find the indices of all elements that occur more than once.
*

View File

@@ -1,6 +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)
/**
* Format a time duration like `HH:MM:SS.mmm`.
*

View File

@@ -1,6 +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)
import type {PhaseNode} from "../pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx";
/**

View File

@@ -1,6 +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)
export type PriorityFilterPredicate<T> = {
priority: number;
predicate: (element: T) => boolean | null; // The predicate and its priority are ignored if it returns null.

View File

@@ -1,6 +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)
import {create} from "zustand";
// the type of a reduced program
@@ -88,8 +85,9 @@ const useProgramStore = create<ProgramState>((set, get) => ({
const rootGoals = phase["goals"] as Record<string, unknown>[];
const flatList: GoalWithDepth[] = [];
// Helper: Define this ONCE, outside the loop
const isGoal = (item: Record<string, unknown>) => {
return item["plan"] !== undefined;
return item["plan"] !== undefined && item["plan"] !== null;
};
// Recursive helper function

11
src/vite-env.d.ts vendored
View File

@@ -1,11 +0,0 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_BASE_URL?: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
declare const __VITE_API_BASE_URL__: string | undefined;

View File

@@ -1,6 +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)
import userEvent from '@testing-library/user-event';
import { render, screen} from '@testing-library/react';
import Counter from '../src/components/components';

View File

@@ -1,6 +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)
import {render, screen, waitFor, fireEvent} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import * as React from "react";

View File

@@ -1,6 +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)
import {render, screen, fireEvent, act, waitFor} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom";

View File

@@ -1,12 +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 { render, screen, act } from "@testing-library/react";
import "@testing-library/jest-dom";
import {type LogRecord, useLogs} from "../../../src/components/Logging/useLogs.ts";
import {type cell, useCell} from "../../../src/utils/cellStore.ts";
import { StrictMode } from "react";
import { API_BASE_URL } from "../../../src/config/api.ts";
jest.mock("../../../src/utils/priorityFiltering.ts", () => ({
applyPriorityPredicates: jest.fn((_log, preds: any[]) =>
@@ -84,7 +80,7 @@ describe("useLogs (unit)", () => {
);
const es = (globalThis as any).__es as MockEventSource;
expect(es).toBeTruthy();
expect(es.url).toBe(`${API_BASE_URL}/logs/stream`);
expect(es.url).toBe("http://localhost:8000/logs/stream");
unmount();
expect(es.close).toHaveBeenCalledTimes(1);

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