237 lines
7.7 KiB
TypeScript
237 lines
7.7 KiB
TypeScript
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
|
// University within the Software Project course.
|
|
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
|
import React, { useEffect, useState } from 'react';
|
|
import styles from './MonitoringPage.module.css';
|
|
import { sendAPICall } from './MonitoringPageAPI';
|
|
import { API_BASE_URL } from '../../config/api.ts';
|
|
|
|
// --- GESTURE COMPONENT ---
|
|
export const GestureControls: React.FC = () => {
|
|
const [selectedGesture, setSelectedGesture] = useState("animations/Stand/BodyTalk/Speaking/BodyTalk_1");
|
|
|
|
const gestures = [
|
|
{ label: "Wave", value: "animations/Stand/Gestures/Hey_1" },
|
|
{ label: "Think", value: "animations/Stand/Emotions/Neutral/Puzzled_1" },
|
|
{ label: "Explain", value: "animations/Stand/Gestures/Explain_4" },
|
|
{ label: "You", value: "animations/Stand/Gestures/You_1" },
|
|
{ label: "Happy", value: "animations/Stand/Emotions/Positive/Happy_1" },
|
|
{ label: "Laugh", value: "animations/Stand/Emotions/Positive/Laugh_2" },
|
|
{ label: "Lonely", value: "animations/Stand/Emotions/Neutral/Lonely_1" },
|
|
{ label: "Suprise", value: "animations/Stand/Emotions/Negative/Surprise_1" },
|
|
{ label: "Hurt", value: "animations/Stand/Emotions/Negative/Hurt_2" },
|
|
{ label: "Angry", value: "animations/Stand/Emotions/Negative/Angry_4" },
|
|
];
|
|
return (
|
|
<div className={styles.gestures}>
|
|
<h4>Gestures</h4>
|
|
<div className={styles.gestureInputGroup}>
|
|
<select
|
|
value={selectedGesture}
|
|
onChange={(e) => setSelectedGesture(e.target.value)}
|
|
>
|
|
{gestures.map(g => <option key={g.value} value={g.value}>{g.label}</option>)}
|
|
</select>
|
|
<button onClick={() => sendAPICall("gesture", selectedGesture)}>
|
|
Actuate
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// --- PRESET SPEECH COMPONENT ---
|
|
export const SpeechPresets: React.FC = () => {
|
|
const phrases = [
|
|
{ label: "Hello, I'm Pepper", text: "Hello, I'm Pepper" },
|
|
{ label: "Repeat please", text: "Could you repeat that please" },
|
|
{ label: "About yourself", text: "Tell me something about yourself" },
|
|
];
|
|
|
|
return (
|
|
<div className={styles.speech}>
|
|
<h4>Speech Presets</h4>
|
|
<ul>
|
|
{phrases.map((phrase, i) => (
|
|
<li key={i}>
|
|
<button
|
|
className={styles.speechBtn}
|
|
onClick={() => sendAPICall("speech", phrase.text)}
|
|
>
|
|
"{phrase.label}"
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// --- DIRECT SPEECH (INPUT) COMPONENT ---
|
|
export const DirectSpeechInput: React.FC = () => {
|
|
const [text, setText] = useState("");
|
|
|
|
const handleSend = () => {
|
|
if (!text.trim()) return;
|
|
sendAPICall("speech", text);
|
|
setText(""); // Clear after sending
|
|
};
|
|
|
|
return (
|
|
<div className={styles.directSpeech}>
|
|
<h4>Direct Pepper Speech</h4>
|
|
<div className={styles.speechInput}>
|
|
<input
|
|
type="text"
|
|
value={text}
|
|
onChange={(e) => setText(e.target.value)}
|
|
placeholder="Type message..."
|
|
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
|
|
/>
|
|
<button onClick={handleSend}>Send</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// --- interface for goals/triggers/norms/conditional norms ---
|
|
export type StatusItem = {
|
|
id?: string | number;
|
|
achieved?: boolean;
|
|
description?: string;
|
|
label?: string;
|
|
norm?: string;
|
|
name?: string;
|
|
level?: number;
|
|
};
|
|
|
|
interface StatusListProps {
|
|
title: string;
|
|
items: StatusItem[];
|
|
type: 'goal' | 'trigger' | 'norm'| 'cond_norm';
|
|
activeIds: Record<string, boolean>;
|
|
setActiveIds?: React.Dispatch<React.SetStateAction<Record<string, boolean>>>;
|
|
currentGoalIndex?: number;
|
|
}
|
|
|
|
// --- STATUS LIST COMPONENT ---
|
|
export const StatusList: React.FC<StatusListProps> = ({
|
|
title,
|
|
items,
|
|
type,
|
|
activeIds,
|
|
setActiveIds,
|
|
currentGoalIndex // Destructure this prop
|
|
}) => {
|
|
return (
|
|
<section className={styles.phaseBox}>
|
|
<h3>{title}</h3>
|
|
<ul>
|
|
{items.map((item, idx) => {
|
|
if (item.id === undefined) return null;
|
|
const isActive = !!activeIds[item.id];
|
|
const showIndicator = type !== 'norm';
|
|
const isCurrentGoal = type === 'goal' && idx === currentGoalIndex;
|
|
const canOverride = (showIndicator && !isActive) || (type === 'cond_norm' && isActive);
|
|
|
|
const indentation = (item.level || 0) * 20;
|
|
|
|
const handleOverrideClick = () => {
|
|
if (!canOverride) return;
|
|
if (type === 'cond_norm' && isActive){
|
|
{/* Unachieve conditional norm */}
|
|
sendAPICall("override_unachieve", String(item.id));
|
|
}
|
|
else {
|
|
if(type === 'goal')
|
|
if(setActiveIds)
|
|
{setActiveIds(prev => ({ ...prev, [String(item.id)]: true }));}
|
|
|
|
sendAPICall("override", String(item.id));
|
|
}
|
|
};
|
|
|
|
return (
|
|
<li key={item.id ?? idx}
|
|
className={styles.statusItem}
|
|
style={{ paddingLeft: `${indentation}px` }}
|
|
>
|
|
{showIndicator && (
|
|
<span
|
|
className={`${styles.statusIndicator} ${isActive ? styles.active : styles.inactive} ${canOverride ? styles.clickable : ''}`}
|
|
onClick={handleOverrideClick}
|
|
>
|
|
{isActive ? "✔️" : "❌"}
|
|
</span>
|
|
)}
|
|
<span
|
|
className={styles.itemDescription}
|
|
style={{
|
|
// Visual Feedback
|
|
textDecoration: isCurrentGoal ? 'underline' : 'none',
|
|
fontWeight: isCurrentGoal ? 'bold' : 'normal',
|
|
color: isCurrentGoal ? '#007bff' : 'inherit',
|
|
backgroundColor: isCurrentGoal ? '#e7f3ff' : 'transparent', // Added subtle highlight
|
|
padding: isCurrentGoal ? '2px 4px' : '0',
|
|
borderRadius: '4px'
|
|
}}
|
|
>
|
|
{item.name || item.norm}
|
|
{isCurrentGoal && " (Current)"}
|
|
</span>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
|
|
// --- Robot Connected ---
|
|
export const RobotConnected = () => {
|
|
|
|
/**
|
|
* The current connection state:
|
|
* - `true`: Robot is connected.
|
|
* - `false`: Robot is not connected.
|
|
* - `null`: Connection status is unknown (initial check in progress).
|
|
*/
|
|
const [connected, setConnected] = useState<boolean | null>(null);
|
|
|
|
useEffect(() => {
|
|
// Open a Server-Sent Events (SSE) connection to receive live ping updates.
|
|
// We're expecting a stream of data like that looks like this: `data = False` or `data = True`
|
|
const eventSource = new EventSource(`${API_BASE_URL}/robot/ping_stream`);
|
|
eventSource.onmessage = (event) => {
|
|
|
|
// Expecting messages in JSON format: `true` or `false`
|
|
//commented out this log as it clutters console logs, but might be useful to debug
|
|
//console.log("received message:", event.data);
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
|
|
try {
|
|
setConnected(data)
|
|
}
|
|
catch {
|
|
console.log("couldnt extract connected from incoming ping data")
|
|
}
|
|
|
|
} catch {
|
|
console.log("Ping message not in correct format:", event.data);
|
|
}
|
|
};
|
|
|
|
// Clean up the SSE connection when the component unmounts.
|
|
return () => eventSource.close();
|
|
}, []);
|
|
|
|
return (
|
|
<div>
|
|
<h3>Connection:</h3>
|
|
<p className={connected ? styles.connected : styles.disconnected }>{connected ? "● Robot is connected" : "● Robot is disconnected"}</p>
|
|
</div>
|
|
)
|
|
}
|