Compare commits
15 Commits
feat/monit
...
feat/monit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4f7b48031 | ||
|
|
b18cd5bfa5 | ||
|
|
09e6287f9d | ||
|
|
8b40001038 | ||
|
|
f9e0eb95f8 | ||
|
|
47c5e94b8f | ||
|
|
b17d1e7618 | ||
|
|
ec211ccbc3 | ||
|
|
9a555165e6 | ||
|
|
f73bbb9d02 | ||
|
|
883f0a95a6 | ||
|
|
a8f9965391 | ||
|
|
6f4471ce6f | ||
|
|
2ca0c9c4c0 | ||
|
|
dcc50fd978 |
@@ -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>;
|
|
||||||
}
|
|
||||||
@@ -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>;
|
|
||||||
}
|
|
||||||
@@ -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>;
|
|
||||||
}
|
|
||||||
@@ -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>;
|
|
||||||
}
|
|
||||||
@@ -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>;
|
|
||||||
}
|
|
||||||
@@ -28,22 +28,6 @@
|
|||||||
position: static; /* ensures it scrolls away */
|
position: static; /* ensures it scrolls away */
|
||||||
}
|
}
|
||||||
|
|
||||||
.controlsButtons {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: .25rem;
|
|
||||||
max-width: 260px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
button {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.phaseProgress {
|
.phaseProgress {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
@@ -93,11 +77,6 @@
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.restartPhase{
|
|
||||||
background-color: rgb(255, 123, 0);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.restartExperiment{
|
.restartExperiment{
|
||||||
background-color: red;
|
background-color: red;
|
||||||
color: white;
|
color: white;
|
||||||
|
|||||||
@@ -30,11 +30,6 @@ import {
|
|||||||
StatusList,
|
StatusList,
|
||||||
RobotConnected
|
RobotConnected
|
||||||
} from './MonitoringPageComponents';
|
} from './MonitoringPageComponents';
|
||||||
import Replay from "../../components/Icons/Replay.tsx";
|
|
||||||
import Pause from "../../components/Icons/Pause.tsx";
|
|
||||||
import Play from "../../components/Icons/Play.tsx";
|
|
||||||
import Next from "../../components/Icons/Next.tsx";
|
|
||||||
import Redo from "../../components/Icons/Redo.tsx";
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
// 1. State management
|
// 1. State management
|
||||||
@@ -106,7 +101,6 @@ function useExperimentLogic() {
|
|||||||
}, [getPhaseIds, getGoalsInPhase, phaseIds, phaseIndex, phaseNames]);
|
}, [getPhaseIds, getGoalsInPhase, phaseIds, phaseIndex, phaseNames]);
|
||||||
|
|
||||||
const handleStatusUpdate = useCallback((data: unknown) => {
|
const handleStatusUpdate = useCallback((data: unknown) => {
|
||||||
|
|
||||||
const payload = data as CondNormsStateUpdate;
|
const payload = data as CondNormsStateUpdate;
|
||||||
if (payload.type !== 'cond_norms_state_update') return;
|
if (payload.type !== 'cond_norms_state_update') return;
|
||||||
|
|
||||||
@@ -146,7 +140,7 @@ function useExperimentLogic() {
|
|||||||
}
|
}
|
||||||
}, [setProgramState]);
|
}, [setProgramState]);
|
||||||
|
|
||||||
const handleControlAction = async (action: "pause" | "play" | "nextPhase" | "resetPhase") => {
|
const handleControlAction = async (action: "pause" | "play" | "nextPhase") => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
switch (action) {
|
switch (action) {
|
||||||
@@ -161,7 +155,6 @@ function useExperimentLogic() {
|
|||||||
case "nextPhase":
|
case "nextPhase":
|
||||||
await nextPhase();
|
await nextPhase();
|
||||||
break;
|
break;
|
||||||
// Case for resetPhase if implemented in API
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -229,7 +222,7 @@ function ControlPanel({
|
|||||||
}: {
|
}: {
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
isPlaying: boolean,
|
isPlaying: boolean,
|
||||||
onAction: (a: "pause" | "play" | "nextPhase" | "resetPhase") => void,
|
onAction: (a: "pause" | "play" | "nextPhase") => void,
|
||||||
onReset: () => void
|
onReset: () => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@@ -240,31 +233,25 @@ function ControlPanel({
|
|||||||
className={!isPlaying ? styles.pausePlayActive : styles.pausePlayInactive}
|
className={!isPlaying ? styles.pausePlayActive : styles.pausePlayInactive}
|
||||||
onClick={() => onAction("pause")}
|
onClick={() => onAction("pause")}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
><Pause /></button>
|
>❚❚</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={isPlaying ? styles.pausePlayActive : styles.pausePlayInactive}
|
className={isPlaying ? styles.pausePlayActive : styles.pausePlayInactive}
|
||||||
onClick={() => onAction("play")}
|
onClick={() => onAction("play")}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
><Play /></button>
|
>▶</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={styles.next}
|
className={styles.next}
|
||||||
onClick={() => onAction("nextPhase")}
|
onClick={() => onAction("nextPhase")}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
><Next /></button>
|
>⏭</button>
|
||||||
|
|
||||||
<button
|
|
||||||
className={styles.restartPhase}
|
|
||||||
onClick={() => onAction("resetPhase")}
|
|
||||||
disabled={loading}
|
|
||||||
><Redo /></button>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={styles.restartExperiment}
|
className={styles.restartExperiment}
|
||||||
onClick={onReset}
|
onClick={onReset}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
><Replay /></button>
|
>⟲</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -284,14 +271,17 @@ function PhaseDashboard({
|
|||||||
setActiveIds: React.Dispatch<React.SetStateAction<Record<string, boolean>>>,
|
setActiveIds: React.Dispatch<React.SetStateAction<Record<string, boolean>>>,
|
||||||
goalIndex: number
|
goalIndex: number
|
||||||
}) {
|
}) {
|
||||||
const getGoals = useProgramStore((s) => s.getGoalsInPhase);
|
const getGoalsWithDepth = useProgramStore((s) => s.getGoalsWithDepth);
|
||||||
const getTriggers = useProgramStore((s) => s.getTriggersInPhase);
|
const getTriggers = useProgramStore((s) => s.getTriggersInPhase);
|
||||||
const getNorms = useProgramStore((s) => s.getNormsInPhase);
|
const getNorms = useProgramStore((s) => s.getNormsInPhase);
|
||||||
|
|
||||||
// Prepare data view models
|
// Prepare data view models
|
||||||
const goals = (getGoals(phaseId) as GoalNode[]).map(g => ({
|
const goals = getGoalsWithDepth(phaseId).map((g) => ({
|
||||||
...g,
|
...g,
|
||||||
achieved: activeIds[g.id] ?? false,
|
id: g.id as string,
|
||||||
|
name: g.name as string,
|
||||||
|
achieved: activeIds[g.id as string] ?? false,
|
||||||
|
level: g.level, // Pass this new property to the UI
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const triggers = (getTriggers(phaseId) as TriggerNode[]).map(t => ({
|
const triggers = (getTriggers(phaseId) as TriggerNode[]).map(t => ({
|
||||||
|
|||||||
@@ -32,16 +32,6 @@ export async function nextPhase(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends an API call to the CB for going to reset the currect phase
|
|
||||||
* In case we can't go to the next phase, the function will throw an error.
|
|
||||||
*/
|
|
||||||
export async function resetPhase(): Promise<void> {
|
|
||||||
const type = "reset_phase"
|
|
||||||
const context = ""
|
|
||||||
sendAPICall(type, context)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends an API call to the CB for going to pause experiment
|
* Sends an API call to the CB for going to pause experiment
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -91,13 +91,14 @@ export const DirectSpeechInput: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// --- interface for goals/triggers/norms/conditional norms ---
|
// --- interface for goals/triggers/norms/conditional norms ---
|
||||||
type StatusItem = {
|
export type StatusItem = {
|
||||||
id?: string | number;
|
id?: string | number;
|
||||||
achieved?: boolean;
|
achieved?: boolean;
|
||||||
description?: string;
|
description?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
norm?: string;
|
norm?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
level?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface StatusListProps {
|
interface StatusListProps {
|
||||||
@@ -129,7 +130,7 @@ export const StatusList: React.FC<StatusListProps> = ({
|
|||||||
const isCurrentGoal = type === 'goal' && idx === currentGoalIndex;
|
const isCurrentGoal = type === 'goal' && idx === currentGoalIndex;
|
||||||
const canOverride = (showIndicator && !isActive) || (type === 'cond_norm' && isActive);
|
const canOverride = (showIndicator && !isActive) || (type === 'cond_norm' && isActive);
|
||||||
|
|
||||||
|
const indentation = (item.level || 0) * 20;
|
||||||
|
|
||||||
const handleOverrideClick = () => {
|
const handleOverrideClick = () => {
|
||||||
if (!canOverride) return;
|
if (!canOverride) return;
|
||||||
@@ -147,7 +148,10 @@ export const StatusList: React.FC<StatusListProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={item.id ?? idx} className={styles.statusItem}>
|
<li key={item.id ?? idx}
|
||||||
|
className={styles.statusItem}
|
||||||
|
style={{ paddingLeft: `${indentation}px` }}
|
||||||
|
>
|
||||||
{showIndicator && (
|
{showIndicator && (
|
||||||
<span
|
<span
|
||||||
className={`${styles.statusIndicator} ${isActive ? styles.active : styles.inactive} ${canOverride ? styles.clickable : ''}`}
|
className={`${styles.statusIndicator} ${isActive ? styles.active : styles.inactive} ${canOverride ? styles.clickable : ''}`}
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ const VisProgUI = () => {
|
|||||||
<SaveLoadPanel></SaveLoadPanel>
|
<SaveLoadPanel></SaveLoadPanel>
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel position="bottom-center">
|
<Panel position="bottom-center">
|
||||||
<button onClick={() => undo()}>undo</button>
|
<button onClick={() => undo()}>Undo</button>
|
||||||
<button onClick={() => redo()}>Redo</button>
|
<button onClick={() => redo()}>Redo</button>
|
||||||
</Panel>
|
</Panel>
|
||||||
<Controls/>
|
<Controls/>
|
||||||
@@ -175,7 +175,7 @@ function VisProgPage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<VisualProgrammingUI/>
|
<VisualProgrammingUI/>
|
||||||
<button onClick={runProgram}>run program</button>
|
<button onClick={runProgram}>Run Program</button>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,4 +107,16 @@ export function useHandleRules(
|
|||||||
// finally we return a function that evaluates all rules using the created context
|
// finally we return a function that evaluates all rules using the created context
|
||||||
return evaluateRules(targetRules, connection, context);
|
return evaluateRules(targetRules, connection, context);
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateConnectionWithRules(
|
||||||
|
connection: Connection,
|
||||||
|
context: ConnectionContext
|
||||||
|
): RuleResult {
|
||||||
|
const rules = useFlowStore.getState().getTargetRules(
|
||||||
|
connection.target!,
|
||||||
|
connection.targetHandle!
|
||||||
|
);
|
||||||
|
|
||||||
|
return evaluateRules(rules,connection, context);
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
type XYPosition,
|
type XYPosition,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
|
import {type ConnectionContext, validateConnectionWithRules} from "./HandleRuleLogic.ts";
|
||||||
import type { FlowState } from './VisProgTypes';
|
import type { FlowState } from './VisProgTypes';
|
||||||
import {
|
import {
|
||||||
NodeDefaults,
|
NodeDefaults,
|
||||||
@@ -129,7 +130,41 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
|
|||||||
* Handles reconnecting an edge between nodes.
|
* Handles reconnecting an edge between nodes.
|
||||||
*/
|
*/
|
||||||
onReconnect: (oldEdge, newConnection) => {
|
onReconnect: (oldEdge, newConnection) => {
|
||||||
get().edgeReconnectSuccessful = true;
|
|
||||||
|
function createContext(
|
||||||
|
source: {id: string, handleId: string},
|
||||||
|
target: {id: string, handleId: string}
|
||||||
|
) : ConnectionContext {
|
||||||
|
const edges = get().edges;
|
||||||
|
const targetConnections = edges.filter(edge => edge.target === target.id && edge.targetHandle === target.handleId).length
|
||||||
|
return {
|
||||||
|
connectionCount: targetConnections,
|
||||||
|
source: source,
|
||||||
|
target: target
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// connection validation
|
||||||
|
const context: ConnectionContext = oldEdge.source === newConnection.source
|
||||||
|
? createContext({id: newConnection.source, handleId: newConnection.sourceHandle!}, {id: newConnection.target, handleId: newConnection.targetHandle!})
|
||||||
|
: createContext({id: newConnection.target, handleId: newConnection.targetHandle!}, {id: newConnection.source, handleId: newConnection.sourceHandle!});
|
||||||
|
|
||||||
|
const result = validateConnectionWithRules(
|
||||||
|
newConnection,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.isSatisfied) {
|
||||||
|
set({
|
||||||
|
edges: get().edges.map(e =>
|
||||||
|
e.id === oldEdge.id ? oldEdge : e
|
||||||
|
),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// further reconnect logic
|
||||||
|
set({ edgeReconnectSuccessful: true });
|
||||||
set({ edges: reconnectEdge(oldEdge, newConnection, get().edges) });
|
set({ edges: reconnectEdge(oldEdge, newConnection, get().edges) });
|
||||||
|
|
||||||
// We make sure to perform any required data updates on the newly reconnected nodes
|
// We make sure to perform any required data updates on the newly reconnected nodes
|
||||||
@@ -188,7 +223,7 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
|
|||||||
// Let's find our node to check if they have a special deletion function
|
// Let's find our node to check if they have a special deletion function
|
||||||
const ourNode = get().nodes.find((n)=>n.id==nodeId);
|
const ourNode = get().nodes.find((n)=>n.id==nodeId);
|
||||||
const ourFunction = Object.entries(NodeDeletes).find(([t])=>t==ourNode?.type)?.[1]
|
const ourFunction = Object.entries(NodeDeletes).find(([t])=>t==ourNode?.type)?.[1]
|
||||||
|
|
||||||
// If there's no function, OR, our function tells us we can delete it, let's do so...
|
// If there's no function, OR, our function tells us we can delete it, let's do so...
|
||||||
if (ourFunction == undefined || ourFunction()) {
|
if (ourFunction == undefined || ourFunction()) {
|
||||||
set({
|
set({
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {allowOnlyConnectionsFromHandle} from "../HandleRules.ts";
|
|||||||
import useFlowStore from '../VisProgStores.tsx';
|
import useFlowStore from '../VisProgStores.tsx';
|
||||||
import { TextField } from '../../../../components/TextField.tsx';
|
import { TextField } from '../../../../components/TextField.tsx';
|
||||||
import { MultilineTextField } from '../../../../components/MultilineTextField.tsx';
|
import { MultilineTextField } from '../../../../components/MultilineTextField.tsx';
|
||||||
|
import {noMatchingLeftRightBelief} from "./BeliefGlobals.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default data structure for a BasicBelief node
|
* The default data structure for a BasicBelief node
|
||||||
@@ -31,11 +32,12 @@ export type BasicBeliefNodeData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// These are all the types a basic belief could be.
|
// These are all the types a basic belief could be.
|
||||||
export type BasicBeliefType = Keyword | Semantic | DetectedObject | Emotion
|
export type BasicBeliefType = Keyword | Semantic | DetectedObject | Emotion | Face
|
||||||
type Keyword = { type: "keyword", id: string, value: string, label: "Keyword said:"};
|
type Keyword = { type: "keyword", id: string, value: string, label: "Keyword said:"};
|
||||||
type Semantic = { type: "semantic", id: string, value: string, description: string, label: "Detected with LLM:"};
|
type Semantic = { type: "semantic", id: string, value: string, description: string, label: "Detected with LLM:"};
|
||||||
type DetectedObject = { type: "object", id: string, value: string, label: "Object found:"};
|
type DetectedObject = { type: "object", id: string, value: string, label: "Object found:"};
|
||||||
type Emotion = { type: "emotion", id: string, value: string, label: "Emotion recognised:"};
|
type Emotion = { type: "emotion", id: string, value: string, label: "Emotion recognised:"};
|
||||||
|
type Face = { type: "face", id: string, value: string, label: "Face detected"};
|
||||||
|
|
||||||
export type BasicBeliefNode = Node<BasicBeliefNodeData>
|
export type BasicBeliefNode = Node<BasicBeliefNodeData>
|
||||||
|
|
||||||
@@ -155,6 +157,7 @@ export default function BasicBeliefNode(props: NodeProps<BasicBeliefNode>) {
|
|||||||
<option value="semantic">Detected with LLM:</option>
|
<option value="semantic">Detected with LLM:</option>
|
||||||
<option value="object">Object found:</option>
|
<option value="object">Object found:</option>
|
||||||
<option value="emotion">Emotion recognised:</option>
|
<option value="emotion">Emotion recognised:</option>
|
||||||
|
<option value="face">Face detected</option>
|
||||||
</select>
|
</select>
|
||||||
{wrapping}
|
{wrapping}
|
||||||
{data.belief.type === "emotion" && (
|
{data.belief.type === "emotion" && (
|
||||||
@@ -189,7 +192,8 @@ export default function BasicBeliefNode(props: NodeProps<BasicBeliefNode>) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<MultiConnectionHandle type="source" position={Position.Right} id="source" rules={[
|
<MultiConnectionHandle type="source" position={Position.Right} id="source" rules={[
|
||||||
allowOnlyConnectionsFromHandle([{nodeType:"trigger",handleId:"TriggerBeliefs"}, {nodeType:"norm",handleId:"NormBeliefs"},{nodeType:"InferredBelief",handleId:"inferred_belief"}]),
|
noMatchingLeftRightBelief,
|
||||||
|
allowOnlyConnectionsFromHandle([{nodeType:"trigger",handleId:"TriggerBeliefs"}, {nodeType:"norm",handleId:"NormBeliefs"},{nodeType:"InferredBelief",handleId:"inferred_belief"}]),
|
||||||
]}/>
|
]}/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -222,6 +226,10 @@ export function BasicBeliefReduce(node: Node, _nodes: Node[]) {
|
|||||||
result["name"] = data.belief.value;
|
result["name"] = data.belief.value;
|
||||||
result["description"] = data.belief.description;
|
result["description"] = data.belief.description;
|
||||||
break;
|
break;
|
||||||
|
case "face":
|
||||||
|
result["face_present"] = true;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { InferredBeliefNodeData } from "./InferredBeliefNode.tsx";
|
|||||||
* Default data for this node
|
* Default data for this node
|
||||||
*/
|
*/
|
||||||
export const InferredBeliefNodeDefaults: InferredBeliefNodeData = {
|
export const InferredBeliefNodeDefaults: InferredBeliefNodeData = {
|
||||||
label: "Inferred Belief",
|
label: "AND/OR",
|
||||||
droppable: true,
|
droppable: true,
|
||||||
inferredBelief: {
|
inferredBelief: {
|
||||||
left: undefined,
|
left: undefined,
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ export default function TriggerNode(props: NodeProps<TriggerNode>) {
|
|||||||
const setName= (value: string) => {
|
const setName= (value: string) => {
|
||||||
updateNodeData(props.id, {...data, name: value})
|
updateNodeData(props.id, {...data, name: value})
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
|
|
||||||
<Toolbar nodeId={props.id} allowDelete={true}/>
|
<Toolbar nodeId={props.id} allowDelete={true}/>
|
||||||
<div className={`${styles.defaultNode} ${styles.nodeTrigger} flex-col gap-sm`}>
|
<div className={`${styles.defaultNode} ${styles.nodeTrigger} flex-col gap-sm`}>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -70,9 +70,9 @@ export default function TriggerNode(props: NodeProps<TriggerNode>) {
|
|||||||
type="target"
|
type="target"
|
||||||
position={Position.Bottom}
|
position={Position.Bottom}
|
||||||
id="TriggerBeliefs"
|
id="TriggerBeliefs"
|
||||||
style={{ left: '40%' }}
|
style={{ left: '40%' }}
|
||||||
rules={[
|
rules={[
|
||||||
allowOnlyConnectionsFromType(['basic_belief', "inferred_belief"]),
|
allowOnlyConnectionsFromType(['basic_belief']),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ export function TriggerConnectionTarget(_thisNode: Node, _sourceNodeId: string)
|
|||||||
const otherNode = nodes.find((x) => x.id === _sourceNodeId)
|
const otherNode = nodes.find((x) => x.id === _sourceNodeId)
|
||||||
if (!otherNode) return;
|
if (!otherNode) return;
|
||||||
|
|
||||||
if (otherNode.type === 'basic_belief'|| otherNode.type ==='inferred_belief') {
|
if (otherNode.type === 'basic_belief' /* TODO: Add the option for an inferred belief */) {
|
||||||
data.condition = _sourceNodeId;
|
data.condition = _sourceNodeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ export function TriggerDisconnectionTarget(_thisNode: Node, _sourceNodeId: strin
|
|||||||
const data = _thisNode.data as TriggerNodeData;
|
const data = _thisNode.data as TriggerNodeData;
|
||||||
// remove if the target of disconnection was our condition
|
// remove if the target of disconnection was our condition
|
||||||
if (_sourceNodeId == data.condition) data.condition = undefined
|
if (_sourceNodeId == data.condition) data.condition = undefined
|
||||||
|
|
||||||
data.plan = deleteGoalInPlanByID(structuredClone(data.plan) as Plan, _sourceNodeId)
|
data.plan = deleteGoalInPlanByID(structuredClone(data.plan) as Plan, _sourceNodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import {create} from "zustand";
|
|||||||
// the type of a reduced program
|
// the type of a reduced program
|
||||||
export type ReducedProgram = { phases: Record<string, unknown>[] };
|
export type ReducedProgram = { phases: Record<string, unknown>[] };
|
||||||
|
|
||||||
|
export type GoalWithDepth = Record<string, unknown> & { level: number };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the type definition of the programStore
|
* the type definition of the programStore
|
||||||
*/
|
*/
|
||||||
@@ -18,6 +20,7 @@ export type ProgramState = {
|
|||||||
getPhaseNames: () => string[];
|
getPhaseNames: () => string[];
|
||||||
getNormsInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
getNormsInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
||||||
getGoalsInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
getGoalsInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
||||||
|
getGoalsWithDepth: (currentPhaseId: string) => GoalWithDepth[];
|
||||||
getTriggersInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
getTriggersInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
||||||
// if more specific utility functions are needed they can be added here:
|
// if more specific utility functions are needed they can be added here:
|
||||||
}
|
}
|
||||||
@@ -70,6 +73,50 @@ const useProgramStore = create<ProgramState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
throw new Error(`phase with id:"${currentPhaseId}" not found`)
|
throw new Error(`phase with id:"${currentPhaseId}" not found`)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getGoalsWithDepth: (currentPhaseId: string) => {
|
||||||
|
const program = get().currentProgram;
|
||||||
|
const phase = program.phases.find(val => val["id"] === currentPhaseId);
|
||||||
|
|
||||||
|
if (!phase) {
|
||||||
|
throw new Error(`phase with id:"${currentPhaseId}" not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootGoals = phase["goals"] as Record<string, unknown>[];
|
||||||
|
const flatList: GoalWithDepth[] = [];
|
||||||
|
|
||||||
|
const isGoal = (item: Record<string, unknown>) => {
|
||||||
|
return item["plan"] !== undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Recursive helper function
|
||||||
|
const traverse = (goals: Record<string, unknown>[], depth: number) => {
|
||||||
|
goals.forEach((goal) => {
|
||||||
|
// 1. Add the current goal to the list
|
||||||
|
flatList.push({ ...goal, level: depth });
|
||||||
|
|
||||||
|
// 2. Check for children
|
||||||
|
const plan = goal["plan"] as Record<string, unknown> | undefined;
|
||||||
|
|
||||||
|
if (plan && Array.isArray(plan["steps"])) {
|
||||||
|
const steps = plan["steps"] as Record<string, unknown>[];
|
||||||
|
|
||||||
|
// 3. FILTER: Only recurse on steps that are actually goals
|
||||||
|
// If we just passed 'steps', we might accidentally add Actions/Speeches to the goal list
|
||||||
|
const childGoals = steps.filter(isGoal);
|
||||||
|
|
||||||
|
if (childGoals.length > 0) {
|
||||||
|
traverse(childGoals, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start traversal
|
||||||
|
traverse(rootGoals, 0);
|
||||||
|
|
||||||
|
return flatList;
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* gets the triggers for the provided phase
|
* gets the triggers for the provided phase
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ describe('MonitoringPage', () => {
|
|||||||
const mockGetPhaseNames = jest.fn();
|
const mockGetPhaseNames = jest.fn();
|
||||||
const mockGetNorms = jest.fn();
|
const mockGetNorms = jest.fn();
|
||||||
const mockGetGoals = jest.fn();
|
const mockGetGoals = jest.fn();
|
||||||
|
const mockGetGoalsWithDepth = jest.fn();
|
||||||
const mockGetTriggers = jest.fn();
|
const mockGetTriggers = jest.fn();
|
||||||
const mockSetProgramState = jest.fn();
|
const mockSetProgramState = jest.fn();
|
||||||
|
|
||||||
@@ -65,6 +66,7 @@ describe('MonitoringPage', () => {
|
|||||||
getNormsInPhase: mockGetNorms,
|
getNormsInPhase: mockGetNorms,
|
||||||
getGoalsInPhase: mockGetGoals,
|
getGoalsInPhase: mockGetGoals,
|
||||||
getTriggersInPhase: mockGetTriggers,
|
getTriggersInPhase: mockGetTriggers,
|
||||||
|
getGoalsWithDepth: mockGetGoalsWithDepth,
|
||||||
setProgramState: mockSetProgramState,
|
setProgramState: mockSetProgramState,
|
||||||
};
|
};
|
||||||
return selector(state);
|
return selector(state);
|
||||||
@@ -81,7 +83,11 @@ describe('MonitoringPage', () => {
|
|||||||
// Default mock return values
|
// Default mock return values
|
||||||
mockGetPhaseIds.mockReturnValue(['phase-1', 'phase-2']);
|
mockGetPhaseIds.mockReturnValue(['phase-1', 'phase-2']);
|
||||||
mockGetPhaseNames.mockReturnValue(['Intro', 'Main']);
|
mockGetPhaseNames.mockReturnValue(['Intro', 'Main']);
|
||||||
mockGetGoals.mockReturnValue([{ id: 'g1', name: 'Goal 1' }, { id: 'g2', name: 'Goal 2' }]);
|
mockGetGoals.mockReturnValue([{ id: 'g1', name: 'Goal 1'}, { id: 'g2', name: 'Goal 2'}]);
|
||||||
|
mockGetGoalsWithDepth.mockReturnValue([
|
||||||
|
{ id: 'g1', name: 'Goal 1', level: 0 },
|
||||||
|
{ id: 'g2', name: 'Goal 2', level: 0 }
|
||||||
|
]);
|
||||||
mockGetTriggers.mockReturnValue([{ id: 't1', name: 'Trigger 1' }]);
|
mockGetTriggers.mockReturnValue([{ id: 't1', name: 'Trigger 1' }]);
|
||||||
mockGetNorms.mockReturnValue([
|
mockGetNorms.mockReturnValue([
|
||||||
{ id: 'n1', norm: 'Norm 1', condition: null },
|
{ id: 'n1', norm: 'Norm 1', condition: null },
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { renderHook, act, cleanup } from '@testing-library/react';
|
import { renderHook, act, cleanup } from '@testing-library/react';
|
||||||
import {
|
import {
|
||||||
sendAPICall,
|
sendAPICall,
|
||||||
nextPhase,
|
nextPhase,
|
||||||
resetPhase,
|
|
||||||
pauseExperiment,
|
pauseExperiment,
|
||||||
playExperiment,
|
playExperiment,
|
||||||
useExperimentLogger,
|
useExperimentLogger,
|
||||||
@@ -116,14 +115,6 @@ describe('MonitoringPageAPI', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('resetPhase sends correct params', async () => {
|
|
||||||
await resetPhase();
|
|
||||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
||||||
expect.any(String),
|
|
||||||
expect.objectContaining({ body: JSON.stringify({ type: 'reset_phase', context: '' }) })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('pauseExperiment sends correct params', async () => {
|
test('pauseExperiment sends correct params', async () => {
|
||||||
await pauseExperiment();
|
await pauseExperiment();
|
||||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ describe("SaveLoadPanel - combined tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("onLoad with invalid JSON does not update store", async () => {
|
test("onLoad with invalid JSON does not update store", async () => {
|
||||||
|
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
const file = new File(["not json"], "bad.json", { type: "application/json" });
|
const file = new File(["not json"], "bad.json", { type: "application/json" });
|
||||||
file.text = jest.fn(() => Promise.resolve(`{"bad json`));
|
file.text = jest.fn(() => Promise.resolve(`{"bad json`));
|
||||||
|
|
||||||
@@ -112,20 +114,19 @@ describe("SaveLoadPanel - combined tests", () => {
|
|||||||
|
|
||||||
render(<SaveLoadPanel />);
|
render(<SaveLoadPanel />);
|
||||||
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||||
expect(input).toBeTruthy();
|
|
||||||
|
|
||||||
// Give some input
|
|
||||||
act(() => {
|
act(() => {
|
||||||
fireEvent.change(input, { target: { files: [file] } });
|
fireEvent.change(input, { target: { files: [file] } });
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(window.alert).toHaveBeenCalledTimes(1);
|
expect(window.alert).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
const nodesAfter = useFlowStore.getState().nodes;
|
const nodesAfter = useFlowStore.getState().nodes;
|
||||||
expect(nodesAfter).toHaveLength(0);
|
expect(nodesAfter).toHaveLength(0);
|
||||||
expect(input.value).toBe("");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Clean up the spy
|
||||||
|
consoleSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("onLoad resolves to null when no file is chosen (user cancels) and does not update store", async () => {
|
test("onLoad resolves to null when no file is chosen (user cancels) and does not update store", async () => {
|
||||||
|
|||||||
@@ -115,6 +115,89 @@ describe('useProgramStore', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getGoalsWithDepth', () => {
|
||||||
|
const complexProgram: ReducedProgram = {
|
||||||
|
phases: [
|
||||||
|
{
|
||||||
|
id: 'phase-nested',
|
||||||
|
goals: [
|
||||||
|
// Level 0: Root Goal 1
|
||||||
|
{
|
||||||
|
id: 'root-1',
|
||||||
|
name: 'Root Goal 1',
|
||||||
|
plan: {
|
||||||
|
steps: [
|
||||||
|
// This is an ACTION (no plan), should be ignored
|
||||||
|
{ id: 'action-1', type: 'speech' },
|
||||||
|
|
||||||
|
// Level 1: Child Goal
|
||||||
|
{
|
||||||
|
id: 'child-1',
|
||||||
|
name: 'Child Goal',
|
||||||
|
plan: {
|
||||||
|
steps: [
|
||||||
|
// Level 2: Grandchild Goal
|
||||||
|
{
|
||||||
|
id: 'grandchild-1',
|
||||||
|
name: 'Grandchild',
|
||||||
|
plan: { steps: [] } // Empty plan is still a plan
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Level 0: Root Goal 2 (Sibling)
|
||||||
|
{
|
||||||
|
id: 'root-2',
|
||||||
|
name: 'Root Goal 2',
|
||||||
|
plan: { steps: [] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should flatten nested goals and assign correct depth levels', () => {
|
||||||
|
useProgramStore.getState().setProgramState(complexProgram);
|
||||||
|
|
||||||
|
const goals = useProgramStore.getState().getGoalsWithDepth('phase-nested');
|
||||||
|
|
||||||
|
// logic: Root 1 -> Child 1 -> Grandchild 1 -> Root 2
|
||||||
|
expect(goals).toHaveLength(4);
|
||||||
|
|
||||||
|
// Check Root 1
|
||||||
|
expect(goals[0]).toEqual(expect.objectContaining({ id: 'root-1', level: 0 }));
|
||||||
|
|
||||||
|
// Check Child 1
|
||||||
|
expect(goals[1]).toEqual(expect.objectContaining({ id: 'child-1', level: 1 }));
|
||||||
|
|
||||||
|
// Check Grandchild 1
|
||||||
|
expect(goals[2]).toEqual(expect.objectContaining({ id: 'grandchild-1', level: 2 }));
|
||||||
|
|
||||||
|
// Check Root 2
|
||||||
|
expect(goals[3]).toEqual(expect.objectContaining({ id: 'root-2', level: 0 }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore steps that are not goals (missing "plan" property)', () => {
|
||||||
|
useProgramStore.getState().setProgramState(complexProgram);
|
||||||
|
const goals = useProgramStore.getState().getGoalsWithDepth('phase-nested');
|
||||||
|
|
||||||
|
// The 'action-1' object should NOT be in the list
|
||||||
|
const action = goals.find(g => g.id === 'action-1');
|
||||||
|
expect(action).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if phase does not exist', () => {
|
||||||
|
useProgramStore.getState().setProgramState(complexProgram);
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
useProgramStore.getState().getGoalsWithDepth('missing-phase')
|
||||||
|
).toThrow('phase with id:"missing-phase" not found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should return the names of all phases in the program', () => {
|
it('should return the names of all phases in the program', () => {
|
||||||
// Define a program specifically with names for this test
|
// Define a program specifically with names for this test
|
||||||
const programWithNames: ReducedProgram = {
|
const programWithNames: ReducedProgram = {
|
||||||
|
|||||||
Reference in New Issue
Block a user