chore: among other things, fixed connection issue
fix: connection issue conditional norm now able to undo and are updated via pings goals are able to be achieved out of turn ref: N25B-400
This commit is contained in:
@@ -164,6 +164,14 @@
|
|||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.statusIndicator.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusIndicator.clickable:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
.clickable {
|
.clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -173,7 +181,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.active {
|
.active {
|
||||||
cursor: default;
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import styles from './MonitoringPage.module.css';
|
import styles from './MonitoringPage.module.css';
|
||||||
import useProgramStore from "../../utils/programStore.ts";
|
import useProgramStore from "../../utils/programStore.ts";
|
||||||
import { GestureControls, SpeechPresets, DirectSpeechInput, StatusList, RobotConnected } from './MonitoringPageComponents.tsx';
|
import { GestureControls, SpeechPresets, DirectSpeechInput, StatusList, RobotConnected } from './MonitoringPageComponents.tsx';
|
||||||
import { nextPhase, useExperimentLogger, pauseExperiment, playExperiment, resetExperiment, resetPhase, type ExperimentStreamData, type GoalUpdate, type TriggerUpdate, type CondNormsStateUpdate, type PhaseUpdate } from ".//MonitoringPageAPI.ts"
|
import { nextPhase, useExperimentLogger, useStatusLogger, pauseExperiment, playExperiment, resetExperiment, resetPhase, type ExperimentStreamData, type GoalUpdate, type TriggerUpdate, type CondNormsStateUpdate, type PhaseUpdate } from ".//MonitoringPageAPI.ts"
|
||||||
|
|
||||||
import type { NormNodeData } from '../VisProgPage/visualProgrammingUI/nodes/NormNode.tsx';
|
import type { NormNodeData } from '../VisProgPage/visualProgrammingUI/nodes/NormNode.tsx';
|
||||||
|
|
||||||
@@ -35,6 +35,7 @@ export type ReducedNorm = { id: string; label?: string; norm?: string; condition
|
|||||||
|
|
||||||
const MonitoringPage: React.FC = () => {
|
const MonitoringPage: React.FC = () => {
|
||||||
const getPhaseIds = useProgramStore((s) => s.getPhaseIds);
|
const getPhaseIds = useProgramStore((s) => s.getPhaseIds);
|
||||||
|
const getPhaseNames = useProgramStore((s) => s.getPhaseNames);
|
||||||
const getNormsInPhase = useProgramStore((s) => s.getNormsInPhase);
|
const getNormsInPhase = useProgramStore((s) => s.getNormsInPhase);
|
||||||
const getGoalsInPhase = useProgramStore((s) => s.getGoalsInPhase);
|
const getGoalsInPhase = useProgramStore((s) => s.getGoalsInPhase);
|
||||||
const getTriggersInPhase = useProgramStore((s) => s.getTriggersInPhase);
|
const getTriggersInPhase = useProgramStore((s) => s.getTriggersInPhase);
|
||||||
@@ -46,9 +47,10 @@ const MonitoringPage: React.FC = () => {
|
|||||||
const [isPlaying, setIsPlaying] = React.useState(false);
|
const [isPlaying, setIsPlaying] = React.useState(false);
|
||||||
|
|
||||||
const phaseIds = getPhaseIds();
|
const phaseIds = getPhaseIds();
|
||||||
|
const phaseNames = getPhaseNames();
|
||||||
|
|
||||||
const [phaseIndex, setPhaseIndex] = React.useState(0);
|
const [phaseIndex, setPhaseIndex] = React.useState(0);
|
||||||
|
|
||||||
//see if we reached end node
|
//see if we reached end node
|
||||||
const [isFinished, setIsFinished] = React.useState(false);
|
const [isFinished, setIsFinished] = React.useState(false);
|
||||||
|
|
||||||
@@ -56,6 +58,7 @@ const MonitoringPage: React.FC = () => {
|
|||||||
// Check for phase updates
|
// Check for phase updates
|
||||||
if (data.type === 'phase_update' && data.id) {
|
if (data.type === 'phase_update' && data.id) {
|
||||||
const payload = data as PhaseUpdate;
|
const payload = data as PhaseUpdate;
|
||||||
|
console.log(`${data.type} received, id : ${data.id}`)
|
||||||
if (payload.id === "end") {
|
if (payload.id === "end") {
|
||||||
setIsFinished(true);
|
setIsFinished(true);
|
||||||
} else {
|
} else {
|
||||||
@@ -73,8 +76,10 @@ const MonitoringPage: React.FC = () => {
|
|||||||
const payload = data as GoalUpdate;
|
const payload = data as GoalUpdate;
|
||||||
const currentPhaseGoals = getGoalsInPhase(phaseIds[phaseIndex]) as ReducedGoal[];
|
const currentPhaseGoals = getGoalsInPhase(phaseIds[phaseIndex]) as ReducedGoal[];
|
||||||
const gIndex = currentPhaseGoals.findIndex((g: ReducedGoal) => g.id === payload.id);
|
const gIndex = currentPhaseGoals.findIndex((g: ReducedGoal) => g.id === payload.id);
|
||||||
|
console.log(`${data.type} received, id : ${data.id}`)
|
||||||
if (gIndex !== -1) {
|
if (gIndex == -1)
|
||||||
|
{console.log(`goal to update with id ${payload.id} not found in current phase ${phaseNames[phaseIndex]}`)}
|
||||||
|
else {
|
||||||
//set current goal to the goal that is just started
|
//set current goal to the goal that is just started
|
||||||
setGoalIndex(gIndex);
|
setGoalIndex(gIndex);
|
||||||
|
|
||||||
@@ -101,24 +106,33 @@ const MonitoringPage: React.FC = () => {
|
|||||||
...prev,
|
...prev,
|
||||||
[payload.id]: payload.achieved
|
[payload.id]: payload.achieved
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
else if (data.type === 'cond_norms_state_update') {
|
}, [getPhaseIds, getGoalsInPhase, phaseIds, phaseIndex]);
|
||||||
|
|
||||||
|
const handleStatusUpdate = React.useCallback((data: any) => {
|
||||||
|
if (data.type === 'cond_norms_state_update') {
|
||||||
const payload = data as CondNormsStateUpdate;
|
const payload = data as CondNormsStateUpdate;
|
||||||
|
|
||||||
setActiveIds((prev) => {
|
setActiveIds((prev) => {
|
||||||
|
const hasChanges = payload.norms.some(
|
||||||
|
(normUpdate) => prev[normUpdate.id] !== normUpdate.active
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasChanges) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
const nextState = { ...prev };
|
const nextState = { ...prev };
|
||||||
// payload.norms is typed on the union, so safe to use directly
|
|
||||||
payload.norms.forEach((normUpdate) => {
|
payload.norms.forEach((normUpdate) => {
|
||||||
nextState[normUpdate.id] = normUpdate.active;
|
nextState[normUpdate.id] = normUpdate.active;
|
||||||
});
|
});
|
||||||
|
|
||||||
return nextState;
|
return nextState;
|
||||||
});
|
});
|
||||||
//commented out to avoid cluttering
|
|
||||||
//console.log("Updated conditional norms state:", payload.norms);
|
|
||||||
}
|
}
|
||||||
}, [getPhaseIds, getGoalsInPhase, phaseIds, phaseIndex]);
|
}, []);
|
||||||
|
//For incoming phase, goals and trigger updates
|
||||||
useExperimentLogger(handleStreamUpdate);
|
useExperimentLogger(handleStreamUpdate);
|
||||||
|
//For pings that update conditional norms
|
||||||
|
useStatusLogger(handleStatusUpdate);
|
||||||
|
|
||||||
if (phaseIds.length === 0) {
|
if (phaseIds.length === 0) {
|
||||||
return <p className={styles.empty}>No program loaded.</p>;
|
return <p className={styles.empty}>No program loaded.</p>;
|
||||||
@@ -229,19 +243,40 @@ const MonitoringPage: React.FC = () => {
|
|||||||
<header className={styles.experimentOverview}>
|
<header className={styles.experimentOverview}>
|
||||||
<div className={styles.phaseName}>
|
<div className={styles.phaseName}>
|
||||||
<h2>Experiment Overview</h2>
|
<h2>Experiment Overview</h2>
|
||||||
<p><strong>Phase</strong> {` ${phaseIndex + 1}`} </p>
|
<p>
|
||||||
|
{isFinished ? (
|
||||||
|
<strong>Experiment finished</strong>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<strong>Phase {phaseIndex + 1}:</strong> {phaseNames[phaseIndex]}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
<div className={styles.phaseProgress}>
|
<div className={styles.phaseProgress}>
|
||||||
{phaseIds.map((id, index) => (
|
{phaseIds.map((id, index) => {
|
||||||
<span
|
// Determine the status of the phase indicator
|
||||||
key={id}
|
let phaseStatusClass = "";
|
||||||
className={`${styles.phase} ${
|
|
||||||
index < phaseIndex ? styles.completed :
|
if (isFinished) {
|
||||||
index === phaseIndex ? styles.current : ""
|
// If the whole experiment is done, all squares are green (completed)
|
||||||
}`}
|
phaseStatusClass = styles.completed;
|
||||||
>
|
} else if (index < phaseIndex) {
|
||||||
{index + 1}
|
// Past phases
|
||||||
</span>
|
phaseStatusClass = styles.completed;
|
||||||
))}
|
} else if (index === phaseIndex) {
|
||||||
|
// The current phase being worked on
|
||||||
|
phaseStatusClass = styles.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={id}
|
||||||
|
className={`${styles.phase} ${phaseStatusClass}`}
|
||||||
|
>
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -314,7 +349,7 @@ const MonitoringPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<StatusList title="Goals" items={goals} type="goal" activeIds={activeIds} currentGoalIndex={goalIndex} />
|
<StatusList title="Goals" items={goals} type="goal" activeIds={activeIds} setActiveIds = {setActiveIds} currentGoalIndex={goalIndex} />
|
||||||
<StatusList title="Triggers" items={triggers} type="trigger" activeIds={activeIds} />
|
<StatusList title="Triggers" items={triggers} type="trigger" activeIds={activeIds} />
|
||||||
<StatusList title="Norms" items={norms} type="norm" activeIds={activeIds} />
|
<StatusList title="Norms" items={norms} type="norm" activeIds={activeIds} />
|
||||||
<StatusList title="Conditional Norms" items={conditionalNorms} type="cond_norm" activeIds={activeIds} />
|
<StatusList title="Conditional Norms" items={conditionalNorms} type="cond_norm" activeIds={activeIds} />
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
import { data } from 'react-router';
|
||||||
|
|
||||||
const API_BASE = "http://localhost:8000";
|
const API_BASE = "http://localhost:8000";
|
||||||
const API_BASE_BP = API_BASE + "/button_pressed"; //UserInterruptAgent endpoint
|
const API_BASE_BP = API_BASE + "/button_pressed"; //UserInterruptAgent endpoint
|
||||||
@@ -6,26 +7,7 @@ const API_BASE_BP = API_BASE + "/button_pressed"; //UserInterruptAgent endpoint
|
|||||||
/**
|
/**
|
||||||
* HELPER: Unified sender function
|
* HELPER: Unified sender function
|
||||||
*/
|
*/
|
||||||
export const sendUserInterrupt = async (type: string, context: string) => {
|
export const sendAPICall = async (type: string, context: string, endpoint?: string) => {
|
||||||
try {
|
|
||||||
const response = await fetch(API_BASE_BP, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({type, context}),
|
|
||||||
});
|
|
||||||
if (!response.ok) throw new Error("Backend response error");
|
|
||||||
console.log(`Interrupt Sent - Type: ${type}, Context: ${context}`);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Failed to send interrupt:`, err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HELPER: Unified sender function
|
|
||||||
* In a real app, you might move this to a /services or /hooks folder
|
|
||||||
*/
|
|
||||||
const sendAPICall = async (type: string, context: string, endpoint?: string) => {
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_BP}${endpoint ?? ""}`, {
|
const response = await fetch(`${API_BASE_BP}${endpoint ?? ""}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -98,25 +80,56 @@ export type ExperimentStreamData = PhaseUpdate | GoalUpdate | TriggerUpdate | Co
|
|||||||
* via updates sent from the backend
|
* via updates sent from the backend
|
||||||
*/
|
*/
|
||||||
export function useExperimentLogger(onUpdate?: (data: ExperimentStreamData) => void) {
|
export function useExperimentLogger(onUpdate?: (data: ExperimentStreamData) => void) {
|
||||||
|
const callbackRef = React.useRef(onUpdate);
|
||||||
|
// Ref is updated every time with on update
|
||||||
|
React.useEffect(() => {
|
||||||
|
callbackRef.current = onUpdate;
|
||||||
|
}, [onUpdate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log("Connecting to Experiment Stream...");
|
||||||
const eventSource = new EventSource(`${API_BASE}/experiment_stream`);
|
const eventSource = new EventSource(`${API_BASE}/experiment_stream`);
|
||||||
|
|
||||||
eventSource.onmessage = (event) => {
|
eventSource.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const parsedData = JSON.parse(event.data) as ExperimentStreamData;
|
const parsedData = JSON.parse(event.data) as ExperimentStreamData;
|
||||||
if (onUpdate) {
|
//call function using the ref
|
||||||
onUpdate(parsedData);
|
callbackRef.current?.(parsedData);
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("Stream parse error:", err);
|
console.warn("Stream parse error:", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
eventSource.onerror = (err) => {
|
eventSource.onerror = (err) => {
|
||||||
console.error("SSE Connection Error:", err);
|
console.error("SSE Connection Error:", err);
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
console.log("Closing Experiment Stream...");
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
};
|
};
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* A hook that listens to the status stream that updates active conditional norms
|
||||||
|
* via updates sent from the backend
|
||||||
|
*/
|
||||||
|
export function useStatusLogger(onUpdate?: (data: any) => void) {
|
||||||
|
const callbackRef = React.useRef(onUpdate);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
callbackRef.current = onUpdate;
|
||||||
}, [onUpdate]);
|
}, [onUpdate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const eventSource = new EventSource(`${API_BASE}/status_stream`);
|
||||||
|
eventSource.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const parsedData = JSON.parse(event.data);
|
||||||
|
callbackRef.current?.(parsedData);
|
||||||
|
} catch (err) { console.warn("Status stream error:", err); }
|
||||||
|
};
|
||||||
|
return () => eventSource.close();
|
||||||
|
}, []); // LEGE dependency array
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import styles from './MonitoringPage.module.css';
|
import styles from './MonitoringPage.module.css';
|
||||||
import { sendUserInterrupt } from './MonitoringPageAPI';
|
import { sendAPICall } from './MonitoringPageAPI';
|
||||||
|
|
||||||
// --- GESTURE COMPONENT ---
|
// --- GESTURE COMPONENT ---
|
||||||
export const GestureControls: React.FC = () => {
|
export const GestureControls: React.FC = () => {
|
||||||
@@ -23,7 +23,7 @@ export const GestureControls: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{gestures.map(g => <option key={g.value} value={g.value}>{g.label}</option>)}
|
{gestures.map(g => <option key={g.value} value={g.value}>{g.label}</option>)}
|
||||||
</select>
|
</select>
|
||||||
<button onClick={() => sendUserInterrupt("gesture", selectedGesture)}>
|
<button onClick={() => sendAPICall("gesture", selectedGesture)}>
|
||||||
Actuate
|
Actuate
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,7 +47,7 @@ export const SpeechPresets: React.FC = () => {
|
|||||||
<li key={i}>
|
<li key={i}>
|
||||||
<button
|
<button
|
||||||
className={styles.speechBtn}
|
className={styles.speechBtn}
|
||||||
onClick={() => sendUserInterrupt("speech", phrase.text)}
|
onClick={() => sendAPICall("speech", phrase.text)}
|
||||||
>
|
>
|
||||||
"{phrase.label}"
|
"{phrase.label}"
|
||||||
</button>
|
</button>
|
||||||
@@ -64,7 +64,7 @@ export const DirectSpeechInput: React.FC = () => {
|
|||||||
|
|
||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
if (!text.trim()) return;
|
if (!text.trim()) return;
|
||||||
sendUserInterrupt("speech", text);
|
sendAPICall("speech", text);
|
||||||
setText(""); // Clear after sending
|
setText(""); // Clear after sending
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,6 +92,7 @@ type StatusItem = {
|
|||||||
description?: string;
|
description?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
norm?: string;
|
norm?: string;
|
||||||
|
name?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface StatusListProps {
|
interface StatusListProps {
|
||||||
@@ -99,6 +100,7 @@ interface StatusListProps {
|
|||||||
items: StatusItem[];
|
items: StatusItem[];
|
||||||
type: 'goal' | 'trigger' | 'norm'| 'cond_norm';
|
type: 'goal' | 'trigger' | 'norm'| 'cond_norm';
|
||||||
activeIds: Record<string, boolean>;
|
activeIds: Record<string, boolean>;
|
||||||
|
setActiveIds?: React.Dispatch<React.SetStateAction<Record<string, boolean>>>;
|
||||||
currentGoalIndex?: number;
|
currentGoalIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +110,7 @@ export const StatusList: React.FC<StatusListProps> = ({
|
|||||||
items,
|
items,
|
||||||
type,
|
type,
|
||||||
activeIds,
|
activeIds,
|
||||||
|
setActiveIds,
|
||||||
currentGoalIndex // Destructure this prop
|
currentGoalIndex // Destructure this prop
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
@@ -118,19 +121,23 @@ export const StatusList: React.FC<StatusListProps> = ({
|
|||||||
if (item.id === undefined) return null;
|
if (item.id === undefined) return null;
|
||||||
const isActive = !!activeIds[item.id];
|
const isActive = !!activeIds[item.id];
|
||||||
const showIndicator = type !== 'norm';
|
const showIndicator = type !== 'norm';
|
||||||
const canOverride = showIndicator && !isActive || (type === 'cond_norm' && isActive);
|
|
||||||
|
|
||||||
const isCurrentGoal = type === 'goal' && idx === currentGoalIndex;
|
const isCurrentGoal = type === 'goal' && idx === currentGoalIndex;
|
||||||
|
const canOverride = (showIndicator && !isActive) || (type === 'cond_norm' && isActive);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleOverrideClick = () => {
|
const handleOverrideClick = () => {
|
||||||
if (!canOverride) return;
|
if (!canOverride) return;
|
||||||
if (type === 'cond_norm' && isActive){
|
if (type === 'cond_norm' && isActive){
|
||||||
{/* Unachieve conditional norm */}
|
{/* Unachieve conditional norm */}
|
||||||
sendUserInterrupt("override_unachieve", String(item.id));
|
sendAPICall("override_unachieve", String(item.id));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
if(type === 'goal')
|
||||||
|
if(setActiveIds)
|
||||||
|
{setActiveIds(prev => ({ ...prev, [String(item.id)]: true }));}
|
||||||
|
|
||||||
sendUserInterrupt("override", String(item.id));
|
sendAPICall("override", String(item.id));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -156,7 +163,7 @@ export const StatusList: React.FC<StatusListProps> = ({
|
|||||||
borderRadius: '4px'
|
borderRadius: '4px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.description || item.label || item.norm}
|
{item.name || item.description || item.label || item.norm}
|
||||||
{isCurrentGoal && " (Current)"}
|
{isCurrentGoal && " (Current)"}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type ProgramState = {
|
|||||||
// Utility functions:
|
// Utility functions:
|
||||||
// to avoid having to manually go through the entire state for every instance where data is required
|
// to avoid having to manually go through the entire state for every instance where data is required
|
||||||
getPhaseIds: () => string[];
|
getPhaseIds: () => 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>[];
|
||||||
getTriggersInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
getTriggersInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
||||||
@@ -43,6 +44,10 @@ const useProgramStore = create<ProgramState>((set, get) => ({
|
|||||||
* gets the ids of all phases in the program
|
* gets the ids of all phases in the program
|
||||||
*/
|
*/
|
||||||
getPhaseIds: () => get().currentProgram.phases.map(entry => entry["id"] as string),
|
getPhaseIds: () => get().currentProgram.phases.map(entry => entry["id"] as string),
|
||||||
|
/**
|
||||||
|
* gets the names of all phases in the program
|
||||||
|
*/
|
||||||
|
getPhaseNames: () => get().currentProgram.phases.map((entry) => (entry["name"] as string)),
|
||||||
/**
|
/**
|
||||||
* gets the norms for the provided phase
|
* gets the norms for the provided phase
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user