Compare commits

4 Commits

9 changed files with 81 additions and 298 deletions

View File

@@ -1,77 +0,0 @@
#!/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

View File

@@ -1,138 +0,0 @@
#!/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

View File

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

View File

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

17
package-lock.json generated
View File

@@ -31,7 +31,6 @@
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.4.0", "globals": "^16.4.0",
"husky": "^9.1.7",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^30.2.0", "jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0", "jest-environment-jsdom": "^30.2.0",
@@ -5544,22 +5543,6 @@
"node": ">=10.17.0" "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": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",

View File

@@ -8,8 +8,7 @@
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "eslint src test", "lint": "eslint src test",
"preview": "vite preview", "preview": "vite preview",
"test": "jest", "test": "jest"
"prepare": "husky"
}, },
"dependencies": { "dependencies": {
"@neodrag/react": "^2.3.1", "@neodrag/react": "^2.3.1",
@@ -35,7 +34,6 @@
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.4.0", "globals": "^16.4.0",
"husky": "^9.1.7",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^30.2.0", "jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0", "jest-environment-jsdom": "^30.2.0",

View File

@@ -98,6 +98,11 @@ University within the Software Project course.
color: white; color: white;
} }
.stop {
background-color: red;
color: white;
}
.restartExperiment{ .restartExperiment{
background-color: red; background-color: red;
color: white; color: white;

View File

@@ -6,17 +6,18 @@ import styles from './MonitoringPage.module.css';
// Store & API // Store & API
import useProgramStore from "../../utils/programStore"; import useProgramStore from "../../utils/programStore";
import { import {
nextPhase, nextPhase,
useExperimentLogger, stopExperiment,
useStatusLogger, useExperimentLogger,
pauseExperiment, useStatusLogger,
playExperiment, pauseExperiment,
type ExperimentStreamData, playExperiment,
type GoalUpdate, type ExperimentStreamData,
type TriggerUpdate, type GoalUpdate,
type CondNormsStateUpdate, type TriggerUpdate,
type PhaseUpdate type CondNormsStateUpdate,
type PhaseUpdate
} from "./MonitoringPageAPI"; } from "./MonitoringPageAPI";
import { graphReducer, runProgram } from '../VisProgPage/VisProgLogic.ts'; import { graphReducer, runProgram } from '../VisProgPage/VisProgLogic.ts';
@@ -26,12 +27,12 @@ import type { GoalNode } from '../VisProgPage/visualProgrammingUI/nodes/GoalNode
import type { TriggerNode } from '../VisProgPage/visualProgrammingUI/nodes/TriggerNode'; import type { TriggerNode } from '../VisProgPage/visualProgrammingUI/nodes/TriggerNode';
// Sub-components // Sub-components
import { import {
GestureControls, GestureControls,
SpeechPresets, SpeechPresets,
DirectSpeechInput, DirectSpeechInput,
StatusList, StatusList,
RobotConnected RobotConnected
} from './MonitoringPageComponents'; } from './MonitoringPageComponents';
import ExperimentLogs from "./components/ExperimentLogs.tsx"; import ExperimentLogs from "./components/ExperimentLogs.tsx";
@@ -76,7 +77,7 @@ function useExperimentLogic() {
setGoalIndex(0); setGoalIndex(0);
} }
} }
} }
else if (data.type === 'goal_update') { else if (data.type === 'goal_update') {
const payload = data as GoalUpdate; const payload = data as GoalUpdate;
const currentPhaseGoals = getGoalsInPhase(phaseIds[phaseIndex]) as GoalNode[]; const currentPhaseGoals = getGoalsInPhase(phaseIds[phaseIndex]) as GoalNode[];
@@ -97,7 +98,7 @@ function useExperimentLogic() {
return nextState; return nextState;
}); });
} }
} }
else if (data.type === 'trigger_update') { else if (data.type === 'trigger_update') {
const payload = data as TriggerUpdate; const payload = data as TriggerUpdate;
setActiveIds((prev) => ({ ...prev, [payload.id]: payload.achieved })); setActiveIds((prev) => ({ ...prev, [payload.id]: payload.achieved }));
@@ -111,7 +112,7 @@ function useExperimentLogic() {
setActiveIds((prev) => { setActiveIds((prev) => {
const hasChanges = payload.norms.some((u) => prev[u.id] !== u.active); const hasChanges = payload.norms.some((u) => prev[u.id] !== u.active);
if (!hasChanges) return prev; if (!hasChanges) return prev;
const nextState = { ...prev }; const nextState = { ...prev };
payload.norms.forEach((u) => { nextState[u.id] = u.active; }); payload.norms.forEach((u) => { nextState[u.id] = u.active; });
return nextState; return nextState;
@@ -144,7 +145,7 @@ function useExperimentLogic() {
} }
}, [setProgramState]); }, [setProgramState]);
const handleControlAction = async (action: "pause" | "play" | "nextPhase") => { const handleControlAction = async (action: "pause" | "play" | "nextPhase" | "stop") => {
try { try {
setLoading(true); setLoading(true);
switch (action) { switch (action) {
@@ -159,6 +160,9 @@ function useExperimentLogic() {
case "nextPhase": case "nextPhase":
await nextPhase(); await nextPhase();
break; break;
case "stop":
await stopExperiment();
break;
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@@ -189,14 +193,14 @@ function useExperimentLogic() {
/** /**
* Visual indicator of progress through experiment phases. * Visual indicator of progress through experiment phases.
*/ */
function PhaseProgressBar({ function PhaseProgressBar({
phaseIds, phaseIds,
phaseIndex, phaseIndex,
isFinished isFinished
}: { }: {
phaseIds: string[], phaseIds: string[],
phaseIndex: number, phaseIndex: number,
isFinished: boolean isFinished: boolean
}) { }) {
return ( return (
<div className={styles.phaseProgress}> <div className={styles.phaseProgress}>
@@ -218,16 +222,16 @@ function PhaseProgressBar({
/** /**
* Main control buttons (Play, Pause, Next, Reset). * Main control buttons (Play, Pause, Next, Reset).
*/ */
function ControlPanel({ function ControlPanel({
loading, loading,
isPlaying, isPlaying,
onAction, onAction,
onReset onReset
}: { }: {
loading: boolean, loading: boolean,
isPlaying: boolean, isPlaying: boolean,
onAction: (a: "pause" | "play" | "nextPhase") => void, onAction: (a: "pause" | "play" | "nextPhase" | "stop") => void,
onReset: () => void onReset: () => void
}) { }) {
return ( return (
<div className={styles.experimentControls}> <div className={styles.experimentControls}>
@@ -245,17 +249,23 @@ function ControlPanel({
disabled={loading} disabled={loading}
></button> ></button>
<button <button
className={styles.next} className={styles.next}
onClick={() => onAction("nextPhase")} onClick={() => onAction("nextPhase")}
disabled={loading} disabled={loading}
></button> ></button>
<button <button
className={styles.restartExperiment} className={styles.restartExperiment}
onClick={onReset} onClick={onReset}
disabled={loading} disabled={loading}
></button> ></button>
<button
className={styles.stop}
onClick={() => onAction("stop")}
disabled={loading}
></button>
</div> </div>
</div> </div>
); );
@@ -353,11 +363,11 @@ const MonitoringPage: React.FC = () => {
<PhaseProgressBar phaseIds={phaseIds} phaseIndex={phaseIndex} isFinished={isFinished} /> <PhaseProgressBar phaseIds={phaseIds} phaseIndex={phaseIndex} isFinished={isFinished} />
</div> </div>
<ControlPanel <ControlPanel
loading={loading} loading={loading}
isPlaying={isPlaying} isPlaying={isPlaying}
onAction={handleControlAction} onAction={handleControlAction}
onReset={resetExperiment} onReset={resetExperiment}
/> />
<div className={styles.connectionStatus}> <div className={styles.connectionStatus}>
@@ -370,17 +380,17 @@ const MonitoringPage: React.FC = () => {
<section className={styles.phaseOverviewText}> <section className={styles.phaseOverviewText}>
<h3>Phase Overview</h3> <h3>Phase Overview</h3>
</section> </section>
{isFinished ? ( {isFinished ? (
<div className={styles.finishedMessage}> <div className={styles.finishedMessage}>
<p>All phases have been successfully completed.</p> <p>All phases have been successfully completed.</p>
</div> </div>
) : ( ) : (
<PhaseDashboard <PhaseDashboard
phaseId={phaseIds[phaseIndex]} phaseId={phaseIds[phaseIndex]}
activeIds={activeIds} activeIds={activeIds}
setActiveIds={setActiveIds} setActiveIds={setActiveIds}
goalIndex={goalIndex} goalIndex={goalIndex}
/> />
)} )}
</main> </main>
@@ -398,4 +408,4 @@ const MonitoringPage: React.FC = () => {
); );
} }
export default MonitoringPage; export default MonitoringPage;

View File

@@ -32,6 +32,12 @@ export async function nextPhase(): Promise<void> {
sendAPICall(type, context) sendAPICall(type, context)
} }
export async function stopExperiment(): Promise<void> {
const type = "stop"
const context = ""
sendAPICall(type, context)
}
/** /**
* Sends an API call to the CB for going to pause experiment * Sends an API call to the CB for going to pause experiment
@@ -95,14 +101,14 @@ export function useExperimentLogger(onUpdate?: (data: ExperimentStreamData) => v
console.log("Closing Experiment Stream..."); console.log("Closing Experiment Stream...");
eventSource.close(); eventSource.close();
}; };
}, []); }, []);
} }
/** /**
* A hook that listens to the status stream that updates active conditional norms * A hook that listens to the status stream that updates active conditional norms
* via updates sent from the backend * via updates sent from the backend
*/ */
export function useStatusLogger(onUpdate?: (data: ExperimentStreamData) => void) { export function useStatusLogger(onUpdate?: (data: ExperimentStreamData) => void) {
const callbackRef = React.useRef(onUpdate); const callbackRef = React.useRef(onUpdate);
React.useEffect(() => { React.useEffect(() => {