Compare commits
3 Commits
feat/simpl
...
feat/monit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bd67debf3 | ||
|
|
e53e1a3958 | ||
|
|
7a89b0aedd |
@@ -7,6 +7,7 @@ import ConnectedRobots from './pages/ConnectedRobots/ConnectedRobots.tsx'
|
|||||||
import VisProg from "./pages/VisProgPage/VisProg.tsx";
|
import VisProg from "./pages/VisProgPage/VisProg.tsx";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import Logging from "./components/Logging/Logging.tsx";
|
import Logging from "./components/Logging/Logging.tsx";
|
||||||
|
import MonitoringPage from './pages/MonitoringPage/MonitoringPage.tsx'
|
||||||
|
|
||||||
function App(){
|
function App(){
|
||||||
const [showLogs, setShowLogs] = useState(false);
|
const [showLogs, setShowLogs] = useState(false);
|
||||||
@@ -25,6 +26,7 @@ function App(){
|
|||||||
<Route path="/editor" element={<VisProg />} />
|
<Route path="/editor" element={<VisProg />} />
|
||||||
<Route path="/robot" element={<Robot />} />
|
<Route path="/robot" element={<Robot />} />
|
||||||
<Route path="/ConnectedRobots" element={<ConnectedRobots />} />
|
<Route path="/ConnectedRobots" element={<ConnectedRobots />} />
|
||||||
|
<Route path="/MonitoringPage" element={<MonitoringPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
{showLogs && <Logging />}
|
{showLogs && <Logging />}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ function Home() {
|
|||||||
<Link to={"/editor"}>Editor →</Link>
|
<Link to={"/editor"}>Editor →</Link>
|
||||||
<Link to={"/template"}>Template →</Link>
|
<Link to={"/template"}>Template →</Link>
|
||||||
<Link to={"/ConnectedRobots"}>Connected Robots →</Link>
|
<Link to={"/ConnectedRobots"}>Connected Robots →</Link>
|
||||||
|
<Link to={"/MonitoringPage"}>MonitoringPage →</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
175
src/pages/MonitoringPage/MonitoringPage.module.css
Normal file
175
src/pages/MonitoringPage/MonitoringPage.module.css
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
.dashboardContainer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr; /* Left = content, Right = logs */
|
||||||
|
grid-template-rows: auto 1fr auto; /* Header, Main, Footer */
|
||||||
|
grid-template-areas:
|
||||||
|
"header logs"
|
||||||
|
"main logs"
|
||||||
|
"footer footer";
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HEADER */
|
||||||
|
.experimentOverview {
|
||||||
|
grid-area: header;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
background: #fff;
|
||||||
|
padding: 1rem;
|
||||||
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||||
|
position: static; /* ensures it scrolls away */
|
||||||
|
}
|
||||||
|
|
||||||
|
.phaseProgress {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phase {
|
||||||
|
display: inline-block;
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
margin: 0 3px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 25px;
|
||||||
|
background: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completed {
|
||||||
|
background-color: #5cb85c;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current {
|
||||||
|
background-color: #f0ad4e;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connected {
|
||||||
|
color: green;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pause{
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next {
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restartPhase{
|
||||||
|
background-color: rgb(255, 123, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.restartExperiment{
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MAIN GRID */
|
||||||
|
.phaseOverview {
|
||||||
|
grid-area: main;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
grid-template-rows: repeat(2, auto);
|
||||||
|
gap: 1rem;
|
||||||
|
background: #fff;
|
||||||
|
padding: 1rem;
|
||||||
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.phaseBox {
|
||||||
|
background: #f9f9f9;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.phaseOverviewText {
|
||||||
|
grid-column: 1 / -1; /* make the title span across both columns */
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0; /* remove default section margin */
|
||||||
|
padding: 0.25rem 0; /* smaller internal space */
|
||||||
|
}
|
||||||
|
|
||||||
|
.phaseOverviewText h3{
|
||||||
|
margin: 0; /* removes top/bottom whitespace */
|
||||||
|
padding: 0; /* keeps spacing tight */
|
||||||
|
}
|
||||||
|
|
||||||
|
.phaseBox h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
padding-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checked::before {
|
||||||
|
content: '✔️ ';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LOGS */
|
||||||
|
.logs {
|
||||||
|
grid-area: logs;
|
||||||
|
background: #fff;
|
||||||
|
padding: 1rem;
|
||||||
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FOOTER */
|
||||||
|
.controlsSection {
|
||||||
|
grid-area: footer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
background: #fff;
|
||||||
|
padding: 1rem;
|
||||||
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gestures,
|
||||||
|
.speech,
|
||||||
|
.directSpeech {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speechInput {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speechInput input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speechInput button {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RESPONSIVE */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.phaseOverview {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controlsSection {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
127
src/pages/MonitoringPage/MonitoringPage.tsx
Normal file
127
src/pages/MonitoringPage/MonitoringPage.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './MonitoringPage.module.css';
|
||||||
|
|
||||||
|
export default function MonitoringPage() {
|
||||||
|
return (
|
||||||
|
<div className={styles.dashboardContainer}>
|
||||||
|
{/* HEADER */}
|
||||||
|
<header className={styles.experimentOverview}>
|
||||||
|
<div className={styles.phaseName}>
|
||||||
|
<h2>Experiment Overview</h2>
|
||||||
|
<p><strong>Phase name:</strong> Rhyming fish</p>
|
||||||
|
<div className={styles.phaseProgress}>
|
||||||
|
<span className={`${styles.phase} ${styles.completed}`}>1</span>
|
||||||
|
<span className={`${styles.phase} ${styles.completed}`}>2</span>
|
||||||
|
<span className={`${styles.phase} ${styles.current}`}>3</span>
|
||||||
|
<span className={styles.phase}>4</span>
|
||||||
|
<span className={styles.phase}>5</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.experimentControls}>
|
||||||
|
<h3>Experiment Controls</h3>
|
||||||
|
<div className={styles.controlsButtons}>
|
||||||
|
<button className={styles.pause}>▶</button>
|
||||||
|
<button className={styles.next}>⏭</button>
|
||||||
|
<button className={styles.restartPhase}>↩</button>
|
||||||
|
<button className={styles.restartExperiment}>⟲</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.connectionStatus}>
|
||||||
|
<h3>Connection:</h3>
|
||||||
|
<p className={styles.connected}>● Robot is connected</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* MAIN GRID */}
|
||||||
|
|
||||||
|
<main className={styles.phaseOverview}>
|
||||||
|
<section className={styles.phaseOverviewText}>
|
||||||
|
<h3>Phase Overview</h3>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.phaseBox}>
|
||||||
|
<h3>Goals</h3>
|
||||||
|
<ul>
|
||||||
|
<li className={styles.checked}>Convince the RP that you are a fish</li>
|
||||||
|
<li>Reference Shakespeare</li>
|
||||||
|
<li>Give a compliment</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.phaseBox}>
|
||||||
|
<h3>Triggers</h3>
|
||||||
|
<ul>
|
||||||
|
<li className={styles.checked}>Convince the RP that you are a fish</li>
|
||||||
|
<li>Reference Shakespeare</li>
|
||||||
|
<li>Give a compliment</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.phaseBox}>
|
||||||
|
<h3>Norms</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Rhyme when talking</li>
|
||||||
|
<li>Talk like a fish</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.phaseBox}>
|
||||||
|
<h3>Conditional Norms</h3>
|
||||||
|
<ul>
|
||||||
|
<li>“RP is sad” - Be nice</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* LOGS */}
|
||||||
|
<aside className={styles.logs}>
|
||||||
|
<h3>Logs</h3>
|
||||||
|
<div className={styles.logHeader}>
|
||||||
|
<span>Global:</span>
|
||||||
|
<button>ALL</button>
|
||||||
|
<button>Add</button>
|
||||||
|
<button className={styles.live}>Live</button>
|
||||||
|
</div>
|
||||||
|
<textarea defaultValue="Example Log: much log"></textarea>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* FOOTER */}
|
||||||
|
<footer className={styles.controlsSection}>
|
||||||
|
<div className={styles.gestures}>
|
||||||
|
<h4>Controls</h4>
|
||||||
|
<ul>
|
||||||
|
<li>Gesture: Wave Left Hand</li>
|
||||||
|
<li>Gesture: Wave Right Hand</li>
|
||||||
|
<li>Gesture: Left Thumbs Up</li>
|
||||||
|
<li>Gesture: Right Thumbs Up</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.speech}>
|
||||||
|
<h4>Speech Options</h4>
|
||||||
|
<ul>
|
||||||
|
<li>\"Hello, my name is pepper.\"</li>
|
||||||
|
<li>\"How is the weather today?\"</li>
|
||||||
|
<li>\"I like your outfit, very pretty.\"</li>
|
||||||
|
<li>\"How is your day going?\"</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.directSpeech}>
|
||||||
|
<h4>Direct Pepper Speech</h4>
|
||||||
|
<ul>
|
||||||
|
<li>[time] Send: *Previous message*</li>
|
||||||
|
<li>[time] Send: *Previous message*</li>
|
||||||
|
<li>[time] Send: *Previous message*</li>
|
||||||
|
</ul>
|
||||||
|
<div className={styles.speechInput}>
|
||||||
|
<input type="text" placeholder="Type message..." />
|
||||||
|
<button>Send</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
/* ---------- Layout ---------- */
|
|
||||||
|
|
||||||
.container {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background: #1e1e1e;
|
|
||||||
color: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: clamp(0.75rem, 2vw, 1.25rem);
|
|
||||||
background: #2a2a2a;
|
|
||||||
border-bottom: 1px solid #3a3a3a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h2 {
|
|
||||||
font-size: clamp(1rem, 2.2vw, 1.4rem);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls button {
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
padding: 0.4rem 0.9rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: none;
|
|
||||||
background: #111;
|
|
||||||
color: white;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls button:disabled {
|
|
||||||
opacity: 0.4;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- Content ---------- */
|
|
||||||
|
|
||||||
.content {
|
|
||||||
flex: 1;
|
|
||||||
padding: 2%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- Grid ---------- */
|
|
||||||
|
|
||||||
.phaseGrid {
|
|
||||||
height: 100%;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
grid-template-rows: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 2%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- Box ---------- */
|
|
||||||
|
|
||||||
.box {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background: #ffffff;
|
|
||||||
color: #1e1e1e;
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.boxHeader {
|
|
||||||
padding: 0.6rem 0.9rem;
|
|
||||||
background: linear-gradient(135deg, #dcdcdc, #e9e9e9);
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: clamp(0.9rem, 1.5vw, 1.05rem);
|
|
||||||
border-bottom: 1px solid #cfcfcf;
|
|
||||||
}
|
|
||||||
|
|
||||||
.boxContent {
|
|
||||||
flex: 1;
|
|
||||||
padding: 0.8rem 1rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- Lists ---------- */
|
|
||||||
|
|
||||||
.iconList {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconList li {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.6rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-size: clamp(0.85rem, 1.3vw, 1rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bulletList {
|
|
||||||
margin: 0;
|
|
||||||
padding-left: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bulletList li {
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- Icons ---------- */
|
|
||||||
|
|
||||||
.successIcon,
|
|
||||||
.failIcon {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 1.5rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: white;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.successIcon {
|
|
||||||
background: #3cb371;
|
|
||||||
}
|
|
||||||
|
|
||||||
.failIcon {
|
|
||||||
background: #e5533d;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- Empty ---------- */
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
opacity: 0.55;
|
|
||||||
font-style: italic;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- Responsive ---------- */
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.phaseGrid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
grid-template-rows: repeat(4, minmax(0, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.leftControls {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.backButton {
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid #555;
|
|
||||||
color: #ddd;
|
|
||||||
padding: 0.35rem 0.75rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.backButton:hover {
|
|
||||||
background: #333;
|
|
||||||
}
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import styles from "./SimpleProgram.module.css";
|
|
||||||
import useProgramStore from "../../utils/programStore.ts";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic container box with a header and content area.
|
|
||||||
*/
|
|
||||||
type BoxProps = {
|
|
||||||
title: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Box: React.FC<BoxProps> = ({ title, children }) => (
|
|
||||||
<div className={styles.box}>
|
|
||||||
<div className={styles.boxHeader}>{title}</div>
|
|
||||||
<div className={styles.boxContent}>{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders a list of goals for a phase.
|
|
||||||
* Expects goal-like objects from the program store.
|
|
||||||
*/
|
|
||||||
const GoalList: React.FC<{ goals: unknown[] }> = ({ goals }) => {
|
|
||||||
if (!goals.length) {
|
|
||||||
return <p className={styles.empty}>No goals defined.</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ul className={styles.iconList}>
|
|
||||||
{goals.map((g, idx) => {
|
|
||||||
const goal = g as {
|
|
||||||
id?: string;
|
|
||||||
description?: string;
|
|
||||||
achieved?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li key={goal.id ?? idx}>
|
|
||||||
<span
|
|
||||||
className={
|
|
||||||
goal.achieved ? styles.successIcon : styles.failIcon
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{goal.achieved ? "✔" : "✖"}
|
|
||||||
</span>
|
|
||||||
{goal.description ?? "Unnamed goal"}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders a list of triggers for a phase.
|
|
||||||
*/
|
|
||||||
const TriggerList: React.FC<{ triggers: unknown[] }> = ({ triggers }) => {
|
|
||||||
if (!triggers.length) {
|
|
||||||
return <p className={styles.empty}>No triggers defined.</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ul className={styles.iconList}>
|
|
||||||
{triggers.map((t, idx) => {
|
|
||||||
const trigger = t as {
|
|
||||||
id?: string;
|
|
||||||
label?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li key={trigger.id ?? idx}>
|
|
||||||
<span className={styles.failIcon}>✖</span>
|
|
||||||
{trigger.label ?? "Unnamed trigger"}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders a list of norms for a phase.
|
|
||||||
*/
|
|
||||||
const NormList: React.FC<{ norms: unknown[] }> = ({ norms }) => {
|
|
||||||
if (!norms.length) {
|
|
||||||
return <p className={styles.empty}>No norms defined.</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ul className={styles.bulletList}>
|
|
||||||
{norms.map((n, idx) => {
|
|
||||||
const norm = n as {
|
|
||||||
id?: string;
|
|
||||||
norm?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
return <li key={norm.id ?? idx}>{norm.norm ?? "Unnamed norm"}</li>;
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays all phase-related information in a grid layout.
|
|
||||||
*/
|
|
||||||
type PhaseGridProps = {
|
|
||||||
norms: unknown[];
|
|
||||||
goals: unknown[];
|
|
||||||
triggers: unknown[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const PhaseGrid: React.FC<PhaseGridProps> = ({
|
|
||||||
norms,
|
|
||||||
goals,
|
|
||||||
triggers,
|
|
||||||
}) => (
|
|
||||||
<div className={styles.phaseGrid}>
|
|
||||||
<Box title="Norms">
|
|
||||||
<NormList norms={norms} />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box title="Triggers">
|
|
||||||
<TriggerList triggers={triggers} />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box title="Goals">
|
|
||||||
<GoalList goals={goals} />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box title="Conditional Norms">
|
|
||||||
<p className={styles.empty}>No conditional norms defined.</p>
|
|
||||||
</Box>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main program viewer.
|
|
||||||
* Reads all data from the program store and allows
|
|
||||||
* navigating between phases.
|
|
||||||
*/
|
|
||||||
const SimpleProgram: React.FC = () => {
|
|
||||||
const getPhaseIds = useProgramStore((s) => s.getPhaseIds);
|
|
||||||
const getNormsInPhase = useProgramStore((s) => s.getNormsInPhase);
|
|
||||||
const getGoalsInPhase = useProgramStore((s) => s.getGoalsInPhase);
|
|
||||||
const getTriggersInPhase = useProgramStore((s) => s.getTriggersInPhase);
|
|
||||||
|
|
||||||
const phaseIds = getPhaseIds();
|
|
||||||
const [phaseIndex, setPhaseIndex] = React.useState(0);
|
|
||||||
|
|
||||||
if (phaseIds.length === 0) {
|
|
||||||
return <p className={styles.empty}>No program loaded.</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const phaseId = phaseIds[phaseIndex];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<header className={styles.header}>
|
|
||||||
<h2>
|
|
||||||
Phase {phaseIndex + 1} / {phaseIds.length}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className={styles.controls}>
|
|
||||||
<button
|
|
||||||
disabled={phaseIndex === 0}
|
|
||||||
onClick={() => setPhaseIndex((i) => i - 1)}
|
|
||||||
>
|
|
||||||
◀ Prev
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
disabled={phaseIndex === phaseIds.length - 1}
|
|
||||||
onClick={() => setPhaseIndex((i) => i + 1)}
|
|
||||||
>
|
|
||||||
Next ▶
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className={styles.content}>
|
|
||||||
<PhaseGrid
|
|
||||||
norms={getNormsInPhase(phaseId)}
|
|
||||||
goals={getGoalsInPhase(phaseId)}
|
|
||||||
triggers={getTriggersInPhase(phaseId)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SimpleProgram;
|
|
||||||
@@ -7,16 +7,14 @@ import {
|
|||||||
MarkerType,
|
MarkerType,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect} from "react";
|
||||||
import {useShallow} from 'zustand/react/shallow';
|
import {useShallow} from 'zustand/react/shallow';
|
||||||
import useProgramStore from "../../utils/programStore.ts";
|
|
||||||
import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx';
|
import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx';
|
||||||
import useFlowStore from './visualProgrammingUI/VisProgStores.tsx';
|
import useFlowStore from './visualProgrammingUI/VisProgStores.tsx';
|
||||||
import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx';
|
import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx';
|
||||||
import styles from './VisProg.module.css'
|
import styles from './VisProg.module.css'
|
||||||
import { NodeReduces, NodeTypes } from './visualProgrammingUI/NodeRegistry.ts';
|
import { NodeReduces, NodeTypes } from './visualProgrammingUI/NodeRegistry.ts';
|
||||||
import SaveLoadPanel from './visualProgrammingUI/components/SaveLoadPanel.tsx';
|
import SaveLoadPanel from './visualProgrammingUI/components/SaveLoadPanel.tsx';
|
||||||
import SimpleProgram from "../SimpleProgram/SimpleProgram.tsx";
|
|
||||||
|
|
||||||
// --| config starting params for flow |--
|
// --| config starting params for flow |--
|
||||||
|
|
||||||
@@ -154,10 +152,6 @@ function runProgram() {
|
|||||||
).then((res) => {
|
).then((res) => {
|
||||||
if (!res.ok) throw new Error("Failed communicating with the backend.")
|
if (!res.ok) throw new Error("Failed communicating with the backend.")
|
||||||
console.log("Successfully sent the program to the backend.");
|
console.log("Successfully sent the program to the backend.");
|
||||||
|
|
||||||
// store reduced program in global program store for further use in the UI
|
|
||||||
// when the program was sent to the backend successfully:
|
|
||||||
useProgramStore.getState().setProgramState(structuredClone(program));
|
|
||||||
}).catch(() => console.log("Failed to send program to the backend."));
|
}).catch(() => console.log("Failed to send program to the backend."));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,31 +174,10 @@ function graphReducer() {
|
|||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
function VisProgPage() {
|
function VisProgPage() {
|
||||||
const [showSimpleProgram, setShowSimpleProgram] = useState(false);
|
|
||||||
const setProgramState = useProgramStore((state) => state.setProgramState);
|
|
||||||
|
|
||||||
const onClick = () => {
|
|
||||||
const phases = graphReducer(); // reduce graph
|
|
||||||
setProgramState({ phases }); // <-- save to store
|
|
||||||
setShowSimpleProgram(true); // show SimpleProgram
|
|
||||||
runProgram(); // send to backend if needed
|
|
||||||
};
|
|
||||||
|
|
||||||
if (showSimpleProgram) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<button onClick={() => setShowSimpleProgram(false)}>
|
|
||||||
Back to Editor ◀
|
|
||||||
</button>
|
|
||||||
<SimpleProgram/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<VisualProgrammingUI/>
|
<VisualProgrammingUI/>
|
||||||
<button onClick={onClick}>run program</button>
|
<button onClick={runProgram}>run program</button>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const initialNodes : Node[] = [
|
|||||||
createNode('start', 'start', {x: 100, y: 100}, {label: "Start"}, false),
|
createNode('start', 'start', {x: 100, y: 100}, {label: "Start"}, false),
|
||||||
createNode('end', 'end', {x: 500, y: 100}, {label: "End"}, false),
|
createNode('end', 'end', {x: 500, y: 100}, {label: "End"}, false),
|
||||||
createNode('phase-1', 'phase', {x:200, y:100}, {label: "Phase 1", children : []}),
|
createNode('phase-1', 'phase', {x:200, y:100}, {label: "Phase 1", children : []}),
|
||||||
createNode('norms-1', 'norm', {x:-200, y:100}, {label: "Initial Norms", normList: ["Be a robot", "get good"], critical:false}),
|
createNode('norms-1', 'norm', {x:-200, y:100}, {label: "Initial Norms", normList: ["Be a robot", "get good"]}),
|
||||||
];
|
];
|
||||||
|
|
||||||
// * Initial edges * /
|
// * Initial edges * /
|
||||||
|
|||||||
@@ -8,5 +8,4 @@ export const NormNodeDefaults: NormNodeData = {
|
|||||||
droppable: true,
|
droppable: true,
|
||||||
norm: "",
|
norm: "",
|
||||||
hasReduce: true,
|
hasReduce: true,
|
||||||
critical: false,
|
|
||||||
};
|
};
|
||||||
@@ -21,7 +21,6 @@ export type NormNodeData = {
|
|||||||
droppable: boolean;
|
droppable: boolean;
|
||||||
norm: string;
|
norm: string;
|
||||||
hasReduce: boolean;
|
hasReduce: boolean;
|
||||||
critical: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NormNode = Node<NormNodeData>
|
export type NormNode = Node<NormNodeData>
|
||||||
@@ -36,16 +35,11 @@ export default function NormNode(props: NodeProps<NormNode>) {
|
|||||||
const {updateNodeData} = useFlowStore();
|
const {updateNodeData} = useFlowStore();
|
||||||
|
|
||||||
const text_input_id = `norm_${props.id}_text_input`;
|
const text_input_id = `norm_${props.id}_text_input`;
|
||||||
const checkbox_id = `goal_${props.id}_checkbox`;
|
|
||||||
|
|
||||||
const setValue = (value: string) => {
|
const setValue = (value: string) => {
|
||||||
updateNodeData(props.id, {norm: value});
|
updateNodeData(props.id, {norm: value});
|
||||||
}
|
}
|
||||||
|
|
||||||
const setCritical = (value: boolean) => {
|
|
||||||
updateNodeData(props.id, {...data, critical: value});
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Toolbar nodeId={props.id} allowDelete={true}/>
|
<Toolbar nodeId={props.id} allowDelete={true}/>
|
||||||
<div className={`${styles.defaultNode} ${styles.nodeNorm}`}>
|
<div className={`${styles.defaultNode} ${styles.nodeNorm}`}>
|
||||||
@@ -58,15 +52,6 @@ export default function NormNode(props: NodeProps<NormNode>) {
|
|||||||
placeholder={"Pepper should ..."}
|
placeholder={"Pepper should ..."}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={"flex-row gap-md align-center"}>
|
|
||||||
<label htmlFor={checkbox_id}>Critical:</label>
|
|
||||||
<input
|
|
||||||
id={checkbox_id}
|
|
||||||
type={"checkbox"}
|
|
||||||
checked={data.critical || false}
|
|
||||||
onChange={(e) => setCritical(e.target.checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Handle type="source" position={Position.Right} id="norms"/>
|
<Handle type="source" position={Position.Right} id="norms"/>
|
||||||
</div>
|
</div>
|
||||||
</>;
|
</>;
|
||||||
@@ -84,7 +69,6 @@ export function NormReduce(node: Node, _nodes: Node[]) {
|
|||||||
id: node.id,
|
id: node.id,
|
||||||
label: data.label,
|
label: data.label,
|
||||||
norm: data.norm,
|
norm: data.norm,
|
||||||
critical: data.critical,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
import {create} from "zustand";
|
|
||||||
|
|
||||||
// the type of a reduced program
|
|
||||||
export type ReducedProgram = { phases: Record<string, unknown>[] };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the type definition of the programStore
|
|
||||||
*/
|
|
||||||
export type ProgramState = {
|
|
||||||
// Basic store functionality:
|
|
||||||
currentProgram: ReducedProgram;
|
|
||||||
setProgramState: (state: ReducedProgram) => void;
|
|
||||||
getProgramState: () => ReducedProgram;
|
|
||||||
|
|
||||||
// Utility functions:
|
|
||||||
// to avoid having to manually go through the entire state for every instance where data is required
|
|
||||||
getPhaseIds: () => string[];
|
|
||||||
getNormsInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
|
||||||
getGoalsInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
|
||||||
getTriggersInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
|
||||||
// if more specific utility functions are needed they can be added here:
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the ProgramStore can be used to access all information of the most recently sent program,
|
|
||||||
* it contains basic functions to set and get the current program.
|
|
||||||
* And it contains some utility functions that allow you to easily gain access
|
|
||||||
* to the norms, triggers and goals of a specific phase.
|
|
||||||
*/
|
|
||||||
const useProgramStore = create<ProgramState>((set, get) => ({
|
|
||||||
currentProgram: { phases: [] as Record<string, unknown>[]},
|
|
||||||
/**
|
|
||||||
* sets the current program by cloning the provided program using a structuredClone
|
|
||||||
*/
|
|
||||||
setProgramState: (program: ReducedProgram) => set({currentProgram: structuredClone(program)}),
|
|
||||||
/**
|
|
||||||
* gets the current program
|
|
||||||
*/
|
|
||||||
getProgramState: () => get().currentProgram,
|
|
||||||
|
|
||||||
// utility functions:
|
|
||||||
/**
|
|
||||||
* gets the ids of all phases in the program
|
|
||||||
*/
|
|
||||||
getPhaseIds: () => get().currentProgram.phases.map(entry => entry["id"] as string),
|
|
||||||
/**
|
|
||||||
* gets the norms for the provided phase
|
|
||||||
*/
|
|
||||||
getNormsInPhase: (currentPhaseId) => {
|
|
||||||
const program = get().currentProgram;
|
|
||||||
const phase = program.phases.find(val => val["id"] === currentPhaseId);
|
|
||||||
if (phase) {
|
|
||||||
return phase["norms"] as Record<string, unknown>[];
|
|
||||||
}
|
|
||||||
throw new Error(`phase with id:"${currentPhaseId}" not found`)
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* gets the goals for the provided phase
|
|
||||||
*/
|
|
||||||
getGoalsInPhase: (currentPhaseId) => {
|
|
||||||
const program = get().currentProgram;
|
|
||||||
const phase = program.phases.find(val => val["id"] === currentPhaseId);
|
|
||||||
if (phase) {
|
|
||||||
return phase["goals"] as Record<string, unknown>[];
|
|
||||||
}
|
|
||||||
throw new Error(`phase with id:"${currentPhaseId}" not found`)
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* gets the triggers for the provided phase
|
|
||||||
*/
|
|
||||||
getTriggersInPhase: (currentPhaseId) => {
|
|
||||||
const program = get().currentProgram;
|
|
||||||
const phase = program.phases.find(val => val["id"] === currentPhaseId);
|
|
||||||
if (phase) {
|
|
||||||
return phase["triggers"] as Record<string, unknown>[];
|
|
||||||
}
|
|
||||||
throw new Error(`phase with id:"${currentPhaseId}" not found`)
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default useProgramStore;
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
|
||||||
import SimpleProgram from "../../../src/pages/SimpleProgram/SimpleProgram";
|
|
||||||
import useProgramStore from "../../../src/utils/programStore";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to preload the program store before rendering.
|
|
||||||
*/
|
|
||||||
function loadProgram(phases: Record<string, unknown>[]) {
|
|
||||||
useProgramStore.getState().setProgramState({ phases });
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("SimpleProgram", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
loadProgram([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("shows empty state when no program is loaded", () => {
|
|
||||||
render(<SimpleProgram />);
|
|
||||||
expect(screen.getByText("No program loaded.")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("renders first phase content", () => {
|
|
||||||
loadProgram([
|
|
||||||
{
|
|
||||||
id: "phase-1",
|
|
||||||
norms: [{ id: "n1", norm: "Be polite" }],
|
|
||||||
goals: [{ id: "g1", description: "Finish task", achieved: true }],
|
|
||||||
triggers: [{ id: "t1", label: "Keyword trigger" }],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
render(<SimpleProgram />);
|
|
||||||
|
|
||||||
expect(screen.getByText("Phase 1 / 1")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Be polite")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Finish task")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Keyword trigger")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("renders empty messages when phase has no data", () => {
|
|
||||||
loadProgram([
|
|
||||||
{
|
|
||||||
id: "phase-1",
|
|
||||||
norms: [],
|
|
||||||
goals: [],
|
|
||||||
triggers: [],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
render(<SimpleProgram />);
|
|
||||||
|
|
||||||
expect(screen.getAllByText("No norms defined.").length).toBeGreaterThanOrEqual(1);
|
|
||||||
expect(screen.getAllByText("No goals defined.").length).toBeGreaterThanOrEqual(1);
|
|
||||||
expect(screen.getAllByText("No triggers defined.").length).toBeGreaterThanOrEqual(1);
|
|
||||||
expect(
|
|
||||||
screen.getByText("No conditional norms defined.")
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("allows navigating between phases", () => {
|
|
||||||
loadProgram([
|
|
||||||
{
|
|
||||||
id: "phase-1",
|
|
||||||
norms: [],
|
|
||||||
goals: [],
|
|
||||||
triggers: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "phase-2",
|
|
||||||
norms: [{ id: "n2", norm: "Be careful" }],
|
|
||||||
goals: [],
|
|
||||||
triggers: [],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
render(<SimpleProgram />);
|
|
||||||
|
|
||||||
expect(screen.getByText("Phase 1 / 2")).toBeInTheDocument();
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByText("Next ▶"));
|
|
||||||
|
|
||||||
expect(screen.getByText("Phase 2 / 2")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Be careful")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("prev button is disabled on first phase", () => {
|
|
||||||
loadProgram([{ id: "phase-1", norms: [], goals: [], triggers: [] }]);
|
|
||||||
|
|
||||||
render(<SimpleProgram />);
|
|
||||||
expect(screen.getByText("◀ Prev")).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("next button is disabled on last phase", () => {
|
|
||||||
loadProgram([{ id: "phase-1", norms: [], goals: [], triggers: [] }]);
|
|
||||||
|
|
||||||
render(<SimpleProgram />);
|
|
||||||
expect(screen.getByText("Next ▶")).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("prev and next buttons enable/disable correctly when navigating", () => {
|
|
||||||
loadProgram([
|
|
||||||
{ id: "p1", norms: [], goals: [], triggers: [] },
|
|
||||||
{ id: "p2", norms: [], goals: [], triggers: [] },
|
|
||||||
]);
|
|
||||||
|
|
||||||
render(<SimpleProgram />);
|
|
||||||
|
|
||||||
const prev = screen.getByText("◀ Prev");
|
|
||||||
const next = screen.getByText("Next ▶");
|
|
||||||
|
|
||||||
expect(prev).toBeDisabled();
|
|
||||||
expect(next).not.toBeDisabled();
|
|
||||||
|
|
||||||
fireEvent.click(next);
|
|
||||||
|
|
||||||
expect(prev).not.toBeDisabled();
|
|
||||||
expect(next).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("renders achieved and unachieved goals with correct icons", () => {
|
|
||||||
loadProgram([
|
|
||||||
{
|
|
||||||
id: "phase-1",
|
|
||||||
norms: [],
|
|
||||||
goals: [
|
|
||||||
{ id: "g1", description: "Done goal", achieved: true },
|
|
||||||
{ id: "g2", description: "Failed goal", achieved: false },
|
|
||||||
],
|
|
||||||
triggers: [],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
render(<SimpleProgram />);
|
|
||||||
|
|
||||||
expect(screen.getByText("✔")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("✖")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Done goal")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Failed goal")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("renders fallback labels when optional fields are missing", () => {
|
|
||||||
loadProgram([
|
|
||||||
{
|
|
||||||
id: "phase-1",
|
|
||||||
norms: [{}],
|
|
||||||
goals: [{}],
|
|
||||||
triggers: [{}],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
render(<SimpleProgram />);
|
|
||||||
|
|
||||||
expect(screen.getByText("Unnamed norm")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Unnamed goal")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Unnamed trigger")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("does not crash when navigating beyond boundaries", () => {
|
|
||||||
loadProgram([
|
|
||||||
{ id: "p1", norms: [], goals: [], triggers: [] },
|
|
||||||
{ id: "p2", norms: [], goals: [], triggers: [] },
|
|
||||||
]);
|
|
||||||
|
|
||||||
render(<SimpleProgram />);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByText("Next ▶"));
|
|
||||||
fireEvent.click(screen.getByText("Next ▶"));
|
|
||||||
|
|
||||||
expect(screen.getByText("Phase 2 / 2")).toBeInTheDocument();
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByText("◀ Prev"));
|
|
||||||
fireEvent.click(screen.getByText("◀ Prev"));
|
|
||||||
|
|
||||||
expect(screen.getByText("Phase 1 / 2")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -98,7 +98,6 @@ describe('NormNode', () => {
|
|||||||
droppable: true,
|
droppable: true,
|
||||||
norm: '',
|
norm: '',
|
||||||
hasReduce: true,
|
hasReduce: true,
|
||||||
critical: false
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -628,54 +627,6 @@ describe('NormNode', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should properly update the store when editing critical checkbox', async () => {
|
|
||||||
const mockNode: Node = {
|
|
||||||
id: 'norm-1',
|
|
||||||
type: 'norm',
|
|
||||||
position: { x: 0, y: 0 },
|
|
||||||
data: {
|
|
||||||
label: 'Test Norm',
|
|
||||||
droppable: true,
|
|
||||||
norm: '',
|
|
||||||
hasReduce: true,
|
|
||||||
critical: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
useFlowStore.setState({
|
|
||||||
nodes: [mockNode],
|
|
||||||
edges: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
renderWithProviders(
|
|
||||||
<NormNode
|
|
||||||
id={mockNode.id}
|
|
||||||
type={mockNode.type as string}
|
|
||||||
data={mockNode.data as any}
|
|
||||||
selected={false}
|
|
||||||
isConnectable={true}
|
|
||||||
zIndex={0}
|
|
||||||
dragging={false}
|
|
||||||
selectable={true}
|
|
||||||
deletable={true}
|
|
||||||
draggable={true}
|
|
||||||
positionAbsoluteX={0}
|
|
||||||
positionAbsoluteY={0}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const checkbox = screen.getByLabelText('Critical:');
|
|
||||||
await user.click(checkbox);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const state = useFlowStore.getState();
|
|
||||||
expect(state.nodes).toHaveLength(1);
|
|
||||||
expect(state.nodes[0].id).toBe('norm-1');
|
|
||||||
expect(state.nodes[0].data.norm).toBe('');
|
|
||||||
expect(state.nodes[0].data.critical).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not affect other nodes when updating one norm node', async () => {
|
it('should not affect other nodes when updating one norm node', async () => {
|
||||||
const norm1: Node = {
|
const norm1: Node = {
|
||||||
id: 'norm-1',
|
id: 'norm-1',
|
||||||
|
|||||||
@@ -2,11 +2,6 @@ import '@testing-library/jest-dom';
|
|||||||
import { cleanup } from '@testing-library/react';
|
import { cleanup } from '@testing-library/react';
|
||||||
import useFlowStore from '../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx';
|
import useFlowStore from '../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx';
|
||||||
|
|
||||||
if (!globalThis.structuredClone) {
|
|
||||||
globalThis.structuredClone = (obj: any) => {
|
|
||||||
return JSON.parse(JSON.stringify(obj));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// To make sure that the tests are working, it's important that you are using
|
// To make sure that the tests are working, it's important that you are using
|
||||||
// this implementation of ResizeObserver and DOMMatrixReadOnly
|
// this implementation of ResizeObserver and DOMMatrixReadOnly
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
import useProgramStore, {type ReducedProgram} from "../../src/utils/programStore.ts";
|
|
||||||
|
|
||||||
|
|
||||||
describe('useProgramStore', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
// Reset store before each test
|
|
||||||
useProgramStore.setState({
|
|
||||||
currentProgram: { phases: [] },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockProgram: ReducedProgram = {
|
|
||||||
phases: [
|
|
||||||
{
|
|
||||||
id: 'phase-1',
|
|
||||||
norms: [{ id: 'norm-1' }],
|
|
||||||
goals: [{ id: 'goal-1' }],
|
|
||||||
triggers: [{ id: 'trigger-1' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'phase-2',
|
|
||||||
norms: [{ id: 'norm-2' }],
|
|
||||||
goals: [{ id: 'goal-2' }],
|
|
||||||
triggers: [{ id: 'trigger-2' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
it('should set and get the program state', () => {
|
|
||||||
useProgramStore.getState().setProgramState(mockProgram);
|
|
||||||
|
|
||||||
const program = useProgramStore.getState().getProgramState();
|
|
||||||
expect(program).toEqual(mockProgram);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the ids of all phases in the program', () => {
|
|
||||||
useProgramStore.getState().setProgramState(mockProgram);
|
|
||||||
|
|
||||||
const phaseIds = useProgramStore.getState().getPhaseIds();
|
|
||||||
expect(phaseIds).toEqual(['phase-1', 'phase-2']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return all norms for a given phase', () => {
|
|
||||||
useProgramStore.getState().setProgramState(mockProgram);
|
|
||||||
|
|
||||||
const norms = useProgramStore.getState().getNormsInPhase('phase-1');
|
|
||||||
expect(norms).toEqual([{ id: 'norm-1' }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return all goals for a given phase', () => {
|
|
||||||
useProgramStore.getState().setProgramState(mockProgram);
|
|
||||||
|
|
||||||
const goals = useProgramStore.getState().getGoalsInPhase('phase-2');
|
|
||||||
expect(goals).toEqual([{ id: 'goal-2' }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return all triggers for a given phase', () => {
|
|
||||||
useProgramStore.getState().setProgramState(mockProgram);
|
|
||||||
|
|
||||||
const triggers = useProgramStore.getState().getTriggersInPhase('phase-1');
|
|
||||||
expect(triggers).toEqual([{ id: 'trigger-1' }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws if phase does not exist when getting norms', () => {
|
|
||||||
useProgramStore.getState().setProgramState(mockProgram);
|
|
||||||
|
|
||||||
expect(() =>
|
|
||||||
useProgramStore.getState().getNormsInPhase('missing-phase')
|
|
||||||
).toThrow('phase with id:"missing-phase" not found');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws if phase does not exist when getting goals', () => {
|
|
||||||
useProgramStore.getState().setProgramState(mockProgram);
|
|
||||||
|
|
||||||
expect(() =>
|
|
||||||
useProgramStore.getState().getGoalsInPhase('missing-phase')
|
|
||||||
).toThrow('phase with id:"missing-phase" not found');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws if phase does not exist when getting triggers', () => {
|
|
||||||
useProgramStore.getState().setProgramState(mockProgram);
|
|
||||||
|
|
||||||
expect(() =>
|
|
||||||
useProgramStore.getState().getTriggersInPhase('missing-phase')
|
|
||||||
).toThrow('phase with id:"missing-phase" not found');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should clone program state when setting it (no shared references should exist)', () => {
|
|
||||||
const changeableMockProgram: ReducedProgram = {
|
|
||||||
phases: [
|
|
||||||
{
|
|
||||||
id: 'phase-1',
|
|
||||||
norms: [{ id: 'norm-1' }],
|
|
||||||
goals: [{ id: 'goal-1' }],
|
|
||||||
triggers: [{ id: 'trigger-1' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'phase-2',
|
|
||||||
norms: [{ id: 'norm-2' }],
|
|
||||||
goals: [{ id: 'goal-2' }],
|
|
||||||
triggers: [{ id: 'trigger-2' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
useProgramStore.getState().setProgramState(changeableMockProgram);
|
|
||||||
|
|
||||||
const storedProgram = useProgramStore.getState().getProgramState();
|
|
||||||
|
|
||||||
// mutate original
|
|
||||||
(changeableMockProgram.phases[0].norms as any[]).push({ id: 'norm-mutated' });
|
|
||||||
|
|
||||||
// store should NOT change
|
|
||||||
expect(storedProgram.phases[0]['norms']).toHaveLength(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user